diff --git a/opentelemetry-java-instrumentation/.gitignore b/opentelemetry-java-instrumentation/.gitignore new file mode 100644 index 000000000..bf00d8f7c --- /dev/null +++ b/opentelemetry-java-instrumentation/.gitignore @@ -0,0 +1,60 @@ +# Maven # +######### +target +/target +**/dependency-reduced-pom.xml + +# Gradle # +######### +!**/gradle/wrapper/* +.gradle +**/build/ +examples/**/build/ + +# Eclipse # +########### +*.launch +.settings +.project +.classpath +# Eclipse is odd in assuming in can use bin to put temp files into it +# This assumes we do not have sub-projects that actually need bin files committed +*/bin/ + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db + +# Intellij Idea # +################# +.idea +*.iml +*.ipr +*.iws +out/ + +# Visual Studio Code # +###################### +.vscode + +# Others # +########## +/logs/* +/bin +/out +/workspace +java-agent/benchmark-integration/perf-test-settings.rc +derby.log +.java-version +hs_err_pid* +replay_pid* + +!java-agent/benchmark/releases/*.jar + diff --git a/opentelemetry-java-instrumentation/CHANGELOG.md b/opentelemetry-java-instrumentation/CHANGELOG.md new file mode 100644 index 000000000..4db1f7522 --- /dev/null +++ b/opentelemetry-java-instrumentation/CHANGELOG.md @@ -0,0 +1,251 @@ +# Changelog + +## Unreleased + +### 🌟 New javaagent instrumentation + +- Spring Integration javaagent instrumentation (#3295) + +### 🌟 New library instrumentation + +- Spring Integration library instrumentation (#3120) + +### 📈 Enhancements + +- Support peer-service-mapping in OkHttp3 instrumentation (#3063) +- Low cardinality span names for Hibernate spans (#3106) +- Propagate context to armeria callbacks (#3108) +- Add attributes to netty connection failure span (#3115) +- Defer initialization of OpenTelemetry in spring-boot-autoconfigure (#3171) +- Support couchbase 3.1.6 (#3194) +- New experimental support for agent extensions (#2881, #3071, #3226, #3237) +- Propagate context to akka http callbacks (#3263) + +### Behavioral changes + +- Update agent logger prefix (#3007) +- Remove khttp instrumentation (#3087) +- Enable akka actor instrumentation by default (#3173) + +### 🛠️ Bug fixes + +- Remove Netty instrumented handler wrapper when original handler is removed (#3026) +- Fix memory leak when Netty handler is a lambda (#3059) +- Fix race condition on Undertow (#2992) +- Remove db.connection_string from redis instrumentation (#3094) +- Fix context propagation leak in Akka instrumentation (#3099) +- Fix webflux handler span sporadically not ending (#3150) +- End span on cancellation of subscription to reactive publishers (#3153) +- End span on cancellation of Guava future (#3175) +- Create Netty connection failure span only when first operation fails (#3228) +- Internal instrumentation should always be enabled by default (#3257) +- Fix context propagation leak in Akka HTTP instrumentation (#3264) +- Only include exporters in the `-all` jar (#3286) +- Fix ForkJoinPool sometimes not instrumented (#3293) + +### 🧰 Tooling + +- Migrate MuzzlePlugin to Java (#2996, #3017) +- Refactor TypeInstrumentation#transformers() method (#3019) +- Change a couple of Longs to Integers in Instrumenter API (#3043) +- Add peer.service to Instrumenter API (#3050) +- Add response type parameter to db attributes extractor (#3093) +- Add optimized Attributes implementation for Instrumenter (#3136) +- Rename ComponentInstaller to AgentListener and add #order() method (#3182) +- Update ByteBuddy (#3254) +- Introduce IgnoredTypesConfigurer SPI to enable defining per-module ignores (#3219) +- Extract agent shadow configuration to conventions script (#3256) +- Deprecate SpanExporterFactory in favor of ConfigurableSpanExporterProvider (#3299) +- Refactor span names class (#3281) +- Move http client/server testing dependencies to internal package (#3305) + +## Version 1.2.0 - 2021-05-14 + +### 🌟 New javaagent instrumentation + +- RxJava 3 + ([#2794](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2794)) + +### 🌟 New library instrumentation + +- RxJava 3 + ([#2794](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2794)) + +### 📈 Enhancements + +- Support sub-millisecond precision for start/end times on Java 9+ + ([#2600](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2600)) +- `@WithSpan` async support added for methods returning async Reactor 3.x types + ([#2714](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2714)) +- `@WithSpan` async support added for methods returning Guava ListenableFuture + ([#2811](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2811)) +- Semantic attributes `code.namespace` and `code.function` captured on JAX-RS internal spans + ([#2805](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2805)) +- Context propagated to reactor-netty callbacks + ([#2850](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2850)) + +### Behavioral changes + +- AWS lambda flush timeout raised to 10 seconds + ([#2855](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2855)) +- `SERVER` span names improved for Spring MVC, Grails, Wicket, and Struts + ([#2814](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2814)) +- `SERVER` span names improved for Servlet filters + ([#2887](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2887)) +- `SERVER` span names improved for Resteasy + ([#2900](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2900)) +- `SERVER` span names improved for Jersey and CXF + ([#2919](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2919)) +- JAX-RS `@ApplicationPath` annotation captured as part of `SERVER` span name + ([#2824](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2824)) +- RequestDispatcher `forward()` and `include()` internal spans removed + ([#2816](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2816)) +- Raised gRPC min version supported to 1.6 in order to use new gRPC context bridge API + ([#2948](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2948)) + +### 🛠️ Bug fixes + +- gRPC context bridging issues + ([#2564](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issue/2564), + [#2959](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issue/2959)) +- URL credentials of the form `https://username:password@www.example.com/` no longer captured + ([#2707](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2707)) +- Spring MVC instrumentation can cause Spring MVC to misroute requests under some conditions + ([#2815](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2815)) +- RxJava2 NoSuchFieldError + ([#2836](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2836)) +- Duplicate http client tracing headers + ([#2842](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2842)) +- Netty 4.1 listeners could not be removed by application + ([#2851](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2851)) +- NPE caused in gRPC ProtoReflectionService + ([#2876](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2876)) +- Context leak when using Ratpack + ([#2910](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2910)) +- Context leak when using Jetty + ([#2920](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2920)) +- Servlet instrumentation overwrites setStatus that was set manually earlier + ([#2929](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2929)) +- Spans not captured on interface default methods annotated with JAX-RS annotations + ([#2930](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2930)) + +### 🧰 Tooling + +- Documented how to write InstrumentationModule line by line + ([#2793](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2793)) +- New instrumenter API used in JMS instrumentation + ([#2803](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2803)) +- Instrumenter API improvements + ([#2860](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2860)) +- Muzzle checks whether used fields are actually declared somewhere + ([#2870](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2870)) +- Extracted javaagent-extension-api from tooling & spi + ([#2879](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2879)) + - You no longer have to depend on the `javaagent-tooling` module to implement custom instrumentations: a new `javaagent-extension-api` module was introduced, containing all the necessary instrumentation classes and interfaces; + - `InstrumentationModule` and `TypeInstrumentation` were moved to the `io.opentelemetry.javaagent.extension.instrumentation` package; + - `AgentElementMatchers`, `ClassLoaderMatcher` and `NameMatchers` were moved to the `io.opentelemetry.javaagent.extension.matcher` package; + - A new SPI `AgentExtension` was introduced: it replaces `ByteBuddyAgentCustomizer`; + - `InstrumentationModule#getOrder()` was renamed to `order()`; + - `InstrumentationModule#additionalHelperClassNames()` has been removed; use `isHelperClass(String)` instead if you use the muzzle compile plugin. If you're not using muzzle, you can override `getMuzzleHelperClassNames()` directly instead; + - `InstrumentationModule#getAllHelperClassNames()` has been removed; you can call `getMuzzleHelperClassNames()` to retrieve all helper class names instead. + +## Version 1.1.0 - 2021-04-14 + +### ☢️ Breaking changes + +- Update servlet attribute names for log injection, from `traceId` and `spanId` to `trace_id` and + `span_id` + ([#2593](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2593)) +- Renamed `runtime.jvm.gc.collection` metric to `runtime.jvm.gc.time` + ([#2616](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2616)) + +### 🌟 New javaagent instrumentation + +- Elasticsearch 7 + ([#2514](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2514), + [#2528](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2528)) +- Couchbase 3.1 + ([#2524](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2524)) +- Grails + ([#2512](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2512)) +- RocketMQ + ([#2263](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2263)) +- Lettuce 6 + ([#2589](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2589)) +- Servlet 5 + ([#2609](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2609)) +- Vaadin web framework + ([#2619](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2619)) +- GWT + ([#2652](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2652)) +- Tapestry web framework + ([#2690](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2690)) +- `@WithSpan` support for methods returning CompletableFuture + ([#2530](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2530)) +- `@WithSpan` support for methods returning async RxJava 2 types + ([#2530](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2530)) + +### 🌟 New library instrumentation + +- Library instrumentation for AWS SDK v1 + ([#2525](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2525)) +- Library instrumentation for Lettuce 5.1 + ([#2533](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2533)) +- RocketMQ + ([#2263](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2263)) +- Lettuce 6 + ([#2589](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2589)) +- Spring Boot Autoconfigure support for `@WithSpan` methods returning CompletableFuture + ([#2618](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2618)) +- Spring Boot Autoconfigure support for `@WithSpan` methods returning async RxJava 2 types + ([#2530](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2530)) + +### 📈 Improvements + +- Move attributes to span builder for use by samplers + ([#2587](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2587)) +- Apache Camel - SNS propagation + ([#2562](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2562)) +- Apache Camel - S3 to SQS propagation + ([#2583](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2583)) +- Added `runtime.jvm.gc.count` metric + ([#2616](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2616)) +- Support reactor netty `HttpClient.from` construction + ([#2650](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2650)) +- Improve akka instrumentation + ([#2737](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2737)) +- Record internal metric for SQL cache misses + ([#2747](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2747)) +- End Netty 4.1 client and server spans when the response has completed, instead of when the + response has started + ([#2641](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2641)) + +### 🛠️ Bug fixes + +- Fix RestTemplateInterceptor so that it calls endExceptionally() on exception + ([#2516](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2516)) +- Fix app failure under Eclipse OSGi + ([#2521](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2521)) +- Fix undertow span ending too early + ([#2560](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2560)) +- Fix context leak in AWS SDK 2.2 and RocketMQ instrumentations + ([#2637](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2637)) +- Fix hang when a webflux http request is made inside of another webflux http request + (e.g. auth filter) + ([#2646](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2646)) +- Fix `@WithSpan` instrumentation breaking Java 6 classes + ([#2699](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2699)) +- Fix context not propagated over JMS when explicit destination used + ([#2702](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2702)) +- Fix StackOverflowError if jdbc driver implementation of Connection getMetaData calls Statement + execute + ([#2756](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2756)) + +### 🧰 Tooling + +- Make muzzle reference creation package(s) configurable + ([#2615](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2615)) +- Instrumentations now can skip defining context store manually + ([#2775](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2775)) +- New Instrumenter API + ([#2596](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2596)) diff --git a/opentelemetry-java-instrumentation/CONTRIBUTING.md b/opentelemetry-java-instrumentation/CONTRIBUTING.md new file mode 100644 index 000000000..ddfd6fcf5 --- /dev/null +++ b/opentelemetry-java-instrumentation/CONTRIBUTING.md @@ -0,0 +1,67 @@ +## Contributing + +Pull requests for bug fixes are welcome, but before submitting new features +or changes to current functionality [open an +issue](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/new) +and discuss your ideas or propose the changes you wish to make. After a +resolution is reached a PR can be submitted for review. + +In order to build and test this whole repository you need JDK 11+. +Some instrumentations and tests may put constraints on which java versions they support. +See [Running the tests](./docs/contributing/running-tests.md) for more details. + +### Building + +#### Snapshot builds + +For developers testing code changes before a release is complete, there are +snapshot builds of the `main` branch. They are available from +the Sonatype OSS snapshots repository at https://oss.sonatype.org/content/repositories/snapshots/ ([browse](https://oss.sonatype.org/content/repositories/snapshots/io/opentelemetry/)) + +#### Building from source + +Build using Java 11: + +```bash +java -version +``` + +```bash +./gradlew assemble +``` + +and then you can find the java agent artifact at + +`javaagent/build/libs/opentelemetry-javaagent--all.jar`. + +### IntelliJ setup + +See [IntelliJ setup](docs/contributing/intellij-setup.md) + +### Style guide + +See [Style guide](docs/contributing/style-guideline.md) + +### Running the tests + +See [Running the tests](docs/contributing/running-tests.md) + +### Writing instrumentation + +See [Writing instrumentation](docs/contributing/writing-instrumentation.md) + +### Understanding the javaagent components + +See [Understanding the javaagent components](docs/contributing/javaagent-jar-components.md) + +### Understanding the javaagent instrumentation testing components + +See [Understanding the javaagent instrumentation testing components](docs/contributing/javaagent-test-infra.md) + +### Debugging + +See [Debugging](docs/contributing/debugging.md) + +### Understanding Muzzle + +See [Understanding Muzzle](docs/contributing/muzzle.md) diff --git a/opentelemetry-java-instrumentation/LICENSE b/opentelemetry-java-instrumentation/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/opentelemetry-java-instrumentation/LICENSE @@ -0,0 +1,201 @@ + 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/opentelemetry-java-instrumentation/README.md b/opentelemetry-java-instrumentation/README.md new file mode 100644 index 000000000..b0854dd5d --- /dev/null +++ b/opentelemetry-java-instrumentation/README.md @@ -0,0 +1,81 @@ +# Overview +This is the project used by Hera to intercept methods and extract trace data, commonly referred to as a "probe". It's based on the open-source version of Opentelemetry, with added Hera-specific instrumentation, exporter, JVM metrics, and other features. + +## Build Dependencies + +### JDK +This project requires JDK version 11 or higher. + +### Gradle +1. Download and install gradle-7.0.1. + +2. Specify the Gradle data source. In the Gradle installation directory, create a new `init.d` directory. Inside `init.d`, create an `init.gradle` file. The file content can be set up as: + + ```gradle + allprojects { + repositories { + mavenLocal() + maven { url 'https://maven.aliyun.com/nexus/content/repositories/central/' } + mavenCentral() + } + } + ``` + + This uses Alibaba's domestic mirror repository, which can speed up the dependency file download. + +3. Add the local environment variable, `GRADLE_USER_HOME=${gradle installation directory}`, and then add `${gradle installation directory}/bin` to the PATH. After adding, you can execute `gradle -v` from any location to check if the environment variable is effective. + +4. After importing the project into IDEA, you need to configure the project's internal Gradle in IDEA: + - `Gradle user home`: Set the download location for Gradle dependencies. + - `Use gradle from`: Choose 'gradle-wrapper.properties' file. + - `Gradle JVM`: Select the directory of the installed JDK 11. + +5. Accelerate dependency import by setting Maven. Set IDEA Build Tools----Maven's `Maven home path` to the commonly used Maven directory. Set `User setting files` to the commonly used Maven settings.xml and `Local repository` to the commonly used Maven repository. With these configurations, Gradle can use existing Maven dependencies, speeding up project import. + +6. Check if there is a .git folder in the opentelemetry-java-instrumentation directory. If not, copy one from the parent directory, or execute `git init` in the opentelemetry-java-instrumentation directory. This is because many gradle plugins in the probe need to use git for version control. + +7. Execute `Reload All Gradle Projects` in the IDEA Gradle toolbar and wait for Gradle to download dependency files. This process may take 30-60 minutes for the first import. + +8. Execute `./gradlew assemble` in the project root directory to build. After successful construction, the `opentelemetry-javaagent-${version}-all.jar` file will be generated in the `javaagent` module's `build/libs` directory. This jar file is the final probe. + +### *Possible Issues +1. If you encounter errors like ClassNotFound, missing symbols, or missing packages, it might be due to incomplete dependency downloads. Click on `Reload All Gradle Projects` to continue the download. + +2. You might see errors like "Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0". This is just a version expiration warning from Gradle and will not cause any issues. + +3. If you get the error "Make sure Gradle is running on a JDK, not JRE", ensure your local environment variables point to a JDK. + +4. If the error "Missing symbol: classpathLoader(toolingRuntime, ClassLoader.getPlatformClassLoader(), project)" appears, check if the JDK version in your local environment variables is JDK 11 by executing `java -version`. + +5. Use the HTTPS Maven repository URL. If a prompt like "Using insecure protocols with repositories...to redirect to a secure protocol (like HTTPS) or allow insecure protocols" appears, please change the repository URL in the `init.d` file we just created to use HTTPS. + +6. When executing `gradlew assemble`, if you get an error like "Could not find opentelemetry-sdk-extension-jaeger-remote-sampler-1.15.0.jar (io.opentelemetry:opentelemetry-sdk-extension-jaeger-remote-sampler:1.15.0)", or similar missing jar file errors, you can comment out the `maven.aliyun.com` repository URL in `build.gradle.kts` under the `allprojects` section and re-execute `gradlew assemble`. + +## Runtime Dependencies +### Environment Variables +`host.ip`: Used to record the current physical machine IP, displayed in trace's process.tags. In Kubernetes, it captures the pod's IP. + +`node.ip`: Records the IP of the current node in Kubernetes. + +`MIONE_LOG_PATH`: Used to specify the log directory on the mione application, storing trace span information in `${MIONE_LOG_PATH}/trace/trace.log`. If not set, it defaults to `/home/work/log/none/trace/trace.log`. + +`mione.app.name`: Records the service name in the format `projectId-projectName`. For example: 1-test, where 1 is the projectId and test is the projectName. If not set, it defaults to "none". + +`TESLA_HOST`: Same as `host.ip`. Used for Nacos registration and the serverIp tag in JVM metrics. + +`JAVAAGENT_PROMETHEUS_PORT`: An available port number on the current machine. It's used for the httpServer that Prometheus uses to pull JVM metrics. Defaults to 55433 if not set. + +`hera.buildin.k8s`: Indicates if the service is deployed in Kubernetes. Set to 1 if it is. + +`MIONE_PROJECT_ENV_NAME`: Name of the current deployment environment, e.g., dev, uat, st, preview, production. + +`MIONE_PROJECT_ENV_ID`: ID of the current deployment environment. + +### JVM Parameters +Various JVM parameters are given for the probe configuration. For instance, `-javaagent:/opt/soft/opentelemetry-javaagent-all-0.0.1.jar` indicates the location of the javaagent probe jar on the server. Many parameters like this one set various properties for the probe. + +## Opentelemetry-java +For more details, configurations, and design principles, please refer to the open-source version of opentelemetry-java-instrumentation: + + +https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/v1.3.x diff --git a/opentelemetry-java-instrumentation/README_cn.md b/opentelemetry-java-instrumentation/README_cn.md new file mode 100644 index 000000000..ee3d1aa8e --- /dev/null +++ b/opentelemetry-java-instrumentation/README_cn.md @@ -0,0 +1,97 @@ +# 概况 +这是Hera用于拦截方法,提取trace数据的项目,我们一般称它为"探针"。是以开源版本的Opentelemetry为基础,添加了Hera相关的instrumentation、exporter、jvm metrics等功能。 +## 构建依赖 + +### jdk +此项目依赖的jdk版本为11及以上版本 + +### gradle +1、下载安装gradle-7.0.1 + +2、指定gradle数据源。在gradle安装目录下,新建init.d目录,在init.d目录下新建init.gradle文件,文件内容可以通过写入: + + allprojects { + repositories { + mavenLocal() + maven { url 'https://maven.aliyun.com/nexus/content/repositories/central/' } + mavenCentral() + } + } + +来使用阿里的国内镜像仓库,加快依赖文件的下载速度 + +3、添加本地环境变量,`GRADLE_USER_HOME=${gradle安装目录}`,然后将${gradle安装目录}/bin目录加入到PATH中。 +加入之后,可以在任意位置执行`gradle -v`查看环境变量是否生效 + +4、将项目导入idea之后,需要配置该项目对应的idea内部的gradle: +`Gradle user home`:需要配置gradle依赖的下载位置 +`Use gradle from`:需要选择 'gradle-wrapper.properties'file +`Gradle JVM`:需要选择自己安装的jdk11的目录 + +5、可以通过设置Maven,加速依赖导入。设置idea Build Tools----Maven的`Maven home path`为常用的Maven目录, +将`User setting files`设置为常用的Maven settings.xml,将`Local repository`设置为常用的Maven repository。通过这些配置,gradle可以使用已有的Maven依赖,加快项目导入速度。 + +6、检查opentelemetry-java-instrumentation目录下是否有.git文件夹,如果没有,需要从父目录copy一份,或者在opentelemetry-java-instrumentation目录下执行git init初始化一份.git文件。这是因为探针中的很多gradle插件需要用到git做版本控制。 + +7、在idea gradle工具栏中执行`Reload All Gradle Projects`,等待gradle下载依赖文件,这个过程对于首次导入来说,可能会花费30-60分钟的时间 + +8、在项目根目录执行`./gradlew assemble`进行构建。构建成功后,会在`javaagent`模块下的`build/libs`目录下生成`opentelemetry-javaagent-${version}-all.jar`,这个jar文件就是最终的探针。 + +### *可能出现的问题 +1、出现ClassNotFound、找不到符号、找不到包等等这些,都是因为依赖没有下载全,需要再次点击Reload All Gradle Projects,会继续下载。 + +2、可能会出现Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0,这种错误信息,这只是gradle的版本过期警告,并不影响。 + +3、Make sure Gradle is running on a JDK, not JRE。这时,需要确认本地环境变量是否有JDK。 + +4、找不到符号:classpathLoader(toolingRuntime, ClassLoader.getPlatformClassLoader(), project)。这时,需要确认本地环境变量里的JDK版本是否为jdk11,可以输入命令java -version确认。 + +5、使用https的Maven仓库地址。如果出现类似于这种提示`Using insecure protocols with repositories...to redirect to a secure protocol (like HTTPS) or allow insecure protocols`,请将本地gradle安装目录下咱们新建的init.d文件中的仓库地址改为https + +6、执行gradlew assemble时,报错:Could not find opentelemetry-sdk-extension-jaeger-remote-sampler-1.15.0.jar (io.opentelemetry:opentelemetry-sdk-extension-jaeger-remote-sampler:1.15.0),类似的找不到jar包的错误,可以把build.gradle.kts中的allprojects下的maven.aliyun.com的仓库地址注释掉,重新执行gradlew assemble + +## 运行依赖 +### 环境变量 +`host.ip`:用于记录当前物理机IP,展示在trace的process.tags里。在k8s里获取的是pod的IP + +`node.ip`:用于记录k8s当前node节点的IP + +`MIONE_LOG_PATH`:用于记录mione应用上的日志目录,将trace span信息存放在${MIONE_LOG_PATH}/trace/trace.log。如果为空,程序里默认使用/home/work/log/none/trace/trace.log。 + +`mione.app.name`:用于记录服务名,格式是projectId-projectName。eg:1-test,1是projectId,test是projectName。如果为空,程序里默认使用none + +`TESLA_HOST`:同host.ip。用于注册nacos、jvm metrics里的serverIp标签。 + +`JAVAAGENT_PROMETHEUS_PORT`:当前物理机可用端口号,用于提供给Prometheus拉取jvm metrics的httpServer使用。如果为空,程序里默认使用55433。 + +`hera.buildin.k8s`:用于记录是否是k8s部署的服务,如果是k8s的服务,标记为1。 + +`MIONE_PROJECT_ENV_NAME`:当前部署环境的名称,eg:dev、uat、st、preview、production + +`MIONE_PROJECT_ENV_ID`:当前部署环境的ID。 + +### JVM参数 +`-javaagent:/opt/soft/opentelemetry-javaagent-all-0.0.1.jar`:用于表示javaagent探针jar包在服务器上的位置,我们一般习惯将探针的jar文件更名为`opentelemetry-javaagent-all-0.0.1.jar`,并放在服务器`/opt/soft`目录下。 + +`-Dotel.resource.attributes=service.name=1-test`:用于表示当前服务的应用名。应用名应当与`mione.app.name`保持一致 + +`-Dotel.traces.exporter=log4j2`:用于表示trace export方式 + +`-Dotel.metrics.exporter=prometheus`:用于表示metrics export方式 + +`-Dotel.javaagent.debug=false`:用于表示是否开启debug日志,一般线上服务不建议开启 + +`-Dotel.exporter.prometheus.nacos.addr=${nacosurl}`:用于表示nacos地址。需要将nacos地址端口进行配置,例如:127.0.0.1:80 + +`-Dotel.javaagent.exclude-classes=com.dianping.cat.*`:过滤不被探针拦截的包。如果使用到了cat,需要将cat所在的目录进行过滤 + +`-Dotel.exporter.log.isasync=true`:用于表示是否开log4j2启异步日志,一般出于性能考虑,会是true + +`-Dotel.exporter.log.pathprefix=/home/work/log/`:用于表示log4j2的日志位置 + +`-Dotel.propagators=tracecontext`:用于表示trace传输的处理类型,目前只用到了tracecontext。 + +## Opentelemetry-java +更多细节、配置、设计原理可以查看开源版opentelemetry-java-instrumentation: + +https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/v1.3.x diff --git a/opentelemetry-java-instrumentation/RELEASING.md b/opentelemetry-java-instrumentation/RELEASING.md new file mode 100644 index 000000000..2e5821490 --- /dev/null +++ b/opentelemetry-java-instrumentation/RELEASING.md @@ -0,0 +1,80 @@ +# Versioning and releasing + +OpenTelemetry Auto-Instrumentation for Java uses [SemVer standard](https://semver.org) for versioning of its artifacts. + +Instead of manually specifying project version (and by extension the version of built artifacts) +in gradle build scripts, we use [nebula-release-plugin](https://github.com/nebula-plugins/nebula-release-plugin) +to calculate the current version based on git tags. This plugin looks for the latest tag of the form +`vX.Y.Z` on the current branch and calculates the current project version as `vX.Y.(Z+1)-SNAPSHOT`. + +## Snapshot builds +Every successful CI build of the master branch automatically executes `./gradlew snapshot` as the last task. +This signals Nebula plugin to build and publish to +[Sonatype OSS snapshots repository](https://oss.sonatype.org/content/repositories/snapshots/io/opentelemetry/) +next _minor_ release version. This means version `vX.(Y+1).0-SNAPSHOT`. + +## Starting the Release + +Open the release build workflow in your browser [here](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/release-build.yml). + +You will see a button that says "Run workflow". Press the button, enter the version number you want +to release in the input field that pops up, and then press "Run workflow". + +This triggers the release process, which builds the artifacts, publishes the artifacts, and creates +and pushes a git tag with the version number. + +## Announcement + +Once the GitHub workflow completes, go to Github [release +page](https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases), press +`Draft a new release` to write release notes about the new release. If there is already a draft +release notes, just point it at the created tag. + +## Patch Release + +All patch releases should include only bug-fixes, and must avoid +adding/modifying the public APIs. + +Open the patch release build workflow in your browser [here](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/patch-release-build.yml). + +You will see a button that says "Run workflow". Press the button, enter the version number you want +to release in the input field for version that pops up and the commits you want to cherrypick for the +patch as a comma-separated list. Then, press "Run workflow". + +If the commits cannot be cleanly applied to the release branch, for example because it has diverged +too much from main, then the workflow will fail before building. In this case, you will need to +prepare the release branch manually. + +This example will assume patching into release branch `v1.2.x` from a git repository with remotes +named `origin` and `upstream`. + +``` +$ git remote -v +origin git@github.com:username/opentelemetry-java.git (fetch) +origin git@github.com:username/opentelemetry-java.git (push) +upstream git@github.com:open-telemetry/opentelemetry-java.git (fetch) +upstream git@github.com:open-telemetry/opentelemetry-java.git (push) +``` + +First, checkout the release branch + +``` +git fetch upstream v1.2.x +git checkout upstream/v1.2.x +``` + +Apply cherrypicks manually and commit. It is ok to apply multiple cherrypicks in a single commit. +Use a commit message such as "Manual cherrypick for commits commithash1, commithash2". + +After committing the change, push to your fork's branch. + +``` +git push origin v1.2.x +``` + +Create a PR to have code review and merge this into upstream's release branch. As this was not +applied automatically, we need to do code review to make sure the manual cherrypick is correct. + +After it is merged, Run the patch release workflow again, but leave the commits input field blank. +The release will be made with the current state of the release branch, which is what you prepared +above. diff --git a/opentelemetry-java-instrumentation/VERSIONING.md b/opentelemetry-java-instrumentation/VERSIONING.md new file mode 100644 index 000000000..4233d6c28 --- /dev/null +++ b/opentelemetry-java-instrumentation/VERSIONING.md @@ -0,0 +1,33 @@ +# OpenTelemetry Java Instrumentation Versioning + +## Compatibility requirements + +Artifacts in this repository follow the same compatibility requirements described in +https://github.com/open-telemetry/opentelemetry-java/blob/main/VERSIONING.md#compatibility-requirements +. + +EXCEPT for the following incompatible changes which are allowed in stable artifacts in this +repository: + +* Changes to the telemetry produced by instrumentation + (there will be some guarantees about telemetry stability in the future, see discussions + in https://github.com/open-telemetry/opentelemetry-specification/issues/1301) +* Changes to configuration properties that contain the word `experimental` +* Changes to configuration properties under the namespace `otel.javaagent.testing` + +This means that: + +* Changes to configuration properties (other than those that contain the word `experimental` + or are under the namespace `otel.javaagent.testing`) will be considered breaking changes + (unless they only affect telemetry produced by instrumentation) + +## Stable vs alpha + +See https://github.com/open-telemetry/opentelemetry-java/blob/main/VERSIONING.md#stable-vs-alpha + +IN PARTICULAR: + +Not all of our artifacts are published as stable artifacts - any non-stable artifact has the suffix +`-alpha` on its version. NONE of the guarantees described above apply to alpha artifacts. They may +require code or environment changes on every release and are not meant for consumption for users +where versioning stability is important. diff --git a/opentelemetry-java-instrumentation/benchmark-e2e/benchmark-e2e.gradle b/opentelemetry-java-instrumentation/benchmark-e2e/benchmark-e2e.gradle new file mode 100644 index 000000000..a72aa0793 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-e2e/benchmark-e2e.gradle @@ -0,0 +1,18 @@ +apply plugin: "otel.java-conventions" + +description = 'e2e benchmark' + +dependencies { + implementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + implementation 'org.slf4j:slf4j-simple:1.6.1' + implementation 'org.testcontainers:testcontainers:1.15.3' +} + +test { + dependsOn ':javaagent:shadowJar' + maxParallelForks = 2 + + doFirst { + jvmArgs "-Dio.opentelemetry.smoketest.agent.shadowJar.path=${project(':javaagent').tasks.shadowJar.archivePath}" + } +} diff --git a/opentelemetry-java-instrumentation/benchmark-e2e/src/main/java/io/opentelemetry/e2ebenchmark/E2EAgentBenchmark.java b/opentelemetry-java-instrumentation/benchmark-e2e/src/main/java/io/opentelemetry/e2ebenchmark/E2EAgentBenchmark.java new file mode 100644 index 000000000..47634f254 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-e2e/src/main/java/io/opentelemetry/e2ebenchmark/E2EAgentBenchmark.java @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.e2ebenchmark; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class E2EAgentBenchmark { + private static final String APP_NAME = + System.getenv() + .getOrDefault( + "APP_IMAGE", + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk8-20201204.400701583"); + + private List> containers; + private static final Logger LOG = LoggerFactory.getLogger(E2EAgentBenchmark.class); + + // docker images + private static final DockerImageName APP_IMAGE = DockerImageName.parse(APP_NAME); + private static final DockerImageName OTLP_COLLECTOR_IMAGE = + DockerImageName.parse("otel/opentelemetry-collector-dev:latest"); + private static final DockerImageName WRK_IMAGE = DockerImageName.parse("quay.io/dim/wrk:stable"); + + @BeforeEach + void setUp() { + containers = new ArrayList<>(); + } + + @AfterEach + void tearDown() { + containers.forEach(GenericContainer::stop); + } + + @Test + void run() throws InterruptedException { + runBenchmark(); + } + + private void runBenchmark() throws InterruptedException { + String agentPath = System.getProperty("io.opentelemetry.smoketest.agent.shadowJar.path"); + + // otlp collector container + GenericContainer collector = + new GenericContainer<>(OTLP_COLLECTOR_IMAGE) + .withNetwork(Network.SHARED) + .withNetworkAliases("collector") + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withExposedPorts(55680, 13133) + .waitingFor(Wait.forHttp("/").forPort(13133)) + .withCopyFileToContainer( + MountableFile.forClasspathResource("collector-config.yml"), + "/etc/collector/collector-config.yml") + .withCommand("--config /etc/collector/collector-config.yml --log-level=DEBUG"); + containers.add(collector); + + // sample app container + GenericContainer app = + new GenericContainer<>(APP_IMAGE) + .withNetwork(Network.SHARED) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withNetworkAliases("app") + .withCopyFileToContainer( + MountableFile.forHostPath(agentPath), "/opentelemetry-javaagent-all.jar") + .withEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "collector:55680") + .withEnv("JAVA_TOOL_OPTIONS", "-javaagent:/opentelemetry-javaagent-all.jar") + .withExposedPorts(8080); + containers.add(app); + + // wrk benchmark container + GenericContainer wrk = + new GenericContainer<>(WRK_IMAGE) + .withNetwork(Network.SHARED) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withCreateContainerCmdModifier(it -> it.withEntrypoint("wrk")) + .withCommand("-t4 -c128 -d300s http://app:8080/ --latency"); + containers.add(wrk); + + wrk.dependsOn(app, collector); + Startables.deepStart(Stream.of(wrk)).join(); + + LOG.info("Benchmark started"); + printContainerMapping(collector); + printContainerMapping(app); + + while (wrk.isRunning()) { + Thread.sleep(1000); + } + + Thread.sleep(5000); + LOG.info("Benchmark complete, wrk output:"); + LOG.info(wrk.getLogs().replace("\n\n", "\n")); + } + + static void printContainerMapping(GenericContainer container) { + LOG.info( + "Container {} ports exposed at {}", + container.getDockerImageName(), + container.getExposedPorts().stream() + .map( + port -> + new AbstractMap.SimpleImmutableEntry<>( + port, + "http://" + + container.getContainerIpAddress() + + ":" + + container.getMappedPort(port))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + } +} diff --git a/opentelemetry-java-instrumentation/benchmark-e2e/src/main/resources/collector-config.yml b/opentelemetry-java-instrumentation/benchmark-e2e/src/main/resources/collector-config.yml new file mode 100644 index 000000000..9a580c6c5 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-e2e/src/main/resources/collector-config.yml @@ -0,0 +1,33 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:55680 + +exporters: + logging: + loglevel: info + +processors: + batch: + +extensions: + health_check: + +service: + extensions: health_check + pipelines: + traces: + processors: + - batch + receivers: + - otlp + exporters: + - logging + metrics: + processors: + - batch + receivers: + - otlp + exporters: + - logging diff --git a/opentelemetry-java-instrumentation/benchmark-integration/README.md b/opentelemetry-java-instrumentation/benchmark-integration/README.md new file mode 100644 index 000000000..7cee6acea --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/README.md @@ -0,0 +1,31 @@ +# Java Agent Performance Tests +Integration level performance tests for the Java Agent. + +## Perf Script Dependencies + +`run-perf-test.sh` requires the following (available on homebrew or a linux package manager): + +* bash (>=4.0) +* wrk +* nc + +## Running a Test +1. Build the shadow jar or the distribution zip for the server you wish to test against. +2. Run the performance test script passing in the agent jars you wish to test. +3. (optional) Save test results csv and ponder the great mysteries of performance optimization. + +### Example +#### Jetty +``` +./gradlew java-agent:benchmark-integration:jetty-perftest:shadowJar +# Compare a baseline (no agent) to the 0.18.0 and 0.19.0 releases. +/usr/local/bin/bash ./run-perf-test.sh jar jetty-perftest/build/libs/jetty-perftest-*-all.jar NoAgent ~/Downloads/dd-java-agent-0.18.0.jar ~/Downloads/dd-java-agent-0.19.0.jar +cp /tmp/perf_results.csv ~/somewhere_else/ +``` +#### Play +``` +./gradlew :java-agent:benchmark-integration:play-perftest:dist +# Compare a baseline (no agent) to the 0.18.0 and 0.19.0 releases. +/usr/local/bin/bash ./run-perf-test.sh play-zip play-perftest/build/distributions/playBinary NoAgent ~/Downloads/dd-java-agent-0.18.0.jar ~/Downloads/dd-java-agent-0.19.0.jar +cp /tmp/perf_results.csv ~/somewhere_else/ +``` diff --git a/opentelemetry-java-instrumentation/benchmark-integration/benchmark-integration.gradle b/opentelemetry-java-instrumentation/benchmark-integration/benchmark-integration.gradle new file mode 100644 index 000000000..a88d83497 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/benchmark-integration.gradle @@ -0,0 +1,17 @@ +plugins { + id "com.github.johnrengelman.shadow" +} + +apply plugin: "otel.java-conventions" + +description = 'Integration Level Agent benchmarks.' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +subprojects { sub -> + sub.apply plugin: 'com.github.johnrengelman.shadow' + sub.apply plugin: "otel.java-conventions" + + javadoc.enabled = false +} diff --git a/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/jetty-perftest.gradle b/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/jetty-perftest.gradle new file mode 100644 index 000000000..c841893ad --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/jetty-perftest.gradle @@ -0,0 +1,15 @@ +dependencies { + implementation project(':javaagent-bootstrap') + implementation project(':benchmark-integration') + + implementation "org.eclipse.jetty:jetty-server:9.4.1.v20170120" + implementation "org.eclipse.jetty:jetty-servlet:9.4.1.v20170120" +} + +jar { + manifest { + attributes( + "Main-Class": "io.opentelemetry.perftest.jetty.JettyPerftest" + ) + } +} diff --git a/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/src/main/java/io/opentelemetry/perftest/Worker.java b/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/src/main/java/io/opentelemetry/perftest/Worker.java new file mode 100644 index 000000000..5696665ad --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/src/main/java/io/opentelemetry/perftest/Worker.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.perftest; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import java.util.concurrent.TimeUnit; + +public final class Worker { + + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + /** Simulate work for the give number of milliseconds. */ + public static void doWork(long workTimeMillis) { + Span span = tracer.spanBuilder("work").startSpan(); + try (Scope ignored = span.makeCurrent()) { + if (span != null) { + span.setAttribute("work-time", workTimeMillis); + span.setAttribute("info", "interesting stuff"); + span.setAttribute("additionalInfo", "interesting stuff"); + } + + long doneTimestamp = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(workTimeMillis); + while (System.nanoTime() < doneTimestamp) { + // busy-wait to simulate work + } + span.end(); + } + } + + private Worker() {} +} diff --git a/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/src/main/java/io/opentelemetry/perftest/jetty/JettyPerftest.java b/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/src/main/java/io/opentelemetry/perftest/jetty/JettyPerftest.java new file mode 100644 index 000000000..6250c6933 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/jetty-perftest/src/main/java/io/opentelemetry/perftest/jetty/JettyPerftest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.perftest.jetty; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.perftest.Worker; +import java.io.IOException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; + +public final class JettyPerftest { + + private static final int PORT = 8080; + private static final String PATH = "/work"; + private static final Server jettyServer = new Server(PORT); + private static final ServletContextHandler servletContext = new ServletContextHandler(); + + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + public static void main(String[] args) throws Exception { + servletContext.addServlet(PerfServlet.class, PATH); + jettyServer.setHandler(servletContext); + jettyServer.start(); + + Runtime.getRuntime() + .addShutdownHook( + new Thread() { + @Override + public void run() { + try { + jettyServer.stop(); + jettyServer.destroy(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + }); + } + + @WebServlet + public static class PerfServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException { + if (request.getParameter("error") != null) { + throw new IllegalStateException("some sync error"); + } + String workVal = request.getParameter("workTimeMillis"); + long workTimeMillis = 0L; + if (null != workVal) { + workTimeMillis = Long.parseLong(workVal); + } + scheduleWork(workTimeMillis); + response.getWriter().print("Did " + workTimeMillis + "ms of work."); + } + + private static void scheduleWork(long workTimeMillis) { + Span span = tracer.spanBuilder("work").startSpan(); + try (Scope ignored = span.makeCurrent()) { + span.setAttribute("work-time", workTimeMillis); + span.setAttribute("info", "interesting stuff"); + span.setAttribute("additionalInfo", "interesting stuff"); + if (workTimeMillis > 0) { + Worker.doWork(workTimeMillis); + } + span.end(); + } + } + } + + private JettyPerftest() {} +} diff --git a/opentelemetry-java-instrumentation/benchmark-integration/perf-test-default-settings.rc b/opentelemetry-java-instrumentation/benchmark-integration/perf-test-default-settings.rc new file mode 100644 index 000000000..5fd15fd21 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/perf-test-default-settings.rc @@ -0,0 +1,17 @@ +# Default settings for running performance tests. +# To customize, copy this file to perf-test-settings.rc and change desired settings. + +# wrk settings +test_warmup_seconds=30 +test_time_seconds=90 +test_num_connections=5 +test_num_threads=5 + +# endpoints to test +declare -A endpoints +endpoints['<1MS']='http://localhost:8080/work' +endpoints['1MS']='http://localhost:8080/work?workTimeMillis=1' +endpoints['2MS']='http://localhost:8080/work?workTimeMillis=2' +endpoints['5MS']='http://localhost:8080/work?workTimeMillis=5' +endpoints['10MS']='http://localhost:8080/work?workTimeMillis=10' +test_order=( '<1MS' '1MS' '2MS' '5MS' '10MS' ) \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/app/controllers/HomeController.scala b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/app/controllers/HomeController.scala new file mode 100644 index 000000000..48e65c3aa --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/app/controllers/HomeController.scala @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package controllers + +/** + * This controller creates an `Action` to handle HTTP requests to the + * application's work page which does busy wait to simulate some work + */ +class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) { + val TRACER: Tracer = OpenTelemetry.getTracerProvider.get("test") + + /** + * Create an Action to perform busy wait + */ + def doGet(workTimeMillis: Option[Long], error: Option[String]) = Action { + implicit request: Request[AnyContent] => + error match { + case Some(x) => throw new IllegalStateException("some sync error") + case None => { + var workTime = workTimeMillis.getOrElse(0L) + scheduleWork(workTime) + Ok("Did " + workTime + "ms of work.") + } + } + + } + + private def scheduleWork(workTimeMillis: Long) { + val span = tracer().spanBuilder("work").startSpan() + val scope = tracer().withSpan(span) + try { + if (span != null) { + span.setAttribute("work-time", workTimeMillis) + span.setAttribute("info", "interesting stuff") + span.setAttribute("additionalInfo", "interesting stuff") + } + if (workTimeMillis > 0) { + Worker.doWork(workTimeMillis) + } + } finally { + span.end() + scope.close() + } + } +} diff --git a/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/app/controllers/Worker.scala b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/app/controllers/Worker.scala new file mode 100644 index 000000000..bdd36f033 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/app/controllers/Worker.scala @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package controllers + +import java.util.concurrent.TimeUnit + +object Worker { + val TRACER: Tracer = + OpenTelemetry.getTracerProvider.get("test") + + def doWork(workTimeMillis: Long) = { + val span = tracer().spanBuilder("work").startSpan() + val scope = tracer().withSpan(span) + try { + if (span != null) { + span.setAttribute("work-time", workTimeMillis) + span.setAttribute("info", "interesting stuff") + span.setAttribute("additionalInfo", "interesting stuff") + } + val doneTimestamp = System.nanoTime + TimeUnit.MILLISECONDS.toNanos( + workTimeMillis + ) + while ( { + System.nanoTime < doneTimestamp + }) { + // busy-wait to simulate work + } + } finally { + span.end() + scope.close() + } + } +} diff --git a/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/application.conf b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/application.conf new file mode 100644 index 000000000..794698ad1 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/application.conf @@ -0,0 +1,3 @@ +# https://www.playframework.com/documentation/latest/Configuration +play.http.secret.key=agentbenchmarktest0xDEADBEEF +http.port=8080 diff --git a/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/logback.xml b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/logback.xml new file mode 100644 index 000000000..72fc3ca1f --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/logback.xml @@ -0,0 +1,29 @@ + + + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/routes b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/routes new file mode 100644 index 000000000..dced33902 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/conf/routes @@ -0,0 +1,7 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# https://www.playframework.com/documentation/latest/ScalaRouting +# ~~~~ + +# An example controller showing a sample home page +GET /work controllers.HomeController.doGet(workTimeMillis: Option[Long], error: Option[String]) diff --git a/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/play-perftest.gradle b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/play-perftest.gradle new file mode 100644 index 000000000..e16082c33 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/play-perftest/play-perftest.gradle @@ -0,0 +1,46 @@ +plugins { + id "org.gradle.playframework" version "0.9" +} + +afterEvaluate { + // assemble fails without this because gradle play plugin looks for the jar file without + // considering the updated archivesBaseName that we have set in java.gradle + archivesBaseName = 'play-perftest' +} + +ext { + playVersion = "2.6.20" + scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") +} + +play { + platform { + playVersion = project.ext.playVersion + scalaVersion = project.ext.scalaVersion + javaVersion = JavaVersion.VERSION_1_8 + } + injectedRoutesGenerator = true +} + +dependencies { + implementation "com.typesafe.play:play-guice_$scalaVersion:$playVersion" + implementation "com.typesafe.play:play-logback_$scalaVersion:$playVersion" + implementation "com.typesafe.play:filters-helpers_$scalaVersion:$playVersion" + + implementation project(':javaagent-bootstrap') + implementation project(':benchmark-integration') +} + +repositories { + mavenLocal() + mavenCentral() + maven { + name "lightbend-maven-releases" + url "https://repo.lightbend.com/lightbend/maven-release" + } + ivy { + name "lightbend-ivy-release" + url "https://repo.lightbend.com/lightbend/ivy-releases" + layout "ivy" + } +} diff --git a/opentelemetry-java-instrumentation/benchmark-integration/run-perf-test.sh b/opentelemetry-java-instrumentation/benchmark-integration/run-perf-test.sh new file mode 100644 index 000000000..00589d313 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark-integration/run-perf-test.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash + +# A script for measuring a server's throughput with or without a java agent. +test_csv_file=/tmp/perf_results.csv +server_output=/tmp/server_output.txt +server_type=$1 +server_package=$2 +agent_jars="${@:3}" +server_pid="" +if [[ "$server_package" = "" ]] || [[ "$server_type" != "play-zip" && "$server_type" != "jar" ]]; then + echo "usage: ./run-perf-test.sh [play-zip|jar] path-to-server-package path-to-agent1 path-to-agent2..." + echo "" + echo "[play-zip|jar] : Specify whether the server will be in zip format (play server) or jar format" + echo "path-to-server-package : Must be a jar or binary zip package which creates an http server on local port 8080 when started." + echo " Note: if the server package is a zip, then the script will attempt to unzip to a temp directory and run the server from there." + echo "path-to-agent* : Each must be a javaagent jar, or NoAgent." + echo "" + echo "Example: This will run the perf tests against myserver.jar. It will run against no agent as a baseline, then against myagent-1.0.jar." + echo " ./run-perf-test.sh /tmp/myserve.jar NoAgent /tmp/myagent-1.0.jar" + echo "" + echo "Test results are saved to $test_csv_file" + exit 1 +fi + +if [ -f perf-test-settings.rc ]; then + echo "loading custom settings" + cat ./perf-test-settings.rc + . ./perf-test-settings.rc +else + echo "loading default settings" + cat ./perf-test-default-settings.rc + . ./perf-test-default-settings.rc +fi +echo "" +echo "" + +unzipped_server_path="" + +# Start up server passed into the script +# Blocks until server is bound to local port 8080 +function start_server { + agent_jar="$1" + javaagent_arg="" + if [ "$agent_jar" != "" -a -f "$agent_jar" ]; then + javaagent_arg="-javaagent:$agent_jar -Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=off" + fi + + if [ "$server_type" = "jar" ]; then + echo "starting server: java $javaagent_arg -jar $server_package" + { /usr/bin/time -l java $javaagent_arg -Xms256m -Xmx256m -jar $server_package ; } 2> $server_output & + else + # make a temp directory to hold the unzipped server + unzip_temp=`mktemp -d` + # perform the unzipping of the play zip + unzip ${server_package} -d ${unzip_temp} &> /dev/null + + if [ $? -eq 0 ]; then + echo "Unzipped server package at ${unzip_temp}" + else + echo "Failed to unzip server package to ${unzip_temp}" + exit 2 + fi + + # get the unzipped directory name + unzipped_dirname=`basename ${unzip_temp}/*` + # unzipped server location, will be removed when the server is stopped + unzipped_server_path=${unzip_temp} + + java_opts_env='JAVA_OPTS="'${javaagent_arg}'"' + # it appears the binary script will always be named playBinary at the time of writing + # no matter what the zip file is named. + play_script=${unzipped_server_path}/${unzipped_dirname}/bin/playBinary + + # have to use env to set JAVA_OPTS because of a gradle play plugin bug: + # https://github.com/gradle/gradle/issues/4471 + if [ "$agent_jar" != "" -a -f "$agent_jar" ]; then + utility_cmd="env JAVA_OPTS=${javaagent_arg} ${play_script}" + else + utility_cmd="${play_script}" + fi + + echo "starting server: ${utility_cmd}" + { /usr/bin/time -l ${utility_cmd} ; } 2> $server_output & + fi + + # Block until server is up + until nc -z localhost 8080; do + sleep 0.5 + done + server_pid=$(lsof -i tcp:8080 | awk '$8 == "TCP" { print $2 }' | uniq) + echo "server $server_pid started" +} + +# Send a kill signal to the running server +# and block until the server is stopped +function stop_server { + echo "Stopping $server_pid" + kill $server_pid + wait + server_pid="" + # clean up the unzipped server + if [[ ${unzipped_server_path} != "" ]] && [[ ${server_type} = "play-zip" ]] && [[ -d ${unzipped_server_path} ]]; then + echo "Cleaning up unzipped server at "${unzipped_server_path} + rm -rf ${unzipped_server_path} + fi +} + +# Warmup and run wrk tests on a single endpoint. +# echos out a file containing raw wrk output +# and a final line of the average requests/second +function test_endpoint { + url=$1 + # warmup + wrk -c $test_num_connections -t$test_num_threads -d ${test_warmup_seconds}s $url >/dev/null + + # run test + wrk_results=/tmp/wrk_results.`date +%s` + wrk -c $test_num_connections -t$test_num_threads -d ${test_time_seconds}s $url > $wrk_results + echo $wrk_results +} + + +trap 'stop_server; exit' SIGINT +trap 'kill $server_pid; exit' SIGTERM +header='Client Version' +for label in "${test_order[@]}"; do + header="$header,$label Latency,$label Throughput" +done +header="$header,Server CPU Burn,Server Max RSS,Server Start RSS,Server Load Increase RSS" +echo $header > $test_csv_file + +for agent_jar in $agent_jars; do + echo "----Testing agent $agent_jar----" + if [ "$agent_jar" == "NoAgent" ]; then + result_row="NoAgent" + start_server "" + else + agent_version=$(java -jar $agent_jar 2>/dev/null) + result_row="$agent_version" + start_server $agent_jar + fi + + + server_start_cpu=$(ps -o 'pid,time' | awk "\$1 == $server_pid { print \$2 }" | awk -F'[:\.]' '{ print ($1 * 3600) + ($2 * 60) + $3 }') + server_start_rss=$(ps -o 'pid,rss' | awk "\$1 == $server_pid { print \$2 }") + + server_total_rss=0 + server_total_rss_count=0 + + for t in "${test_order[@]}"; do + label="$t" + url="${endpoints[$label]}" + echo "--Testing $label -- $url--" + test_output_file=$(test_endpoint $url) + let server_total_rss=$server_total_rss+$(ps -o 'pid,rss' | awk "\$1 == $server_pid { print \$2 }") + let server_total_rss_count=$server_total_rss_count+1 + cat $test_output_file + avg_latency=$(awk '$1 == "Latency" { print $2 }' $test_output_file) + avg_throughput=$(awk '$1 == "Requests/sec:" { print $2 }' $test_output_file) + result_row="$result_row,$avg_latency,$avg_throughput" + rm $test_output_file + done + + server_stop_cpu=$(ps -o 'pid,time' | awk "\$1 == $server_pid { print \$2 }" | awk -F'[:\.]' '{ print ($1 * 3600) + ($2 * 60) + $3 }') + + let server_cpu=$server_stop_cpu-$server_start_cpu + + server_load_increase_rss=$(echo "scale=2; ( $server_total_rss / $server_total_rss_count ) - $server_start_rss" | bc) + + stop_server + + server_max_rss=$(awk '/.* maximum resident set size/ { print $1 }' $server_output) + rm $server_output + + echo "$result_row,$server_cpu,$server_max_rss,$server_start_rss,$server_load_increase_rss" >> $test_csv_file + echo "----/Testing agent $agent_jar----" + echo "" +done + +echo "" +echo "DONE. Test results saved to $test_csv_file" diff --git a/opentelemetry-java-instrumentation/benchmark/benchmark.gradle b/opentelemetry-java-instrumentation/benchmark/benchmark.gradle new file mode 100644 index 000000000..03879206f --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/benchmark.gradle @@ -0,0 +1,50 @@ +plugins { + id "me.champeau.jmh" + id "com.github.johnrengelman.shadow" +} + +apply plugin: "otel.java-conventions" + +dependencies { + jmh platform(project(":dependencyManagement")) + + jmh "run.mone:opentelemetry-api" + jmh "net.bytebuddy:byte-buddy-agent" + + jmh project(':instrumentation-api') + jmh project(':javaagent-tooling') + jmh project(':javaagent-extension-api') + + jmh "com.github.ben-manes.caffeine:caffeine" + + jmh 'javax.servlet:javax.servlet-api:4.0.1' + jmh 'com.google.http-client:google-http-client:1.19.0' + jmh 'org.eclipse.jetty:jetty-server:9.4.1.v20170120' + jmh 'org.eclipse.jetty:jetty-servlet:9.4.1.v20170120' + + // used to provide lots of classes for TypeMatchingBenchmark + jmh 'org.springframework:spring-web:4.3.28.RELEASE' +} + +jmh { + profilers = ['io.opentelemetry.benchmark.UsedMemoryProfiler', 'gc'] + + duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE + + def jmhIncludeSingleClass = project.findProperty('jmhIncludeSingleClass') + if (jmhIncludeSingleClass != null) { + includes = [jmhIncludeSingleClass] + } +} + +tasks.named('jmh').configure { + dependsOn(':javaagent:shadowJar') +} + +/* +If using libasyncProfiler, use the following to generate nice svg flamegraphs. +sed '/unknown/d' benchmark/build/reports/jmh/profiler.txt | sed '/^thread_start/d' | sed '/not_walkable/d' > benchmark/build/reports/jmh/profiler-cleaned.txt +(using https://github.com/brendangregg/FlameGraph) +./flamegraph.pl --color=java benchmark/build/reports/jmh/profiler-cleaned.txt > benchmark/build/reports/jmh/jmh-main.svg + */ + diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/ClassRetransformingBenchmark.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/ClassRetransformingBenchmark.java new file mode 100644 index 000000000..a6153a0ed --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/ClassRetransformingBenchmark.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark; + +import io.opentelemetry.benchmark.classes.TracedClass; +import io.opentelemetry.benchmark.classes.UntracedClass; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import net.bytebuddy.agent.ByteBuddyAgent; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +public class ClassRetransformingBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + private final Instrumentation inst = ByteBuddyAgent.install(); + } + + @Benchmark + public void testUntracedRetransform(BenchmarkState state) throws UnmodifiableClassException { + state.inst.retransformClasses(UntracedClass.class); + } + + @Benchmark + public void testTracedRetransform(BenchmarkState state) throws UnmodifiableClassException { + state.inst.retransformClasses(TracedClass.class); + } + + @Fork( + jvmArgsAppend = + "-javaagent:/path/to/opentelemetry-java-instrumentation" + + "/javaagent/build/libs/opentelemetry-javaagent.jar") + public static class WithAgent extends ClassRetransformingBenchmark {} +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/HttpBenchmark.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/HttpBenchmark.java new file mode 100644 index 000000000..c966f813c --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/HttpBenchmark.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark; + +import io.opentelemetry.benchmark.classes.HttpClass; +import java.io.IOException; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HttpBenchmark { + + private static final Logger logger = LoggerFactory.getLogger(HttpBenchmark.class); + + @State(Scope.Benchmark) + public static class BenchmarkState { + @Setup(Level.Trial) + public void doSetup() { + try { + jettyServer = new HttpClass().buildJettyServer(); + jettyServer.start(); + // Make sure it's actually running + while (!AbstractLifeCycle.STARTED.equals(jettyServer.getState())) { + Thread.sleep(500); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @TearDown(Level.Trial) + public void doTearDown() { + try { + jettyServer.stop(); + } catch (Exception e) { + logger.warn("Error", e); + } finally { + jettyServer.destroy(); + } + } + + HttpClass http = new HttpClass(); + Server jettyServer; + } + + @Benchmark + public void testMakingRequest(BenchmarkState state) throws IOException { + state.http.executeRequest(); + } + + @Fork( + jvmArgsAppend = { + "-javaagent:/path/to/opentelemetry-java-instrumentation/java-agent/build/libs/opentelemetry-javaagent.jar", + "-Dotel.traces.exporter=logging" + }) + public static class WithAgent extends ClassRetransformingBenchmark {} +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/IgnoredTypesMatcherBenchmark.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/IgnoredTypesMatcherBenchmark.java new file mode 100644 index 000000000..5b279569e --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/IgnoredTypesMatcherBenchmark.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.tooling.ignore.AdditionalLibraryIgnoredTypesConfigurer; +import io.opentelemetry.javaagent.tooling.ignore.IgnoredTypesBuilderImpl; +import io.opentelemetry.javaagent.tooling.ignore.IgnoredTypesMatcher; +import java.util.concurrent.TimeUnit; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; + +@Fork(1) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 10, time = 1) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class IgnoredTypesMatcherBenchmark { + + private static final TypeDescription springType = + new TypeDescription.Latent("org.springframework.test.SomeClass", 0, null); + private static final TypeDescription testAppType = + new TypeDescription.Latent("com.example.myapp.Main", 0, null); + + private static final ElementMatcher ignoredTypesMatcher; + + static { + IgnoredTypesBuilderImpl builder = new IgnoredTypesBuilderImpl(); + new AdditionalLibraryIgnoredTypesConfigurer().configure(Config.get(), builder); + ignoredTypesMatcher = new IgnoredTypesMatcher(builder.buildIgnoredTypesTrie()); + } + + @Benchmark + public boolean springType() { + return ignoredTypesMatcher.matches(springType); + } + + @Benchmark + public boolean appType() { + return ignoredTypesMatcher.matches(testAppType); + } +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/InstrumenterBenchmark.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/InstrumenterBenchmark.java new file mode 100644 index 000000000..7c48cd0b4 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/InstrumenterBenchmark.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@Fork(3) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 5, time = 1) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Thread) +public class InstrumenterBenchmark { + + private static final Instrumenter INSTRUMENTER = + Instrumenter.newBuilder( + OpenTelemetry.noop(), + "benchmark", + HttpSpanNameExtractor.create(ConstantHttpAttributesExtractor.INSTANCE)) + .addAttributesExtractor(ConstantHttpAttributesExtractor.INSTANCE) + .addAttributesExtractor(new ConstantNetAttributesExtractor()) + .newInstrumenter(); + + @Benchmark + public Context start() { + return INSTRUMENTER.start(Context.root(), null); + } + + @Benchmark + public Context startEnd() { + Context context = INSTRUMENTER.start(Context.root(), null); + INSTRUMENTER.end(context, null, null, null); + return context; + } + + static class ConstantHttpAttributesExtractor extends HttpAttributesExtractor { + static final HttpAttributesExtractor INSTANCE = + new ConstantHttpAttributesExtractor(); + + @Override + protected @Nullable String method(Void unused) { + return "GET"; + } + + @Override + protected @Nullable String url(Void unused) { + return "https://opentelemetry.io/benchmark"; + } + + @Override + protected @Nullable String target(Void unused) { + return "/benchmark"; + } + + @Override + protected @Nullable String host(Void unused) { + return "opentelemetry.io"; + } + + @Override + protected @Nullable String route(Void unused) { + return "/benchmark"; + } + + @Override + protected @Nullable String scheme(Void unused) { + return "https"; + } + + @Override + protected @Nullable String userAgent(Void unused) { + return "OpenTelemetryBot"; + } + + @Override + protected @Nullable Long requestContentLength(Void unused, @Nullable Void unused2) { + return 100L; + } + + @Override + protected @Nullable Long requestContentLengthUncompressed(Void unused, @Nullable Void unused2) { + return null; + } + + @Override + protected @Nullable String flavor(Void unused, @Nullable Void unused2) { + return SemanticAttributes.HttpFlavorValues.HTTP_2_0; + } + + @Override + protected @Nullable String serverName(Void unused, @Nullable Void unused2) { + return null; + } + + @Override + protected @Nullable String clientIp(Void unused, @Nullable Void unused2) { + return null; + } + + @Override + protected @Nullable Integer statusCode(Void unused, Void unused2) { + return 200; + } + + @Override + protected @Nullable Long responseContentLength(Void unused, Void unused2) { + return 100L; + } + + @Override + protected @Nullable Long responseContentLengthUncompressed(Void unused, Void unused2) { + return null; + } + } + + static class ConstantNetAttributesExtractor + extends InetSocketAddressNetAttributesExtractor { + + private static final InetSocketAddress ADDRESS = + InetSocketAddress.createUnresolved("localhost", 8080); + + @Override + public @Nullable InetSocketAddress getAddress(Void unused, @Nullable Void unused2) { + return ADDRESS; + } + + @Override + public @Nullable String transport(Void unused) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + } +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/TypeMatchingBenchmark.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/TypeMatchingBenchmark.java new file mode 100644 index 000000000..91b6cc49f --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/TypeMatchingBenchmark.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.io.File; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode(Mode.SingleShotTime) +@Fork(5) +@Warmup(iterations = 0) +@Measurement(iterations = 1) +@OutputTimeUnit(MILLISECONDS) +public class TypeMatchingBenchmark { + + private static final Set classNames; + + static { + classNames = new HashSet<>(); + String classPath = System.getProperty("java.class.path"); + for (String path : classPath.split(File.pathSeparator)) { + if (!path.endsWith(".jar")) { + continue; + } + try (JarFile jarFile = new JarFile(path)) { + Enumeration e = jarFile.entries(); + while (e.hasMoreElements()) { + JarEntry jarEntry = e.nextElement(); + String name = jarEntry.getName(); + if (name.endsWith(".class")) { + name = name.replace('/', '.'); + name = name.substring(0, name.length() - ".class".length()); + classNames.add(name); + } + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + + @Benchmark + public void loadLotsOfClasses() throws ClassNotFoundException { + for (String className : classNames) { + try { + Class.forName(className, false, TypeMatchingBenchmark.class.getClassLoader()); + } catch (NoClassDefFoundError e) { + // many classes in the jar files have optional dependencies which are not present + } + } + } + + @Fork( + jvmArgsAppend = + "-javaagent:/path/to/opentelemetry-java-instrumentation" + + "/javaagent/build/libs/opentelemetry-javaagent.jar") + public static class WithAgent extends TypeMatchingBenchmark {} +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/UsedMemoryProfiler.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/UsedMemoryProfiler.java new file mode 100644 index 000000000..841340ea3 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/UsedMemoryProfiler.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark; + +import java.util.ArrayList; +import java.util.Collection; +import org.openjdk.jmh.infra.BenchmarkParams; +import org.openjdk.jmh.infra.IterationParams; +import org.openjdk.jmh.profile.InternalProfiler; +import org.openjdk.jmh.results.AggregationPolicy; +import org.openjdk.jmh.results.IterationResult; +import org.openjdk.jmh.results.Result; +import org.openjdk.jmh.results.ScalarResult; + +public class UsedMemoryProfiler implements InternalProfiler { + private long totalHeapBefore; + private long usedHeapBefore; + + @Override + public String getDescription() { + return "Used memory heap profiler"; + } + + @Override + public void beforeIteration(BenchmarkParams benchmarkParams, IterationParams iterationParams) { + System.gc(); + System.runFinalization(); + + totalHeapBefore = Runtime.getRuntime().totalMemory(); + usedHeapBefore = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + } + + @Override + public Collection afterIteration( + BenchmarkParams benchmarkParams, IterationParams iterationParams, IterationResult result) { + + long totalHeap = Runtime.getRuntime().totalMemory(); + long usedHeap = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + + Collection results = new ArrayList<>(); + results.add( + new ScalarResult("heap.total.before", totalHeapBefore, "bytes", AggregationPolicy.AVG)); + results.add( + new ScalarResult("heap.used.before", usedHeapBefore, "bytes", AggregationPolicy.AVG)); + results.add(new ScalarResult("heap.total.after", totalHeap, "bytes", AggregationPolicy.AVG)); + results.add(new ScalarResult("heap.used.after", usedHeap, "bytes", AggregationPolicy.AVG)); + + return results; + } +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/WeakMapBenchmark.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/WeakMapBenchmark.java new file mode 100644 index 000000000..fe11cbefc --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/WeakMapBenchmark.java @@ -0,0 +1,119 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@Fork(3) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 5, time = 1) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Thread) +public class WeakMapBenchmark { + + private static final WeakConcurrentMap weakConcurrentMap = + new WeakConcurrentMap<>(true, true); + + private static final WeakConcurrentMap weakConcurrentMapInline = + new WeakConcurrentMap.WithInlinedExpunction<>(); + + private static final Cache caffeineCache = + Caffeine.newBuilder().weakKeys().build(); + private static final Map caffeineMap = caffeineCache.asMap(); + + private String key; + + @Setup + public void setUp() { + key = new String(Thread.currentThread().getName()); + } + + @Benchmark + @Threads(1) + public void threads01_weakConcurrentMap(Blackhole blackhole) { + blackhole.consume(weakConcurrentMap.put(key, "foo")); + blackhole.consume(weakConcurrentMap.get(key)); + blackhole.consume(weakConcurrentMap.remove(key)); + } + + @Benchmark + @Threads(5) + public void threads05_weakConcurrentMap(Blackhole blackhole) { + blackhole.consume(weakConcurrentMap.put(key, "foo")); + blackhole.consume(weakConcurrentMap.get(key)); + blackhole.consume(weakConcurrentMap.remove(key)); + } + + @Benchmark + @Threads(10) + public void threads10_weakConcurrentMap(Blackhole blackhole) { + blackhole.consume(weakConcurrentMap.put(key, "foo")); + blackhole.consume(weakConcurrentMap.get(key)); + blackhole.consume(weakConcurrentMap.remove(key)); + } + + @Benchmark + @Threads(1) + public void threads01_weakConcurrentMap_inline(Blackhole blackhole) { + blackhole.consume(weakConcurrentMapInline.put(key, "foo")); + blackhole.consume(weakConcurrentMapInline.get(key)); + blackhole.consume(weakConcurrentMapInline.remove(key)); + } + + @Benchmark + @Threads(5) + public void threads05_weakConcurrentMap_inline(Blackhole blackhole) { + blackhole.consume(weakConcurrentMapInline.put(key, "foo")); + blackhole.consume(weakConcurrentMapInline.get(key)); + blackhole.consume(weakConcurrentMapInline.remove(key)); + } + + @Benchmark + @Threads(10) + public void threads10_weakConcurrentMap_inline(Blackhole blackhole) { + blackhole.consume(weakConcurrentMapInline.put(key, "foo")); + blackhole.consume(weakConcurrentMapInline.get(key)); + blackhole.consume(weakConcurrentMapInline.remove(key)); + } + + @Benchmark + @Threads(1) + public void threads01_caffeine(Blackhole blackhole) { + blackhole.consume(caffeineMap.put(key, "foo")); + blackhole.consume(caffeineMap.get(key)); + blackhole.consume(caffeineMap.remove(key)); + } + + @Benchmark + @Threads(5) + public void threads05_caffeine(Blackhole blackhole) { + blackhole.consume(caffeineMap.put(key, "foo")); + blackhole.consume(caffeineMap.get(key)); + blackhole.consume(caffeineMap.remove(key)); + } + + @Benchmark + @Threads(10) + public void threads10_caffeine(Blackhole blackhole) { + blackhole.consume(caffeineMap.put(key, "foo")); + blackhole.consume(caffeineMap.get(key)); + blackhole.consume(caffeineMap.remove(key)); + } +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/A.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/A.java new file mode 100644 index 000000000..b5c79c9bb --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/A.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface A { + void a(); +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/B.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/B.java new file mode 100644 index 000000000..f5701e4c3 --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/B.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface B extends A { + void b(); +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/C.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/C.java new file mode 100644 index 000000000..eb80f723e --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/C.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface C extends A, B { + void c(); +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/D.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/D.java new file mode 100644 index 000000000..14d6d32ac --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/D.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface D extends A, B, C { + void d(); +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/E.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/E.java new file mode 100644 index 000000000..ec2915cdf --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/E.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface E extends B, C, D { + void e(); +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/F.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/F.java new file mode 100644 index 000000000..f7a720e2c --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/F.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public abstract class F implements E { + public abstract void f(); + + public void g() {} +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/HttpClass.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/HttpClass.java new file mode 100644 index 000000000..bf5800ccf --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/HttpClass.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +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.javanet.NetHttpTransport; +import java.io.IOException; +import java.net.InetSocketAddress; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; + +public class HttpClass { + private static final String contextPath = "/path"; + private static final Integer port = 18888; + + public Server buildJettyServer() { + System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog"); + System.setProperty("org.eclipse.jetty.LEVEL", "WARN"); + + Server jettyServer = new Server(new InetSocketAddress("localhost", port)); + ServletContextHandler servletContext = new ServletContextHandler(); + + servletContext.addServlet(HttpClassServlet.class, contextPath); + jettyServer.setHandler(servletContext); + return jettyServer; + } + + @WebServlet + public static class HttpClassServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + resp.setContentType("application/json"); + resp.setStatus(HttpServletResponse.SC_OK); + resp.getWriter().println("{ \"status\": \"ok\"}"); + } + } + + private final HttpRequestFactory requestFactory = new NetHttpTransport().createRequestFactory(); + + public void executeRequest() throws IOException { + String url = "http://localhost:" + port + contextPath; + + HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url)); + request.setThrowExceptionOnExecuteError(false); + request.execute(); + } +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/TracedClass.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/TracedClass.java new file mode 100644 index 000000000..82a3ff01f --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/TracedClass.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; + +public class TracedClass extends UntracedClass { + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + @Override + public void f() { + tracer.spanBuilder("f").startSpan().end(); + } + + @Override + public void e() { + tracer.spanBuilder("e").startSpan().end(); + } + + @Override + public void d() { + tracer.spanBuilder("d").startSpan().end(); + } + + @Override + public void c() { + tracer.spanBuilder("c").startSpan().end(); + } + + @Override + public void b() { + tracer.spanBuilder("b").startSpan().end(); + } + + @Override + public void a() { + tracer.spanBuilder("a").startSpan().end(); + } +} diff --git a/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/UntracedClass.java b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/UntracedClass.java new file mode 100644 index 000000000..4e36faf5e --- /dev/null +++ b/opentelemetry-java-instrumentation/benchmark/src/jmh/java/io/opentelemetry/benchmark/classes/UntracedClass.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.benchmark.classes; + +public class UntracedClass extends F { + @Override + public void f() {} + + @Override + public void e() {} + + @Override + public void d() {} + + @Override + public void c() {} + + @Override + public void b() {} + + @Override + public void a() {} +} diff --git a/opentelemetry-java-instrumentation/bom-alpha/bom-alpha.gradle b/opentelemetry-java-instrumentation/bom-alpha/bom-alpha.gradle new file mode 100644 index 000000000..a05fcfe67 --- /dev/null +++ b/opentelemetry-java-instrumentation/bom-alpha/bom-alpha.gradle @@ -0,0 +1,42 @@ +plugins { + id("java-platform") +} + +apply plugin: "otel.publish-conventions" + +description = "OpenTelemetry Instrumentation Bill of Materials (Alpha)" +group = "io.opentelemetry.instrumentation" +archivesBaseName = "opentelemetry-instrumentation-bom-alpha" + +rootProject.subprojects.forEach { subproject -> + if (!subproject.name.startsWith("bom")) { + evaluationDependsOn(subproject.path) + } +} + +javaPlatform { + allowDependencies() +} + +def otelVersion = findProperty("otelVersion") + +dependencies { + api(platform("run.mone:opentelemetry-bom:${otelVersion}")) + api(platform("run.mone:opentelemetry-bom-alpha:${otelVersion}")) +} + +afterEvaluate { + dependencies { + + constraints { + rootProject.subprojects.sort { "$it.archivesBaseName" } + .collect { it } + .findAll { it.name != project.name && it.name != 'javaagent'} + .forEach { project -> + project.plugins.withId("maven-publish") { + api(project) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/build.gradle b/opentelemetry-java-instrumentation/build.gradle new file mode 100644 index 000000000..2c4b94494 --- /dev/null +++ b/opentelemetry-java-instrumentation/build.gradle @@ -0,0 +1,174 @@ +import java.time.Duration +import nebula.plugin.release.git.opinion.Strategies + +plugins { + id 'idea' + + id "io.github.gradle-nexus.publish-plugin" + id "nebula.release" + + id 'org.gradle.test-retry' apply false + + id 'org.unbroken-dome.test-sets' apply false + id 'com.github.ben-manes.versions' + + id "com.diffplug.spotless" + id "net.ltgt.errorprone" apply false + id "net.ltgt.nullaway" apply false +} + +nexusPublishing { + packageGroup = "io.opentelemetry" + + repositories { + sonatype { + username = System.getenv('SONATYPE_USER') + password = System.getenv('SONATYPE_KEY') + } + } + + connectTimeout = Duration.ofMinutes(5) + clientTimeout = Duration.ofMinutes(5) + + transitionCheckOptions { + // We have many artifacts so Maven Central takes a long time on its compliance checks. This sets + // the timeout for waiting for the repository to close to a comfortable 50 minutes. + maxRetries.set(300) + delayBetween.set(Duration.ofSeconds(10)) + } +} + + +// Enable after verifying Maven Central publishing once through manual closing +// tasks.release.finalizedBy tasks.closeAndReleaseRepository + +description = 'OpenTelemetry instrumentations for Java' + +allprojects { + apply plugin: 'idea' + + idea { + module { + downloadJavadoc = false + downloadSources = false + } + } + + plugins.withId('net.ltgt.errorprone') { + dependencies { + errorprone "com.google.errorprone:error_prone_core" + } + + tasks.withType(JavaCompile).configureEach { + options.errorprone { + enabled = rootProject.findProperty("disableErrorProne") != "true" + disableWarningsInGeneratedCode = true + allDisabledChecksAsWarnings = true + + excludedPaths = ".*/build/generated/.*" + + // Doesn't work well with Java 8 + disable("FutureReturnValueIgnored") + + // Require Guava + disable("AutoValueImmutableFields") + disable("StringSplitter") + disable("ImmutableMemberCollection") + + // Don't currently use this (to indicate a local variable that's mutated) but could + // consider for future. + disable("Var") + + // Don't support Android without desugar + disable("AndroidJdkLibsChecker") + disable("Java7ApiChecker") + disable("StaticOrDefaultInterfaceMethod") + + // Great check, but for bytecode manipulation it's too common to separate over + // onEnter / onExit + // TODO(anuraaga): Only disable for auto instrumentation project. + disable("MustBeClosedChecker") + + // Common to avoid an allocation. Revisit if it's worth opt-in suppressing instead of + // disabling entirely. + disable("MixedMutabilityReturnType") + + // We end up using obsolete types if a library we're instrumenting uses them. + disable("JdkObsolete") + disable("JavaUtilDate") + + // Limits API possibilities + disable("NoFunctionalReturnType") + + // Storing into a variable in onEnter triggers this unfortunately. + // TODO(anuraaga): Only disable for auto instrumentation project. + disable("UnusedVariable") + + // TODO(anuraaga): Remove this, we use this pattern in several tests and it will mean + // some moving. + disable("DefaultPackage") + + // TODO(anuraaga): Remove this, all our advice classes miss constructors but probably should + // address this. + disable("PrivateConstructorForUtilityClass") + + // TODO(anuraaga): Remove this, probably after instrumenter API migration instead of dealing + // with older APIs. + disable("InconsistentOverloads") + disable("TypeParameterNaming") + + // We don't use tools that recognize. + disable("InlineMeSuggester") + disable("DoNotCallSuggester") + + if (name.contains("Jmh") || name.contains("Test")) { + disable("HashCodeToString") + disable("MemberName") + } + } + } + } + + plugins.withId('net.ltgt.nullaway') { + dependencies { + errorprone "com.uber.nullaway:nullaway" + } + + nullaway { + annotatedPackages.addAll("io.opentelemetry", "com.linecorp.armeria,com.google.common") + } + + tasks.withType(JavaCompile).configureEach { + if (!name.toLowerCase().contains("test")) { + options.errorprone { + nullaway { + severity = net.ltgt.gradle.errorprone.CheckSeverity.ERROR + } + } + } + } + } +} + +allprojects { + repositories { + mavenLocal() + maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } + maven { url 'https://maven.aliyun.com/repository/public/' } + maven { url 'https://repo1.maven.org/maven2/' } + mavenCentral() + } +} + +apply plugin: 'com.diffplug.spotless' + +spotless { + // this formatting is applied at the root level, as some of these files are not in a submodules + // and would be missed otherwise + format 'misc', { + target '.gitignore', '*.md', 'docs/**/*.md' + indentWithSpaces() + trimTrailingWhitespace() + endWithNewline() + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/build.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/build.gradle.kts new file mode 100644 index 000000000..d6f482e61 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/build.gradle.kts @@ -0,0 +1,58 @@ +plugins { + `java-gradle-plugin` + `kotlin-dsl` + // When updating, update below in dependencies too + id("com.diffplug.spotless") version "5.13.0" +} + +spotless { + java { + googleJavaFormat("1.10.0") + licenseHeaderFile(rootProject.file("../gradle/enforcement/spotless.license.java"), "(package|import|public)") + target("src/**/*.java") + } +} + +gradlePlugin { + plugins { + create("muzzle-plugin") { + id = "muzzle" + implementationClass = "io.opentelemetry.instrumentation.gradle.muzzle.MuzzlePlugin" + } + } +} + +repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + implementation(gradleApi()) + implementation(localGroovy()) + + implementation("org.eclipse.aether:aether-connector-basic:1.1.0") + implementation("org.eclipse.aether:aether-transport-http:1.1.0") + implementation("org.apache.maven:maven-aether-provider:3.3.9") + + // When updating, update above in plugins too + implementation("com.diffplug.spotless:spotless-plugin-gradle:5.13.0") + implementation("com.google.guava:guava:30.1-jre") + implementation("gradle.plugin.com.github.jengelman.gradle.plugins:shadow:7.0.0") + implementation("org.ow2.asm:asm:7.0-beta") + implementation("org.ow2.asm:asm-tree:7.0-beta") + implementation("org.apache.httpcomponents:httpclient:4.5.10") + implementation("org.gradle:test-retry-gradle-plugin:1.2.1") + // When updating, also update dependencyManagement/dependencyManagement.gradle.kts + implementation("net.bytebuddy:byte-buddy-gradle-plugin:1.11.2") + implementation("net.ltgt.gradle:gradle-errorprone-plugin:2.0.1") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1") + testImplementation("org.assertj:assertj-core:3.19.0") +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ByteBuddyPluginConfigurator.java b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ByteBuddyPluginConfigurator.java new file mode 100644 index 000000000..f186dd9f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ByteBuddyPluginConfigurator.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.bytebuddy; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import net.bytebuddy.build.gradle.ByteBuddySimpleTask; +import net.bytebuddy.build.gradle.Transformation; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.compile.AbstractCompile; + +/** + * Starting from version 1.10.15, ByteBuddy gradle plugin transformation task autoconfiguration is + * hardcoded to be applied to javaCompile task. This causes the dependencies to be resolved during + * an afterEvaluate that runs before any afterEvaluate specified in the build script, which in turn + * makes it impossible to add dependencies in afterEvaluate. Additionally the autoconfiguration will + * attempt to scan the entire project for tasks which depend on the compile task, to make each task + * that depends on compile also depend on the transformation task. This is an extremely inefficient + * operation in this project to the point of causing a stack overflow in some environments. + * + *

To avoid all the issues with autoconfiguration, this class manually configures the ByteBuddy + * transformation task. This also allows it to be applied to source languages other than Java. The + * transformation task is configured to run between the compile and the classes tasks, assuming no + * other task depends directly on the compile task, but instead other tasks depend on classes task. + * Contrary to how the ByteBuddy plugin worked in versions up to 1.10.14, this changes the compile + * task output directory, as starting from 1.10.15, the plugin does not allow the source and target + * directories to be the same. The transformation task then writes to the original output directory + * of the compile task. + */ +public class ByteBuddyPluginConfigurator { + private static final List LANGUAGES = Arrays.asList("java", "scala", "kotlin"); + + private final Project project; + private final SourceSet sourceSet; + private final String pluginClassName; + private final FileCollection inputClasspath; + + public ByteBuddyPluginConfigurator( + Project project, SourceSet sourceSet, String pluginClassName, FileCollection inputClasspath) { + this.project = project; + this.sourceSet = sourceSet; + this.pluginClassName = pluginClassName; + + // add build resources dir to classpath if it's present + File resourcesDir = sourceSet.getOutput().getResourcesDir(); + this.inputClasspath = + resourcesDir == null ? inputClasspath : inputClasspath.plus(project.files(resourcesDir)); + } + + public void configure() { + String taskName = getTaskName(); + + List> languageTasks = + LANGUAGES.stream() + .map( + language -> { + if (project.fileTree("src/" + sourceSet.getName() + "/" + language).isEmpty()) { + return null; + } + String compileTaskName = sourceSet.getCompileTaskName(language); + if (!project.getTasks().getNames().contains(compileTaskName)) { + return null; + } + TaskProvider compileTask = project.getTasks().named(compileTaskName); + + // We also process resources for SPI classes. + return createLanguageTask( + compileTask, taskName + language, sourceSet.getProcessResourcesTaskName()); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + TaskProvider byteBuddyTask = + project.getTasks().register(taskName, task -> task.dependsOn(languageTasks)); + + project + .getTasks() + .named(sourceSet.getClassesTaskName()) + .configure(task -> task.dependsOn(byteBuddyTask)); + } + + private TaskProvider createLanguageTask( + TaskProvider compileTaskProvider, String name, String processResourcesTaskName) { + return project + .getTasks() + .register( + name, + ByteBuddySimpleTask.class, + task -> { + task.setGroup("Byte Buddy"); + task.getOutputs().cacheIf(unused -> true); + + Task maybeCompileTask = compileTaskProvider.get(); + if (maybeCompileTask instanceof AbstractCompile) { + AbstractCompile compileTask = (AbstractCompile) maybeCompileTask; + File classesDirectory = compileTask.getDestinationDir(); + File rawClassesDirectory = + new File(classesDirectory.getParent(), classesDirectory.getName() + "raw") + .getAbsoluteFile(); + + task.dependsOn(compileTask); + compileTask.setDestinationDir(rawClassesDirectory); + + task.setSource(rawClassesDirectory); + task.setTarget(classesDirectory); + task.setClassPath(compileTask.getClasspath()); + + task.dependsOn(compileTask, processResourcesTaskName); + } + + task.getTransformations().add(createTransformation(inputClasspath, pluginClassName)); + }); + } + + private String getTaskName() { + if (SourceSet.MAIN_SOURCE_SET_NAME.equals(sourceSet.getName())) { + return "byteBuddy"; + } else { + return sourceSet.getName() + "ByteBuddy"; + } + } + + private static Transformation createTransformation( + FileCollection classPath, String pluginClassName) { + Transformation transformation = new ClasspathTransformation(classPath, pluginClassName); + transformation.setPlugin(ClasspathByteBuddyPlugin.class); + return transformation; + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathByteBuddyPlugin.java b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathByteBuddyPlugin.java new file mode 100644 index 000000000..e211a59e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathByteBuddyPlugin.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.bytebuddy; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.build.Plugin; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.dynamic.DynamicType; + +/** + * Starting from version 1.10.15, ByteBuddy gradle plugin transformations require that plugin + * classes are given as class instances instead of a class name string. To be able to still use a + * plugin implementation that is not a buildscript dependency, this reimplements the previous logic + * by taking a delegate class name and class path as arguments and loading the plugin class from the + * provided classloader when the plugin is instantiated. + */ +public class ClasspathByteBuddyPlugin implements Plugin { + private final Plugin delegate; + + /** + * classPath and className argument resolvers are explicitly added by {@link + * ClasspathTransformation}, sourceDirectory is automatically resolved as by default any {@link + * File} argument is resolved to source directory. + */ + public ClasspathByteBuddyPlugin( + Iterable classPath, File sourceDirectory, String className) { + this.delegate = pluginFromClassPath(classPath, sourceDirectory, className); + } + + @Override + public DynamicType.Builder apply( + DynamicType.Builder builder, + TypeDescription typeDescription, + ClassFileLocator classFileLocator) { + + return delegate.apply(builder, typeDescription, classFileLocator); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + @Override + public boolean matches(TypeDescription typeDefinitions) { + return delegate.matches(typeDefinitions); + } + + private static Plugin pluginFromClassPath( + Iterable classPath, File sourceDirectory, String className) { + try { + ClassLoader classLoader = classLoaderFromClassPath(classPath, sourceDirectory); + Class clazz = Class.forName(className, false, classLoader); + return (Plugin) clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException("Failed to create ByteBuddy plugin instance", e); + } + } + + private static ClassLoader classLoaderFromClassPath( + Iterable classPath, File sourceDirectory) { + List urls = new ArrayList<>(); + urls.add(fileAsUrl(sourceDirectory)); + + for (File file : classPath) { + urls.add(fileAsUrl(file)); + } + + return new URLClassLoader(urls.toArray(new URL[0]), ByteBuddy.class.getClassLoader()); + } + + private static URL fileAsUrl(File file) { + try { + return file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException("Cannot resolve " + file + " as URL", e); + } + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathTransformation.java b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathTransformation.java new file mode 100644 index 000000000..6d8cd0c3e --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathTransformation.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.bytebuddy; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import net.bytebuddy.build.Plugin.Factory.UsingReflection.ArgumentResolver; +import net.bytebuddy.build.gradle.Transformation; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; + +/** + * Special implementation of {@link Transformation} is required as classpath argument must be + * exposed to Gradle via {@link Classpath} annotation, which cannot be done if it is returned by + * {@link Transformation#getArguments()}. + */ +public class ClasspathTransformation extends Transformation { + private final Iterable classpath; + private final String pluginClassName; + + public ClasspathTransformation(Iterable classpath, String pluginClassName) { + this.classpath = classpath; + this.pluginClassName = pluginClassName; + } + + @Classpath + public Iterable getClasspath() { + return classpath; + } + + @Input + public String getPluginClassName() { + return pluginClassName; + } + + protected List makeArgumentResolvers() { + return Arrays.asList( + new ArgumentResolver.ForIndex(0, classpath), + new ArgumentResolver.ForIndex(2, pluginClassName)); + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/AcceptableVersions.java b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/AcceptableVersions.java new file mode 100644 index 000000000..f3e5dcf81 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/AcceptableVersions.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.muzzle; + +import java.util.Collection; +import java.util.Locale; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import org.eclipse.aether.version.Version; + +class AcceptableVersions implements Predicate { + private static final Pattern GIT_SHA_PATTERN = Pattern.compile("^.*-[0-9a-f]{7,}$"); + + private final Collection skipVersions; + + AcceptableVersions(Collection skipVersions) { + this.skipVersions = skipVersions; + } + + @Override + public boolean test(Version version) { + if (version == null) { + return false; + } + String versionString = version.toString().toLowerCase(Locale.ROOT); + if (skipVersions.contains(versionString)) { + return false; + } + + boolean draftVersion = + versionString.contains("rc") + || versionString.contains(".cr") + || versionString.contains("alpha") + || versionString.contains("beta") + || versionString.contains("-b") + || versionString.contains(".m") + || versionString.contains("-m") + || versionString.contains("-dev") + || versionString.contains("-ea") + || versionString.contains("-atlassian-") + || versionString.contains("public_draft") + || versionString.contains("snapshot") + || GIT_SHA_PATTERN.matcher(versionString).matches(); + + return !draftVersion; + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzleDirective.java b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzleDirective.java new file mode 100644 index 000000000..e959be933 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzleDirective.java @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.muzzle; + +import java.util.Collections; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; + +public abstract class MuzzleDirective { + + private static final Pattern NORMALIZE_NAME_SLUG = Pattern.compile("[^a-zA-Z0-9]+"); + + public MuzzleDirective() { + getName().convention(""); + getSkipVersions().convention(Collections.emptySet()); + getAdditionalDependencies().convention(Collections.emptyList()); + getExcludedDependencies().convention(Collections.emptyList()); + getAssertPass().convention(false); + getAssertInverse().convention(false); + getCoreJdk().convention(false); + } + + public abstract Property getName(); + + public abstract Property getGroup(); + + public abstract Property getModule(); + + public abstract Property getVersions(); + + public abstract SetProperty getSkipVersions(); + + public abstract ListProperty getAdditionalDependencies(); + + public abstract ListProperty getExcludedDependencies(); + + public abstract Property getAssertPass(); + + public abstract Property getAssertInverse(); + + public abstract Property getCoreJdk(); + + public void coreJdk() { + getCoreJdk().set(true); + } + + /** + * Adds extra dependencies to the current muzzle test. + * + * @param compileString An extra dependency in the gradle canonical form: + * '::'. + */ + public void extraDependency(String compileString) { + getAdditionalDependencies().add(compileString); + } + + /** + * Adds transitive dependencies to exclude from the current muzzle test. + * + * @param excludeString A dependency in the gradle canonical form: ':' + */ + public void excludeDependency(String excludeString) { + getExcludedDependencies().add(excludeString); + } + + public void skip(String... version) { + getSkipVersions().addAll(version); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (getCoreJdk().getOrElse(false)) { + if (getAssertPass().getOrElse(false)) { + sb.append("Pass"); + } else { + sb.append("Fail"); + } + sb.append("-core-jdk"); + } else { + if (getAssertPass().getOrElse(false)) { + sb.append("pass"); + } else { + sb.append("fail"); + } + sb.append(getGroup().get()) + .append(':') + .append(getModule().get()) + .append(':') + .append(getVersions().get()); + } + return sb.toString(); + } + + String getNameSlug() { + return NORMALIZE_NAME_SLUG.matcher(getName().get().trim()).replaceAll("-"); + } + + Set getNormalizedSkipVersions() { + return getSkipVersions().getOrElse(Collections.emptySet()).stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzleExtension.java b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzleExtension.java new file mode 100644 index 000000000..d7a056ac5 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzleExtension.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.muzzle; + +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; + +public abstract class MuzzleExtension { + + private final ObjectFactory objectFactory; + + @Inject + public MuzzleExtension(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + public abstract ListProperty getDirectives(); + + public void pass(Action action) { + MuzzleDirective pass = objectFactory.newInstance(MuzzleDirective.class); + action.execute(pass); + pass.getAssertPass().set(true); + getDirectives().add(pass); + } + + public void fail(Action action) { + MuzzleDirective fail = objectFactory.newInstance(MuzzleDirective.class); + action.execute(fail); + fail.getAssertPass().set(false); + getDirectives().add(fail); + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePlugin.java b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePlugin.java new file mode 100644 index 000000000..e28ca5067 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePlugin.java @@ -0,0 +1,533 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.muzzle; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.SecureClassLoader; +import java.util.ArrayList; +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 java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.impl.DefaultServiceLocator; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResolutionException; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transport.http.HttpTransporterFactory; +import org.eclipse.aether.version.Version; +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.repositories.ArtifactRepository; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; + +public class MuzzlePlugin implements Plugin { + /** Select a random set of versions to test */ + private static final int RANGE_COUNT_LIMIT = 10; + + private static volatile ClassLoader TOOLING_LOADER; + + @Override + public void apply(Project project) { + MuzzleExtension muzzleConfig = + project.getExtensions().create("muzzle", MuzzleExtension.class, project.getObjects()); + + // compileMuzzle compiles all projects required to run muzzle validation. + // Not adding group and description to keep this task from showing in `gradle tasks`. + TaskProvider compileMuzzle = + project + .getTasks() + .register( + "compileMuzzle", + task -> { + task.dependsOn(":javaagent-bootstrap:classes"); + task.dependsOn(":javaagent-tooling:classes"); + task.dependsOn(":javaagent-extension-api:classes"); + task.dependsOn(project.getTasks().named(JavaPlugin.CLASSES_TASK_NAME)); + }); + + TaskProvider muzzle = + project + .getTasks() + .register( + "muzzle", + task -> { + task.setGroup("Muzzle"); + task.setDescription("Run instrumentation muzzle on compile time dependencies"); + task.dependsOn(compileMuzzle); + }); + + project + .getTasks() + .register( + "printMuzzleReferences", + task -> { + task.setGroup("Muzzle"); + task.setDescription("Print references created by instrumentation muzzle"); + task.dependsOn(compileMuzzle); + task.doLast( + unused -> { + ClassLoader instrumentationCL = createInstrumentationClassloader(project); + try { + Method assertionMethod = + instrumentationCL + .loadClass( + "io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil") + .getMethod("printMuzzleReferences", ClassLoader.class); + assertionMethod.invoke(null, instrumentationCL); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + }); + + boolean hasRelevantTask = + project.getGradle().getStartParameter().getTaskNames().stream() + .anyMatch( + taskName -> { + // removing leading ':' if present + if (taskName.startsWith(":")) { + taskName = taskName.substring(1); + } + String projectPath = project.getPath().substring(1); + // Either the specific muzzle task in this project or the top level, full-project + // muzzle task. + return taskName.equals(projectPath + ":muzzle") || taskName.equals("muzzle"); + }); + if (!hasRelevantTask) { + // Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly + // run. + return; + } + + RepositorySystem system = newRepositorySystem(); + RepositorySystemSession session = newRepositorySystemSession(system, project); + + project.afterEvaluate( + unused -> { + // use runAfter to set up task finalizers in version order + TaskProvider runAfter = muzzle; + + for (MuzzleDirective muzzleDirective : muzzleConfig.getDirectives().get()) { + project.getLogger().info("configured " + muzzleDirective); + + if (muzzleDirective.getCoreJdk().get()) { + runAfter = addMuzzleTask(muzzleDirective, null, project, runAfter); + } else { + for (Artifact singleVersion : + muzzleDirectiveToArtifacts(project, muzzleDirective, system, session)) { + runAfter = addMuzzleTask(muzzleDirective, singleVersion, project, runAfter); + } + if (muzzleDirective.getAssertInverse().get()) { + for (MuzzleDirective inverseDirective : + inverseOf(project, muzzleDirective, system, session)) { + for (Artifact singleVersion : + muzzleDirectiveToArtifacts(project, inverseDirective, system, session)) { + runAfter = addMuzzleTask(inverseDirective, singleVersion, project, runAfter); + } + } + } + } + } + }); + } + + /** Create a classloader with core agent classes and project instrumentation on the classpath. */ + private static ClassLoader createInstrumentationClassloader(Project project) { + project.getLogger().info("Creating instrumentation classpath for: " + project.getName()); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + FileCollection runtimeClasspath = + sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath(); + + return classpathLoader(runtimeClasspath, getOrCreateToolingLoader(project), project); + } + + private static synchronized ClassLoader getOrCreateToolingLoader(Project project) { + if (TOOLING_LOADER == null) { + project.getLogger().info("creating classpath for auto-tooling"); + FileCollection toolingRuntime = project.getConfigurations().getByName("toolingRuntime"); + TOOLING_LOADER = + classpathLoader(toolingRuntime, ClassLoader.getPlatformClassLoader(), project); + } + return TOOLING_LOADER; + } + + private static ClassLoader classpathLoader( + FileCollection classpath, ClassLoader parent, Project project) { + URL[] urls = + StreamSupport.stream(classpath.spliterator(), false) + .map( + file -> { + project.getLogger().info("--" + file); + try { + return file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + }) + .toArray(URL[]::new); + return new URLClassLoader(urls, parent); + } + + /** + * Configure a muzzle task to pass or fail a given version. + * + * @param versionArtifact version to assert against. + * @param instrumentationProject instrumentation being asserted against. + * @param runAfter Task which runs before the new muzzle task. + * @return The created muzzle task. + */ + private static TaskProvider addMuzzleTask( + MuzzleDirective muzzleDirective, + Artifact versionArtifact, + Project instrumentationProject, + TaskProvider runAfter) { + final String taskName; + if (muzzleDirective.getCoreJdk().get()) { + taskName = "muzzle-Assert" + muzzleDirective; + } else { + StringBuilder sb = new StringBuilder("muzzle-Assert"); + if (muzzleDirective.getAssertPass().isPresent()) { + sb.append("Pass"); + } else { + sb.append("Fail"); + } + sb.append('-') + .append(versionArtifact.getGroupId()) + .append('-') + .append(versionArtifact.getArtifactId()) + .append('-') + .append(versionArtifact.getVersion()); + if (!muzzleDirective.getName().get().isEmpty()) { + sb.append(muzzleDirective.getNameSlug()); + } + taskName = sb.toString(); + } + Configuration config = instrumentationProject.getConfigurations().create(taskName); + + if (!muzzleDirective.getCoreJdk().get()) { + ModuleDependency dep = + (ModuleDependency) + instrumentationProject + .getDependencies() + .create( + versionArtifact.getGroupId() + + ':' + + versionArtifact.getArtifactId() + + ':' + + versionArtifact.getVersion()); + dep.setTransitive(true); + // The following optional transitive dependencies are brought in by some legacy module such as + // log4j 1.x but are no + // longer bundled with the JVM and have to be excluded for the muzzle tests to be able to run. + exclude(dep, "com.sun.jdmk", "jmxtools"); + exclude(dep, "com.sun.jmx", "jmxri"); + for (String excluded : muzzleDirective.getExcludedDependencies().get()) { + String[] parts = excluded.split(":"); + exclude(dep, parts[0], parts[1]); + } + + config.getDependencies().add(dep); + } + for (String additionalDependency : muzzleDirective.getAdditionalDependencies().get()) { + if (countColons(additionalDependency) < 2) { + // Dependency definition without version, use the artifact's version. + additionalDependency = additionalDependency + ':' + versionArtifact.getVersion(); + } + ModuleDependency dep = + (ModuleDependency) instrumentationProject.getDependencies().create(additionalDependency); + dep.setTransitive(true); + config.getDependencies().add(dep); + } + + TaskProvider muzzleTask = + instrumentationProject + .getTasks() + .register( + taskName, + task -> { + task.dependsOn( + instrumentationProject.getConfigurations().named("runtimeClasspath")); + task.doLast( + unused -> { + ClassLoader instrumentationCL = + createInstrumentationClassloader(instrumentationProject); + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + ClassLoader bogusLoader = + new SecureClassLoader() { + @Override + public String toString() { + return "bogus"; + } + }; + Thread.currentThread().setContextClassLoader(bogusLoader); + ClassLoader userCL = + createClassLoaderForTask(instrumentationProject, taskName); + try { + // find all instrumenters, get muzzle, and assert + Method assertionMethod = + instrumentationCL + .loadClass( + "io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil") + .getMethod( + "assertInstrumentationMuzzled", + ClassLoader.class, + ClassLoader.class, + boolean.class); + assertionMethod.invoke( + null, + instrumentationCL, + userCL, + muzzleDirective.getAssertPass().get()); + } catch (Exception e) { + throw new IllegalStateException(e); + } finally { + Thread.currentThread().setContextClassLoader(ccl); + } + + for (Thread thread : Thread.getAllStackTraces().keySet()) { + if (thread.getContextClassLoader() == bogusLoader + || thread.getContextClassLoader() == instrumentationCL + || thread.getContextClassLoader() == userCL) { + throw new GradleException( + "Task " + + taskName + + " has spawned a thread: " + + thread + + " with classloader " + + thread.getContextClassLoader() + + ". This will prevent GC of dynamic muzzle classes. Aborting muzzle run."); + } + } + }); + }); + runAfter.configure(task -> task.finalizedBy(muzzleTask)); + return muzzleTask; + } + + /** Create a classloader with dependencies for a single muzzle task. */ + private static ClassLoader createClassLoaderForTask(Project project, String muzzleTaskName) { + ConfigurableFileCollection userUrls = project.getObjects().fileCollection(); + project.getLogger().info("Creating task classpath"); + userUrls.from( + project + .getConfigurations() + .getByName(muzzleTaskName) + .getResolvedConfiguration() + .getFiles()); + return classpathLoader( + userUrls.plus(project.getConfigurations().getByName("bootstrapRuntime")), + ClassLoader.getPlatformClassLoader(), + project); + } + + /** Convert a muzzle directive to a list of artifacts */ + private static Set muzzleDirectiveToArtifacts( + Project instrumentationProject, + MuzzleDirective muzzleDirective, + RepositorySystem system, + RepositorySystemSession session) { + Artifact directiveArtifact = + new DefaultArtifact( + muzzleDirective.getGroup().get(), + muzzleDirective.getModule().get(), + "jar", + muzzleDirective.getVersions().get()); + + VersionRangeRequest rangeRequest = new VersionRangeRequest(); + rangeRequest.setRepositories(getProjectRepositories(instrumentationProject)); + rangeRequest.setArtifact(directiveArtifact); + final VersionRangeResult rangeResult; + try { + rangeResult = system.resolveVersionRange(session, rangeRequest); + } catch (VersionRangeResolutionException e) { + throw new IllegalStateException(e); + } + + Set allVersionArtifacts = + filterVersions(rangeResult, muzzleDirective.getNormalizedSkipVersions()).stream() + .map( + version -> + new DefaultArtifact( + muzzleDirective.getGroup().get(), + muzzleDirective.getModule().get(), + "jar", + version)) + .collect(Collectors.toSet()); + + if (allVersionArtifacts.isEmpty()) { + throw new GradleException("No muzzle artifacts found for " + muzzleDirective); + } + + return allVersionArtifacts; + } + + private static List getProjectRepositories(Project project) { + List repositories = new ArrayList<>(); + // Manually add mavenCentral until https://github.com/gradle/gradle/issues/17295 + // Adding mavenLocal is much more complicated but hopefully isn't required for normal usage of + // Muzzle. + repositories.add( + new RemoteRepository.Builder( + "MavenCentral", "default", "https://repo.maven.apache.org/maven2/") + .build()); + for (ArtifactRepository repository : project.getRepositories()) { + if (repository instanceof MavenArtifactRepository) { + repositories.add( + new RemoteRepository.Builder( + repository.getName(), + "default", + ((MavenArtifactRepository) repository).getUrl().toString()) + .build()); + } + } + return repositories; + } + + /** Create a list of muzzle directives which assert the opposite of the given MuzzleDirective. */ + private static Set inverseOf( + Project instrumentationProject, + MuzzleDirective muzzleDirective, + RepositorySystem system, + RepositorySystemSession session) { + Set inverseDirectives = new HashSet<>(); + + Artifact allVersionsArtifact = + new DefaultArtifact( + muzzleDirective.getGroup().get(), muzzleDirective.getModule().get(), "jar", "[,)"); + Artifact directiveArtifact = + new DefaultArtifact( + muzzleDirective.getGroup().get(), + muzzleDirective.getModule().get(), + "jar", + muzzleDirective.getVersions().get()); + + List repos = getProjectRepositories(instrumentationProject); + VersionRangeRequest allRangeRequest = new VersionRangeRequest(); + allRangeRequest.setRepositories(repos); + allRangeRequest.setArtifact(allVersionsArtifact); + final VersionRangeResult allRangeResult; + try { + allRangeResult = system.resolveVersionRange(session, allRangeRequest); + } catch (VersionRangeResolutionException e) { + throw new IllegalStateException(e); + } + + VersionRangeRequest rangeRequest = new VersionRangeRequest(); + rangeRequest.setRepositories(repos); + rangeRequest.setArtifact(directiveArtifact); + final VersionRangeResult rangeResult; + try { + rangeResult = system.resolveVersionRange(session, rangeRequest); + } catch (VersionRangeResolutionException e) { + throw new IllegalStateException(e); + } + + allRangeResult.getVersions().removeAll(rangeResult.getVersions()); + + for (String version : + filterVersions(allRangeResult, muzzleDirective.getNormalizedSkipVersions())) { + MuzzleDirective inverseDirective = + instrumentationProject.getObjects().newInstance(MuzzleDirective.class); + inverseDirective.getGroup().set(muzzleDirective.getGroup()); + inverseDirective.getModule().set(muzzleDirective.getModule()); + inverseDirective.getVersions().set(version); + inverseDirective.getAssertPass().set(!muzzleDirective.getAssertPass().get()); + inverseDirective.getExcludedDependencies().set(muzzleDirective.getExcludedDependencies()); + inverseDirectives.add(inverseDirective); + } + + return inverseDirectives; + } + + private static Set filterVersions(VersionRangeResult range, Set skipVersions) { + Set result = new HashSet<>(); + + AcceptableVersions predicate = new AcceptableVersions(skipVersions); + if (predicate.test(range.getLowestVersion())) { + result.add(range.getLowestVersion().toString()); + } + if (predicate.test(range.getHighestVersion())) { + result.add(range.getHighestVersion().toString()); + } + + List copy = new ArrayList<>(range.getVersions()); + Collections.shuffle(copy); + for (Version version : copy) { + if (result.size() >= RANGE_COUNT_LIMIT) { + break; + } + if (predicate.test(version)) { + result.add(version.toString()); + } + } + + return result; + } + + /** Create muzzle's repository system */ + private static RepositorySystem newRepositorySystem() { + DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator(); + locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); + locator.addService(TransporterFactory.class, HttpTransporterFactory.class); + + return locator.getService(RepositorySystem.class); + } + + /** Create muzzle's repository system session */ + private static RepositorySystemSession newRepositorySystemSession( + RepositorySystem system, Project project) { + DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); + File muzzleRepo = project.file("build/muzzleRepo"); + LocalRepository localRepo = new LocalRepository(muzzleRepo); + session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo)); + return session; + } + + private static int countColons(String s) { + int count = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == ':') { + count++; + } + } + return count; + } + + private static void exclude(ModuleDependency dependency, String group, String module) { + Map exclusions = new HashMap<>(); + exclusions.put("group", group); + exclusions.put("module", module); + dependency.exclude(exclusions); + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt new file mode 100644 index 000000000..195da08a4 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle + +import org.gradle.api.JavaVersion +import org.gradle.api.provider.Property + +abstract class OtelJavaExtension { + abstract val minJavaVersionSupported: Property + + abstract val maxJavaVersionForTests: Property + + init { + minJavaVersionSupported.convention(JavaVersion.VERSION_1_8) + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.instrumentation-conventions.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.instrumentation-conventions.gradle.kts new file mode 100644 index 000000000..d28fdb51b --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.instrumentation-conventions.gradle.kts @@ -0,0 +1,95 @@ +/** Common setup for manual instrumentation of libraries and javaagent instrumentation. */ + +/** + * We define three dependency configurations to use when adding dependencies to libraries being + * instrumented. + * + * - library: A dependency on the instrumented library. Results in the dependency being added to + * compileOnly and testImplementation. If the build is run with -PtestLatestDeps=true, the + * version when added to testImplementation will be overridden by `+`, the latest version + * possible. For simple libraries without different behavior between versions, it is possible + * to have a single dependency on library only. + * + * - testLibrary: A dependency on a library for testing. This will usually be used to either + * a) use a different version of the library for compilation and testing and b) to add a helper + * that is only required for tests (e.g., library-testing artifact). The dependency will be + * added to testImplementation and will have a version of `+` when testing latest deps as + * described above. + * + * - latestDepTestLibrary: A dependency on a library for testing when testing of latest dependency + * version is enabled. This dependency will be added as-is to testImplementation, but only if + * -PtestLatestDeps=true. The version will not be modified but it will be given highest + * precedence. Use this to restrict the latest version dependency from the default `+`, for + * example to restrict to just a major version by specifying `2.+`. + */ + +val testLatestDeps = gradle.startParameter.projectProperties["testLatestDeps"] == "true" +extra["testLatestDeps"] = testLatestDeps + +configurations { + val library by creating { + isCanBeResolved = false + isCanBeConsumed = false + } + val testLibrary by creating { + isCanBeResolved = false + isCanBeConsumed = false + } + val latestDepTestLibrary by creating { + isCanBeResolved = false + isCanBeConsumed = false + } + + val testImplementation by getting + + listOf(library, testLibrary).forEach { configuration -> + // We use whenObjectAdded and copy into the real configurations instead of extension to allow + // mutating the version for latest dep tests. + configuration.dependencies.whenObjectAdded { + val dep = copy() + if (testLatestDeps) { + (dep as ExternalDependency).version { + require("+") + } + } + testImplementation.dependencies.add(dep) + } + } + if (testLatestDeps) { + latestDepTestLibrary.dependencies.whenObjectAdded { + val dep = copy() + val declaredVersion = dep.version + if (declaredVersion != null) { + (dep as ExternalDependency).version { + strictly(declaredVersion) + } + } + testImplementation.dependencies.add(dep) + } + } + named("compileOnly") { + extendsFrom(library) + } +} + +if (testLatestDeps) { + afterEvaluate { + if (tasks.names.contains("latestDepTest")) { + val latestDepTest by tasks.existing + tasks.named("test").configure { + dependsOn(latestDepTest) + } + } + } +} + +when (projectDir.name) { + "javaagent", "library", "testing" -> { + // We don't use this group anywhere in our config, but we need to make sure it is unique per + // instrumentation so Gradle doesn't merge projects with same name due to a bug in Gradle. + // https://github.com/gradle/gradle/issues/847 + // In otel.publish-conventions, we set the maven group, which is what matters, to the correct + // value. + group = "io.opentelemetry.${projectDir.parentFile.name}" + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.jacoco-conventions.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.jacoco-conventions.gradle.kts new file mode 100644 index 000000000..ffbaeed1c --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.jacoco-conventions.gradle.kts @@ -0,0 +1,19 @@ +plugins { + jacoco +} + +jacoco { + toolVersion = "0.8.6" +} + +tasks { + named("jacocoTestReport") { + dependsOn("test") + + reports { + xml.isEnabled = true + csv.isEnabled = false + html.destination = file("${buildDir}/reports/jacoco/") + } + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts new file mode 100644 index 000000000..19ec7ea1f --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -0,0 +1,253 @@ +import io.opentelemetry.instrumentation.gradle.OtelJavaExtension +import java.time.Duration +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + +plugins { + `java-library` + groovy + checkstyle + codenarc + + id("org.gradle.test-retry") + id("net.ltgt.errorprone") + + id("otel.spotless-conventions") +} + +val otelJava = extensions.create("otelJava") + +afterEvaluate { + if (findProperty("mavenGroupId") == "io.opentelemetry.javaagent.instrumentation") { + base.archivesBaseName = "opentelemetry-javaagent-${base.archivesBaseName}" + } else { + base.archivesBaseName = "opentelemetry-${base.archivesBaseName}" + } +} + +// Version to use to compile code and run tests. +val DEFAULT_JAVA_VERSION = JavaVersion.VERSION_11 + +java { + toolchain { + languageVersion.set(otelJava.minJavaVersionSupported.map { JavaLanguageVersion.of(Math.max(it.majorVersion.toInt(), DEFAULT_JAVA_VERSION.majorVersion.toInt())) }) + } + + // See https://docs.gradle.org/current/userguide/upgrading_version_5.html, Automatic target JVM version + disableAutoTargetJvm() + withJavadocJar() + withSourcesJar() +} + +tasks.withType().configureEach { + with(options) { + release.set(otelJava.minJavaVersionSupported.map { it.majorVersion.toInt() }) + compilerArgs.add("-Werror") + } +} + +// Groovy and Scala compilers don't actually understand --release option +afterEvaluate { + tasks.withType().configureEach { + sourceCompatibility = otelJava.minJavaVersionSupported.get().majorVersion + targetCompatibility = otelJava.minJavaVersionSupported.get().majorVersion + } + tasks.withType().configureEach { + sourceCompatibility = otelJava.minJavaVersionSupported.get().majorVersion + targetCompatibility = otelJava.minJavaVersionSupported.get().majorVersion + } +} + +evaluationDependsOn(":dependencyManagement") +val dependencyManagementConf = configurations.create("dependencyManagement") { + isCanBeConsumed = false + isCanBeResolved = false + isVisible = false +} +afterEvaluate { + configurations.configureEach { + if (isCanBeResolved && !isCanBeConsumed) { + extendsFrom(dependencyManagementConf) + } + } +} + +// Force 4.0, or 4.1 to the highest version of that branch. Since 4.0 and 4.1 often have +// compatibility issues we can't just force to the highest version using normal BOM dependencies. +abstract class NettyAlignmentRule : ComponentMetadataRule { + override fun execute(ctx: ComponentMetadataContext) { + with(ctx.details) { + if (id.group == "io.netty" && id.name != "netty") { + if (id.version.startsWith("4.1.")) { + belongsTo("io.netty:netty-bom:4.1.65.Final", false) + } else if (id.version.startsWith("4.0.")) { + belongsTo("io.netty:netty-bom:4.0.56.Final", false) + } + } + } + } +} + +dependencies { + add(dependencyManagementConf.name, platform(project(":dependencyManagement"))) + + components.all() + + compileOnly("org.checkerframework:checker-qual") + + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine") + + testImplementation("org.objenesis:objenesis") + testImplementation("org.spockframework:spock-core") + testImplementation("ch.qos.logback:logback-classic") + testImplementation("org.slf4j:log4j-over-slf4j") + testImplementation("org.slf4j:jcl-over-slf4j") + testImplementation("org.slf4j:jul-to-slf4j") + testImplementation("info.solidsoft.spock:spock-global-unroll") + testImplementation("com.github.stefanbirkner:system-rules") +} + +tasks { + named("jar") { + // By default Gradle Jar task can put multiple files with the same name + // into a Jar. This may lead to confusion. For example if auto-service + // annotation processing creates files with same name in `scala` and + // `java` directory this would result in Jar having two files with the + // same name in it. Which in turn would result in only one of those + // files being actually considered when that Jar is used leading to very + // confusing failures. Instead we should 'fail early' and avoid building such Jars. + duplicatesStrategy = DuplicatesStrategy.FAIL + + manifest { + attributes( + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + "Implementation-Vendor" to "OpenTelemetry", + "Implementation-URL" to "https://github.com/open-telemetry/opentelemetry-java-instrumentation" + ) + } + } + + named("javadoc") { + with(options as StandardJavadocDocletOptions) { + source = "8" + encoding = "UTF-8" + docEncoding = "UTF-8" + charSet = "UTF-8" + breakIterator(true) + + links("https://docs.oracle.com/javase/8/docs/api/") + + addStringOption("Xdoclint:none", "-quiet") + // non-standard option to fail on warnings, see https://bugs.openjdk.java.net/browse/JDK-8200363 + addStringOption("Xwerror", "-quiet") + } + } + + withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + } +} + +normalization { + runtimeClasspath { + metaInf { + ignoreAttribute("Implementation-Version") + } + } +} + +fun isJavaVersionAllowed(version: JavaVersion): Boolean { + if (otelJava.minJavaVersionSupported.get().compareTo(version) > 0) { + return false + } + if (otelJava.maxJavaVersionForTests.isPresent() && otelJava.maxJavaVersionForTests.get().compareTo(version) < 0) { + return false + } + return true +} + +val testJavaVersion = gradle.startParameter.projectProperties.get("testJavaVersion")?.let(JavaVersion::toVersion) +val resourceClassesCsv = listOf("Host", "Os", "Process", "ProcessRuntime").map { "io.opentelemetry.sdk.extension.resources.${it}ResourceProvider" }.joinToString(",") +tasks.withType().configureEach { + useJUnitPlatform() + + // There's no real harm in setting this for all tests even if any happen to not be using context + // propagation. + jvmArgs("-Dio.opentelemetry.context.enableStrictContext=${rootProject.findProperty("enableStrictContext") ?: false}") + // TODO(anuraaga): Have agent map unshaded to shaded. + jvmArgs("-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=${rootProject.findProperty("enableStrictContext") ?: false}") + + // Disable default resource providers since they cause lots of output we don't need. + jvmArgs("-Dotel.java.disabled.resource.providers=${resourceClassesCsv}") + + val trustStore = project(":testing-common").file("src/misc/testing-keystore.p12") + inputs.file(trustStore) + // Work around payara not working when this is set for some reason. + if (project.name != "jaxrs-2.0-payara-testing") { + jvmArgs("-Djavax.net.ssl.trustStore=${trustStore.absolutePath}") + jvmArgs("-Djavax.net.ssl.trustStorePassword=testing") + } + + // All tests must complete within 15 minutes. + // This value is quite big because with lower values (3 mins) we were experiencing large number of false positives + timeout.set(Duration.ofMinutes(15)) + + retry { + // You can see tests that were retried by this mechanism in the collected test reports and build scans. + maxRetries.set(if (System.getenv("CI") != null) 5 else 0) + } + + reports { + junitXml.isOutputPerTestCase = true + } + + testLogging { + exceptionFormat = TestExceptionFormat.FULL + } +} + +afterEvaluate { + tasks.withType().configureEach { + if (testJavaVersion != null) { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(testJavaVersion.majorVersion)) + }) + isEnabled = isJavaVersionAllowed(testJavaVersion) + } else { + // We default to testing with Java 11 for most tests, but some tests don't support it, where we change + // the default test task's version so commands like `./gradlew check` can test all projects regardless + // of Java version. + if (!isJavaVersionAllowed(DEFAULT_JAVA_VERSION) && otelJava.maxJavaVersionForTests.isPresent) { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(otelJava.maxJavaVersionForTests.get().majorVersion)) + }) + } + } + + if (plugins.hasPlugin("org.unbroken-dome.test-sets") && configurations.findByName("latestDepTestRuntime") != null) { + doFirst { + val testArtifacts = configurations.testRuntimeClasspath.get().resolvedConfiguration.resolvedArtifacts + val latestTestArtifacts = configurations.getByName("latestDepTestRuntimeClasspath").resolvedConfiguration.resolvedArtifacts + if (testArtifacts == latestTestArtifacts) { + throw IllegalStateException("latestDepTest dependencies are identical to test") + } + } + } + } +} + +codenarc { + configFile = rootProject.file("gradle/enforcement/codenarc.groovy") + toolVersion = "2.0.0" +} + +checkstyle { + configFile = rootProject.file("gradle/enforcement/checkstyle.xml") + // this version should match the version of google_checks.xml used as basis for above configuration + toolVersion = "8.37" + maxWarnings = 0 +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.library-instrumentation.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.library-instrumentation.gradle.kts new file mode 100644 index 000000000..697c4cb20 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.library-instrumentation.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("otel.java-conventions") + id("otel.jacoco-conventions") + id("otel.publish-conventions") + id("otel.instrumentation-conventions") +} + +extra["mavenGroupId"] = "io.opentelemetry.instrumentation" + +base.archivesBaseName = projectDir.parentFile.name + +dependencies { + api(project(":instrumentation-api")) + + api("run.mone:opentelemetry-api") + + testImplementation(project(":testing-common")) +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.publish-conventions.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.publish-conventions.gradle.kts new file mode 100644 index 000000000..7f59b91d0 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.publish-conventions.gradle.kts @@ -0,0 +1,108 @@ +import com.github.jengelman.gradle.plugins.shadow.ShadowExtension + +plugins { + `maven-publish` + signing +} + +publishing { + publications { + register("maven") { + if (tasks.names.contains("shadowJar") && findProperty("noShadowPublish") != true) { + the().component(this) + // These two are here just to satisfy Maven Central + artifact(tasks["sourcesJar"]) + artifact(tasks["javadocJar"]) + } else { + plugins.withId("java-platform") { + from(components["javaPlatform"]) + } + plugins.withId("java-library") { + from(components["java"]) + } + } + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + if (findProperty("otel.stable") != "true") { + val versionParts = version.split('-').toMutableList() + versionParts[0] += "-alpha" + version = versionParts.joinToString("-") + } + + afterEvaluate { + val mavenGroupId: String? by project + if (mavenGroupId != null) { + groupId = mavenGroupId + } + artifactId = artifactPrefix(project, base.archivesBaseName) + base.archivesBaseName + + if (!groupId.startsWith("io.opentelemetry.")) { + throw GradleException("groupId is not set for this project or its parent ${project.parent}") + } + + pom.description.set(project.description + ?: "Instrumentation of Java libraries using OpenTelemetry.") + } + + pom { + name.set("OpenTelemetry Instrumentation for Java") + url.set("https://github.com/open-telemetry/opentelemetry-java-instrumentation") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + + developers { + developer { + id.set("opentelemetry") + name.set("OpenTelemetry") + url.set("https://github.com/open-telemetry/opentelemetry-java-instrumentation/discussions") + } + } + + scm { + connection.set("scm:git:git@github.com:open-telemetry/opentelemetry-java-instrumentation.git") + developerConnection.set("scm:git:git@github.com:open-telemetry/opentelemetry-java-instrumentation.git") + url.set("git@github.com:open-telemetry/opentelemetry-java-instrumentation.git") + } + } + } + } +} + +fun artifactPrefix(p: Project, archivesBaseName: String): String { + if (archivesBaseName.startsWith("opentelemetry")) { + return "" + } + if (p.name.startsWith("opentelemetry")) { + return "" + } + if (p.name.startsWith("javaagent")) { + return "opentelemetry-" + } + if (p.group == "io.opentelemetry.javaagent.instrumentation") { + return "opentelemetry-javaagent-" + } + return "opentelemetry-" +} + +//rootProject.tasks.named("release").configure { +// finalizedBy(tasks["publishToSonatype"]) +//} + +// Stub out entire signing block off of CI since Gradle provides no way of lazy configuration of +// signing tasks. +if (System.getenv("CI") != null) { + signing { + useInMemoryPgpKeys(System.getenv("GPG_PRIVATE_KEY"), System.getenv("GPG_PASSWORD")) + sign(publishing.publications["maven"]) + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.scala-conventions.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.scala-conventions.gradle.kts new file mode 100644 index 000000000..ecf36f896 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.scala-conventions.gradle.kts @@ -0,0 +1,19 @@ +// Enable testing scala code in groovy spock tests. + +plugins { + scala +} + +dependencies { + testImplementation("org.scala-lang:scala-library") +} + +tasks { + named("compileTestGroovy") { + sourceSets.test { + withConvention(ScalaSourceSet::class) { + classpath = classpath.plus(files(scala.classesDirectory)) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.shadow-conventions.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.shadow-conventions.gradle.kts new file mode 100644 index 000000000..06b9673a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.shadow-conventions.gradle.kts @@ -0,0 +1,34 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id("com.github.johnrengelman.shadow") +} + +tasks.withType().configureEach { + mergeServiceFiles() + + exclude("**/module-info.class") + exclude("META-INF/jandex.idx") // from caffeine + + // Prevents conflict with other SLF4J instances. Important for premain. + relocate("org.slf4j", "io.opentelemetry.javaagent.slf4j") + // rewrite dependencies calling Logger.getLogger + relocate("java.util.logging.Logger", "io.opentelemetry.javaagent.bootstrap.PatchLogger") + + // prevents conflict with library instrumentation + relocate("io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.shaded.instrumentation") + + // relocate(OpenTelemetry API) + relocate("io.opentelemetry.api", "io.opentelemetry.javaagent.shaded.io.opentelemetry.api") + relocate("io.opentelemetry.semconv", "io.opentelemetry.javaagent.shaded.io.opentelemetry.semconv") + relocate("io.opentelemetry.context", "io.opentelemetry.javaagent.shaded.io.opentelemetry.context") + + // relocate(the OpenTelemetry extensions that are used by instrumentation modules) + // these extensions live in the AgentClassLoader, and are injected into the user"s class loader + // by the instrumentation modules that use them + relocate("io.opentelemetry.extension.aws", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.aws") + relocate("io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin") + + // this is for instrumentation on opentelemetry-api itself + relocate("application.io.opentelemetry", "io.opentelemetry") +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.spotless-conventions.gradle.kts b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.spotless-conventions.gradle.kts new file mode 100644 index 000000000..3a6f18671 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/main/kotlin/otel.spotless-conventions.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("com.diffplug.spotless") +} + +spotless { + java { + googleJavaFormat("1.10.0") + licenseHeaderFile(rootProject.file("gradle/enforcement/spotless.license.java"), "(package|import|public|// Includes work from:)") + target("src/**/*.java") + } + groovy { + licenseHeaderFile(rootProject.file("gradle/enforcement/spotless.license.java"), "(package|import|class)") + } + scala { + scalafmt() + licenseHeaderFile(rootProject.file("gradle/enforcement/spotless.license.java"), "(package|import|public)") + target("src/**/*.scala") + } + kotlin { + // ktfmt() // only supports 4 space indentation + ktlint().userData(mapOf("indent_size" to "2", "continuation_indent_size" to "2")) + licenseHeaderFile(rootProject.file("gradle/enforcement/spotless.license.java"), "(package|import|public)") + } + format("misc") { + // not using "**/..." to help keep spotless fast + target(".gitignore", "*.md", "src/**/*.md", "*.sh", "src/**/*.properties") + indentWithSpaces() + trimTrailingWhitespace() + endWithNewline() + } +} diff --git a/opentelemetry-java-instrumentation/buildSrc/src/test/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePluginTest.java b/opentelemetry-java-instrumentation/buildSrc/src/test/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePluginTest.java new file mode 100644 index 000000000..2f2a6e4c9 --- /dev/null +++ b/opentelemetry-java-instrumentation/buildSrc/src/test/java/io/opentelemetry/instrumentation/gradle/muzzle/MuzzlePluginTest.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.gradle.muzzle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import org.eclipse.aether.version.Version; +import org.junit.jupiter.api.Test; + +class MuzzlePluginTest { + + @Test + void rangeRequest() { + AcceptableVersions predicate = new AcceptableVersions(Collections.emptyList()); + + assertThat(predicate.test(new TestVersion("10.1.0-rc2+19-8e20bb26"))).isFalse(); + assertThat(predicate.test(new TestVersion("2.4.5.BUILD-SNAPSHOT"))).isFalse(); + } + + static class TestVersion implements Version { + + private final String version; + + TestVersion(String version) { + this.version = version; + } + + @Override + public int compareTo(Version o) { + return toString().compareTo(o.toString()); + } + + @Override + public String toString() { + return version; + } + } +} diff --git a/opentelemetry-java-instrumentation/code-of-conduct.md b/opentelemetry-java-instrumentation/code-of-conduct.md new file mode 100644 index 000000000..0099566bf --- /dev/null +++ b/opentelemetry-java-instrumentation/code-of-conduct.md @@ -0,0 +1,3 @@ +# OpenTelemetry Community Code of Conduct + +Please refer to our [OpenTelemetry Community Code of Conduct](https://github.com/open-telemetry/community/blob/main/code-of-conduct.md) diff --git a/opentelemetry-java-instrumentation/dependencyManagement/dependencyManagement.gradle.kts b/opentelemetry-java-instrumentation/dependencyManagement/dependencyManagement.gradle.kts new file mode 100644 index 000000000..28d8f99e1 --- /dev/null +++ b/opentelemetry-java-instrumentation/dependencyManagement/dependencyManagement.gradle.kts @@ -0,0 +1,152 @@ +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask + +plugins { + `java-platform` + + id("com.github.ben-manes.versions") +} + +data class DependencySet(val group: String, val version: String, val modules: List) + +val dependencyVersions = hashMapOf() +rootProject.extra["versions"] = dependencyVersions + +val otelVersion = "0.5.0-opensource-SNAPSHOT" +rootProject.extra["otelVersion"] = otelVersion + +// Need both BOM and -all +val groovyVersion = "2.5.11" + +// We don't force libraries we instrument to new versions since we compile and test against specific +// old baseline versions +// but we do try to force those libraries' transitive dependencies to new versions where possible +// so that we don't end up with explosion of dependency versions in Intellij, which causes +// Intellij to spend lots of time indexing all of those different dependency versions, +// and makes debugging painful because Intellij has no idea which dependency version's source +// to use when stepping through code. +// +// Sometimes libraries we instrument do require a specific version of a transitive dependency +// and that can be applied in the specific instrumentation gradle file, e.g. +// configurations.testRuntimeClasspath.resolutionStrategy.force "com.google.guava:guava:19.0" + +val DEPENDENCY_BOMS = listOf( + "com.fasterxml.jackson:jackson-bom:2.12.3", + "com.google.guava:guava-bom:30.1.1-jre", + "org.codehaus.groovy:groovy-bom:${groovyVersion}", + "run.mone:opentelemetry-bom:${otelVersion}", + "run.mone:opentelemetry-bom-alpha:${otelVersion}", + "org.junit:junit-bom:5.7.2" +) + +val DEPENDENCY_SETS = listOf( + DependencySet( + "com.google.auto.value", + "1.8.1", + listOf("auto-value", "auto-value-annotations") + ), + DependencySet( + "com.google.errorprone", + "2.7.1", + listOf("error_prone_annotations", "error_prone_core") + ), + DependencySet( + "io.prometheus", + "0.11.0", + listOf("simpleclient", "simpleclient_common", "simpleclient_httpserver") + ), + DependencySet( + "net.bytebuddy", + // When updating, also update buildSrc/build.gradle.kts + "1.11.2", + listOf("byte-buddy", "byte-buddy-agent") + ), + DependencySet( + "org.mockito", + "3.11.1", + listOf("mockito-core", "mockito-junit-jupiter") + ), + DependencySet( + "org.slf4j", + "1.7.30", + listOf("slf4j-api", "slf4j-simple", "log4j-over-slf4j", "jcl-over-slf4j", "jul-to-slf4j") + ), + DependencySet( + "org.testcontainers", + "1.15.3", + listOf("testcontainers", "junit-jupiter") + ) +) + +val DEPENDENCIES = listOf( + "ch.qos.logback:logback-classic:1.2.3", + "com.blogspot.mydailyjava:weak-lock-free:0.18", + "com.github.ben-manes.caffeine:caffeine:2.9.0", + "com.github.stefanbirkner:system-lambda:1.2.0", + "com.github.stefanbirkner:system-rules:1.19.0", + "com.google.auto.service:auto-service:1.0", + "com.uber.nullaway:nullaway:0.9.1", + "commons-beanutils:commons-beanutils:1.9.4", + "commons-cli:commons-cli:1.4", + "commons-codec:commons-codec:1.15", + "commons-collections:commons-collections:3.2.2", + "commons-digester:commons-digester:2.1", + "commons-fileupload:commons-fileupload:1.4", + "commons-io:commons-io:2.10.0", + "commons-lang:commons-lang:2.6", + "commons-logging:commons-logging:1.2", + "commons-validator:commons-validator:1.7", + "info.solidsoft.spock:spock-global-unroll:0.5.1", + "io.netty:netty:3.10.6.Final", + "org.assertj:assertj-core:3.19.0", + "org.awaitility:awaitility:4.1.0", + "org.checkerframework:checker-qual:3.14.0", + "org.codehaus.groovy:groovy-all:${groovyVersion}", + "org.objenesis:objenesis:3.2", + "org.spockframework:spock-core:1.3-groovy-2.5", + "org.scala-lang:scala-library:2.11.12", + "org.springframework.boot:spring-boot-dependencies:2.3.1.RELEASE" +) + +javaPlatform { + allowDependencies() +} + +dependencies { + for (bom in DEPENDENCY_BOMS) { + api(enforcedPlatform(bom)) + val split = bom.split(':') + dependencyVersions[split[0]] = split[2] + } + constraints { + for (set in DEPENDENCY_SETS) { + for (module in set.modules) { + api("${set.group}:${module}:${set.version}") + dependencyVersions[set.group] = set.version + } + } + for (dependency in DEPENDENCIES) { + api(dependency) + val split = dependency.split(':') + dependencyVersions[split[0]] = split[2] + } + } +} + +fun isNonStable(version: String): Boolean { + val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) } + val regex = "^[0-9,.v-]+(-r)?$".toRegex() + val isGuava = version.endsWith("-jre") + val isStable = stableKeyword || regex.matches(version) || isGuava + return isStable.not() +} + +tasks { + named("dependencyUpdates") { + revision = "release" + checkConstraints = true + + rejectVersionIf { + isNonStable(candidate.version) + } + } +} diff --git a/opentelemetry-java-instrumentation/docs/agent-config.md b/opentelemetry-java-instrumentation/docs/agent-config.md new file mode 100644 index 000000000..8bf41ed6c --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/agent-config.md @@ -0,0 +1,64 @@ +# Agent Configuration + +## NOTE: subject to change! + +Note: The environment variables/system properties in this document are very likely to change over time. +Please check back here when trying out a new version! + +Please report any bugs or unexpected behavior you find. + +## Contents + +* [SDK Autoconfiguration](#sdk-autoconfiguration) +* [Peer service name](#peer-service-name) +* [DB statement sanitization](#db-statement-sanitization) +* [Suppressing specific auto-instrumentation](#suppressing-specific-auto-instrumentation) + +## SDK Autoconfiguration + +The SDK's autoconfiguration module is used for basic configuration of the agent. Read the +[docs](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure) +to find settings such as configuring export or sampling. + +Here are some quick links into those docs for the configuration options for specific portions of the SDK & agent: + +* [Exporters](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#exporters) + + [OTLP exporter (both span and metric exporters)](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#otlp-exporter-both-span-and-metric-exporters) + + [Jaeger exporter](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#jaeger-exporter) + + [Zipkin exporter](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#zipkin-exporter) + + [Prometheus exporter](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#prometheus-exporter) + + [Logging exporter](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#logging-exporter) +* [Trace context propagation](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#propagator) +* [OpenTelemetry Resource and service name](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#opentelemetry-resource) +* [Batch span processor](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#batch-span-processor) +* [Sampler](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#sampler) +* [Span limits](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#span-limits) +* [Using SPI to further configure the SDK](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure/README.md#customizing-the-opentelemetry-sdk) + +## Peer service name + +The [peer service name](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/span-general.md#general-remote-service-attributes) is the name of a remote service being connected to. It corresponds to `service.name` in the [Resource](https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/resource/semantic_conventions#service) for the local service. + +| System property | Environment variable | Description | +|--------------------------------------|--------------------------------------|----------------------------------------------------------------------------------| +| `otel.instrumentation.common.peer-service-mapping` | `OTEL_INSTRUMENTATION_COMMON_PEER_SERVICE_MAPPING` | Used to specify a mapping from hostnames or IP addresses to peer services, as a comma-separated list of host=name pairs. The peer service is added as an attribute to a span whose host or IP match the mapping. For example, if set to 1.2.3.4=cats-service,dogs-abcdef123.serverlessapis.com=dogs-api, requests to `1.2.3.4` will have a `peer.service` attribute of `cats-service` and requests to `dogs-abcdef123.serverlessapis.com` will have an attribute of `dogs-api`. | + +## DB statement sanitization + +The agent sanitizes all database queries/statements before setting the `db.statement` semantic attribute: +all values (strings, numbers) in the query string are replaced with a question mark `?`. + +Examples: +* SQL query `SELECT a from b where password="secret"` will appear as `SELECT a from b where password=?` in the exported span; +* Redis command `HSET map password "secret"` will appear as `HSET map password ?` in the exported span. + +This behavior is turned on by default for all database instrumentations. +The following property may be used to disable it: + +| System property | Environment variable | Description | +|-------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------------| +| `otel.instrumentation.common.db-statement-sanitizer.enabled` | `OTEL_INSTRUMENTATION_COMMON_DB_STATEMENT_SANITIZER_ENABLED` | Enables the DB statement sanitization. The default value is `true`. | + +## Suppressing specific auto-instrumentation + +See [suppressing specific auto-instrumentation](suppressing-instrumentation.md) diff --git a/opentelemetry-java-instrumentation/docs/agent-features.md b/opentelemetry-java-instrumentation/docs/agent-features.md new file mode 100644 index 000000000..559577a53 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/agent-features.md @@ -0,0 +1,38 @@ +# OpenTelemetry Java Agent Features + +This lists out some of the features specific to java agents that OpenTelemetry Auto Instrumentation +provides. + +- Bundled exporters + - [OTLP](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md) + - Jaeger gRPC + - Logging + - Zipkin +- Bundled propagators + - [W3C TraceContext / Baggage](https://www.w3.org/TR/trace-context/) + - All Java [trace propagator extensions](https://github.com/open-telemetry/opentelemetry-java/tree/master/extensions/trace-propagators) +- Environment variable configuration as per [spec](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-environment-variables.md) + - Additional support for system properties for same variables by transforming UPPER_UNDERSCORE -> lower.dot + - Ability to disable individual instrumentation, or only enable certain ones. +- Ability to load a custom exporter via an external JAR library +- Isolation from application + - Separate Agent classloader with almost all agent-specific classes + - OpenTelemetry SDK initialized in Agent classloader + - Shading of instrumentation libraries when used in agent + - API bridge for application usage of API to access the Agent classloader's SDK + - API bridge not applied if user brings incompatible API version, preventing linkage errors (similar to safety mechanism below) +- [Safety mechanisms](./safety-mechanisms.md) to prevent application linkage errors + - Collect all references from instrumentation to library and only apply instrumentation if they exist in application + - Verify above at compile time + - Instrumentation tests that run the java agent in a near-production configuration + - Ability to run tests against a fixed version and the latest version of dependencies + - Docker-based smoke tests to verify agent behavior across JVM runtimes, Java application servers +- Ability to create custom distributions, agents with different components / configuration + - Can set different defaults for properties + - Can customize tracer configuration programmatically + - Can provide custom exporter, propagator, sampler + - Can hook into bytebuddy to customize bytecode manipulation +- Noteworthy instrumentation + - Log injection of IDs (logback, log4j2, log4j) + - Automatic context propagation across `Executor`s + - Ability to instrument methods in the application if user adds `@WithSpan` annotation diff --git a/opentelemetry-java-instrumentation/docs/contributing/classloader-state.svg b/opentelemetry-java-instrumentation/docs/contributing/classloader-state.svg new file mode 100644 index 000000000..25a2c1da0 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/classloader-state.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/docs/contributing/debugging.md b/opentelemetry-java-instrumentation/docs/contributing/debugging.md new file mode 100644 index 000000000..28a28aec4 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/debugging.md @@ -0,0 +1,37 @@ +### Debugging + +Debugging java agent can be a challenging task since some instrumentation +code is directly inlined into target classes. + +#### Advice methods + +Breakpoints do not work in advice methods, because their code is directly inlined +by ByteBuddy into the target class. It is good to keep these methods as small as possible. +The advice methods are annotated with: + +```java +@net.bytebuddy.asm.Advice.OnMethodEnter +@net.bytebuddy.asm.Advice.OnMethodExit +``` + +The best approach to debug advice methods and agent initialization is to use the following statements: + +```java +System.out.println() +Thread.dumpStack() +``` + +#### Agent initialization code + +If you want to debug agent initialization code (e.g. `OpenTelemetryAgent`, `AgentInitializer`, +`AgentInstaller`, `OpenTelemetryInstaller`, etc.) then it's important to specify the `-agentlib:` JVM arg +before the `-javaagent:` JVM arg and use `suspend=y` (see full example below). + +#### Enabling debugging + +The following example shows remote debugger configuration. The breakpoints +should work in any code except ByteBuddy advice methods. + +```bash +java -agentlib:jdwp="transport=dt_socket,server=y,suspend=y,address=5000" -javaagent:opentelemetry-javaagent--all.jar -jar app.jar +``` diff --git a/opentelemetry-java-instrumentation/docs/contributing/initialization-sequence.svg b/opentelemetry-java-instrumentation/docs/contributing/initialization-sequence.svg new file mode 100644 index 000000000..88726d6d0 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/initialization-sequence.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/docs/contributing/intellij-setup.md b/opentelemetry-java-instrumentation/docs/contributing/intellij-setup.md new file mode 100644 index 000000000..009bd451c --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/intellij-setup.md @@ -0,0 +1,17 @@ +### IntelliJ setup + +**NB!** Please ensure that Intellij uses the same java installation as you do for building this project +from command line. +This ensures that Gradle task avoidance and build cache work properly and can greatly reduce +build time. + +Suggested plugins and settings: + +* Editor > Code Style > Java/Groovy > Imports + * Class count to use import with '*': `9999` (some number sufficiently large that is unlikely to matter) + * Names count to use static import with '*': `9999` + * Import Layout: + ![import layout](https://user-images.githubusercontent.com/734411/43430811-28442636-94ae-11e8-86f1-f270ddcba023.png) +* [Google Java Format](https://plugins.jetbrains.com/plugin/8527-google-java-format) +* [Save Actions](https://plugins.jetbrains.com/plugin/7642-save-actions) + ![Recommended Settings](save-actions.png) diff --git a/opentelemetry-java-instrumentation/docs/contributing/javaagent-jar-components.md b/opentelemetry-java-instrumentation/docs/contributing/javaagent-jar-components.md new file mode 100644 index 000000000..c01d0b2e2 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/javaagent-jar-components.md @@ -0,0 +1,102 @@ +### Understanding the javaagent components + +The javaagent jar can logically be divided into 3 parts: + +* Modules that live in the system class loader +* Modules that live in the bootstrap class loader +* Modules that live in the agent class loader + +### Modules that live in the system class loader + +#### `javaagent` module + +This module consists of single class +`io.opentelemetry.javaagent.OpenTelemetryAgent` which implements [Java +instrumentation +agent](https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html). +This class is loaded during application startup by application classloader. +Its sole responsibility is to push agent's classes into JVM's bootstrap +classloader and immediately delegate to +`io.opentelemetry.javaagent.bootstrap.AgentInitializer` (now in the bootstrap class loader) +class from there. + +### Modules that live in the bootstrap class loader + +#### `javaagent-bootstrap` module + +`io.opentelemetry.javaagent.bootstrap.AgentInitializer` and a few other classes that live in the bootstrap class +loader but are not used directly by auto-instrumentation + +#### `instrumentation-api` and `javaagent-api` modules + +These modules contain support classes for actual instrumentations to be loaded +later and separately. These classes should be available from all possible +classloaders in the running application. For this reason the `javaagent` module puts +all these classes into JVM's bootstrap classloader. For the same reason this +module should be as small as possible and have as few dependencies as +possible. Otherwise, there is a risk of accidentally exposing this classes to +the actual application. + +`instrumentation-api` contains classes that are needed for both library and auto-instrumentation, +while `javaagent-api` contains classes that are only needed for auto-instrumentation. + +### Modules that live in the agent class loader + +#### `javaagent-tooling`, `javaagent-extension-api` modules and `instrumentation` submodules + +Contains everything necessary to make instrumentation machinery work, +including integration with [ByteBuddy](https://bytebuddy.net/) and actual +library-specific instrumentations. As these classes depend on many classes +from different libraries, it is paramount to hide all these classes from the +host application. This is achieved in the following way: + +- When `javaagent` module builds the final agent, it moves all classes from +`instrumentation` submodules, `javaagent-tooling` and `javaagent-extension-api` modules +into a separate folder inside final jar file, called`inst`. +In addition, the extension of all class files is changed from `class` to `classdata`. +This ensures that general classloaders cannot find nor load these classes. +- When `io.opentelemetry.javaagent.bootstrap.AgentInitializer` is invoked, it creates an +instance of `io.opentelemetry.javaagent.bootstrap.AgentClassLoader`, loads an +`io.opentelemetry.javaagent.tooling.AgentInstaller` from that `AgentClassLoader` +and then passes control on to the `AgentInstaller` (now in the +`AgentClassLoader`). The `AgentInstaller` then installs all of the +instrumentations with the help of ByteBuddy. Instead of using agent classloader all agent classes +could be shaded and used from the bootstrap classloader. However, this opens de-serialization +security vulnerability and in addition to that the shaded classes are harder to debug. + +The complicated process above ensures that the majority of +auto-instrumentation agent's classes are totally isolated from application +classes, and an instrumented class from arbitrary classloader in JVM can +still access helper classes from bootstrap classloader. + +#### Agent jar structure + +If you now look inside +`javaagent/build/libs/opentelemetry-javaagent--all.jar`, you will see the +following "clusters" of classes: + +Available in the system class loader: + +- `io/opentelemetry/javaagent/bootstrap/AgentBootstrap` - the one class from `javaagent` +module + +Available in the bootstrap class loader: + +- `io/opentelemetry/javaagent/bootstrap/` - contains the `javaagent-bootstrap` module +- `io/opentelemetry/javaagent/instrumentation/api/` - contains the `javaagent-api` module +- `io/opentelemetry/javaagent/shaded/instrumentation/api/` - contains the `instrumentation-api` module, + shaded during creation of `javaagent` jar file by Shadow Gradle plugin +- `io/opentelemetry/javaagent/shaded/io/` - contains the OpenTelemetry API and its dependency gRPC +Context, both shaded during creation of `javaagent` jar file by Shadow Gradle plugin +- `io/opentelemetry/javaagent/slf4j/` - contains SLF4J and its simple logger implementation, shaded +during creation of `javaagent` jar file by Shadow Gradle plugin + +Available in the agent class loader: +- `inst/` - contains `javaagent-tooling` and `javaagent-extension-api` modules and + `instrumentation` submodules, loaded and isolated inside `AgentClassLoader`. + Including OpenTelemetry SDK (and the built-in exporters when using the `-all` artifact). + +![Agent initialization sequence](initialization-sequence.svg) +[Image source](https://docs.google.com/drawings/d/1FyRd11emnHvNWzUXLdpMNyf2R-auZlJsicNg8FpU_Ys) +![Agent classloader state](classloader-state.svg) +[Image source](https://docs.google.com/drawings/d/1WlJ_VHuo_t4RurQ6_qiQHdEBgRLc22l7L5f5dFRqgB8) diff --git a/opentelemetry-java-instrumentation/docs/contributing/javaagent-test-infra.md b/opentelemetry-java-instrumentation/docs/contributing/javaagent-test-infra.md new file mode 100644 index 000000000..8e027a4e3 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/javaagent-test-infra.md @@ -0,0 +1,38 @@ +### Understanding the javaagent instrumentation testing components + +Javaagent instrumentation tests are run using a fully shaded `-javaagent` in order to perform +the same bytecode instrumentation as when the agent is run against a normal app. + +There are a few key components that make this possible, described below. + +### gradle/instrumentation.gradle + +* shades the instrumentation +* adds jvm args to the test configuration + * -javaagent:[agent for testing] + * -Dotel.javaagent.experimental.initializer.jar=[shaded instrumentation jar] + +The `otel.javaagent.experimental.initializer.jar` property is used to load the shaded instrumentation jar into the +`AgentClassLoader`, so that the javaagent jar doesn't need to be re-built each time. + +### :testing:agent-exporter + +This contains the span and metric exporters that are used. + +These are in-memory exporters, so that the tests can verify the spans and metrics being exported. + +These exporters and the in-memory data live in the `AgentClassLoader`, so tests must access them +using reflection. To simplify this, they store the in-memory data using the OTLP protobuf objects, +so that they can be serialized into byte arrays inside the `AgentClassLoader`, then passed back +to the tests and deserialized inside their class loader where they can be verified. The +`:testing-common` module (described below) hides this complexity from instrumentation test authors. + +### :agent-for-testing + +This is a custom distro of the javaagent that embeds the `:testing:agent-exporter`. + +### :testing-common + +This module provides methods to help verify the span and metric data produced by the +instrumentation, hiding the complexity of accessing the in-memory exporters that live in the +`AgentClassLoader`. diff --git a/opentelemetry-java-instrumentation/docs/contributing/muzzle.md b/opentelemetry-java-instrumentation/docs/contributing/muzzle.md new file mode 100644 index 000000000..c14a8a3fe --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/muzzle.md @@ -0,0 +1,149 @@ +# Muzzle + +Muzzle is a safety feature of the Java agent that prevents applying instrumentation when a mismatch +between the instrumentation code and the instrumented application code is detected. +It ensures API compatibility between symbols (classes, methods, fields) on the application classpath +and references to those symbols made by instrumentation advices defined in the agent. +In other words, muzzle ensures that the API symbols used by the agent are compatible with the API +symbols on the application classpath. + +Muzzle will prevent loading an instrumentation if it detects any mismatch or conflict. + +## How it works + +Muzzle has two phases: +* at compile time it collects references to the third-party symbols and used helper classes; +* at runtime it compares those references to the actual API symbols on the classpath. + +### Compile-time reference collection + +The compile-time reference collection and code generation process is implemented using a ByteBuddy +plugin (called `MuzzleCodeGenerationPlugin`). + +For each instrumentation module the ByteBuddy plugin collects symbols referring to both internal and +third party APIs used by the currently processed module's type +instrumentations (`InstrumentationModule#typeInstrumentations()`). The reference collection process +starts from advice classes (values of the map returned by the +`TypeInstrumentation#transformers()` method) and traverses the class graph until it encounters a +reference to a non-instrumentation class (determined by `InstrumentationClassPredicate` and +the `InstrumentationModule#isHelperClass(String)` predicate). Aside from references, +the collection process also builds a graph of dependencies between internal instrumentation helper +classes - this dependency graph is later used to construct a list of helper classes that will be +injected to the application classloader (`InstrumentationModule#getMuzzleHelperClassNames()`). +Muzzle also automatically generates the `InstrumentationModule#getMuzzleContextStoreClasses()` +method. + +If you extend any of these `getMuzzle...()` methods in your `InstrumentationModule`, the muzzle +compile plugin will not override your code: muzzle will only override those methods that do not have +a custom implementation. + +All collected references are then used to create a `ReferenceMatcher` instance. This matcher +is stored in the instrumentation module class in the method `InstrumentationModule#getMuzzleReferenceMatcher()` +and is shared between all type instrumentations. The bytecode of this method (basically an array of +`Reference` builder calls) and the `getMuzzleHelperClassNames()` is generated automatically by the +ByteBuddy plugin using an ASM code visitor. + +The source code of the compile-time plugin is located in the `javaagent-tooling` module, +package `io.opentelemetry.javaagent.tooling.muzzle.collector`. + +### Runtime reference matching + +The runtime reference matching process is implemented as a ByteBuddy matcher in `InstrumentationModule`. +`MuzzleMatcher` uses the `getMuzzleReferenceMatcher()` method generated during the compilation phase +to verify that the class loader of the instrumented type has all necessary symbols (classes, +methods, fields). If the `ReferenceMatcher` finds any mismatch between collected references and the +actual application classpath types the whole instrumentation is discarded. + +It is worth noting that because the muzzle check is expensive, it is only performed after a match +has been made by the `InstrumentationModule#classLoaderMatcher()` and `TypeInstrumentation#typeMatcher()` +matchers. The result of muzzle matcher is cached per classloader, so that it is only executed +once for the whole instrumentation module. + +The source code of the runtime muzzle matcher is located in the `javaagent-tooling` module, +in the class `Instrumenter.Default` and under the package `io.opentelemetry.javaagent.tooling.muzzle`. + +## Muzzle gradle plugin + +The muzzle gradle plugin allows to perform the runtime reference matching process against different +third party library versions, when the project is built. + +Muzzle gradle plugin is just an additional utility for enhanced build-time checking +to alert us when there are breaking changes in the underlying third party library +that will cause the instrumentation not to get applied. +**Even without using it muzzle reference matching is _always_ active in runtime**, +it's not an optional feature. + +The gradle plugin defines two tasks: + +* `muzzle` task runs the runtime muzzle verification against different library versions: + ```sh + ./gradlew :instrumentation:google-http-client-1.19:muzzle + ``` + If a new, incompatible version of the instrumented library is published it fails the build. + +* `printMuzzleReferences` task prints all API references in a given module: + ```sh + ./gradlew :instrumentation:google-http-client-1.19:printMuzzleReferences + ``` + +The muzzle plugin needs to be configured in the module's `.gradle` file. +Example: + +```groovy +muzzle { + // it is expected that muzzle fails the runtime check for this component + fail { + group = "commons-httpclient" + module = "commons-httpclient" + // versions from this range are checked + versions = "[,4.0)" + // this version is not checked by muzzle + skip('3.1-jenkins-1') + } + // it is expected that muzzle passes the runtime check for this component + pass { + group = 'org.springframework' + module = 'spring-webmvc' + versions = "[3.1.0.RELEASE,]" + // except these versions + skip('1.2.1', '1.2.2', '1.2.3', '1.2.4') + skip('3.2.1.RELEASE') + // this dependency will be added to the classpath when muzzle check is run + extraDependency "javax.servlet:javax.servlet-api:3.0.1" + // verify that all other versions - [,3.1.0.RELEASE) in this case - fail the muzzle runtime check + assertInverse = true + } +} +``` + +* Using either `pass` or `fail` directive allows to specify whether muzzle should treat the + reference check failure as expected behavior; +* `versions` is a version range, where `[]` is inclusive and `()` is exclusive. It is not needed to + specify the exact version to start/end, e.g. `[1.0.0,4)` would usually behave in the same way as + `[1.0.0,4.0.0-Alpha)`; +* `assertInverse` is basically a shortcut for adding an opposite directive for all library versions + that are not included in the specified `versions` range; +* `extraDependency` allows putting additional libs on the classpath just for the compile-time check; + this is usually used for jars that are not bundled with the instrumented lib but always present + in the runtime anyway. + +The source code of the gradle plugin is located in the `buildSrc` directory. + +### Covering all versions and `assertInverse` + +Ideally when using the muzzle gradle plugin we should aim to cover all versions of the instrumented +library. Expecting muzzle check failures from some library versions is a way to ensure that the +instrumentation will not be applied to them in the runtime - and won't break anything in the +instrumented application. + +The easiest way it can be done is by adding `assertInverse = true` to the `pass` muzzle +directive. The plugin will add an implicit `fail` directive that contains all other versions of the +instrumented library. +It is worth using `assertInverse = true` by default when writing instrumentation modules, even for +very old library versions. The muzzle plugin will ensure that those old versions won't be +accidentally instrumented when we know that the instrumentation will not work properly for them. +Having a `fail` directive forces the authors of the instrumentation module to properly specify +`classLoaderMatcher()` so that only the desired version range is instrumented. + +In more complicated scenarios it may be required to use multiple `pass` and `fail` directives +to cover as many versions as possible. diff --git a/opentelemetry-java-instrumentation/docs/contributing/running-tests.md b/opentelemetry-java-instrumentation/docs/contributing/running-tests.md new file mode 100644 index 000000000..58100af35 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/running-tests.md @@ -0,0 +1,55 @@ +## Running the tests + +### Java versions + +Open Telemetry Auto Instrumentation's minimal supported version is java 8. +All jar files that we produce, unless noted otherwise, have bytecode +compatible with java 8 runtime. Our test suite is executed against +java 8, all LTS versions and the latest non-LTS version. + +Some libraries that we auto-instrument may have higher minimal requirements. +In this case we compile and test corresponding auto-instrumentation with +higher java version as required by library. The resulting classes will have +higher bytecode level, but as it matches library's java version, no runtime +problem arise. + +### Instrumentation tests + +Executing `./gradlew instrumentation:test` will run tests for all supported +auto-instrumentations using that java version which runs the Gradle build +itself. These tests usually use the minimal supported version of the +instrumented library. + +#### Executing tests with specific java version + +We run all tests on Java 11 by default, along with Java 8 and 15. To run on the later, set the +`testJavaVersion` Gradle property to the desired major version, e.g., `./gradlew test -PtestJavaVersion=8`, +`./gradlew test -PtestJavaVersion=15`. If you don't have a JDK of these versions +installed, Gradle will automatically download it for you. + +#### Executing tests against the latest versions of libraries under instrumentation + +This is done as part of the nightly build in order to catch when a new version of a library is +released that breaks our instrumentation tests. + +To run these tests locally, add `-PtestLatestDeps=true` to your existing `gradlew` command line. + +#### Executing single test + +Executing `./gradlew :instrumentation::test --tests ` will run only the selected test. + +### Smoke tests + +The smoke tests are not run by default since they take a long time and are not relevant for most +contributions. +If you need to run a specific smoke test: + +``` +./gradlew :smoke-tests:test -PsmokeTestSuite=glassfish -PrunSmokeTests=true +``` + +If you are on Windows and you want to run the tests using linux containers: + +``` +USE_LINUX_CONTAINERS=1 ./gradlew :smoke-tests:test -PsmokeTestSuite=glassfish -PrunSmokeTests=true +``` diff --git a/opentelemetry-java-instrumentation/docs/contributing/save-actions.png b/opentelemetry-java-instrumentation/docs/contributing/save-actions.png new file mode 100644 index 000000000..4cfb2123c Binary files /dev/null and b/opentelemetry-java-instrumentation/docs/contributing/save-actions.png differ diff --git a/opentelemetry-java-instrumentation/docs/contributing/style-guideline.md b/opentelemetry-java-instrumentation/docs/contributing/style-guideline.md new file mode 100644 index 000000000..3c31298ea --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/style-guideline.md @@ -0,0 +1,98 @@ +## Style guideline + +We follow the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). + +### Auto-formatting + +The build will fail if the source code is not formatted according to the google java style. + +The main goal is to avoid extensive reformatting caused by different IDEs having different opinion +about how things should be formatted by establishing. + +Running + +```bash +./gradlew spotlessApply +``` + +reformats all the files that need reformatting. + +Running + +```bash +./gradlew spotlessCheck +``` + +runs formatting verify task only. + +#### Pre-commit hook + +To completely delegate code style formatting to the machine, +there is a pre-commit hook setup to verify formatting before committing. +It can be activated with this command: + +```bash +git config core.hooksPath .githooks +``` + +#### Editorconfig + +As additional convenience for IntelliJ users, we provide `.editorconfig` +file. IntelliJ will automatically use it to adjust its code formatting settings. +It does not support all required rules, so you still have to run +`spotlessApply` from time to time. + +### Additional checks + +The build uses checkstyle to verify some parts of the Google Java Style Guide that cannot be handled +by auto-formatting. + +To run these checks locally: + +``` +./gradlew checkstyleMain checkstyleTest +``` + +### Static imports + +We leverage static imports for many common types of operations. However, not all static methods or +constants are necessarily good candidates for a static import. The following list is a very +rough guideline of what are commonly accepted static imports: + +* Test assertions (JUnit and AssertJ) +* Mocking/stubbing in tests (with Mockito) +* Collections helpers (such as `singletonList()` and `Collectors.toList()`) +* ByteBuddy `ElementMatchers` (for building instrumentation modules) +* Immutable constants (where clearly named) +* Singleton instances (especially where clearly named an hopefully immutable) +* `tracer()` methods that expose tracer singleton instances + +### Ordering of class contents + +The following order is preferred: + +* Static fields (final before non-final) +* Instance fields (final before non-final) +* Constructors +* Methods +* Nested classes + +If methods call each other, it's nice if the calling method is ordered (somewhere) above +the method that it calls. So, for one example, a private method would be ordered (somewhere) below +the non-private methods that use it. + +In static utility classes (where all members are static), the private constructor +(used to prevent construction) should be ordered after methods instead of before methods. + +### `final` keyword usage + +Public classes should be declared `final` where possible. + +Methods should only be declared `final` if they are in non-final public classes. + +Fields should be declared `final` where possible. + +Method parameters should never be declared `final`. + +Local variables should only be declared `final` if they are not initialized inline +(declaring these vars `final` can help prevent accidental double-initialization). diff --git a/opentelemetry-java-instrumentation/docs/contributing/writing-instrumentation-module.md b/opentelemetry-java-instrumentation/docs/contributing/writing-instrumentation-module.md new file mode 100644 index 000000000..7cf382acd --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/writing-instrumentation-module.md @@ -0,0 +1,322 @@ +# Writing an `InstrumentationModule` step by step + +`InstrumentationModule` is the central piece of any OpenTelemetry javaagent instrumentation. There +are many conventions that our javaagent uses, many pitfalls and not obvious patterns that one has to +follow when implementing a module. + +This doc attempts to describe how a javaagent instrumentation should be implemented and document all +quirks that may affect your instrumentation. In addition to this file, we suggest reading +the `InstrumentationModule` and `TypeInstrumentation` Javadocs, as they often provide more detailed +explanations of how to use a particular method (and why it works the way it does). + +## `InstrumentationModule` + +An `InstrumentationModule` describes a set of individual `TypeInstrumentation`s that need to be +applied together to correctly instrument a particular library. Type instrumentations grouped in a +module share helper classes, [muzzle runtime checks](muzzle.md), have the same classloader criteria +for being applied, and get enabled/disabled together. + +The OpenTelemetry javaagent finds all modules by using the Java `ServiceLoader` API. To make your +instrumentation visible you need to make sure that a proper `META-INF/services/` file is present in +the javaagent jar. The easiest way to do it is using `@AutoService`: + +```java + +@AutoService(InstrumentationModule.class) +class MyLibraryInstrumentationModule extends InstrumentationModule { + // ... +} +``` + +An `InstrumentationModule` needs to have at least one name. The user of the javaagent can +[suppress a chosen instrumentation](../suppressing-instrumentation.md) by referring to it by one of +its names. The instrumentation module names use kebab-case. The main instrumentation name (the first +one) is supposed to be the same as the gradle module name (excluding the version suffix if it has one). + +```java +public MyLibraryInstrumentationModule() { + super("my-library", "my-library-1.0"); +} +``` + +For detailed information on `InstrumentationModule` names please read the +`InstrumentationModule#InstrumentationModule(String, String...)` Javadoc. + +### `order()` + +If you need to have instrumentations applied in a specific order (for example your custom +instrumentation enriches the built-in servlet one and thus needs to run after it) you can override +the `order()` method to specify the ordering: + +```java +@Override +public int order() { + return 1; +} +``` + +Higher `order()` means that the instrumentation module will be applied later. The default value is +`0`. + +### `isHelperClass()` + +The OpenTelemetry javaagent picks up helper classes used in the instrumentation/advice classes and +injects them into the application classpath. It can automatically find those classes that follow our +package conventions (see [muzzle docs](muzzle.md#compile-time-reference-collection) for more info on +this topic), but it is also possible to explicitly tell which packages/classes are supposed to be +treated as helper classes by implementing `isHelperClass(String)`: + +```java +@Override +public boolean isHelperClass(String className) { + return className.startsWith("org.my.library.opentelemetry"); +} +``` + +### `helperResourceNames()` + +Some libraries may expose SPI interfaces that you can easily implement to provide +telemetry-gathering capabilities. The OpenTelemetry javaagent is able to inject `ServiceLoader` +service provider files, but it needs to be told which ones explicitly: + +```java +@Override +public List helperResourceNames() { + return singletonList("META-INF/services/org.my.library.SpiClass"); +} +``` + +All classes referenced by service providers defined in the `helperResourceNames()` method will be +treated as helper classes: they'll be checked for invalid references and automatically injected into +the application classloader. + +### `classLoaderMatcher()` + +Different versions of the same library often need completely different instrumentations: +for example, servlet 3 introduces several new async classes that need to be instrumented to produce +correct telemetry data. An `InstrumentationModule` can define additional criteria for checking +whether an instrumentation should be applied: + +```java +@Override +public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("org.my.library.Version2Class"); +} +``` + +The above example will skip instrumenting the application code if it does not contain the class that was +introduced in the version your instrumentation covers. + +### `typeInstrumentations()` + +Finally, an `InstrumentationModule` implementation needs to provide at least one +`TypeInstrumentation` implementation: + +```java +@Override +public List typeInstrumentations() { + return Collections.singletonList(new MyTypeInstrumentation()); +} +``` + +A module with no type instrumentations does nothing. + +## `TypeInstrumentation` + +A `TypeInstrumentation` describe the changes that need to be made to a single type. Depending on the +instrumented library, they may only make sense in conjunction with other type instrumentations +(grouped together in a module). + +```java +class MyTypeInstrumentation implements TypeInstrumentation { + // ... +} +``` + +### `typeMatcher()` + +A type instrumentation needs to declare what class (or classes) are going to be instrumented: + +```java +@Override +public ElementMatcher typeMatcher() { + return named("org.my.library.SomeClass"); +} +``` + +### `classLoaderOptimization()` + +When you need to instrument all classes that implement a particular interface, or all classes that +are annotated with a particular annotation you should also implement the +`classLoaderOptimization()` method. Matching classes by their name is quite fast, but actually +inspecting the bytecode (e.g. implements, has annotation, has method) is a rather expensive +operation. The matcher returned by the `classLoaderOptimization()` method makes the +`TypeInstrumentation` significantly faster when instrumenting applications that do not contain the +library. + +```java +@Override +public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.my.library.SomeInterface"); +} + +@Override +public ElementMatcher typeMatcher() { + return implementsInterface(named("org.my.library.SomeInterface")); +} +``` + +### `transform(TypeTransformer)` + +The last `TypeInstrumentation` method describes what transformations should be applied to the +matched type. Type `TypeTransformer` interface (implemented internally by the agent) defines a set +of available transformations that you can apply: + +* Calling `applyAdviceToMethod(ElementMatcher, String)` allows you to + apply an advice class (the second parameter) to all matching methods (the first parameter). It is + suggested to make the method matchers as strict as possible - the type instrumentation should + only instrument the code that it's supposed to, not more. +* `applyTransformer(AgentBuilder.Transformer)` allows you to inject an arbitrary ByteBuddy + transformer. This is an advanced, low-level option that will not be subjected to muzzle safety + checks and helper class detection - use it responsibly. + +```java +@Override +public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isPublic() + .and(named("someMethod")) + .and(takesArguments(2)) + .and(takesArgument(0, String.class)) + .and(takesArgument(1, named("org.my.library.MyLibraryClass"))), + this.getClass().getName() + "$MethodAdvice"); +} +``` + +For matching built-in Java types you can use the `takesArgument(0, String.class)` form. Classes +originating from the instrumented library need to be matched using the `named()` matcher. + +Implementations of `TypeInstrumentation` will often implement advice classes as static inner +classes. These classes are referred to by name when applying advice classes to methods in +the `transform()` method. + +You probably noticed in the example above that the advice class is being referenced in a slightly +peculiar way: + +```java +this.getClass().getName() + "$MethodAdvice" +``` + +Simply referring to the inner class and calling `getName()` would be easier to read and understand +than this odd mix of string concatenation, but please note that **this is intentional** +and should be maintained. + +Instrumentation modules are loaded by the agent's class loader, and this string concatenation is an +optimization that prevents the actual advice class from being loaded into the agent's class loader. + +## Advice classes + +Advice classes are not really "classes", they're raw pieces of code that will be pasted directly into +the instrumented library class files. You should not treat them as ordinary, plain Java classes - +unfortunately many standard practices do not apply to them: + +* if they're inner classes they MUST be static; +* they MUST only contain static methods; +* they MUST NOT contain any state (fields) whatsoever - static constants included! Only the advice + methods' content is copied to the instrumented code, the constants are not; +* inner advice classes defined in an `InstrumentationModule` or a `TypeInstrumentation` MUST NOT use + anything from the outer class (loggers, constants, etc); +* reusing code by extracting a common method and/or parent class will most likely not work properly: + instead you can create additional helper classes to store any reusable code; +* they SHOULD NOT contain any methods other than `@Advice`-annotated method. + +```java +@SuppressWarnings("unused") +public static class MethodAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(/* ... */) { + // ... + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit(/* ... */) { + // ... + } +} +``` + +It is important to include the `suppress = Throwable.class` property in `@Advice`-annotated methods. +Exceptions thrown by the advice methods will get caught and handled by a special `ExceptionHandler` +that OpenTelemetry javaagent defines. The handler makes sure to properly log all unexpected +exceptions. + +The `OnMethodEnter` and `OnMethodExit` advice methods often need to share several pieces +of information. We use local variables prefixed with `otel` to pass context, scope (and sometimes +more) between those methods. + +```java +@Advice.OnMethodEnter(suppress = Throwable.class) +public static void onEnter(@Advice.Argument(1) Object request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + // ... +} +``` + +Usually, for telemetry-producing instrumentations those two methods follow the pattern below: + +```java +@Advice.OnMethodEnter(suppress = Throwable.class) +public static void onEnter(@Advice.Argument(1) Object request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = Java8BytecodeBridge.currentContext(); + + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); +} + +@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) +public static void onExit(@Advice.Argument(1) Object request, + @Advice.Return Object response, + @Advice.Thrown Throwable exception, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + instrumenter().end(context, request, response, exception); +} +``` + +You may have noticed that the example above does not use `Context.current()`, but a +`Java8BytecodeBridge` method. This is intentional: if you are instrumenting a pre-Java 8 library, +then inlining Java 8 default method calls (or static methods in an interface) into that library +will result in a `java.lang.VerifyError` at runtime, since Java 8 default method invocations are not +legal in Java 7 (and prior) bytecode. +Because OpenTelemetry API has many common default/static interface methods (e.g. `Span.current()`), +the `javaagent-api` artifact has a class `Java8BytecodeBridge` which provides static methods +for accessing these default methods from advice. +In fact, we suggest avoiding Java 8 language features in advice classes at all - sometimes you don't +know what bytecode version is used by the instrumented class. + +Sometimes there is a need to associate some context class with an instrumented library class, +and the library does not offer a way to do this. The OpenTelemetry javaagent provides the +`ContextStore` for that purpose: + +```java +ContextStore contextStore = + InstrumentationContext.get(Runnable.class, Context.class); +``` + +A `ContextStore` is conceptually very similar to a map. It is not a simple map though: +the javaagent uses a lot of bytecode modification magic to make this optimal. +Because of this, retrieving a `ContextStore` instance is rather limited: +the `InstrumentationContext#get()` method can only be called in advice classes, and it MUST receive +class references as its parameters - it won't work with variables, method params etc. +Both the key class and the context class must be known at compile time for it to work. diff --git a/opentelemetry-java-instrumentation/docs/contributing/writing-instrumentation.md b/opentelemetry-java-instrumentation/docs/contributing/writing-instrumentation.md new file mode 100644 index 000000000..0be2124a9 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/contributing/writing-instrumentation.md @@ -0,0 +1,163 @@ +# Writing instrumentation + +**Warning**: The repository is still in the process of migrating to the structure described here. + +Any time we want to add OpenTelemetry support for a new Java library, e.g., so usage +of that library has tracing, we must write new instrumentation for that library. Let's +go over some terms first. + +**Library instrumentation**: This is logic that creates spans and enriches them with data +using library-specific monitoring APIs. For example, when instrumenting an RPC library, +the instrumentation will use some library-specific functionality to listen to events such +as the start and end of a request and will execute code to start and end spans in these +listeners. Many of these libraries will provide interception type APIs such as the gRPC +`ClientInterceptor` or servlet's `Filter`. Others will provide a Java interface whose methods +correspond to a request, and instrumentation can define an implementation which delegates +to the standard, wrapping methods with the logic to manage spans. Users will add code to their +apps that initialize the classes provided by library instrumentation, and the library instrumentation +can be found inside the user's app itself. + +Some libraries will have no way of intercepting requests because they only expose static APIs +and no interception hooks. For these libraries it is not possible to create library +instrumentation. + +**Java agent instrumentation**: This is logic that is similar to library instrumentation, but instead +of a user initializing classes themselves, a Java agent automatically initializes them during +class loading by manipulating byte code. This allows a user to develop their apps without thinking +about instrumentation and get it "for free". Often, the agent instrumentation will generate +bytecode that is more or less identical to what a user would have written themselves in their app. + +In addition to automatically initializing library instrumentation, agent instrumentation can be used +for libraries where library instrumentation is not possible, such as `URLConnection`, because it can +intercept even the JDK's classes. Such libraries will not have library instrumentation but will have +agent instrumentation. + +## Folder Structure + +Please also refer to some of our existing instrumentation for examples of our structure, for example, +[aws-sdk-2.2](../../instrumentation/aws-sdk/aws-sdk-2.2). + +When writing new instrumentation, create a new subfolder of `instrumentation` to correspond to the +instrumented library and the oldest version being targeted. Ideally an old version of the library is +targeted in a way that the instrumentation applies to a large range of versions, but this may be +restricted by the interception APIs provided by the library. + +Within the subfolder, create three folders `library` (skip if library instrumentation is not possible), +`javaagent`, and `testing`. + +For example, if we are targeting an RPC framework `yarpc` at version `1.0` we would have a tree like + +``` +instrumentation -> + ... + yarpc-1.0 -> + javaagent + yarpc-1.0-javaagent.gradle + library + yarpc-1.0-library.gradle + testing + yarpc-1.0-testing.gradle +``` + +and in the top level `settings.gradle` + +```groovy + +include 'instrumentation:yarpc-1.0:javaagent' +include 'instrumentation:yarpc-1.0:library' +include 'instrumentation:yarpc-1.0:testing' +``` + +## Writing library instrumentation + +Begin by writing the instrumentation for the library in `library`. This generally involves defining a +`Tracer` and using the typed tracers in our `instrumentation-common` library to create and annotate +spans as part of the implementation of an interceptor for the library. The module should generally +only depend on the OpenTelemetry API, `instrumentation-common`, and the instrumented library itself. +[instrumentation-library.gradle](../../gradle/instrumentation-library.gradle) needs to be applied to +configure build tooling for the library. + +## Writing instrumentation tests + +Once the instrumentation is completed, we add tests to the `testing` module. Tests will +generally apply to both library and agent instrumentation, with the only difference being how a client +or server is initialized. In a library test, there will be code calling into the instrumentation API, +while in an agent test, it will generally just use the underlying library's API as is. Create tests in an +abstract class with an abstract method that returns an instrumented object like a client. The class +should itself extend from `InstrumentationSpecification` to be recognized by Spock and include helper +methods for assertions. + +After writing a test or two, go back to the `library` package, make sure it has a test dependency on the +`testing` submodule and add a test that inherits from the abstract test class. You should implement +the method to initialize the client using the library's mechanism to register interceptors, perhaps +a method like `registerInterceptor` or wrapping the result of a library factory when delegating. The +test should implement the `LibraryTestTrait` trait for common setup logic. If the tests pass, +library instrumentation is working OK. + +## Writing Java agent instrumentation + +Now that we have working instrumentation, we can implement agent instrumentation so users of the agent +do not have to modify their apps to use it. Make sure the `javaagent` submodule has a dependency on the +`library` submodule and a test dependency on the `testing` submodule. Agent instrumentation defines +classes to match against to generate bytecode for. You will often match against the class you used +in the test for library instrumentation, for example the builder of a client. And then you could +match against the method that creates the builder, for example its constructor. Agent instrumentation +can inject byte code to be run after the constructor returns, which would invoke e.g., +`registerInterceptor` and initialize the instrumentation. Often, the code inside the byte code +decorator will be identical to the one in the test you wrote above - the agent does the work for +initializing the instrumentation library, so a user doesn't have to. +You can find a detailed explanation of how to implement a javaagent instrumentation +[here](writing-instrumentation-module.md). + +With that written, let's add tests for the agent instrumentation. We basically want to ensure that +the instrumentation works without the user knowing about the instrumentation. Add a test that extends +the base class you wrote earlier, but in this, create a client using none of the APIs in our project, +only the ones offered by the library. Implement the `AgentTestTrait` trait for common setup logic, +and try running. All the tests should pass for agent instrumentation too. + +Note that all the tests inside the `javaagent` module will be run using the shaded `-javaagent` +in order to perform the same bytecode instrumentation as when the agent is run against a normal app. +This means that the javaagent instrumentation will be inside the javaagent (inside of the +`AgentClassLoader`) and will not be directly accessible to your test code. See the next section in +case you need to write unit tests that directly access the javaagent instrumentation. + +## Writing Java agent unit tests + +As mentioned above, tests in the `javaagent` module cannot access the javaagent instrumentation +classes directly. + +Ideally javaagent instrumentation is just a thin wrapper over library instrumentation, and so there +is no need to write unit tests that directly access the javaagent instrumentation classes. + +If you still want to write a unit test against javaagent instrumentation, add another module +named `javaagent-unit-tests`. Continuing with the example above: + +``` +instrumentation -> + ... + yarpc-1.0 -> + javaagent + yarpc-1.0-javaagent.gradle + javaagent-unit-tests + yarpc-1.0-javaagent-unit-tests.gradle + ... +``` + +## Various instrumentation gotchas + +### Instrumenting code that is not available as a maven dependency + +If instrumented server or library jar isn't available from a maven repository you can create a +module with stub classes that define only the methods that you need for writing the integration. +Methods in stub class can just `throw new UnsupportedOperationException()` these classes are only +used to compile the advice classes and won't be packaged into agent. During runtime real classes +from instrumented server or library will be used. + +Create a module called `compile-stub` and add `compile-stub.gradle` with following content +``` +apply plugin: "otel.java-conventions" +``` +In javaagent module add compile only dependency with +``` +compileOnly project(':instrumentation:xxx:compile-stub') +``` diff --git a/opentelemetry-java-instrumentation/docs/ga-requirements.md b/opentelemetry-java-instrumentation/docs/ga-requirements.md new file mode 100644 index 000000000..96a0a1ae7 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/ga-requirements.md @@ -0,0 +1,88 @@ +### P1 (e.g. cannot GA without these): +* ✅End-to-end tests ([#298](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/298#issuecomment-664162169)) + * ✅OTLP, Jaeger and Zipkin ([#1541](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1541)) + * ✅Spring Boot and Wildfly + * (Wildfly chosen due to common javaagent issues around jboss modules and jboss logging) + * ✅Java 8, 11, and the latest Java version +* Benchmarking ([#595](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/595)) + * Runtime overhead benchmark + * Startup overhead benchmark +* All captured span attributes must either be from semantic attributes or be instrumentation-specific + * TODO define convention for instrumentation-specific attributes, e.g. "elasticsearch.*" +* Basic configuration points + * Add custom auto-instrumentation + * ✅Ability to build "custom distro" +* Documentation + * All configuration options + * Standard OpenTelemetry SDK + Exporter configuration options + * Auto-instrumentation configuration options (e.g. disable/enable, peer.service mapping) + * For each instrumentation + * Document any instrumentation-specific configuration + * How to troubleshoot (start documenting common issues somewhere) +* Library (manual) instrumentations for a few libraries commonly used with Spring: + Spring WebMVC, Spring WebFlux, Spring RestTemplate, JDBC + * (this requirement is to ensure that we have a good path forward for supporting both auto and manual instrumentation) + +### P2 +* Contributor experience (tag "contributor experience" plus tag "cleanup" plus tag "sporadic test failure") + * New contributor documentation + * How to write new instrumentation (auto, library, tests) + * How to understand and fix muzzle issues + * How to submit your first PR (CLA, check for CI failures, note about sporadic failures) + * Faster builds + * Fewer sporadic CI failures + * Publish a debug jar without the classdata obfuscation + +### P3 +* Auto-collected metrics + * System / JVM metrics (https://github.com/open-telemetry/opentelemetry-specification/issues/651) + * Request metrics (https://github.com/open-telemetry/opentelemetry-specification/issues/522, https://github.com/open-telemetry/opentelemetry-specification/pull/657) +* Library (manual) instrumentations for more libraries commonly used with Spring + * Spring Kafka, Spring AMQP, Reactor, java.util.concurrent +* Library (manual) instrumentations for libraries commonly used with Android + * OkHttp, gRPC +* Document the basic configuration points + * How to write your own auto-instrumentation + * (much of this can be shared with contributor documentation below) + * How to build your own "custom distro" +* Complete instrumentation documentation, with commitment to keeping this up-to-date going forward + * Document all spans that it captures + * Span names + * Span attributes (including explanation of any non-semantic attributes) + * Events + * Document any other effects (e.g. updating SERVER span name with route) + +### Instrumentation prioritization + +When it comes to prioritizing work, sometimes it's helpful to know the relative importance of a +particular instrumentation, e.g. making improvements in Spring WebFlux instrumentation would +generally take priority over making improvement in Grizzly instrumentation. + +This is only intended as a guide for prioritizing work. + +### P1 + +* Apache AsyncHttpClient +* Apache HttpClient +* Cassandra Driver +* gRPC +* HttpURLConnection +* JAX-RS +* JDBC +* Jedis +* JMS +* Kafka +* Lettuce +* MongoDB Drivers +* Netty +* OkHttp +* RabbitMQ +* Reactor +* Servlet +* Spring Scheduling +* Spring Web MVC +* Spring Webflux + +### P2 + +* All others diff --git a/opentelemetry-java-instrumentation/docs/java-7-rationale.md b/opentelemetry-java-instrumentation/docs/java-7-rationale.md new file mode 100644 index 000000000..1042eff8c --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/java-7-rationale.md @@ -0,0 +1,69 @@ +## Rationale for not supporting Java 7 + +### Android support is no longer tied to Java 7 + +Even for supporting old Android API levels: + +> If you're building your app using Android Gradle plugin 4.0.0 or higher, the plugin extends +> support for using a number of Java 8 language APIs without requiring a minimum API level for +> your app. + +(https://developer.android.com/studio/write/java8-support#library-desugaring) + +There are some Java 8 APIs that Android does not desugar, but we can use +[animal sniffer plugin](https://github.com/xvik/gradle-animalsniffer-plugin) to ensure we don't use +those particular Java 8 APIs that are not available in the base Android level we decide to support, +e.g. OkHttp takes this approach to +[ensure compliance with Android API level 21](https://github.com/square/okhttp/blob/96a2118dd447ebc28a64d9b11a431ca642edc441/build.gradle#L144-L153) + +We will use this approach for the `instrumentation-api` module and for any library (manual) +instrumentation that would be useful to Android developers +(e.g. library instrumentation for OkHttp). + +### Modern test tooling requires Java 8+ + +Both JUnit 5 and Testcontainers require Java 8+. + +### Auto-instrumentation (Javaagent) + +We could run tests against Java 8+ and ensure Java 7 compliance by using similar animal sniffer +technique as above. + +But bytecode instrumentation tends to be much more sensitive to Java versions than normal code, and +we would lose a lot of confidence in the quality of our Java 7 support without being able to run our +standard tests against it. + +Another option would be to run the "code under test" in a separate JVM from our test harness, which +would allow us to use Java 8 for our test harness (e.g. use JUnit 5 and Testcontainers), while +running our "code under test" inside of Java 7. This is an attractive approach (and e.g. Glowroot +does this, though not to run on older JVMs, but to run with the `-javaagent` flag because I didn't +think about hacking the `-javaagent` flag directly into the test JVM). But this approach does come +with a more complex testing and debugging story due to propagating tests and parameters, and +debugging across two separate JVMs. And new contributor experience +[has a very high priority for this project](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/master/docs/ga-requirements.md#p2) +(compared to say commercial tools who can invest more in onboarding their employees onto a more +complex codebase). + +### Library (manual) instrumentation + +We believe that Java 7 users are primarily in maintenance mode and not interested in cracking open +their code anymore and adding library (manual) instrumentation, so we don't believe there is much +interest in library instrumentation targeting Java 7. + +### Java 7 usage + +Certainly one factor to consider is what percentage of production applications are running Java 7. + +Luckily, New Relic +[published their numbers recently](https://blog.newrelic.com/technology/state-of-java), +so we know that ~2.5% of production applications are still running Java 7 as of March 2020. + +### Alternatives for Java 7 users + +We understand the situations that lead applications to get stuck on Java 7 (we've been there +ourselves), and we agree that those applications need monitoring too. + +Our decision may have been different if those Java 7 users did not have any other alternative +for codeless monitoring, but there are many existing codeless monitoring solutions that still +support Java 7 (both open source and commercial), and probably many of those applications, having +been in production for a long time already, are already using one of those solutions. diff --git a/opentelemetry-java-instrumentation/docs/logger-mdc-instrumentation.md b/opentelemetry-java-instrumentation/docs/logger-mdc-instrumentation.md new file mode 100644 index 000000000..08be0348d --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/logger-mdc-instrumentation.md @@ -0,0 +1,38 @@ +# Logger MDC auto-instrumentation + +The Mapped Diagnostic Context (MDC) is + +> an instrument for distinguishing interleaved log output from different sources. +> — [log4j MDC documentation](http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/MDC.html) + +It contains thread-local contextual information which is later copied to each logging event captured +by a logging library. + +The OTel Java agent injects several pieces of information about the current span into each logging +event's MDC copy: + +- `trace_id` - the current trace id + (same as `Span.current().getSpanContext().getTraceId()`); +- `span_id` - the current span id + (same as `Span.current().getSpanContext().getSpanId()`); +- `trace_flags` - the current trace flags, formatted according to W3C traceflags format + (same as `Span.current().getSpanContext().getTraceFlags().asHex()`). + +Those three pieces of information can be included in log statements produced by the logging library +by specifying them in the pattern/format. Example for Spring Boot configuration (which uses +logback): + +```properties +logging.pattern.console = %d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %n +``` + +This way any services or tools that parse the application logs can correlate traces/spans with log +statements. + +## Supported logging libraries + +| Library | Version | +|---------|---------| +| Log4j 1 | 1.2+ | +| Log4j 2 | 2.7+ | +| Logback | 1.0+ | diff --git a/opentelemetry-java-instrumentation/docs/manual-instrumentation.md b/opentelemetry-java-instrumentation/docs/manual-instrumentation.md new file mode 100644 index 000000000..3323796c5 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/manual-instrumentation.md @@ -0,0 +1,121 @@ +# Manual instrumentation + +For most users, the out-of-the-box instrumentation is completely sufficient and nothing more has to +be done. Sometimes, however, users wish to add attributes to the otherwise automatic spans, +or they might want to manually create spans for their own custom code. + +## Contents + +- [Manual instrumentation](#manual-instrumentation) +- [Dependencies](#dependencies) + * [Maven](#maven) + * [Gradle](#gradle) +- [Adding attributes to the current span](#adding-attributes-to-the-current-span) +- [Creating spans around methods with `@WithSpan`](#creating-spans-around-methods-with-withspan) + * [Suppressing `@WithSpan` instrumentation](#suppressing-withspan-instrumentation) + * [Creating spans around methods with `otel.instrumentation.methods.include`](#creating-spans-around-methods-with-otelinstrumentationmethodsinclude) +- [Creating spans manually with a Tracer](#creating-spans-manually-with-a-tracer) + +# Dependencies + +> :warning: prior to version 1.0.0, `opentelemetry-javaagent-all.jar` +only supports manual instrumentation using the `opentelemetry-api` version with the same version +number as the Java agent you are using. Starting with 1.0.0, the Java agent will start supporting +multiple (1.0.0+) versions of `opentelemetry-api`. + +You'll need to add a dependency on the `opentelemetry-api` library to get started; if you intend to +use the `@WithSpan` annotation, also include the `opentelemetry-extension-annotations` dependency. + +## Maven + +```xml + + + io.opentelemetry + opentelemetry-api + 1.0.0 + + + io.opentelemetry + opentelemetry-extension-annotations + 1.0.0 + + +``` + +## Gradle + +```groovy +dependencies { + implementation('run.mone:opentelemetry-api:1.0.0') + implementation('run.mone:opentelemetry-extension-annotations:1.0.0') +} +``` + +# Adding attributes to the current span + +A common need when instrumenting an application is to capture additional application-specific or +business-specific information as additional attributes to an existing span from the automatic +instrumentation. Grab the current span with `Span.current()` and use the `setAttribute()` +methods: + +```java +import io.opentelemetry.api.trace.Span; + +// ... + +Span span = Span.current(); +span.setAttribute(..., ...); +``` + +# Creating spans around methods with `@WithSpan` + +Another common situation is to capture a span corresponding to one of your methods. The +`@WithSpan` annotation makes this straightforward: + +```java +import io.opentelemetry.extension.annotations.WithSpan; + +public class MyClass { + @WithSpan + public void MyLogic() { + <...> + } +} +``` + +Each time the application invokes the annotated method, it creates a span that denote its duration +and provides any thrown exceptions. Unless specified as an argument to the annotation, the span name +will be `.`. + +## Suppressing `@WithSpan` instrumentation + +Suppressing `@WithSpan` is useful if you have code that is over-instrumented using `@WithSpan` +and you want to suppress some of them without modifying the code. + +| System property | Environment variable | Purpose | +|---------------------------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| otel.instrumentation.opentelemetry-annotations.exclude-methods | OTEL_INSTRUMENTATION_OPENTELEMETRY_ANNOTATIONS_EXCLUDE_METHODS | Suppress `@WithSpan` instrumentation for specific methods. +Format is "my.package.MyClass1[method1,method2];my.package.MyClass2[method3]" | + +## Creating spans around methods with otel.instrumentation.methods.include +This is a way to to create a span around a first-party code method without using `@WithSpan`. + +| System property | Environment variable | Purpose | +|---------------------------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| otel.instrumentation.methods.include | | Add instrumentation for specific methods in lieu of `@WithSpan`. +Format is "my.package.MyClass1[method1,method2];my.package.MyClass2[method3]" | + +# Creating spans manually with a Tracer + +If `@WithSpan` doesn't work for your specific use case, you're still in luck! + +The underlying OpenTelemetry API allows you to [obtain a tracer](https://github.com/open-telemetry/opentelemetry-java/blob/main/QUICKSTART.md#tracing) +that can be used to [manually create spans](https://github.com/open-telemetry/opentelemetry-java/blob/main/QUICKSTART.md#create-a-basic-span) +and execute code within the scope of that span. + +See the [OpenTelemetry Java +QuickStart](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md#tracing) +for a detailed en example of how to configure OpenTelemetry with code and +how to use the `Tracer`, `Scope` and `Span` interfaces to +instrument your application. diff --git a/opentelemetry-java-instrumentation/docs/misc/inter-thread-context-propagation.md b/opentelemetry-java-instrumentation/docs/misc/inter-thread-context-propagation.md new file mode 100644 index 000000000..b12aaf9fa --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/misc/inter-thread-context-propagation.md @@ -0,0 +1,106 @@ +# The story of context propagation across threads + +## The need +Take a look at the following two pseudo-code snippets (see below for explanations). + +``` +Executor pool = Executors.newFixedThreadPool(10) + +public void doGet(HttpServletRequest request, HttpServletResponse response) { + Future f1 = pool.submit(() -> { + return userRepository.queryShippingAddress(requet) + }) + Future f2 = pool.submit(() -> { + return warehouse.currentState(requet) + }) + writeResponse(response, f1.get(), f2.get()) +} +``` + +``` +Executor pool = Executors.newFixedThreadPool(10) + +public void doGet(HttpServletRequest request, HttpServletResponse response) { + final AsyncContext acontext = request.startAsync(); + acontext.start(() -> { + String address = userRepository.queryShippingAddress(requet) + HttpServletResponse response = acontext.getResponse(); + writeResponse(response, address) + acontext.complete(); + } +} +``` + +In both cases request processing requires some potentially long operation and application developer +wants to do them off the main thread. In the first case this hand-off between request accepting thread +and request processing thread happens manually, by submitting work into some thread pool. +In the second case it is the framework that handles separate thread pool and passing work to it. + +In cases like this proper tracing solution should still combine into a single trace all the work +required for request processing, regardless in what thread that work happened. With proper +parent-child relationship between span: span representing shipping address query should be the child +of the span which denotes accepting HTTP request. + +## The solution +Java auto instrumentation uses an obvious solution to the requirement above: we attach current execution +context (represented in the code by `Context`) with each `Runnable`, `Callable` and `ForkJoinTask`. +"Current" means the context active on the thread which calls `Executor.execute` (and its analogues +such as `submit`, `invokeAll` etc) at the moment of that call. Whenever some other thread starts +actual execution of that `Runnable` (or `Callable` or `ForkJoinTask`), that context get restored +on that thread for the duration of the execution. This can be illustrated by the following pseudo-code: + +``` + var job = () -> { + try(Scope scope = this.context.makeCurrent()) { + return userRepository.queryShippingAddress(requet) + }} + job.context = Context.current() + Future f1 = pool.submit() + +``` + +## The drawback +Here is a simplified example of what async servlet processing may look like +``` +protected void service(HttpServletRequest req, HttpServletResponse resp) { + //This method is instrumented and we start new scope here + AsyncContext context = req.startAsync() + // When the runnable below is being submitted by servlet engine to an executor service + // it will capture the current context (together with the current span) with it + context.start { + // When Runnable starts, we reactive the captured context + // So this method is executed with the same context as the original "service" method + resp.writer.print("Hello world!") + context.complete() + } +} +``` +If we now take a look inside `context.complete` method from above it may be implemented like this: + +``` +//Here we still have the same context from above active +//It gets attached to this new runnable +pool.submit(new AcceptRequestRunnable() { +// The same context from above is propagated here as well +// Thus new reqeust processing will start while having a context active with some span inside +// That span will be used as parent spans for new spans created for a new request + ... +}) +``` + +This means that mechanism described in the previous section will propagate the execution context +of one request processing to a thread accepting some next, unrelated, request. +This will result in spans representing the accepting and processing of the second request will join +the same trace as those of the first span. This mistakenly correlates unrelated requests and may lead +to huge traces being active for hours and hours. + +In addition this makes some of our tests extremely flaky. + +## The currently accepted trade-offs +We acknowledge the problem with too active context propagation. We still think that out of the box +support for asynchronous multi-threaded traces is very important. We have diagnostics in place to +help us with detecting when we too eagerly propagate the execution context too far. We hope to +gradually find framework-specific countermeasures to such problem and solve them one by one. + +In the meantime, processing new incoming request in the given JVM and creating new `SERVER` span +always starts with a clean context. diff --git a/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-1.png b/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-1.png new file mode 100644 index 000000000..00946edca Binary files /dev/null and b/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-1.png differ diff --git a/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-1.vsdx b/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-1.vsdx new file mode 100644 index 000000000..558674346 Binary files /dev/null and b/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-1.vsdx differ diff --git a/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-2.png b/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-2.png new file mode 100644 index 000000000..2f185cb6b Binary files /dev/null and b/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-2.png differ diff --git a/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-2.vsdx b/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-2.vsdx new file mode 100644 index 000000000..661161c36 Binary files /dev/null and b/opentelemetry-java-instrumentation/docs/misc/interop-design/alt-design-2.vsdx differ diff --git a/opentelemetry-java-instrumentation/docs/misc/interop-design/design.png b/opentelemetry-java-instrumentation/docs/misc/interop-design/design.png new file mode 100644 index 000000000..b9f4c4024 Binary files /dev/null and b/opentelemetry-java-instrumentation/docs/misc/interop-design/design.png differ diff --git a/opentelemetry-java-instrumentation/docs/misc/interop-design/design.vsdx b/opentelemetry-java-instrumentation/docs/misc/interop-design/design.vsdx new file mode 100644 index 000000000..a04320ac3 Binary files /dev/null and b/opentelemetry-java-instrumentation/docs/misc/interop-design/design.vsdx differ diff --git a/opentelemetry-java-instrumentation/docs/misc/interop-design/interop-design.md b/opentelemetry-java-instrumentation/docs/misc/interop-design/interop-design.md new file mode 100644 index 000000000..1d675b679 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/misc/interop-design/interop-design.md @@ -0,0 +1,133 @@ +# Interoperability design + +## Problem statement + +These two things must seamlessly interoperate: + +* Instrumentation provided by the Java agent +* Instrumentation provided by the user app, using any 1.0+ version of the OpenTelemetry API + +## Design + +![Design](design.png) + +The orange components above may or may not be present, depending on whether the user app uses +the OpenTelemetry API (either directly or transitively via an instrumented library). + +The rest of this section describes the components in the diagram above. + +**Instrumented Libraries** - libraries that emit telemetry themselves directly +using the OpenTelemetry API. + +**Versioned Bridge** - a bridge between the version of the OpenTelemetry API +brought by the user app, and the (shaded) OpenTelemetry API used internally by the Java agent. +The Java agent will rewrite io.opentelemetry.api.OpenTelemetry via bytecode instrumentation +so that users will get the versioned bridge as their implementation of the OpenTelemetry API. +In order to implement the OpenTelemetry API brought by the user app, +the versioned bridge needs to be injected into the class loader where that OpenTelemetry API lives. +The Java agent will inject an appropriate version of the bridge +that supports the version of the OpenTelemetry API brought by the user. +If the Java agent does not recognize the version of the OpenTelemetry API brought by the user app, +then it will not inject one (e.g. running an old version of the Java agent with a new version +of OpenTelemetry API). + +**Bytecode Instrumentation** - bytecode instrumentation of well-known libraries. +This instrumentation needs to be injected into the class loader where the given library lives +(which could be the bootstrap class loader in cases like HttpURLConnection instrumentation). +In general, bytecode instrumentation will not be applied to libraries that are already instrumented +with OpenTelemetry API, e.g. if a future version of MongoDB emits telemetry via OpenTelemetry API, +then bytecode instrumentation will not be applied to those versions of MongoDB. + +**Internal OpenTelemetry API** - we want the bytecode instrumentation to bind directly +to the OpenTelemetry API so that we can share code between bytecode and user/library instrumentation +where possible. +But the bytecode instrumentation can be injected into any class loader (e.g. HttpURLConnection), +and we cannot put OpenTelemetry API directly (unmodified) into the bootstrap class loader, +or it will cause version conflicts with user-brought OpenTelemetry API versions, +and so we must shade[1] the internal OpenTelemetry API. + +**Internal OpenTelemetry SDK** - unlike the internal OpenTelemetry API, this does not need to live +in the bootstrap class loader, and thus can live in an isolated class loader to avoid conflict +with user-brought OpenTelemetry SDK (advantage of isolated class loader is to reduce shading +and attack surface area). +But it must still be partially shaded to match the shading of the internal OpenTelemetry API. +By default, this will be the standard OpenTelemetry SDK, but different vendors could replace this +with their own OpenTelemetry SDK implementation for advanced use cases. + +**Internal OpenTelemetry Exporter** - same as above, this will live in an isolated class loader +in order to reduce shading and attack surface area, but still must be partially shaded +to match the shading of the internal OpenTelemetry API. +Different vendors will provide their own exporter here. +There may be an option for users to bring their own (unshaded) OpenTelemetry Exporter +and have the agent perform the required shading on the fly. + +#### Open Questions + +If there are multiple apps running in the same JVM, how to distinguish between them? + +#### Risks + +User code could cast OpenTelemetry API objects to the underlying OpenTelemetry SDK classes, +which would throw ClassCastException if it finds the Versioned Bridge class instead. +This may lead us to apply the Versioned Bridge as bytecode instrumentation +on the user-brought OpenTelemetry SDK, which is not as clean, but would avoid this issue. + +Users who have configured the underlying SDK / exporter could be surprised +that the Java agent takes over, and their configuration work / exporter is not carried over. + +## Alternate Design 1 + +If the user brings the OpenTelemetry API and the OpenTelemetry SDK, +the bytecode instrumentation could use the OpenTelemetry API brought by the user: + +![Alternate Design 1](alt-design-1.png) + +The Java agent could check which version of the OpenTelemetry API was brought by the user, +and only apply the bytecode instrumentation if it's compatible +with that version of the OpenTelemetry API. + +#### Advantages + +Users who have performed programmatic configuration of the SDK / exporter +do not lose that configuration. + +If there are multiple apps running in the same JVM, it's easy to distinguish them +since they can each have their own SDK / exporter configuration. + +#### Disadvantages + +Classes outside of the user app cannot be instrumented since those classes do not have access +to the OpenTelemetry API brought by the user app. This includes classes brought by the JVM +(e.g. HttpURLConnection, java.util.logging, ExecutorService) +and the application server (e.g. HttpServlet and other Java EE classes). + +## Alternate Design 2 + +The above disadvantage is pretty serious, so more likely +if we want the bytecode instrumentation to use the OpenTelemetry API brought by the user, +we would end up with something more like this: + +![Alternate Design 2](alt-design-2.png) + +This would work by having new server/consumer spans set the Class Loader Bridge +(pointing to the OpenTelemetry API brought by the user) into a ThreadLocal, +so that instrumentation on all classes during that span would be able to look up the bridge +and emit telemetry. + +#### Advantages + +Same as Alternate Design 1 above + +#### Disadvantages + +Bytecode instrumentation on classes outside of the user app cannot emit any telemetry +if there is not already an active span. This includes classes brought by the JVM +(e.g. HttpURLConnection, java.util.logging, ExecutorService) +and the application server (e.g. HttpServlet and other Java EE classes). + +## Terminology + +[1] Shading dependencies is the process of including and renaming dependencies +(thus relocating the classes & rewriting affected bytecode & resources) +to create a private copy that you bundle alongside your own code +(https://softwareengineering.stackexchange.com/questions/297276/what-is-a-shaded-java-dependency) diff --git a/opentelemetry-java-instrumentation/docs/safety-mechanisms.md b/opentelemetry-java-instrumentation/docs/safety-mechanisms.md new file mode 100644 index 000000000..dfecbb922 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/safety-mechanisms.md @@ -0,0 +1,80 @@ +# OpenTelemetry Java Agent Safety Mechanisms + +This document outlines the safety mechanisms we have in place to have confidence +that the Java agent can be attached to a user's application with a very low chance of +affecting it negatively, for example introducing crashes. + +## Instrumentation tests + +All instrumentation are written with instrumentation tests - these can be considered the unit tests +of this project. + +Instrumentation tests are run using a fully shaded `-javaagent` in order to perform the same bytecode +instrumentation as when the agent is run against a normal app. +By then exercising the instrumented library in a way a user would, for example by issuing requests +from an HTTP client, we can assert on the spans that should be generated, including their semantic +attributes. A problem in the instrumentation will generally cause spans to be reported incorrectly +or not reported at all, and we can find these situations with the instrumentation tests. + +## Latest dep tests + +Instrumentation tests are generally run against the lowest version of a library that we support +to ensure a baseline against users with old dependency versions. Due to the nature of the agent +and locations where we instrument private APIs, the agent may fail on a newly released version +of the library. We run instrumentation tests additionally against the latest version of the +library, as fetched from Maven, as part of a nightly build. If a new version of a library will +not work with the agent, we find out through this build and can address it by the next release +of the agent. + +## Muzzle compile time checks + +Muzzle is the tool we use to ensure we do not apply agent instrumentation if it would break the +user's app. Details on its implementation can be found [here](./contributing/muzzle.md). + +Continuous build runs a muzzle compile time check for every library. This check will select random +versions of the library available in Maven and check if our agent will cleanly apply to it. The +check collects all references that the agent code makes, e.g., classes that are used and methods that +are called, and verifies the references exist in that version of the library. This is important +because if we apply the agent with missing references, it will generally cause crashes in the user's +app such as `NoSuchMethodError`. We cannot check every single version of every library in every build, it +would be too slow and wasteful of contributed resources. But by selecting random versions every +build, over time we can be confident that we know the agent can be used on all versions of a library +without causing linkage errors due to missing references. + +## Muzzle runtime checks + +The set of references from the agent used at Muzzle during compile time is also stored in the agent's +code itself. Similar to the compile time check, we also do a validation of the references available +in the user's app vs what is referenced by the agent instrumentation. If the references do not match +up, we will not load the instrumentation at runtime, preventing applying instrumentation that could +potentially cause linkage errors. + +## Classloader separation + +See more detail about the classloader separation [here](./contributing/javaagent-jar-components.md). + +The Java agent makes sure to include as little code as possible in the user app's classloader, and +all code that is included is either unique to the agent itself or shaded in the agent build. This is +because if the agent included classes that are also used by the user's app and there was a version +mismatch, it could cause linkage crashes. + +Instead of executing code in the app's classloader, the agent has its own agent classloader where +instrumentation is loaded and exporters and the SDK is configured. Only when applying an +instrumentation (which will have passed Muzzle runtime checks) do we inject any additional classes +that are needed by the instrumentation into the user's classloader. These classes are always either +unique to the agent or shaded versions of public libraries such as our library instrumentation +modules and cannot cause version conflicts. + +To ensure agent classes are not automatically loaded into the user's classloader, possibly by an +eager loading application server, they are hidden in the agent JAR as standard, non-Java files. +All packages are moved into a subdirectory `inst` and all classes are renamed from `.class` to +`.classdata`, ensuring applications with any sort of automatic classpath scanning will not find +agent classes. The agent classloader understands this convention and unobfuscates when loading +classes. + +## Smoke tests + +We run docker-based smoke tests which have simple instrumented apps running under various JVMs +and application servers. In particular, application servers sometimes have fragile behavior using +internal details of the JVM which an agent can cause problems with. Smoke tests ensure compatibility +with a wide variety of application servers. diff --git a/opentelemetry-java-instrumentation/docs/scope.md b/opentelemetry-java-instrumentation/docs/scope.md new file mode 100644 index 000000000..1a22a0871 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/scope.md @@ -0,0 +1,10 @@ +### Scope of this repository + +Both javaagent and library-based approaches to the following: + +* Instrumentation for specific Java libraries and frameworks + * Emitting spans and metrics (and in the future logs) +* System metrics +* MDC logging integrations + * Encoding traceId/spanId into logs +* Spring Boot starters diff --git a/opentelemetry-java-instrumentation/docs/semantic-conventions.md b/opentelemetry-java-instrumentation/docs/semantic-conventions.md new file mode 100644 index 000000000..ceb237a7b --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/semantic-conventions.md @@ -0,0 +1,102 @@ +# Semantic conventions + +This document describes which [OpenTelemetry Semantic Conventions](https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/trace/semantic_conventions) +are implemented by Java autoinstrumentation and which ones are not. + +## Http Server + +| Attribute | Required | Implemented? | +|---|:---:|:---:| +| `http.method` | Y | + | +| `http.url` | N | + | +| `http.target` | N | - [1] | +| `http.host` | N | - [1] | +| `http.scheme` | N | - [1] | +| `http.status_code` | Y | + | +| `http.flavor` | N | + [3] | +| `http.user_agent` | N | + | +| `http.request_content_length` | N | - | +| `http.request_content_length_uncompressed` | N | - | +| `http.response_content_length` | N | - | +| `http.response_content_length_uncompressed` | N | - | +| `http.server_name` | N | - | +| `http.route` | N | - | +| `http.client_ip` | N | + | + +**[1]:** As the majority of Java frameworks don't provide a standard way to obtain "The full request +target as passed in a HTTP request line or equivalent.", we don't set `http.target` semantic +attribute. As either it or `http.url` is required, we set the latter. This, in turn, makes setting +`http.schema` and `http.host` unnecessary duplication. Therefore, we do not set them as well. + +**[3]:** In case of Armeria, return values are [SessionProtocol](https://github.com/line/armeria/blob/master/core/src/main/java/com/linecorp/armeria/common/SessionProtocol.java), +not values defined by spec. + + +## Http Client + +| Attribute | Required | Implemented? | +|---|:---:|:---:| +| `http.method` | Y | + | +| `http.url` | N | + | +| `http.target` | N | - [1] | +| `http.host` | N | - [1] | +| `http.scheme` | N | - [1] | +| `http.status_code` | Y | + | +| `http.flavor` | N | + [3] | +| `http.user_agent` | N | + | +| `http.request_content_length` | N | - | +| `http.request_content_length_uncompressed` | N | - | +| `http.response_content_length` | N | - | +| `http.response_content_length_uncompressed` | N | - | + +**[1]:** As the majority of Java frameworks don't provide a standard way to obtain "The full request +target as passed in a HTTP request line or equivalent.", we don't set `http.target` semantic +attribute. As either it or `http.url` is required, we set the latter. This, in turn, makes setting +`http.schema` and `http.host` unnecessary duplication. Therefore, we do not set them as well. + +**[3]:** In case of Armeria, return values are [SessionProtocol](https://github.com/line/armeria/blob/master/core/src/main/java/com/linecorp/armeria/common/SessionProtocol.java), +not values defined by spec. + +## RPC + +| Attribute | Required | Implemented? | +| -------------- | :---: | :---: | +| `rpc.system` | Y | + | +| `rpc.service` | N | + | +| `rpc.method` | N | + | + +## Database + +| Attribute | Required | Implemented? | +| -------------- | :---: | :---: | +| `db.system` | Y | + | +| `db.connection_string` | N | only set for Redis, JDBC and MongoDB | +| `db.user` | N | only set for JDBC| +| `db.jdbc.driver_classname` | N | - | +| `db.mssql.instance_name` | N | - | +| `db.name` | N | only set of JDBC, Mongo, Geode and MongoDB | +| `db.statement` | N | +, except for ElasticSearch and Memcached, see `db.operation` | +| `db.operation` | N | only set for ElasticSearch, Memcached and JDBC | +| `db.cassandra.keyspace` | Y | + | +| `db.hbase` | Y | -, HBase is not supported | +| `db.redis.database_index` | N | only set for Lettuce driver, not for Jedis | +| `db.mongodb.collection` | Y | - | + +## Messaging + + Attribute name | Required? | Implemented? | +| -------------- | :-----: | :---: | +| `messaging.system` | Y | + | +| `messaging.destination` | Y | + | +| `messaging.destination_kind` | Y | + | +| `messaging.temp_destination` | N | - | +| `messaging.protocol` | N | - | +| `messaging.protocol_version` | N | - | +| `messaging.url` | N | - | +| `messaging.message_id` | N | only for JMS | +| `messaging.conversation_id` | N | only for JMS | +| `messaging.message_payload_size_bytes` | N | only for RabbitMQ and Kafka [1] | +| `messaging.message_payload_compressed_size_bytes` | N | - | +| `messaging.operation` | for consumers only | + + +**[1]:** Kafka consumer instrumentation sets this to the serialized size of the value diff --git a/opentelemetry-java-instrumentation/docs/standalone-library-instrumentation.md b/opentelemetry-java-instrumentation/docs/standalone-library-instrumentation.md new file mode 100644 index 000000000..d17051020 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/standalone-library-instrumentation.md @@ -0,0 +1,27 @@ +# Standalone library instrumentation + +This repository also publishes standalone instrumentation for several libraries (and growing) +that can be used if you prefer that over using the Java agent: + +* [Apache Dubbo](../instrumentation/apache-dubbo-2.7/library) +* [Armeria](../instrumentation/armeria-1.3/library) +* [AWS Lambda](../instrumentation/aws-lambda-1.0/library) +* [AWS SDK 1.11](../instrumentation/aws-sdk/aws-sdk-1.11/library) +* [AWS SDK 2.2+](../instrumentation/aws-sdk/aws-sdk-2.2/library) +* [gRPC](../instrumentation/grpc-1.6/library) +* [Guava](../instrumentation/guava-10.0/library) +* [Lettuce](../instrumentation/lettuce/lettuce-5.1/library) +* [Log4j](../instrumentation/log4j/log4j-2.13.2/library) +* [Logback](../instrumentation/logback-1.0/library) +* [MongoDB Driver](../instrumentation/mongo/mongo-3.1/library) +* [OkHttp](../instrumentation/okhttp/okhttp-3.0/library) +* [OSHI](../instrumentation/oshi/library) +* [Reactor](../instrumentation/reactor-3.1/library) +* [RocketMQ](../instrumentation/rocketmq-client-4.8/library) +* [Runtime metrics](../instrumentation/runtime-metrics/library) +* [RxJava 1.0](../instrumentation/rxjava/rxjava-1.0/library) +* [RxJava 2.0](../instrumentation/rxjava/rxjava-2.0/library) +* [RxJava 3.0](../instrumentation/rxjava/rxjava-3.0/library) +* [Spring RestTemplate](../instrumentation/spring/spring-web-3.1/library) +* [Spring Web MVC](../instrumentation/spring/spring-webmvc-3.1/library) +* [Spring WebFlux Client](../instrumentation/spring/spring-webflux-5.0/library) diff --git a/opentelemetry-java-instrumentation/docs/supported-libraries.md b/opentelemetry-java-instrumentation/docs/supported-libraries.md new file mode 100644 index 000000000..3b317422f --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/supported-libraries.md @@ -0,0 +1,151 @@ + +# Supported libraries, frameworks, application servers, and JVMs + +We automatically instrument and support a huge number of libraries, frameworks, +and application servers... right out of the box! + +Don't see your favorite tool listed here? Consider [filing an issue](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues), +or [contributing](../CONTRIBUTING.md). + +## Contents + + * [Libraries / Frameworks](#libraries--frameworks) + * [Application Servers](#application-servers) + * [JVMs and Operating Systems](#jvms-and-operating-systems) + * [Disabled instrumentations](#disabled-instrumentations) + + [Grizzly instrumentation](#grizzly-instrumentation) + +## Libraries / Frameworks + +These are the supported libraries and frameworks: + +| Library/Framework | Versions | +|---------------------------------------------------------------------------------------------------------------------------------------|--------------------------------| +| [Akka HTTP](https://doc.akka.io/docs/akka-http/current/index.html) | 10.0+ | +| [Apache Axis2](https://axis.apache.org/axis2/java/core/) | 1.6+ | +| [Apache CXF JAX-RS](https://cxf.apache.org/) | 3.2+ | +| [Apache CXF JAX-RS Client](https://cxf.apache.org/) | 3.0+ | +| [Apache CXF JAX-WS](https://cxf.apache.org/) | 3.0+ | +| [Apache Dubbo](https://github.com/apache/dubbo/) | 2.7+ (not including 3.x yet) | +| [Apache HttpAsyncClient](https://hc.apache.org/index.html) | 4.1+ | +| [Apache HttpClient](https://hc.apache.org/index.html) | 2.0+ | +| [Apache RocketMQ](https://rocketmq.apache.org/) | 4.8+ | +| [Apache Tapestry](https://tapestry.apache.org/) | 5.4+ | +| [Apache Wicket](https://wicket.apache.org/) | 8.0+ | +| [Armeria](https://armeria.dev) | 1.3+ | +| [AsyncHttpClient](https://github.com/AsyncHttpClient/async-http-client) | 1.9+ | +| [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html) | 1.0+ | +| [AWS SDK](https://aws.amazon.com/sdk-for-java/) | 1.11.x and 2.2.0+ | +| [Cassandra Driver](https://github.com/datastax/java-driver) | 3.0+ | +| [Couchbase Client](https://github.com/couchbase/couchbase-java-client) | 2.0+ and 3.1+ | +| [Dropwizard Views](https://www.dropwizard.io/en/latest/manual/views.html) | 0.7+ | +| [Elasticsearch API](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) | 5.0+ | +| [Elasticsearch REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html) | 5.0+ | +| [Finatra](https://github.com/twitter/finatra) | 2.9+ | +| [Geode Client](https://geode.apache.org/) | 1.4+ | +| [Grails](https://grails.org/) | 3.0+ | +| [Google HTTP Client](https://github.com/googleapis/google-http-java-client) | 1.19+ | +| [Grizzly](https://javaee.github.io/grizzly/httpserverframework.html) | 2.0+ (disabled by default) | +| [gRPC](https://github.com/grpc/grpc-java) | 1.6+ | +| [GWT](http://www.gwtproject.org/) | 2.0+ | +| [Hibernate](https://github.com/hibernate/hibernate-orm) | 3.3+ | +| [HttpURLConnection](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpURLConnection.html) | Java 8+ | +| [http4k ](https://www.http4k.org/guide/modules/opentelemetry/) | 3.270.0+ | +| [Hystrix](https://github.com/Netflix/Hystrix) | 1.4+ | +| [JAX-RS](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/package-summary.html) | 0.5+ | +| [JAX-RS Client](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html) | 2.0+ | +| [JAX-WS](https://jakarta.ee/specifications/xml-web-services/2.3/apidocs/javax/xml/ws/package-summary.html) | 2.0+ (not including 3.x yet) | +| [Java Http Client](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html) | Java 11+ | +| [JDBC](https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/package-summary.html) | Java 8+ | +| [Jedis](https://github.com/xetorthio/jedis) | 1.4+ | +| [Jersey](https://eclipse-ee4j.github.io/jersey/) | 2.0+ (not including 3.x yet) | +| [JMS](https://javaee.github.io/javaee-spec/javadocs/javax/jms/package-summary.html) | 1.1+ | +| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | +| [Kafka](https://kafka.apache.org/20/javadoc/overview-summary.html) | 0.11+ | +| [khttp](https://khttp.readthedocs.io) | 0.1+ | +| [Kubernetes Client](https://github.com/kubernetes-client/java) | 7.0+ | +| [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | +| [Log4j 1](https://logging.apache.org/log4j/1.2/) | 1.2+ | +| [Log4j 2](https://logging.apache.org/log4j/2.x/) | 2.7+ | +| [Logback](http://logback.qos.ch/) | 1.0+ | +| [Metro](https://projects.eclipse.org/projects/ee4j.metro) | 2.2+ (not including 3.x yet) | +| [Mojarra](https://projects.eclipse.org/projects/ee4j.mojarra) | 1.2+ (not including 3.x yet) | +| [MongoDB Driver](https://mongodb.github.io/mongo-java-driver/) | 3.1+ | +| [MyFaces](https://myfaces.apache.org/) | 1.2+ (not including 3.x yet) | +| [Netty](https://github.com/netty/netty) | 3.8+ | +| [OkHttp](https://github.com/square/okhttp/) | 3.0+ | +| [Play](https://github.com/playframework/playframework) | 2.4+ (not including 2.8.x yet) | +| [Play WS](https://github.com/playframework/play-ws) | 1.0+ | +| [RabbitMQ Client](https://github.com/rabbitmq/rabbitmq-java-client) | 2.7+ | +| [Ratpack](https://github.com/ratpack/ratpack) | 1.4+ | +| [Reactor](https://github.com/reactor/reactor-core) | 3.1+ | +| [Reactor Netty](https://github.com/reactor/reactor-netty) | 0.9+ | +| [Rediscala](https://github.com/etaty/rediscala) | 1.8+ | +| [Redisson](https://github.com/redisson/redisson) | 3.0+ | +| [RESTEasy](https://resteasy.github.io/) | 3.0+ | +| [RMI](https://docs.oracle.com/en/java/javase/11/docs/api/java.rmi/java/rmi/package-summary.html) | Java 8+ | +| [RxJava](https://github.com/ReactiveX/RxJava) | 1.0+ | +| [Servlet](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/package-summary.html) | 2.2+ | +| [Spark Web Framework](https://github.com/perwendel/spark) | 2.3+ | +| [Spring Batch](https://spring.io/projects/spring-batch) | 3.0+ | +| [Spring Data](https://spring.io/projects/spring-data) | 1.8+ | +| [Spring Scheduling](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/package-summary.html) | 3.1+ | +| [Spring Web MVC](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/package-summary.html) | 3.1+ | +| [Spring Webflux](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/package-summary.html) | 5.0+ | +| [Spring Web Services](https://spring.io/projects/spring-ws) | 2.0+ | +| [Spymemcached](https://github.com/couchbase/spymemcached) | 2.12+ | +| [Struts2](https://github.com/apache/struts) | 2.3+ | +| [Twilio](https://github.com/twilio/twilio-java) | 6.6+ (not including 8.x yet) | +| [Undertow](https://undertow.io/) | 1.4+ | +| [Vaadin](https://vaadin.com/) | 14.2+ | +| [Vert.x](https://vertx.io) | 3.0+ | +| [Vert.x RxJava2](https://vertx.io/docs/vertx-rx/java2/) | 3.5+ | + + OpenTelemetry support provided by the library + +## Application Servers + +These are the supported application servers: + +| Application server | Version | JVM | OS | +| ----------------------------------------------------------------------------------------- | --------------------------- | ---------------- | ------------------------------ | +| [Glassfish](https://javaee.github.io/glassfish/) | 5.0.x, 5.1.x | OpenJDK 8, 11 | Ubuntu 18, Windows Server 2019 | +| [JBoss EAP](https://www.redhat.com/en/technologies/jboss-middleware/application-platform) | 7.1.x, 7.3.x | OpenJDK 8, 11 | Ubuntu 18, Windows Server 2019 | +| [Jetty](https://www.eclipse.org/jetty/) | 9.4.x, 10.0.x, 11.0.x | OpenJDK 8, 11 | Ubuntu 20 | +| [Payara](https://www.payara.fish/) | 5.0.x, 5.1.x | OpenJDK 8, 11 | Ubuntu 18, Windows Server 2019 | +| [Tomcat](http://tomcat.apache.org/) | 7.0.x, 8.5.x, 9.0.x, 10.0.x | OpenJDK 8, 11 | Ubuntu 18 | +| [TomEE](https://tomee.apache.org/) | 7.x, 8.x | OpenJDK 8, 11 | Ubuntu 18 | +| [Weblogic](https://www.oracle.com/java/weblogic/) | 12 | Oracle JDK 8 | Oracle Linux 7, 8 | +| [Weblogic](https://www.oracle.com/java/weblogic/) | 14 | Oracle JDK 8, 11 | Oracle Linux 7, 8 | +| [Websphere Liberty Profile](https://www.ibm.com/cloud/websphere-liberty) | 20.0.0.12 | OpenJDK 8, 11 | Ubuntu 18, Windows Server 2019 | +| [WildFly](https://www.wildfly.org/) | 13.0.x | OpenJDK 8 | Ubuntu 18, Windows Server 2019 | +| [WildFly](https://www.wildfly.org/) | 17.0.1, 21.0.0 | OpenJDK 8, 11 | Ubuntu 18, Windows Server 2019 | + +## JVMs and operating systems + +These are the supported JVM version and OS configurations which the javaagent is tested on: + +| JVM | Versions | OS | +| ------------------------------------------------- | --------- | ------------------------------ | +| [AdoptOpenJDK Hotspot](https://adoptopenjdk.net/) | 8, 11, 15 | Ubuntu 18, Windows Server 2019 | +| [AdoptOpenJDK OpenJ9](https://adoptopenjdk.net/) | 8, 11, 15 | Ubuntu 18, Windows Server 2019 | + +## Disabled instrumentations + +Some instrumentations can produce too many spans and make traces very noisy. +For this reason, the following instrumentations are disabled by default: + +- `jdbc-datasource` which creates spans whenever the `java.sql.DataSource#getConnection` method is called. + +To enable them, add the `otel.instrumentation..enabled` system property: +`-Dotel.instrumentation.jdbc-datasource.enabled=true` + +### Grizzly instrumentation + +When you use +[Grizzly](https://javaee.github.io/grizzly/httpserverframework.html) for +Servlet-based applications, you get better experience from Servlet-specific +support. As these two instrumentations conflict with each other, more generic +instrumentation for Grizzly HTTP server is disabled by default. If needed, +you can enable it by adding the following system property: +`-Dotel.instrumentation.grizzly.enabled=true` diff --git a/opentelemetry-java-instrumentation/docs/suppressing-instrumentation.md b/opentelemetry-java-instrumentation/docs/suppressing-instrumentation.md new file mode 100644 index 000000000..e86c0fc79 --- /dev/null +++ b/opentelemetry-java-instrumentation/docs/suppressing-instrumentation.md @@ -0,0 +1,121 @@ +## Disabling the agent entirely + +You can disable the agent using `-Dotel.javaagent.enabled=false` +(or using the equivalent environment variable `OTEL_JAVAAGENT_ENABLED=false`). + +## Suppressing specific agent instrumentation + +You can suppress agent instrumentation of specific libraries by using +`-Dotel.instrumentation.[name].enabled=false` where `name` is the corresponding instrumentation `name`: + +| Library/Framework | Instrumentation name | +|-------------------|----------------------| +| Additional methods tracing | methods | +| Additional tracing annotations | external-annotations | +| Akka Actor | akka-actor| +| Akka HTTP | akka-http| +| Apache Axis2 | axis2| +| Apache Camel | apache-camel| +| Apache Cassandra | cassandra| +| Apache CXF | cxf| +| Apache Dubbo | apache-dubbo| +| Apache Geode | geode| +| Apache HttpAsyncClient | apache-httpasyncclient| +| Apache HttpClient | apache-httpclient| +| Apache Kafka | kafka | +| Apache RocketMQ | rocketmq-client| +| Apache Tapestry | tapestry| +| Apache Tomcat | tomcat| +| Apache Wicket | wicket| +| Armeria | armeria| +| AsyncHttpClient (AHC) | async-http-client| +| AWS Lambda | aws-lambda| +| AWS SDK | aws-sdk| +| Couchbase | couchbase| +| Dropwizard Views | dropwizard-views | +| Eclipse OSGi | eclipse-osgi | +| Elasticsearch client | elasticsearch-transport| +| Elasticsearch REST client | elasticsearch-rest| +| Google Guava | guava| +| Google HTTP client | google-http-client| +| Google Web Toolkit | gwt| +| Grails | grails| +| GRPC | grpc| +| Hibernate | hibernate| +| Java EE Grizzly | grizzly| +| Java HTTP Client | java-http-client | +| Java `HttpURLConnection` | http-url-connection | +| Java JDBC | jdbc | +| Java JDBC `DataSource` | jdbc-datasource | +| Java RMI | rmi| +| Java Servlet | servlet| +| java.util.concurrent | executor | +| JAX-RS (Client) | jaxrs-client| +| JAX-RS (Server) | jaxrs| +| JAX-WS | jaxws| +| JAX-WS Metro | metro| +| Jetty | jetty| +| JMS | jms| +| JSF Mojarra | mojarra| +| JSF MyFaces | myfaces| +| JSP | jsp | +| K8s Client | kubernetes-client| +| Kotlin HTTP (kHttp) | khttp | +| kotlinx.coroutines | kotlinx-coroutines | +| Log4j | log4j| +| Logback | logback| +| MongoDB | mongo | +| Netflix Hystrix | hystrix| +| Netty | netty| +| OkHttp | okhttp| +| OpenLiberty | liberty | +| OpenTelemetry Trace annotations | opentelemetry-annotations | +| OSHI (Operating System and Hardware Information) | oshi | +| Play Framework | play| +| Play WS HTTP Client | play-ws| +| RabbitMQ Client | rabbitmq| +| Ratpack | ratpack| +| ReactiveX RxJava | rxjava2, rxjava3 | +| Reactor | reactor| +| Reactor Netty | reactor-netty| +| Redis Jedis | jedis| +| Redis Lettuce | lettuce| +| Rediscala | rediscala| +| Scala executors | scala-executors | +| Spark Web Framework | spark| +| Spring Core | spring-core| +| Spring Data | spring-data| +| Spring Scheduling | spring-scheduling| +| Spring Webflux | spring-webflux| +| Spring WebMVC | spring-webmvc| +| Spring WS | spring-ws| +| Spymemcached | spymemcached| +| Struts | struts| +| Twilio SDK | twilio| +| Twitter Finatra | finatra| +| Undertow | undertow| +| Vaadin | vaadin| +| Vert.x RxJava2 | vertx | + +### Even more fine-grained control + +You can also exclude specific classes from being instrumented. + +This can be useful to completely silence spans from a given class/package. + +Or as a quick workaround for an instrumentation bug, when byte code in one specific class is problematic. + +This option should not be used lightly, as it can leave some instrumentation partially applied, +which could have unknown side-effects. + +If you find yourself needing to use this, it would be great if you could drop us an issue explaining why, +so that we can try to come up with a better solution to address your need. + +| System property | Environment variable | Purpose | +|--------------------------------|--------------------------------|---------------------------------------------------------------------------------------------------| +| otel.javaagent.exclude-classes | OTEL_JAVAAGENT_EXCLUDE_CLASSES | Suppresses all instrumentation for specific classes, format is "my.package.MyClass,my.package2.*" | + +## Enable manual instrumentation only + +You can suppress all auto instrumentations but have support for manual instrumentation with `@WithSpan` and normal API interactions by using +`-Dotel.instrumentation.common.default-enabled=false -Dotel.instrumentation.opentelemetry-annotations.enabled=true` diff --git a/opentelemetry-java-instrumentation/examples/distro/README.md b/opentelemetry-java-instrumentation/examples/distro/README.md new file mode 100644 index 000000000..1e2d7066a --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/README.md @@ -0,0 +1,61 @@ +## Introduction + +This repository serves as a collection of examples of extending functionality of OpenTelemetry Java instrumentation agent. +It demonstrates how to repackage the aforementioned agent adding custom functionality. +For every extension point provided by OpenTelemetry Java instrumentation, this repository contains an example of +its usage. + +## General structure + +This repository has four main submodules: + +* `custom` contains all custom functionality, SPI and other extensions +* `agent` contains the main repackaging functionality and, optionally, an entry point to the agent, if one wishes to +customize that +* `instrumentation` contains custom instrumentations added by vendor +* `smoke-tests` contains simple tests to verify that resulting agent builds and applies correctly + +## Extensions examples + +* [DemoIdGenerator](custom/src/main/java/com/example/javaagent/DemoIdGenerator.java) - custom `IdGenerator` +* [DemoPropagator](custom/src/main/java/com/example/javaagent/DemoPropagator.java) - custom `TextMapPropagator` +* [DemoPropertySource](custom/src/main/java/com/example/javaagent/DemoPropertySource.java) - default configuration +* [DemoSampler](custom/src/main/java/com/example/javaagent/DemoSampler.java) - custom `Sampler` +* [DemoSpanProcessor](custom/src/main/java/com/example/javaagent/DemoSpanProcessor.java) - custom `SpanProcessor` +* [DemoSpanExporter](custom/src/main/java/com/example/javaagent/DemoSpanExporter.java) - custom `SpanExporter` +* [DemoServlet3InstrumentationModule](instrumentation/servlet-3/src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java) - additional instrumentation + +## Instrumentation customisation + +There are several options to override or customise instrumentation provided by the upstream agent. +The following description follows one specific use-case: + +> Instrumentation X from Otel distribution creates span that I don't like and I want to change it in my vendor distro. + +As an example, let us take some database client instrumentation that creates a span for database call +and extracts data from db connection to provide attributes for that span. + +### I don't want this span at all +The easiest case. You can just pre-configure your distribution and disable given instrumentation. + +### I want to add/modify some attributes and their values does NOT depend on a specific db connection instance. +E.g. you want to add some data from call stack as span attribute. +In this case just provide your custom `SpanProcessor`. +No need for touching instrumentation itself. + +### I want to add/modify some attributes and their values depend on a specific db connection instance. +Write a _new_ instrumentation which injects its own advice into the same method as the original one. +Use `getOrder` method to ensure it is run after the original instrumentation. +Now you can augment current span with new information. + +See [DemoServlet3Instrumentation](instrumentation/servlet-3/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java). + +### I want to remove some attributes +Write custom exporter or use attribute filtering functionality in Collector. + +### I don't like Otel span at all. I want to significantly modify it and its lifecycle +Disable existing instrumentation. +Write a new one, which injects `Advice` into the same (or better) method as the original instrumentation. +Write your own `Advice` for this. +Use existing `Tracer` directly or extend it. +As you have your own `Advice`, you can control which `Tracer` you use. diff --git a/opentelemetry-java-instrumentation/examples/distro/agent/build.gradle b/opentelemetry-java-instrumentation/examples/distro/agent/build.gradle new file mode 100644 index 000000000..b1adb6210 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/agent/build.gradle @@ -0,0 +1,60 @@ +plugins { + id("com.github.johnrengelman.shadow") version "6.0.0" +} + +apply from: "$rootDir/gradle/shadow.gradle" + +def relocatePackages = ext.relocatePackages + +configurations { + customShadow +} + +dependencies { + customShadow project(path: ":custom", configuration: "shadow") + customShadow project(path: ":instrumentation", configuration: "shadow") + implementation "io.opentelemetry.javaagent:opentelemetry-javaagent:${versions.opentelemetryJavaagent}:all" +} + +CopySpec isolateSpec() { + return copySpec { + configurations.customShadow.files.each { + from(zipTree(it)) { + into("inst") + rename("(^.*)\\.class\$", "\$1.classdata") + } + } + } +} + + +tasks { + shadowJar { + dependsOn ':custom:shadowJar' + dependsOn ':instrumentation:shadowJar' + with isolateSpec() + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + mergeServiceFiles { + include("inst/META-INF/services/*") + } + exclude("**/module-info.class") + + relocatePackages(it) + + manifest { + attributes.put("Main-Class", "io.opentelemetry.javaagent.OpenTelemetryAgent") + attributes.put("Agent-Class", "io.opentelemetry.javaagent.OpenTelemetryAgent") + attributes.put("Premain-Class", "io.opentelemetry.javaagent.OpenTelemetryAgent") + attributes.put("Can-Redefine-Classes", "true") + attributes.put("Can-Retransform-Classes", "true") + attributes.put("Implementation-Vendor", "Demo") + attributes.put("Implementation-Version", "demo-${project.version}-otel-${versions.opentelemetryJavaagent}") + } + } + + assemble { + dependsOn(shadowJar) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/examples/distro/build.gradle b/opentelemetry-java-instrumentation/examples/distro/build.gradle new file mode 100644 index 000000000..4a0833721 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/build.gradle @@ -0,0 +1,53 @@ +group 'io.opentelemetry.example' +version '1.0-SNAPSHOT' + +subprojects { + version = rootProject.version + + apply plugin: "java" + + ext { + versions = [ + opentelemetry : "1.2.0", + opentelemetryJavaagent: "1.2.0", + bytebuddy : "1.10.18", + guava : "30.1-jre" + ] + versions.opentelemetryAlpha = "${versions.opentelemetry}-alpha" + versions.opentelemetryJavaagentAlpha = "${versions.opentelemetryJavaagent}-alpha" + + deps = [ + bytebuddy : "net.bytebuddy:byte-buddy:${versions.bytebuddy}", + bytebuddyagent : "net.bytebuddy:byte-buddy-agent:${versions.bytebuddy}", + autoservice : [ + "com.google.auto.service:auto-service:1.0-rc7", + "com.google.auto:auto-common:0.8", + "com.google.guava:guava:${versions.guava}", + ], + autoValueAnnotations: "com.google.auto.value:auto-value-annotations:${versions.autoValue}", + ] + } + + repositories { + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + mavenCentral() + } + + dependencies { + testImplementation("org.mockito:mockito-core:3.3.3") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.2") + } + + tasks { + test { + useJUnitPlatform() + } + + compileJava { + options.release.set(11) + } + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/build.gradle b/opentelemetry-java-instrumentation/examples/distro/custom/build.gradle new file mode 100644 index 000000000..fb466ba1a --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/build.gradle @@ -0,0 +1,24 @@ +plugins { + id "java" + id("com.github.johnrengelman.shadow") version "6.0.0" +} + +apply from: "$rootDir/gradle/shadow.gradle" + +def relocatePackages = ext.relocatePackages + +dependencies { + compileOnly("run.mone:opentelemetry-sdk:${versions.opentelemetry}") + compileOnly("run.mone:opentelemetry-sdk-extension-autoconfigure:${versions.opentelemetryAlpha}") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:${versions.opentelemetryJavaagentAlpha}") +} + +tasks { + shadowJar { + mergeServiceFiles() + + exclude("**/module-info.class") + + relocatePackages(it) + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoIdGenerator.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoIdGenerator.java new file mode 100644 index 000000000..1b8f3ab38 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoIdGenerator.java @@ -0,0 +1,25 @@ +package com.example.javaagent; + +import io.opentelemetry.sdk.trace.IdGenerator; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Custom {@link IdGenerator} which provides span and trace ids. + * + * @see io.opentelemetry.sdk.trace.SdkTracerProvider + * @see DemoSdkTracerProviderConfigurer + */ +public class DemoIdGenerator implements IdGenerator { + private static final AtomicLong traceId = new AtomicLong(0); + private static final AtomicLong spanId = new AtomicLong(0); + + @Override + public String generateSpanId() { + return String.format("%016d", spanId.incrementAndGet()); + } + + @Override + public String generateTraceId() { + return String.format("%032d", traceId.incrementAndGet()); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropagator.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropagator.java new file mode 100644 index 000000000..9cd1bc94e --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropagator.java @@ -0,0 +1,44 @@ +package com.example.javaagent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.List; + +/** + * See + * OpenTelemetry Specification for more information about Propagators. + * + * @see DemoPropagatorProvider + */ +public class DemoPropagator implements TextMapPropagator { + private static final String FIELD = "X-demo-field"; + private static final ContextKey PROPAGATION_START_KEY = ContextKey.named("propagation.start"); + + @Override + public List fields() { + return Collections.singletonList(FIELD); + } + + @Override + public void inject(Context context, C carrier, TextMapSetter setter) { + Long propagationStart = context.get(PROPAGATION_START_KEY); + if (propagationStart == null) { + propagationStart = System.currentTimeMillis(); + } + setter.set(carrier, FIELD, String.valueOf(propagationStart)); + } + + @Override + public Context extract(Context context, C carrier, TextMapGetter getter) { + String propagationStart = getter.get(carrier, FIELD); + if (propagationStart != null) { + return context.with(PROPAGATION_START_KEY, Long.valueOf(propagationStart)); + } else { + return context; + } + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropagatorProvider.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropagatorProvider.java new file mode 100644 index 000000000..ca00a1681 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropagatorProvider.java @@ -0,0 +1,22 @@ +package com.example.javaagent; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +/** + * Registers the custom propagator used by this example. + * + * @see ConfigurablePropagatorProvider + * @see DemoPropagator + */ +public class DemoPropagatorProvider implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return new DemoPropagator(); + } + + @Override + public String getName() { + return "demo"; + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropertySource.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropertySource.java new file mode 100644 index 000000000..3bbe53e0c --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoPropertySource.java @@ -0,0 +1,22 @@ +package com.example.javaagent; + +import io.opentelemetry.javaagent.spi.config.PropertySource; +import java.util.Map; + +/** + * {@link PropertySource} is an SPI provided by OpenTelemetry Java instrumentation agent. + * By implementing it custom distributions can supply their own default configuration. + * The configuration priority, from highest to lowest is: + * system properties -> environment variables -> configuration file -> PropertySource SPI -> hard-coded defaults + */ +public class DemoPropertySource implements PropertySource { + + @Override + public Map getProperties() { + return Map.of( + "otel.exporter.otlp.endpoint", "http://collector:55680", + "otel.exporter.otlp.insecure", "true", + "otel.config.max.attrs", "16" + ); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoResourceProvider.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoResourceProvider.java new file mode 100644 index 000000000..79f36f07e --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoResourceProvider.java @@ -0,0 +1,14 @@ +package com.example.javaagent; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +public class DemoResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + Attributes attributes = Attributes.builder().put("custom.resource", "demo").build(); + return Resource.create(attributes); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSampler.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSampler.java new file mode 100644 index 000000000..67bcade15 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSampler.java @@ -0,0 +1,35 @@ +package com.example.javaagent; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; + +/** + * This demo sampler filters out all internal spans whose name contains string "greeting". + *

+ * See + * OpenTelemetry Specification for more information about span sampling. + * + * @see DemoSdkTracerProviderConfigurer + */ +public class DemoSampler implements Sampler { + @Override + public SamplingResult shouldSample(Context parentContext, String traceId, String name, + SpanKind spanKind, Attributes attributes, List parentLinks) { + if (spanKind == SpanKind.INTERNAL && name.contains("greeting")) { + return SamplingResult.create(SamplingDecision.DROP); + } else { + return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE); + } + } + + @Override + public String getDescription() { + return "DemoSampler"; + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSdkTracerProviderConfigurer.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSdkTracerProviderConfigurer.java new file mode 100644 index 000000000..d833c92e5 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSdkTracerProviderConfigurer.java @@ -0,0 +1,28 @@ +package com.example.javaagent; + +import io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; + +/** + * This is one of the main entry points for Instrumentation Agent's customizations. + * It allows configuring {@link SdkTracerProviderBuilder}. + * See the {@link #configure(SdkTracerProviderBuilder)} method below. + *

+ * Also see https://github.com/open-telemetry/opentelemetry-java/issues/2022 + * + * @see SdkTracerProviderConfigurer + * @see DemoPropagatorProvider + */ +public class DemoSdkTracerProviderConfigurer implements SdkTracerProviderConfigurer { + @Override + public void configure(SdkTracerProviderBuilder tracerProvider) { + tracerProvider + .setIdGenerator(new DemoIdGenerator()) + .setSpanLimits(SpanLimits.builder().setMaxNumberOfAttributes(1024).build()) + .setSampler(new DemoSampler()) + .addSpanProcessor(new DemoSpanProcessor()) + .addSpanProcessor(SimpleSpanProcessor.create(new DemoSpanExporter())); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSpanExporter.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSpanExporter.java new file mode 100644 index 000000000..476093a76 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSpanExporter.java @@ -0,0 +1,30 @@ +package com.example.javaagent; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; + +/** + * See + * OpenTelemetry Specification for more information about {@link SpanExporter}. + * + * @see DemoSdkTracerProviderConfigurer + */ +public class DemoSpanExporter implements SpanExporter { + @Override + public CompletableResultCode export(Collection spans) { + System.out.printf("%d spans exported%n", spans.size()); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSpanProcessor.java b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSpanProcessor.java new file mode 100644 index 000000000..8abe2183e --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/java/com/example/javaagent/DemoSpanProcessor.java @@ -0,0 +1,45 @@ +package com.example.javaagent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +/** + * See + * OpenTelemetry Specification for more information about {@link SpanProcessor}. + * + * @see DemoSdkTracerProviderConfigurer + */ +public class DemoSpanProcessor implements SpanProcessor { + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + span.setAttribute("custom", "demo"); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + + } + + @Override + public boolean isEndRequired() { + return false; + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.javaagent.spi.config.PropertySource b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.javaagent.spi.config.PropertySource new file mode 100644 index 000000000..1274076e5 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.javaagent.spi.config.PropertySource @@ -0,0 +1 @@ +com.example.javaagent.DemoPropertySource \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider new file mode 100644 index 000000000..b1ccca2b5 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -0,0 +1 @@ +com.example.javaagent.DemoPropagatorProvider \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider new file mode 100644 index 000000000..95ac24bfb --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -0,0 +1 @@ +com.example.javaagent.DemoResourceProvider \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer new file mode 100644 index 000000000..81d953738 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/custom/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer @@ -0,0 +1 @@ +com.example.javaagent.DemoSdkTracerProviderConfigurer \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/examples/distro/gradle/instrumentation.gradle b/opentelemetry-java-instrumentation/examples/distro/gradle/instrumentation.gradle new file mode 100644 index 000000000..bd3f980d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/gradle/instrumentation.gradle @@ -0,0 +1,61 @@ +apply plugin: 'java' +apply plugin: 'com.github.johnrengelman.shadow' + +apply from: "$rootDir/gradle/shadow.gradle" + +def relocatePackages = ext.relocatePackages + +configurations { + testInstrumentation + testAgent +} + +dependencies { + compileOnly("run.mone:opentelemetry-sdk:${versions.opentelemetry}") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-api:${versions.opentelemetryJavaagentAlpha}") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:${versions.opentelemetryJavaagentAlpha}") + + compileOnly deps.bytebuddy + compileOnly deps.bytebuddyagent + annotationProcessor deps.autoservice + compileOnly deps.autoservice + + // the javaagent that is going to be used when running instrumentation unit tests + testAgent("io.opentelemetry.javaagent:opentelemetry-agent-for-testing:${versions.opentelemetryJavaagentAlpha}") + // test dependencies + testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common:${versions.opentelemetryJavaagentAlpha}") + testImplementation("run.mone:opentelemetry-sdk-testing:${versions.opentelemetry}") + testImplementation("org.assertj:assertj-core:3.19.0") +} + +shadowJar { + configurations = [project.configurations.runtimeClasspath, project.configurations.testInstrumentation] + mergeServiceFiles() + + archiveFileName = 'agent-testing.jar' + + relocatePackages(it) +} + +tasks.withType(Test).configureEach { + inputs.file(shadowJar.archiveFile) + + jvmArgs "-Dotel.javaagent.debug=true" + jvmArgs "-javaagent:${configurations.testAgent.files.first().absolutePath}" + jvmArgs "-Dotel.javaagent.experimental.initializer.jar=${shadowJar.archiveFile.get().asFile.absolutePath}" + jvmArgs "-Dotel.javaagent.testing.additional-library-ignores.enabled=false" + jvmArgs "-Dotel.javaagent.testing.fail-on-context-leak=true" + // prevent sporadic gradle deadlocks, see SafeLogger for more details + jvmArgs "-Dotel.javaagent.testing.transform-safe-logging.enabled=true" + + dependsOn shadowJar + + // The sources are packaged into the testing jar so we need to make sure to exclude from the test + // classpath, which automatically inherits them, to ensure our shaded versions are used. + classpath = classpath.filter { + if (it == file("$buildDir/resources/main") || it == file("$buildDir/classes/java/main")) { + return false + } + return true + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/gradle/shadow.gradle b/opentelemetry-java-instrumentation/examples/distro/gradle/shadow.gradle new file mode 100644 index 000000000..fce270eab --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/gradle/shadow.gradle @@ -0,0 +1,20 @@ +ext.relocatePackages = { shadowJar -> + // Prevents conflict with other SLF4J instances. Important for premain. + shadowJar.relocate 'org.slf4j', 'io.opentelemetry.javaagent.slf4j' + // rewrite dependencies calling Logger.getLogger + shadowJar.relocate 'java.util.logging.Logger', 'io.opentelemetry.javaagent.bootstrap.PatchLogger' + + // rewrite library instrumentation dependencies + shadowJar.relocate "io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.shaded.instrumentation" + + // relocate OpenTelemetry API usage + shadowJar.relocate "io.opentelemetry.api", "io.opentelemetry.javaagent.shaded.io.opentelemetry.api" + shadowJar.relocate "io.opentelemetry.semconv", "io.opentelemetry.javaagent.shaded.io.opentelemetry.semconv" + shadowJar.relocate "io.opentelemetry.context", "io.opentelemetry.javaagent.shaded.io.opentelemetry.context" + + // relocate the OpenTelemetry extensions that are used by instrumentation modules + // these extensions live in the AgentClassLoader, and are injected into the user's class loader + // by the instrumentation modules that use them + shadowJar.relocate "io.opentelemetry.extension.aws", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.aws" + shadowJar.relocate "io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin" +} diff --git a/opentelemetry-java-instrumentation/examples/distro/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java-instrumentation/examples/distro/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..912744eeb --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857 +size 58910 diff --git a/opentelemetry-java-instrumentation/examples/distro/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java-instrumentation/examples/distro/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..be52383ef --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/opentelemetry-java-instrumentation/examples/distro/gradlew b/opentelemetry-java-instrumentation/examples/distro/gradlew new file mode 100755 index 000000000..fbd7c5158 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java-instrumentation/examples/distro/gradlew.bat b/opentelemetry-java-instrumentation/examples/distro/gradlew.bat new file mode 100644 index 000000000..5093609d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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/opentelemetry-java-instrumentation/examples/distro/instrumentation/build.gradle b/opentelemetry-java-instrumentation/examples/distro/instrumentation/build.gradle new file mode 100644 index 000000000..3c7ade182 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/instrumentation/build.gradle @@ -0,0 +1,31 @@ +plugins { + id("com.github.johnrengelman.shadow") version "6.0.0" +} + +apply from: "$rootDir/gradle/shadow.gradle" + +def relocatePackages = ext.relocatePackages + +Project instr_project = project +subprojects { + afterEvaluate { Project subProj -> + if (subProj.getPlugins().hasPlugin('java')) { + // Make it so all instrumentation subproject tests can be run with a single command. + instr_project.tasks.test.dependsOn(subProj.tasks.test) + + instr_project.dependencies { + implementation(project(subProj.getPath())) + } + } + } +} + +shadowJar { + mergeServiceFiles() + + exclude '**/module-info.class' + + duplicatesStrategy = DuplicatesStrategy.FAIL + + relocatePackages(it) +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/build.gradle b/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/build.gradle new file mode 100644 index 000000000..7c4f6f7a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/build.gradle @@ -0,0 +1,18 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly "javax.servlet:javax.servlet-api:3.0.1" + + testInstrumentation "io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-common:${versions.opentelemetryJavaagentAlpha}" + testInstrumentation "io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-2.2:${versions.opentelemetryJavaagentAlpha}" + testInstrumentation "io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-servlet-3.0:${versions.opentelemetryJavaagentAlpha}" + + testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common:${versions.opentelemetryJavaagentAlpha}") { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + + testImplementation "com.squareup.okhttp3:okhttp:3.12.12" + testImplementation "javax.servlet:javax.servlet-api:3.0.1" + testImplementation "org.eclipse.jetty:jetty-server:8.0.0.v20110901" + testImplementation "org.eclipse.jetty:jetty-servlet:8.0.0.v20110901" +} diff --git a/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java b/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java new file mode 100644 index 000000000..3ad52a354 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java @@ -0,0 +1,51 @@ +package com.example.javaagent.instrumentation; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.NameMatchers.namedOneOf; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.*; + +public class DemoServlet3Instrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(namedOneOf("javax.servlet.Filter", "javax.servlet.http.HttpServlet")); + } + + @Override + public Map, String> transformers() { + return singletonMap( + namedOneOf("doFilter", "service") + .and(takesArgument(0, named("javax.servlet.ServletRequest"))) + .and(takesArgument(1, named("javax.servlet.ServletResponse"))) + .and(isPublic()), + this.getClass().getName() + "$DemoServlet3Advice"); + } + + @SuppressWarnings("unused") + public static class DemoServlet3Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(value = 1) ServletResponse response) { + if (!(response instanceof HttpServletResponse)) { + return; + } + + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + if (!httpServletResponse.containsHeader("X-server-id")) { + httpServletResponse.setHeader( + "X-server-id", Java8BytecodeBridge.currentSpan().getSpanContext().getTraceId()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java b/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java new file mode 100644 index 000000000..f5d177a7c --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java @@ -0,0 +1,43 @@ +package com.example.javaagent.instrumentation; + +import static io.opentelemetry.javaagent.extension.matcher.NameMatchers.namedOneOf; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This is a demo instrumentation which hooks into servlet invocation and modifies the http + * response. + */ +@AutoService(InstrumentationModule.class) +public final class DemoServlet3InstrumentationModule extends InstrumentationModule { + public DemoServlet3InstrumentationModule() { + super("servlet-demo", "servlet-3"); + } + + /* + We want this instrumentation to be applied after the standard servlet instrumentation. + The latter creates a server span around http request. + This instrumentation needs access to that server span. + */ + @Override + public int order() { + return 1; + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return ClassLoaderMatcher.hasClassesNamed("javax.servlet.http.HttpServlet"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new DemoServlet3Instrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/test/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationTest.java b/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/test/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationTest.java new file mode 100644 index 000000000..bb752c07a --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/instrumentation/servlet-3/src/test/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationTest.java @@ -0,0 +1,94 @@ +package com.example.javaagent.instrumentation; + +import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import java.io.IOException; +import java.io.Writer; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * This is a demo instrumentation test that verifies that the custom servlet instrumentation was applied. + */ +class DemoServlet3InstrumentationTest { + @RegisterExtension + static final AgentInstrumentationExtension instrumentation = AgentInstrumentationExtension + .create(); + + static final OkHttpClient httpClient = new OkHttpClient(); + + static int port; + static Server server; + + @BeforeAll + static void startServer() throws Exception { + port = PortUtils.findOpenPort(); + server = new Server(port); + for (var connector : server.getConnectors()) { + connector.setHost("localhost"); + } + + var servletContext = new ServletContextHandler(null, null); + servletContext.addServlet(DefaultServlet.class, "/"); + servletContext.addServlet(TestServlet.class, "/servlet"); + server.setHandler(servletContext); + + server.start(); + } + + @AfterAll + static void stopServer() throws Exception { + server.stop(); + server.destroy(); + } + + @Test + void shouldAddCustomHeader() throws Exception { + // given + var request = + new Request.Builder() + .url(HttpUrl.get("http://localhost:" + port + "/servlet")) + .get() + .build(); + + // when + var response = httpClient.newCall(request).execute(); + + // then + assertEquals(200, response.code()); + assertEquals("result", response.body().string()); + + assertThat(instrumentation.waitForTraces(1)) + .hasSize(1) + .hasTracesSatisfyingExactly(trace -> trace.hasSize(1) + .hasSpansSatisfyingExactly(span -> span.hasName("/servlet").hasKind(SpanKind.SERVER))); + + var traceId = instrumentation.spans().get(0).getTraceId(); + assertEquals(traceId, response.header("X-server-id")); + } + + public static class TestServlet extends HttpServlet { + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException { + try (Writer writer = response.getWriter()) { + writer.write("result"); + response.setStatus(200); + } + } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/examples/distro/settings.gradle b/opentelemetry-java-instrumentation/examples/distro/settings.gradle new file mode 100644 index 000000000..3fe07b603 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/settings.gradle @@ -0,0 +1,8 @@ +rootProject.name = 'opentelemetry-java-instrumentation-demo' + +include "agent" +include "custom" +include "instrumentation" +include "instrumentation:servlet-3" +include "smoke-tests" + diff --git a/opentelemetry-java-instrumentation/examples/distro/smoke-tests/build.gradle b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/build.gradle new file mode 100644 index 000000000..62ea916b1 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/build.gradle @@ -0,0 +1,27 @@ +plugins { + id "java" +} + +dependencies { + testImplementation("org.testcontainers:testcontainers:1.15.3") + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.11.2") + testImplementation("com.google.protobuf:protobuf-java-util:3.12.4") + testImplementation("com.squareup.okhttp3:okhttp:3.12.12") + testImplementation("run.mone:opentelemetry-proto") + testImplementation("run.mone:opentelemetry-api") + + testImplementation("ch.qos.logback:logback-classic:1.2.3") +} + +tasks.test { + useJUnitPlatform() + + testLogging.showStandardStreams = true + + def shadowTask = project(":agent").tasks.shadowJar + inputs.files(layout.files(shadowTask)) + + doFirst { + jvmArgs("-Dio.opentelemetry.smoketest.agent.shadowJar.path=${shadowTask.archiveFile.get()}") + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/OkHttpUtils.java b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/OkHttpUtils.java new file mode 100644 index 000000000..b8956aa29 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/OkHttpUtils.java @@ -0,0 +1,23 @@ +package com.example.javaagent.smoketest; + +import java.util.concurrent.TimeUnit; +import okhttp3.OkHttpClient; + +public class OkHttpUtils { + + static OkHttpClient.Builder clientBuilder() { + TimeUnit unit = TimeUnit.MINUTES; + return new OkHttpClient.Builder() + .connectTimeout(1, unit) + .writeTimeout(1, unit) + .readTimeout(1, unit); + } + + public static OkHttpClient client() { + return client(false); + } + + public static OkHttpClient client(boolean followRedirects) { + return clientBuilder().followRedirects(followRedirects).build(); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SmokeTest.java b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SmokeTest.java new file mode 100644 index 000000000..194ae50bf --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SmokeTest.java @@ -0,0 +1,192 @@ +package com.example.javaagent.smoketest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.Span; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.MountableFile; + +abstract class SmokeTest { + private static final Logger logger = LoggerFactory.getLogger(SmokeTest.class); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + protected static OkHttpClient client = OkHttpUtils.client(); + + private static final Network network = Network.newNetwork(); + protected static final String agentPath = + System.getProperty("io.opentelemetry.smoketest.agent.shadowJar.path"); + + protected abstract String getTargetImage(int jdk); + + /** + * Subclasses can override this method to customise target application's environment + */ + protected Map getExtraEnv() { + return Collections.emptyMap(); + } + + private static GenericContainer backend; + private static GenericContainer collector; + + @BeforeAll + static void setupSpec() { + backend = + new GenericContainer<>( + "ghcr.io/open-telemetry/java-test-containers:smoke-fake-backend-20210324.684269693") + .withExposedPorts(8080) + .waitingFor(Wait.forHttp("/health").forPort(8080)) + .withNetwork(network) + .withNetworkAliases("backend") + .withLogConsumer(new Slf4jLogConsumer(logger)); + backend.start(); + + collector = + new GenericContainer<>("otel/opentelemetry-collector-dev:latest") + .dependsOn(backend) + .withNetwork(network) + .withNetworkAliases("collector") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withCopyFileToContainer( + MountableFile.forClasspathResource("/otel.yaml"), "/etc/otel.yaml") + .withCommand("--config /etc/otel.yaml"); + collector.start(); + } + + protected GenericContainer target; + + void startTarget(int jdk) { + target = + new GenericContainer<>(getTargetImage(jdk)) + .withExposedPorts(8080) + .withNetwork(network) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withCopyFileToContainer( + MountableFile.forHostPath(agentPath), "/opentelemetry-javaagent.jar") + .withEnv("JAVA_TOOL_OPTIONS", "-javaagent:/opentelemetry-javaagent.jar") + .withEnv("OTEL_BSP_MAX_EXPORT_BATCH", "1") + .withEnv("OTEL_BSP_SCHEDULE_DELAY", "10") + .withEnv("OTEL_PROPAGATORS", "tracecontext,baggage,demo") + .withEnv(getExtraEnv()); + target.start(); + } + + @AfterEach + void cleanup() throws IOException { + client + .newCall( + new Request.Builder() + .url( + String.format( + "http://localhost:%d/clear-requests", backend.getMappedPort(8080))) + .build()) + .execute() + .close(); + } + + void stopTarget() { + target.stop(); + } + + @AfterAll + static void cleanupSpec() { + backend.stop(); + collector.stop(); + } + + protected static int countResourcesByValue(Collection traces, String resourceName, String value) { + return (int) traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .flatMap(it -> it.getResource().getAttributesList().stream()) + .filter(kv -> kv.getKey().equals(resourceName) && kv.getValue().getStringValue().equals(value)) + .count(); + } + + protected static int countSpansByName( + Collection traces, String spanName) { + return (int) getSpanStream(traces).filter(it -> it.getName().equals(spanName)).count(); + } + + protected static int countSpansByAttributeValue( + Collection traces, String attributeName, String attributeValue) { + return (int) getSpanStream(traces) + .flatMap(it -> it.getAttributesList().stream()) + .filter(kv -> kv.getKey().equals(attributeName) && kv.getValue().getStringValue().equals(attributeValue)) + .count(); + } + + protected static Stream getSpanStream(Collection traces) { + return traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .flatMap(it -> it.getInstrumentationLibrarySpansList().stream()) + .flatMap(it -> it.getSpansList().stream()); + } + + protected Collection waitForTraces() + throws IOException, InterruptedException { + String content = waitForContent(); + + return StreamSupport.stream(OBJECT_MAPPER.readTree(content).spliterator(), false) + .map( + it -> { + ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); + // TODO(anuraaga): Register parser into object mapper to avoid de -> re -> + // deserialize. + try { + JsonFormat.parser().merge(OBJECT_MAPPER.writeValueAsString(it), builder); + } catch (InvalidProtocolBufferException | JsonProcessingException e) { + e.printStackTrace(); + } + return builder.build(); + }) + .collect(Collectors.toList()); + } + + private String waitForContent() throws IOException, InterruptedException { + long previousSize = 0; + long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30); + String content = "[]"; + while (System.currentTimeMillis() < deadline) { + + Request request = + new Request.Builder() + .url(String.format("http://localhost:%d/get-traces", backend.getMappedPort(8080))) + .build(); + + try (ResponseBody body = client.newCall(request).execute().body()) { + content = body.string(); + } + + if (content.length() > 2 && content.length() == previousSize) { + break; + } + previousSize = content.length(); + System.out.printf("Current content size %d%n", previousSize); + TimeUnit.MILLISECONDS.sleep(500); + } + + return content; + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SpringBootSmokeTest.java b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SpringBootSmokeTest.java new file mode 100644 index 000000000..813605add --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SpringBootSmokeTest.java @@ -0,0 +1,52 @@ +package com.example.javaagent.smoketest; + +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import java.io.IOException; +import java.util.Collection; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SpringBootSmokeTest extends SmokeTest { + + protected String getTargetImage(int jdk) { + return "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk" + jdk + + "-20210218.577304949"; + } + + @Test + public void springBootSmokeTestOnJDK() throws IOException, InterruptedException { + startTarget(11); + String url = String.format("http://localhost:%d/greeting", target.getMappedPort(8080)); + Request request = new Request.Builder().url(url).get().build(); + + String currentAgentVersion = + (String) new JarFile(agentPath) + .getManifest() + .getMainAttributes() + .get(Attributes.Name.IMPLEMENTATION_VERSION); + + Response response = client.newCall(request).execute(); + System.out.println(response.headers().toString()); + + Collection traces = waitForTraces(); + + Assertions.assertNotNull(response.header("X-server-id")); + Assertions.assertEquals(1, response.headers("X-server-id").size()); + Assertions.assertTrue(TraceId.isValid(response.header("X-server-id"))); + Assertions.assertEquals("Hi!", response.body().string()); + Assertions.assertEquals(1, countSpansByName(traces, "/greeting")); + Assertions.assertEquals(0, countSpansByName(traces, "WebController.greeting")); + Assertions.assertEquals(1, countSpansByName(traces, "WebController.withSpan")); + Assertions.assertEquals(2, countSpansByAttributeValue(traces, "custom", "demo")); + Assertions.assertNotEquals(0, + countResourcesByValue(traces, "telemetry.auto.version", currentAgentVersion)); + Assertions.assertNotEquals(0, countResourcesByValue(traces, "custom.resource", "demo")); + + stopTarget(); + } +} diff --git a/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/resources/logback.xml new file mode 100644 index 000000000..ab55cbd1c --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/resources/logback.xml @@ -0,0 +1,22 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/resources/otel.yaml b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/resources/otel.yaml new file mode 100644 index 000000000..a25201f89 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/distro/smoke-tests/src/test/resources/otel.yaml @@ -0,0 +1,31 @@ +extensions: + health_check: + pprof: + endpoint: 0.0.0.0:1777 + zpages: + endpoint: 0.0.0.0:55679 + +receivers: + otlp: + protocols: + grpc: + zipkin: + +processors: + batch: + +exporters: + logging: + loglevel: debug + otlp: + endpoint: backend:8080 + insecure: true + +service: + pipelines: + traces: + receivers: [otlp, zipkin] + processors: [batch] + exporters: [logging, otlp] + + extensions: [health_check, pprof, zpages] diff --git a/opentelemetry-java-instrumentation/examples/extension/README.md b/opentelemetry-java-instrumentation/examples/extension/README.md new file mode 100644 index 000000000..259b01156 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/README.md @@ -0,0 +1,75 @@ +## Introduction + +This repository demonstrates how to create an extension archive to use with `otel.javaagent.experimental.extensions` +configuration option of the OpenTelemetry Java instrumentation agent. + +For every extension point provided by OpenTelemetry Java instrumentation, this repository contains an example of +its usage. + +Please carefully read both the source code and Gradle build script file `build.gradle`. +They contain a lot of documentation and comments explaining the purpose of all major pieces. + +## How to use extension archive + +When you build this project by running `./gradlew build` you will get a jar file in +`build/libs/opentelemetry-java-instrumentation-extension-demo-1.0-all.jar`. +Copy this jar file to a machine running the application that you are monitoring with +OpenTelemetry Java instrumentation agent. + +Assuming that your command line looks similar to this: +``` +java -javaagent:path/to/opentelemetry-javaagent-all.jar \ + -jar myapp.jar +``` +change it to this: +``` +java -javaagent:path/to/opentelemetry-javaagent-all.jar \ + -Dotel.javaagent.experimental.extensions=path/to/extension.jar + -jar myapp.jar +``` +specifying the full path and the correct name of your extensions jar. + +## Extensions examples + +* [DemoIdGenerator](src/main/java/com/example/javaagent/DemoIdGenerator.java) - custom `IdGenerator` +* [DemoPropagator](src/main/java/com/example/javaagent/DemoPropagator.java) - custom `TextMapPropagator` +* [DemoPropertySource](src/main/java/com/example/javaagent/DemoPropertySource.java) - default configuration +* [DemoSampler](src/main/java/com/example/javaagent/DemoSampler.java) - custom `Sampler` +* [DemoSpanProcessor](src/main/java/com/example/javaagent/DemoSpanProcessor.java) - custom `SpanProcessor` +* [DemoSpanExporter](src/main/java/com/example/javaagent/DemoSpanExporter.java) - custom `SpanExporter` +* [DemoServlet3InstrumentationModule](src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java) - additional instrumentation + +## Instrumentation customisation + +There are several options to override or customise instrumentation provided by the upstream agent. +The following description follows one specific use-case: + +> Instrumentation X from Otel distribution creates span that I don't like and I want to change it. + +As an example, let us take some database client instrumentation that creates a span for database call +and extracts data from db connection to provide attributes for that span. + +### I don't want this span at all +The easiest case. You can just pre-configure the agent in your extension and disable given instrumentation. + +### I want to add/modify some attributes and their values does NOT depend on a specific db connection instance. +E.g. you want to add some data from call stack as span attribute. +In this case just provide your custom `SpanProcessor`. +No need for touching instrumentation itself. + +### I want to add/modify some attributes and their values depend on a specific db connection instance. +Write a _new_ instrumentation which injects its own advice into the same method as the original one. +Use `order` method to ensure it is run after the original instrumentation. +Now you can augment current span with new information. + +See [DemoServlet3InstrumentationModule](src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java). + +### I want to remove some attributes +Write custom exporter or use attribute filtering functionality in Collector. + +### I don't like Otel span at all. I want to significantly modify it and its lifecycle +Disable existing instrumentation. +Write a new one, which injects `Advice` into the same (or better) method as the original instrumentation. +Write your own `Advice` for this. +Use existing `Tracer` directly or extend it. +As you have your own `Advice`, you can control which `Tracer` you use. diff --git a/opentelemetry-java-instrumentation/examples/extension/build.gradle b/opentelemetry-java-instrumentation/examples/extension/build.gradle new file mode 100644 index 000000000..297d3a3dd --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/build.gradle @@ -0,0 +1,132 @@ +plugins { + id "java" + + /* + Instrumentation agent extension mechanism expects a single jar containing everything required + for your extension. This also includes any external libraries that your extension uses and + cannot access from application classpath (see comment below about `javax.servlet-api` dependency). + + Thus we use Shadow Gradle plugin to package our classes and all required runtime dependencies + into a single jar. + See https://imperceptiblethoughts.com/shadow/ for more details about Shadow plugin. + */ + id "com.github.johnrengelman.shadow" version "6.1.0" +} + +group 'io.opentelemetry.example' +version '1.0' + +ext { + versions = [ + opentelemetry : "1.2.0", + opentelemetryAlpha : "1.2.0-alpha", + opentelemetryJavaagent : "1.3.0-SNAPSHOT", + opentelemetryJavaagentAlpha: "1.3.0-alpha-SNAPSHOT", + ] + + deps = [ + autoservice: dependencies.create(group: 'com.google.auto.service', name: 'auto-service', version: '1.0') + ] +}opentelemetry + +repositories { + mavenLocal() + mavenCentral() + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } +} + +configurations { + /* + We create a separate gradle configuration to grab a published Otel instrumentation agent. + We don't need the agent during development of this extension module. + This agent is used only during integration test. + */ + otel +} + +dependencies { + /* + Interfaces and SPIs that we implement. We use `compileOnly` dependency because during + runtime all necessary classes are provided by javaagent itself. + */ + compileOnly("run.mone:opentelemetry-sdk-extension-autoconfigure:${versions.opentelemetryAlpha}") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-api:${versions.opentelemetryJavaagentAlpha}") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:${versions.opentelemetryJavaagentAlpha}") + + //Provides @AutoService annotation that makes registration of our SPI implementations much easier + compileOnly deps.autoservice + annotationProcessor deps.autoservice + + /* + Used by our demo instrumentation module to reference classes of the target instrumented library. + We again use `compileOnly` here because during runtime these classes are provided by the + actual application that we instrument. + + NB! Only Advice (and "helper") classes of instrumentation modules can access classes from application classpath. + See https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/contributing/writing-instrumentation-module.md#advice-classes + */ + compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.0.1' + + /* + This dependency is required for DemoSpanProcessor both during compile and runtime. + Only dependencies added to `implementation` configuration will be picked up by Shadow plugin + and added to the resulting jar for our extension's distribution. + */ + implementation 'org.apache.commons:commons-lang3:3.11' + + //All dependencies below are only for tests + testImplementation("org.testcontainers:testcontainers:1.15.3") + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.11.2") + testImplementation("com.google.protobuf:protobuf-java-util:3.12.4") + testImplementation("com.squareup.okhttp3:okhttp:3.12.12") + testImplementation("io.opentelemetry:opentelemetry-api:${versions.opentelemetry}") + testImplementation("io.opentelemetry:opentelemetry-proto:${versions.opentelemetryAlpha}") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.2") + testRuntimeOnly("ch.qos.logback:logback-classic:1.2.3") + + //Otel Java instrumentation that we use and extend during integration tests + otel("io.opentelemetry.javaagent:opentelemetry-javaagent:${versions.opentelemetryJavaagent}:all") +} + +//Extracts manifest from OpenTelemetry Java agent to reuse it later +task agentManifest(type: Copy) { + from zipTree(configurations.otel.singleFile).matching { + include 'META-INF/MANIFEST.MF' + } + into buildDir +} + +//Produces a copy of upstream javaagent with this extension jar included inside it +//The location of extension directory inside agent jar is hard-coded in the agent source code +task extendedAgent(type: Jar) { + dependsOn agentManifest + archiveFileName = "opentelemetry-javaagent-all.jar" + manifest.from "$buildDir/META-INF/MANIFEST.MF" + from zipTree(configurations.otel.singleFile) + from(tasks.shadowJar.archiveFile) { + into "extensions" + } +} + +tasks { + test { + useJUnitPlatform() + + inputs.files(layout.files(tasks.shadowJar)) + inputs.files(layout.files(tasks.extendedAgent)) + + systemProperty 'io.opentelemetry.smoketest.agentPath', configurations.otel.singleFile.absolutePath + systemProperty 'io.opentelemetry.smoketest.extendedAgentPath', tasks.extendedAgent.archiveFile.get().asFile.absolutePath + systemProperty 'io.opentelemetry.smoketest.extensionPath', tasks.shadowJar.archiveFile.get().asFile.absolutePath + } + + compileJava { + options.release.set(11) + } + + assemble.dependsOn(shadowJar) +} diff --git a/opentelemetry-java-instrumentation/examples/extension/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java-instrumentation/examples/extension/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..912744eeb --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857 +size 58910 diff --git a/opentelemetry-java-instrumentation/examples/extension/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java-instrumentation/examples/extension/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..be52383ef --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/opentelemetry-java-instrumentation/examples/extension/gradlew b/opentelemetry-java-instrumentation/examples/extension/gradlew new file mode 100755 index 000000000..fbd7c5158 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java-instrumentation/examples/extension/gradlew.bat b/opentelemetry-java-instrumentation/examples/extension/gradlew.bat new file mode 100644 index 000000000..5093609d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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/opentelemetry-java-instrumentation/examples/extension/settings.gradle b/opentelemetry-java-instrumentation/examples/extension/settings.gradle new file mode 100644 index 000000000..b56be86b6 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'opentelemetry-java-instrumentation-extension-demo' diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoIdGenerator.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoIdGenerator.java new file mode 100644 index 000000000..1b8f3ab38 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoIdGenerator.java @@ -0,0 +1,25 @@ +package com.example.javaagent; + +import io.opentelemetry.sdk.trace.IdGenerator; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Custom {@link IdGenerator} which provides span and trace ids. + * + * @see io.opentelemetry.sdk.trace.SdkTracerProvider + * @see DemoSdkTracerProviderConfigurer + */ +public class DemoIdGenerator implements IdGenerator { + private static final AtomicLong traceId = new AtomicLong(0); + private static final AtomicLong spanId = new AtomicLong(0); + + @Override + public String generateSpanId() { + return String.format("%016d", spanId.incrementAndGet()); + } + + @Override + public String generateTraceId() { + return String.format("%032d", traceId.incrementAndGet()); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropagator.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropagator.java new file mode 100644 index 000000000..9cd1bc94e --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropagator.java @@ -0,0 +1,44 @@ +package com.example.javaagent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.List; + +/** + * See + * OpenTelemetry Specification for more information about Propagators. + * + * @see DemoPropagatorProvider + */ +public class DemoPropagator implements TextMapPropagator { + private static final String FIELD = "X-demo-field"; + private static final ContextKey PROPAGATION_START_KEY = ContextKey.named("propagation.start"); + + @Override + public List fields() { + return Collections.singletonList(FIELD); + } + + @Override + public void inject(Context context, C carrier, TextMapSetter setter) { + Long propagationStart = context.get(PROPAGATION_START_KEY); + if (propagationStart == null) { + propagationStart = System.currentTimeMillis(); + } + setter.set(carrier, FIELD, String.valueOf(propagationStart)); + } + + @Override + public Context extract(Context context, C carrier, TextMapGetter getter) { + String propagationStart = getter.get(carrier, FIELD); + if (propagationStart != null) { + return context.with(PROPAGATION_START_KEY, Long.valueOf(propagationStart)); + } else { + return context; + } + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropagatorProvider.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropagatorProvider.java new file mode 100644 index 000000000..3f3438c97 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropagatorProvider.java @@ -0,0 +1,24 @@ +package com.example.javaagent; + +import com.google.auto.service.AutoService; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +/** + * Registers the custom propagator used by this example. + * + * @see ConfigurablePropagatorProvider + * @see DemoPropagator + */ +@AutoService(ConfigurablePropagatorProvider.class) +public class DemoPropagatorProvider implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return new DemoPropagator(); + } + + @Override + public String getName() { + return "demo"; + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropertySource.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropertySource.java new file mode 100644 index 000000000..0c0f24a5d --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoPropertySource.java @@ -0,0 +1,24 @@ +package com.example.javaagent; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.spi.config.PropertySource; +import java.util.Map; + +/** + * {@link PropertySource} is an SPI provided by OpenTelemetry Java instrumentation agent. + * By implementing it custom distributions can supply their own default configuration. + * The configuration priority, from highest to lowest is: + * system properties -> environment variables -> configuration file -> PropertySource SPI -> hard-coded defaults + */ +@AutoService(PropertySource.class) +public class DemoPropertySource implements PropertySource { + + @Override + public Map getProperties() { + return Map.of( + "otel.exporter.otlp.endpoint", "http://collector:55680", + "otel.exporter.otlp.insecure", "true", + "otel.config.max.attrs", "16" + ); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoResourceProvider.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoResourceProvider.java new file mode 100644 index 000000000..cad1ba098 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoResourceProvider.java @@ -0,0 +1,16 @@ +package com.example.javaagent; + +import com.google.auto.service.AutoService; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +@AutoService(ResourceProvider.class) +public class DemoResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + Attributes attributes = Attributes.builder().put("custom.resource", "demo").build(); + return Resource.create(attributes); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSampler.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSampler.java new file mode 100644 index 000000000..67bcade15 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSampler.java @@ -0,0 +1,35 @@ +package com.example.javaagent; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; + +/** + * This demo sampler filters out all internal spans whose name contains string "greeting". + *

+ * See + * OpenTelemetry Specification for more information about span sampling. + * + * @see DemoSdkTracerProviderConfigurer + */ +public class DemoSampler implements Sampler { + @Override + public SamplingResult shouldSample(Context parentContext, String traceId, String name, + SpanKind spanKind, Attributes attributes, List parentLinks) { + if (spanKind == SpanKind.INTERNAL && name.contains("greeting")) { + return SamplingResult.create(SamplingDecision.DROP); + } else { + return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE); + } + } + + @Override + public String getDescription() { + return "DemoSampler"; + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSdkTracerProviderConfigurer.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSdkTracerProviderConfigurer.java new file mode 100644 index 000000000..8ad8ab828 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSdkTracerProviderConfigurer.java @@ -0,0 +1,30 @@ +package com.example.javaagent; + +import com.google.auto.service.AutoService; +import io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; + +/** + * This is one of the main entry points for Instrumentation Agent's customizations. + * It allows configuring {@link SdkTracerProviderBuilder}. + * See the {@link #configure(SdkTracerProviderBuilder)} method below. + *

+ * Also see https://github.com/open-telemetry/opentelemetry-java/issues/2022 + * + * @see SdkTracerProviderConfigurer + * @see DemoPropagatorProvider + */ +@AutoService(SdkTracerProviderConfigurer.class) +public class DemoSdkTracerProviderConfigurer implements SdkTracerProviderConfigurer { + @Override + public void configure(SdkTracerProviderBuilder tracerProvider) { + tracerProvider + .setIdGenerator(new DemoIdGenerator()) + .setSpanLimits(SpanLimits.builder().setMaxNumberOfAttributes(1024).build()) + .setSampler(new DemoSampler()) + .addSpanProcessor(new DemoSpanProcessor()) + .addSpanProcessor(SimpleSpanProcessor.create(new DemoSpanExporter())); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSpanExporter.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSpanExporter.java new file mode 100644 index 000000000..476093a76 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSpanExporter.java @@ -0,0 +1,30 @@ +package com.example.javaagent; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; + +/** + * See + * OpenTelemetry Specification for more information about {@link SpanExporter}. + * + * @see DemoSdkTracerProviderConfigurer + */ +public class DemoSpanExporter implements SpanExporter { + @Override + public CompletableResultCode export(Collection spans) { + System.out.printf("%d spans exported%n", spans.size()); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSpanProcessor.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSpanProcessor.java new file mode 100644 index 000000000..32aa2f319 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/DemoSpanProcessor.java @@ -0,0 +1,52 @@ +package com.example.javaagent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import org.apache.commons.lang3.RandomStringUtils; + +/** + * See + * OpenTelemetry Specification for more information about {@link SpanProcessor}. + * + * @see DemoSdkTracerProviderConfigurer + */ +public class DemoSpanProcessor implements SpanProcessor { + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + /* + The sole purpose of this attribute is to introduce runtime dependency on some external library. + We need this to demonstrate how extension can use them. + */ + span.setAttribute("random", RandomStringUtils.random(10)); + span.setAttribute("custom", "demo"); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + + } + + @Override + public boolean isEndRequired() { + return false; + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java new file mode 100644 index 000000000..7238f4c0e --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java @@ -0,0 +1,54 @@ +package com.example.javaagent.instrumentation; + +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +public class DemoServlet3Instrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return AgentElementMatchers.safeHasSuperType( + namedOneOf("javax.servlet.Filter", "javax.servlet.http.HttpServlet")); + } + + @Override + public void transform(TypeTransformer typeTransformer) { + typeTransformer.applyAdviceToMethod( + namedOneOf("doFilter", "service") + .and( + ElementMatchers.takesArgument( + 0, ElementMatchers.named("javax.servlet.ServletRequest"))) + .and( + ElementMatchers.takesArgument( + 1, ElementMatchers.named("javax.servlet.ServletResponse"))) + .and(ElementMatchers.isPublic()), + this.getClass().getName() + "$DemoServlet3Advice"); + } + + @SuppressWarnings("unused") + public static class DemoServlet3Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(value = 1) ServletResponse response) { + if (!(response instanceof HttpServletResponse)) { + return; + } + + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + if (!httpServletResponse.containsHeader("X-server-id")) { + httpServletResponse.setHeader( + "X-server-id", Java8BytecodeBridge.currentSpan().getSpanContext().getTraceId()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java new file mode 100644 index 000000000..65ec43e4a --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java @@ -0,0 +1,41 @@ +package com.example.javaagent.instrumentation; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This is a demo instrumentation which hooks into servlet invocation and modifies the http + * response. + */ +@AutoService(InstrumentationModule.class) +public final class DemoServlet3InstrumentationModule extends InstrumentationModule { + public DemoServlet3InstrumentationModule() { + super("servlet-demo", "servlet-3"); + } + + /* + We want this instrumentation to be applied after the standard servlet instrumentation. + The latter creates a server span around http request. + This instrumentation needs access to that server span. + */ + @Override + public int order() { + return 1; + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return ClassLoaderMatcher.hasClassesNamed("javax.servlet.http.HttpServlet"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new DemoServlet3Instrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java b/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java new file mode 100644 index 000000000..5b5ac1763 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java @@ -0,0 +1,217 @@ +package com.example.javaagent.smoketest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.Span; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.MountableFile; + +abstract class IntegrationTest { + private static final Logger logger = LoggerFactory.getLogger(IntegrationTest.class); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + protected static OkHttpClient client = OkHttpUtils.client(); + + private static final Network network = Network.newNetwork(); + protected static final String agentPath = + System.getProperty("io.opentelemetry.smoketest.agentPath"); + //Javaagent with extensions embedded inside it + protected static final String extendedAgentPath = + System.getProperty("io.opentelemetry.smoketest.extendedAgentPath"); + protected static final String extensionPath = + System.getProperty("io.opentelemetry.smoketest.extensionPath"); + + protected abstract String getTargetImage(int jdk); + + /** + * Subclasses can override this method to customise target application's environment + */ + protected Map getExtraEnv() { + return Collections.emptyMap(); + } + + private static GenericContainer backend; + private static GenericContainer collector; + + @BeforeAll + static void setupSpec() { + backend = + new GenericContainer<>( + "ghcr.io/open-telemetry/java-test-containers:smoke-fake-backend-20210324.684269693") + .withExposedPorts(8080) + .waitingFor(Wait.forHttp("/health").forPort(8080)) + .withNetwork(network) + .withNetworkAliases("backend") + .withLogConsumer(new Slf4jLogConsumer(logger)); + backend.start(); + + collector = + new GenericContainer<>("otel/opentelemetry-collector-dev:latest") + .dependsOn(backend) + .withNetwork(network) + .withNetworkAliases("collector") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withCopyFileToContainer( + MountableFile.forClasspathResource("/otel.yaml"), "/etc/otel.yaml") + .withCommand("--config /etc/otel.yaml"); + collector.start(); + } + + protected GenericContainer target; + + void startTarget(String extensionLocation) { + target = buildTargetContainer(agentPath, extensionLocation); + target.start(); + } + + void startTargetWithExtendedAgent() { + target = buildTargetContainer(extendedAgentPath, null); + target.start(); + } + + private GenericContainer buildTargetContainer(String agentPath, String extensionLocation) { + GenericContainer result = + new GenericContainer<>(getTargetImage(11)) + .withExposedPorts(8080) + .withNetwork(network) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withCopyFileToContainer( + MountableFile.forHostPath(agentPath), "/opentelemetry-javaagent.jar") + //Adds instrumentation agent with debug configuration to the target application + .withEnv("JAVA_TOOL_OPTIONS", + "-javaagent:/opentelemetry-javaagent.jar -Dotel.javaagent.debug=true") + .withEnv("OTEL_BSP_MAX_EXPORT_BATCH", "1") + .withEnv("OTEL_BSP_SCHEDULE_DELAY", "10") + .withEnv("OTEL_PROPAGATORS", "tracecontext,baggage,demo") + .withEnv(getExtraEnv()); + //If external extensions are requested + if (extensionLocation != null) { + //Asks instrumentation agent to include extensions from given location into its runtime + result = result.withCopyFileToContainer( + MountableFile.forHostPath(extensionPath), "/opentelemetry-extensions.jar") + .withEnv("OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS", extensionLocation); + } + return result; + } + + @AfterEach + void cleanup() throws IOException { + client + .newCall( + new Request.Builder() + .url( + String.format( + "http://localhost:%d/clear", backend.getMappedPort(8080))) + .build()) + .execute() + .close(); + } + + void stopTarget() { + target.stop(); + } + + @AfterAll + static void cleanupSpec() { + backend.stop(); + collector.stop(); + } + + protected static int countResourcesByValue(Collection traces, + String resourceName, String value) { + return (int) traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .flatMap(it -> it.getResource().getAttributesList().stream()) + .filter( + kv -> kv.getKey().equals(resourceName) && kv.getValue().getStringValue().equals(value)) + .count(); + } + + protected static int countSpansByName( + Collection traces, String spanName) { + return (int) getSpanStream(traces).filter(it -> it.getName().equals(spanName)).count(); + } + + protected static int countSpansByAttributeValue( + Collection traces, String attributeName, String attributeValue) { + return (int) getSpanStream(traces) + .flatMap(it -> it.getAttributesList().stream()) + .filter(kv -> kv.getKey().equals(attributeName) && kv.getValue().getStringValue() + .equals(attributeValue)) + .count(); + } + + protected static Stream getSpanStream(Collection traces) { + return traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .flatMap(it -> it.getInstrumentationLibrarySpansList().stream()) + .flatMap(it -> it.getSpansList().stream()); + } + + protected Collection waitForTraces() + throws IOException, InterruptedException { + String content = waitForContent(); + + return StreamSupport.stream(OBJECT_MAPPER.readTree(content).spliterator(), false) + .map( + it -> { + ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); + try { + JsonFormat.parser().merge(OBJECT_MAPPER.writeValueAsString(it), builder); + } catch (InvalidProtocolBufferException | JsonProcessingException e) { + e.printStackTrace(); + } + return builder.build(); + }) + .collect(Collectors.toList()); + } + + private String waitForContent() throws IOException, InterruptedException { + long previousSize = 0; + long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30); + String content = "[]"; + while (System.currentTimeMillis() < deadline) { + + Request request = + new Request.Builder() + .url(String.format("http://localhost:%d/get-traces", backend.getMappedPort(8080))) + .build(); + + try (ResponseBody body = client.newCall(request).execute().body()) { + content = body.string(); + } + + if (content.length() > 2 && content.length() == previousSize) { + break; + } + previousSize = content.length(); + System.out.printf("Current content size %d%n", previousSize); + TimeUnit.MILLISECONDS.sleep(500); + } + + return content; + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/OkHttpUtils.java b/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/OkHttpUtils.java new file mode 100644 index 000000000..b8956aa29 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/OkHttpUtils.java @@ -0,0 +1,23 @@ +package com.example.javaagent.smoketest; + +import java.util.concurrent.TimeUnit; +import okhttp3.OkHttpClient; + +public class OkHttpUtils { + + static OkHttpClient.Builder clientBuilder() { + TimeUnit unit = TimeUnit.MINUTES; + return new OkHttpClient.Builder() + .connectTimeout(1, unit) + .writeTimeout(1, unit) + .readTimeout(1, unit); + } + + public static OkHttpClient client() { + return client(false); + } + + public static OkHttpClient client(boolean followRedirects) { + return clientBuilder().followRedirects(followRedirects).build(); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java b/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java new file mode 100644 index 000000000..078454846 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java @@ -0,0 +1,74 @@ +package com.example.javaagent.smoketest; + +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import java.io.IOException; +import java.util.Collection; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SpringBootIntegrationTest extends IntegrationTest { + + protected String getTargetImage(int jdk) { + return "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk" + jdk + + "-20210218.577304949"; + } + + @Test + public void extensionsAreLoadedFromJar() throws IOException, InterruptedException { + startTarget("/opentelemetry-extensions.jar"); + + testAndVerify(); + + stopTarget(); + } + + @Test + public void extensionsAreLoadedFromFolder() throws IOException, InterruptedException { + startTarget("/"); + + testAndVerify(); + + stopTarget(); + } + + @Test + public void extensionsAreLoadedFromJavaagent() throws IOException, InterruptedException { + startTargetWithExtendedAgent(); + + testAndVerify(); + + stopTarget(); + } + + private void testAndVerify() throws IOException, InterruptedException { + String url = String.format("http://localhost:%d/greeting", target.getMappedPort(8080)); + Request request = new Request.Builder().url(url).get().build(); + + String currentAgentVersion = + (String) new JarFile(agentPath) + .getManifest() + .getMainAttributes() + .get(Attributes.Name.IMPLEMENTATION_VERSION); + + Response response = client.newCall(request).execute(); + + Collection traces = waitForTraces(); + + Assertions.assertNotNull(response.header("X-server-id")); + Assertions.assertEquals(1, response.headers("X-server-id").size()); + Assertions.assertTrue(TraceId.isValid(response.header("X-server-id"))); + Assertions.assertEquals("Hi!", response.body().string()); + Assertions.assertEquals(1, countSpansByName(traces, "/greeting")); + Assertions.assertEquals(0, countSpansByName(traces, "WebController.greeting")); + Assertions.assertEquals(1, countSpansByName(traces, "WebController.withSpan")); + Assertions.assertEquals(2, countSpansByAttributeValue(traces, "custom", "demo")); + Assertions.assertNotEquals(0, + countResourcesByValue(traces, "telemetry.auto.version", currentAgentVersion)); + Assertions.assertNotEquals(0, countResourcesByValue(traces, "custom.resource", "demo")); + } +} diff --git a/opentelemetry-java-instrumentation/examples/extension/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/examples/extension/src/test/resources/logback.xml new file mode 100644 index 000000000..3fefcd72b --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/examples/extension/src/test/resources/otel.yaml b/opentelemetry-java-instrumentation/examples/extension/src/test/resources/otel.yaml new file mode 100644 index 000000000..a25201f89 --- /dev/null +++ b/opentelemetry-java-instrumentation/examples/extension/src/test/resources/otel.yaml @@ -0,0 +1,31 @@ +extensions: + health_check: + pprof: + endpoint: 0.0.0.0:1777 + zpages: + endpoint: 0.0.0.0:55679 + +receivers: + otlp: + protocols: + grpc: + zipkin: + +processors: + batch: + +exporters: + logging: + loglevel: debug + otlp: + endpoint: backend:8080 + insecure: true + +service: + pipelines: + traces: + receivers: [otlp, zipkin] + processors: [batch] + exporters: [logging, otlp] + + extensions: [health_check, pprof, zpages] diff --git a/opentelemetry-java-instrumentation/gradle.properties b/opentelemetry-java-instrumentation/gradle.properties new file mode 100644 index 000000000..e7ed03a2e --- /dev/null +++ b/opentelemetry-java-instrumentation/gradle.properties @@ -0,0 +1,9 @@ +org.gradle.parallel=true +org.gradle.caching=true + +org.gradle.priority=low + +# Gradle default is 256m which causes issues with our build - https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory +org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m + +org.gradle.warning.mode=fail diff --git a/opentelemetry-java-instrumentation/gradle/enforcement/checkstyle.xml b/opentelemetry-java-instrumentation/gradle/enforcement/checkstyle.xml new file mode 100644 index 000000000..4a532b0ba --- /dev/null +++ b/opentelemetry-java-instrumentation/gradle/enforcement/checkstyle.xml @@ -0,0 +1,392 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/gradle/enforcement/codenarc.groovy b/opentelemetry-java-instrumentation/gradle/enforcement/codenarc.groovy new file mode 100644 index 000000000..bf13e492c --- /dev/null +++ b/opentelemetry-java-instrumentation/gradle/enforcement/codenarc.groovy @@ -0,0 +1,411 @@ +ruleset { + // rulesets/basic.xml + /* + AssertWithinFinallyBlock + AssignmentInConditional + BigDecimalInstantiation + BitwiseOperatorInConditional + BooleanGetBoolean + BrokenNullCheck + BrokenOddnessCheck + ClassForName + ComparisonOfTwoConstants + ComparisonWithSelf + ConstantAssertExpression + ConstantIfExpression + ConstantTernaryExpression + DeadCode + DoubleNegative + DuplicateCaseStatement + DuplicateMapKey + DuplicateSetValue + EmptyCatchBlock + EmptyClass + EmptyElseBlock + EmptyFinallyBlock + EmptyForStatement + EmptyIfStatement + EmptyInstanceInitializer + EmptyMethod + EmptyStaticInitializer + EmptySwitchStatement + EmptySynchronizedStatement + EmptyTryBlock + EmptyWhileStatement + EqualsAndHashCode + EqualsOverloaded + ExplicitGarbageCollection + ForLoopShouldBeWhileLoop + HardCodedWindowsFileSeparator + HardCodedWindowsRootDirectory + IntegerGetInteger + RandomDoubleCoercedToZero + RemoveAllOnSelf + ReturnFromFinallyBlock + ThrowExceptionFromFinallyBlock + */ + + // rulesets/braces.xml + ElseBlockBraces + ForStatementBraces + IfStatementBraces + WhileStatementBraces + + // rulesets/concurrency.xml + /* + BusyWait + DoubleCheckedLocking + InconsistentPropertyLocking + InconsistentPropertySynchronization + NestedSynchronization + StaticCalendarField + StaticConnection + StaticDateFormatField + StaticMatcherField + StaticSimpleDateFormatField + SynchronizedMethod + SynchronizedOnBoxedPrimitive + SynchronizedOnGetClass + SynchronizedOnReentrantLock + SynchronizedOnString + SynchronizedOnThis + SynchronizedReadObjectMethod + SystemRunFinalizersOnExit + ThisReferenceEscapesConstructor + ThreadGroup + ThreadLocalNotStaticFinal + ThreadYield + UseOfNotifyMethod + VolatileArrayField + VolatileLongOrDoubleField + WaitOutsideOfWhileLoop + */ + + // rulesets/convention.xml + /* + ConfusingTernary + CouldBeElvis + HashtableIsObsolete + IfStatementCouldBeTernary + InvertedIfElse + LongLiteralWithLowerCaseL + ParameterReassignment + TernaryCouldBeElvis + VectorIsObsolete + */ + + // rulesets/design.xml + /* + AbstractClassWithPublicConstructor + AbstractClassWithoutAbstractMethod + BooleanMethodReturnsNull + BuilderMethodWithSideEffects + CloneableWithoutClone + CloseWithoutCloseable + CompareToWithoutComparable + ConstantsOnlyInterface + EmptyMethodInAbstractClass + FinalClassWithProtectedMember + ImplementationAsType + LocaleSetDefault + PrivateFieldCouldBeFinal + PublicInstanceField + ReturnsNullInsteadOfEmptyArray + ReturnsNullInsteadOfEmptyCollection + SimpleDateFormatMissingLocale + StatelessSingleton + */ + + // rulesets/dry.xml + /* + DuplicateListLiteral + DuplicateMapLiteral + DuplicateNumberLiteral + DuplicateStringLiteral + */ + + // rulesets/enhanced.xml + /* + CloneWithoutCloneable + JUnitAssertEqualsConstantActualValue + UnsafeImplementationAsMap + */ + + // rulesets/exceptions.xml + /* + CatchArrayIndexOutOfBoundsException + CatchError + CatchException + CatchIllegalMonitorStateException + CatchIndexOutOfBoundsException + CatchNullPointerException + CatchRuntimeException + CatchThrowable + ConfusingClassNamedException + ExceptionExtendsError + ExceptionNotThrown + MissingNewInThrowStatement + ReturnNullFromCatchBlock + SwallowThreadDeath + ThrowError + ThrowException + ThrowNullPointerException + ThrowRuntimeException + ThrowThrowable + */ + + // rulesets/formatting.xml + /* + BracesForClass + BracesForForLoop + BracesForIfElse + BracesForMethod + BracesForTryCatchFinally + ClassJavadoc + ClosureStatementOnOpeningLineOfMultipleLineClosure + LineLength + SpaceAfterCatch + SpaceAfterClosingBrace + SpaceAfterComma + SpaceAfterFor + SpaceAfterIf + SpaceAfterOpeningBrace + SpaceAfterSemicolon + SpaceAfterSwitch + SpaceAfterWhile + SpaceAroundClosureArrow + SpaceAroundMapEntryColon + SpaceAroundOperator + SpaceBeforeClosingBrace + SpaceBeforeOpeningBrace + */ + + // rulesets/generic.xml + /* + IllegalClassMember + IllegalClassReference + IllegalPackageReference + IllegalRegex + IllegalString + RequiredRegex + RequiredString + StatelessClass + */ + + // rulesets/grails.xml + /* + GrailsDomainHasEquals + GrailsDomainHasToString + GrailsDomainReservedSqlKeywordName + GrailsDomainWithServiceReference + GrailsDuplicateConstraint + GrailsDuplicateMapping + GrailsPublicControllerMethod + GrailsServletContextReference + GrailsSessionReference // DEPRECATED + GrailsStatelessService + */ + + // rulesets/groovyism.xml + /* + AssignCollectionSort + AssignCollectionUnique + ClosureAsLastMethodParameter + CollectAllIsDeprecated + ConfusingMultipleReturns + ExplicitArrayListInstantiation + ExplicitCallToAndMethod + ExplicitCallToCompareToMethod + ExplicitCallToDivMethod + ExplicitCallToEqualsMethod + ExplicitCallToGetAtMethod + ExplicitCallToLeftShiftMethod + ExplicitCallToMinusMethod + ExplicitCallToModMethod + ExplicitCallToMultiplyMethod + ExplicitCallToOrMethod + ExplicitCallToPlusMethod + ExplicitCallToPowerMethod + ExplicitCallToRightShiftMethod + ExplicitCallToXorMethod + ExplicitHashMapInstantiation + ExplicitHashSetInstantiation + ExplicitLinkedHashMapInstantiation + ExplicitLinkedListInstantiation + ExplicitStackInstantiation + ExplicitTreeSetInstantiation + GStringAsMapKey + GStringExpressionWithinString + GetterMethodCouldBeProperty + GroovyLangImmutable + UseCollectMany + UseCollectNested + */ + + // rulesets/imports.xml + DuplicateImport + ImportFromSamePackage +// ImportFromSunPackages +// MisorderedStaticImports + UnnecessaryGroovyImport + UnusedImport + + // rulesets/jdbc.xml + /* + DirectConnectionManagement + JdbcConnectionReference + JdbcResultSetReference + JdbcStatementReference + */ + + // rulesets/junit.xml + /* + ChainedTest + CoupledTestCase + JUnitAssertAlwaysFails + JUnitAssertAlwaysSucceeds + JUnitFailWithoutMessage + JUnitLostTest + JUnitPublicField + JUnitPublicNonTestMethod + JUnitSetUpCallsSuper + JUnitStyleAssertions + JUnitTearDownCallsSuper + JUnitTestMethodWithoutAssert + JUnitUnnecessarySetUp + JUnitUnnecessaryTearDown + JUnitUnnecessaryThrowsException + SpockIgnoreRestUsed + UnnecessaryFail + UseAssertEqualsInsteadOfAssertTrue + UseAssertFalseInsteadOfNegation + UseAssertNullInsteadOfAssertEquals + UseAssertSameInsteadOfAssertTrue + UseAssertTrueInsteadOfAssertEquals + UseAssertTrueInsteadOfNegation + */ + + // rulesets/logging.xml + /* + LoggerForDifferentClass + LoggerWithWrongModifiers + LoggingSwallowsStacktrace + MultipleLoggers + PrintStackTrace + Println + SystemErrPrint + SystemOutPrint + */ + + // rulesets/naming.xml + AbstractClassName + ClassName { + regex = '^[A-Z][\\$a-zA-Z0-9]*$' + } + ClassNameSameAsFilename +// ConfusingMethodName +// FactoryMethodName + FieldName { + regex = '^_?[a-z][a-zA-Z0-9]*$' + finalRegex = '^_?[a-z][a-zA-Z0-9]*$' + // can be either constant (ABC_XYZ) or non-constant (abcXyz) + staticFinalRegex = '^[A-Z][A-Z_0-9]*$|^_?[a-z][a-zA-Z0-9]*$' + } + InterfaceName + MethodName { + regex = '^[a-z][\\$_a-zA-Z0-9]*$|^.*\\s.*$' + } + ObjectOverrideMisspelledMethodName + ParameterName + PropertyName + VariableName { + finalRegex = '^[a-z][a-zA-Z0-9]*$' + } + + // rulesets/security.xml + /* + FileCreateTempFile + InsecureRandom + JavaIoPackageAccess + NonFinalPublicField + NonFinalSubclassOfSensitiveInterface + ObjectFinalize + PublicFinalizeMethod + SystemExit + UnsafeArrayDeclaration + */ + + // rulesets/serialization.xml + /* + EnumCustomSerializationIgnored + SerialPersistentFields + SerialVersionUID + SerializableClassMustDefineSerialVersionUID + */ + + // rulesets/size.xml + /* + AbcComplexity // DEPRECATED: Use the AbcMetric rule instead. Requires the GMetrics jar + AbcMetric // Requires the GMetrics jar + ClassSize + CrapMetric // Requires the GMetrics jar and a Cobertura coverage file + CyclomaticComplexity // Requires the GMetrics jar + MethodCount + MethodSize + NestedBlockDepth + */ + + // rulesets/unnecessary.xml + AddEmptyString + ConsecutiveLiteralAppends + ConsecutiveStringConcatenation + UnnecessaryBigDecimalInstantiation + UnnecessaryBigIntegerInstantiation + UnnecessaryBooleanExpression + UnnecessaryBooleanInstantiation +// UnnecessaryCallForLastElement + UnnecessaryCallToSubstring + UnnecessaryCatchBlock +// UnnecessaryCollectCall + UnnecessaryCollectionCall + UnnecessaryConstructor + UnnecessaryDefInFieldDeclaration + UnnecessaryDefInMethodDeclaration + UnnecessaryDefInVariableDeclaration + UnnecessaryDotClass + UnnecessaryDoubleInstantiation + UnnecessaryElseStatement + UnnecessaryFinalOnPrivateMethod + UnnecessaryFloatInstantiation +// UnnecessaryGString +// UnnecessaryGetter + UnnecessaryIfStatement + UnnecessaryInstanceOfCheck + UnnecessaryInstantiationToGetClass + UnnecessaryIntegerInstantiation + UnnecessaryLongInstantiation + UnnecessaryModOne + UnnecessaryNullCheck + UnnecessaryNullCheckBeforeInstanceOf +// UnnecessaryObjectReferences + UnnecessaryOverridingMethod +// UnnecessaryPackageReference + UnnecessaryParenthesesForMethodCallWithClosure + UnnecessaryPublicModifier +// UnnecessaryReturnKeyword +// UnnecessarySelfAssignment + UnnecessarySemicolon + UnnecessaryStringInstantiation +// UnnecessarySubstring + UnnecessaryTernaryExpression + UnnecessaryTransientModifier + + // rulesets/unused.xml + UnusedArray +// UnusedMethodParameter + UnusedObject + UnusedPrivateField + UnusedPrivateMethod + UnusedPrivateMethodParameter + UnusedVariable +} diff --git a/opentelemetry-java-instrumentation/gradle/enforcement/spotless-groovy.properties b/opentelemetry-java-instrumentation/gradle/enforcement/spotless-groovy.properties new file mode 100644 index 000000000..c1d6b757d --- /dev/null +++ b/opentelemetry-java-instrumentation/gradle/enforcement/spotless-groovy.properties @@ -0,0 +1,12 @@ +# Disable formatting errors +ignoreFormatterProblems=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=2 +org.eclipse.jdt.core.formatter.indentation.size=1 +org.eclipse.jdt.core.formatter.indentation.text_block_indentation=indent by one +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.continuation_indentation=1 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=1 +groovy.formatter.longListLength=50 +groovy.formatter.multiline.indentation=1 +groovy.formatter.remove.unnecessary.semicolons=true diff --git a/opentelemetry-java-instrumentation/gradle/enforcement/spotless.license.java b/opentelemetry-java-instrumentation/gradle/enforcement/spotless.license.java new file mode 100644 index 000000000..b504712ee --- /dev/null +++ b/opentelemetry-java-instrumentation/gradle/enforcement/spotless.license.java @@ -0,0 +1,5 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + diff --git a/opentelemetry-java-instrumentation/gradle/instrumentation.gradle b/opentelemetry-java-instrumentation/gradle/instrumentation.gradle new file mode 100644 index 000000000..1c7282f1d --- /dev/null +++ b/opentelemetry-java-instrumentation/gradle/instrumentation.gradle @@ -0,0 +1,138 @@ +// common gradle file for instrumentation +import io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator + +apply plugin: 'net.bytebuddy.byte-buddy' +apply plugin: 'otel.shadow-conventions' + +ext { + mavenGroupId = 'io.opentelemetry.javaagent.instrumentation' + // Shadow is only for testing, not publishing. + noShadowPublish = true +} + +apply plugin: "otel.java-conventions" +if (project.ext.find("skipPublish") != true) { + apply plugin: "otel.publish-conventions" +} + +apply plugin: "otel.instrumentation-conventions" + +if (projectDir.name == 'javaagent') { + apply plugin: 'muzzle' + + archivesBaseName = projectDir.parentFile.name +} + +configurations { + toolingRuntime { + canBeConsumed = false + canBeResolved = true + } + + bootstrapRuntime { + canBeConsumed = false + canBeResolved = true + } +} + +afterEvaluate { + dependencies { + compileOnly project(':instrumentation-api') + compileOnly project(':javaagent-api') + compileOnly project(':javaagent-bootstrap') + // Apply common dependencies for instrumentation. + compileOnly(project(':javaagent-extension-api')) { + // OpenTelemetry SDK is not needed for compilation + exclude group: 'run.mone', module: 'opentelemetry-sdk' + exclude group: 'run.mone', module: 'opentelemetry-sdk-metrics' + } + compileOnly(project(':javaagent-tooling')) { + // OpenTelemetry SDK is not needed for compilation + exclude group: 'run.mone', module: 'opentelemetry-sdk' + exclude group: 'run.mone', module: 'opentelemetry-sdk-metrics' + } + compileOnly "net.bytebuddy:byte-buddy" + annotationProcessor "com.google.auto.service:auto-service" + compileOnly "com.google.auto.service:auto-service" + compileOnly "org.slf4j:slf4j-api" + + testImplementation "run.mone:opentelemetry-api" + + testImplementation project(':testing-common') + testAnnotationProcessor "net.bytebuddy:byte-buddy" + testCompileOnly "net.bytebuddy:byte-buddy" + + testImplementation "org.testcontainers:testcontainers" + + toolingRuntime(project(path: ":javaagent-tooling", configuration: 'instrumentationMuzzle')) + toolingRuntime(project(path: ":javaagent-extension-api", configuration: 'instrumentationMuzzle')) + + bootstrapRuntime(project(path: ":javaagent-bootstrap", configuration: 'instrumentationMuzzle')) + } + + def pluginName = 'io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin' + new ByteBuddyPluginConfigurator(project, sourceSets.main, pluginName, + configurations.toolingRuntime + configurations.runtimeClasspath + ).configure() +} + +configurations { + testInstrumentation { + canBeResolved = true + canBeConsumed = false + } +} + +tasks.named('shadowJar').configure { + configurations = [project.configurations.runtimeClasspath, project.configurations.testInstrumentation] + + archiveFileName = 'agent-testing.jar' +} + +evaluationDependsOn(":testing:agent-for-testing") + +// need to run this after evaluate because testSets plugin adds new test tasks +afterEvaluate { + tasks.withType(Test).configureEach { + inputs.file(shadowJar.archiveFile) + + jvmArgs "-Dotel.javaagent.debug=true" + jvmArgs "-javaagent:${project(":testing:agent-for-testing").tasks.shadowJar.archiveFile.get().asFile.absolutePath}" + jvmArgs "-Dotel.javaagent.experimental.initializer.jar=${shadowJar.archiveFile.get().asFile.absolutePath}" + jvmArgs "-Dotel.javaagent.testing.additional-library-ignores.enabled=false" + def failOnContextLeak = findProperty('failOnContextLeak') + jvmArgs "-Dotel.javaagent.testing.fail-on-context-leak=${failOnContextLeak == null || failOnContextLeak}" + // prevent sporadic gradle deadlocks, see SafeLogger for more details + jvmArgs "-Dotel.javaagent.testing.transform-safe-logging.enabled=true" + + dependsOn shadowJar + dependsOn ":testing:agent-for-testing:shadowJar" + + // We do fine-grained filtering of the classpath of this codebase's sources since Gradle's + // configurations will include transitive dependencies as well, which tests do often need. + classpath = classpath.filter { + // The sources are packaged into the testing jar so we need to make sure to exclude from the test + // classpath, which automatically inherits them, to ensure our shaded versions are used. + if (file("$buildDir/resources/main").equals(it) || file("$buildDir/classes/java/main").equals(it)) { + return false + } + // If agent depends on some shared instrumentation module that is not a testing module, it will + // be packaged into the testing jar so we need to make sure to exclude from the test classpath. + String libPath = it.absolutePath + String instrumentationPath = file("${rootDir}/instrumentation/").absolutePath + if (libPath.startsWith(instrumentationPath) && + libPath.endsWith(".jar") && + !libPath.substring(instrumentationPath.size()).contains("testing")) { + return false + } + return true + } + } +} + +configurations.configureEach { + if (it.name.toLowerCase().endsWith('testruntimeclasspath')) { + // Added by agent, don't let Gradle bring it in when running tests. + exclude module: 'javaagent-bootstrap' + } +} diff --git a/opentelemetry-java-instrumentation/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java-instrumentation/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..c9d55ea1c --- /dev/null +++ b/opentelemetry-java-instrumentation/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637 +size 59203 diff --git a/opentelemetry-java-instrumentation/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java-instrumentation/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ff541e597 --- /dev/null +++ b/opentelemetry-java-instrumentation/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.1-bin.zip +distributionSha256Sum=dccda8aa069563c8ba2f6cdfd0777df0e34a5b4d15138ca8b9757e94f4e8a8cb +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/opentelemetry-java-instrumentation/gradlew b/opentelemetry-java-instrumentation/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/opentelemetry-java-instrumentation/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java-instrumentation/gradlew.bat b/opentelemetry-java-instrumentation/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/opentelemetry-java-instrumentation/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/opentelemetry-java-instrumentation/instrumentation-api-caching/instrumentation-api-caching.gradle b/opentelemetry-java-instrumentation/instrumentation-api-caching/instrumentation-api-caching.gradle new file mode 100644 index 000000000..1d6dd03e6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api-caching/instrumentation-api-caching.gradle @@ -0,0 +1,43 @@ +plugins { + id "com.github.johnrengelman.shadow" +} + +group = 'io.opentelemetry.instrumentation' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +configurations { + shadowInclude { + canBeResolved = true + canBeConsumed = false + } +} + +dependencies { + compileOnly "com.github.ben-manes.caffeine:caffeine" + shadowInclude("com.github.ben-manes.caffeine:caffeine") { + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'org.checkerframework', module: 'checker-qual' + } + + compileOnly "com.blogspot.mydailyjava:weak-lock-free" + shadowInclude "com.blogspot.mydailyjava:weak-lock-free" +} + +shadowJar { + configurations = [project.configurations.shadowInclude] + + archiveClassifier.set("") + + relocate "com.github.benmanes.caffeine", "io.opentelemetry.instrumentation.api.internal.shaded.caffeine" + relocate "com.blogspot.mydailyjava.weaklockfree", "io.opentelemetry.instrumentation.api.internal.shaded.weaklockfree" + + minimize() +} + +jar { + enabled = false + + dependsOn shadowJar +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/com/github/benmanes/caffeine/cache/CacheImplementations.java b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/com/github/benmanes/caffeine/cache/CacheImplementations.java new file mode 100644 index 000000000..f48459eb5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/com/github/benmanes/caffeine/cache/CacheImplementations.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.github.benmanes.caffeine.cache; + +// Caffeine uses reflection to load cache implementations based on parameters specified by a user. +// We use gradle-shadow-plugin to minimize the dependency on Caffeine, but it does not allow +// specifying classes to keep, only artifacts. It's a relatively simple workaround for us to use +// this non-public class to create a static link to the required implementations we use. +final class CacheImplementations { + + // Each type of cache has a cache implementation and a node implementation. + + // Strong keys, strong values, maximum size + SSMS ssms; // cache + PSMS psms; // node + + // Weak keys, strong values, maximum size + WSMS wsms; // cache + FSMS fsms; // node + + // Weak keys, weak values + WI wi; // cache + FW fw; // node + + private CacheImplementations() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/Cache.java b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/Cache.java new file mode 100644 index 000000000..0a12f4386 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/Cache.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.caching; + +import java.util.function.Function; + +/** A cache from keys to values. */ +public interface Cache { + + /** Returns a new {@link CacheBuilder} to configure a {@link Cache}. */ + static CacheBuilder newBuilder() { + return new CacheBuilder(); + } + + /** + * Returns the cached value associated with the provided {@code key}. If no value is cached yet, + * computes the value using {@code mappingFunction}, stores the result, and returns it. + */ + V computeIfAbsent(K key, Function mappingFunction); + + /** + * Returns the cached value associated with the provided {@code key} if present, or {@code null} + * otherwise. + */ + V get(K key); + + /** Puts the {@code value} into the cache for the {@code key}. */ + void put(K key, V value); + + /** Removes a value for {@code key} if present. */ + void remove(K key); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/CacheBuilder.java b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/CacheBuilder.java new file mode 100644 index 000000000..109361bef --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/CacheBuilder.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.caching; + +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.Executor; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A builder of {@link Cache}. */ +public final class CacheBuilder { + + private static final long UNSET = -1; + + private boolean weakKeys; + private boolean weakValues; + private long maximumSize = UNSET; + @Nullable private Executor executor = null; + + /** Sets the maximum size of the cache. */ + public CacheBuilder setMaximumSize(long maximumSize) { + this.maximumSize = maximumSize; + return this; + } + + /** + * Sets that keys should be referenced weakly. If used, keys will use identity comparison, not + * {@link Object#equals(Object)}. + */ + public CacheBuilder setWeakKeys() { + this.weakKeys = true; + return this; + } + + /** Sets that values should be referenced weakly. */ + public CacheBuilder setWeakValues() { + this.weakValues = true; + return this; + } + + // Visible for testing + CacheBuilder setExecutor(Executor executor) { + this.executor = executor; + return this; + } + + /** Returns a new {@link Cache} with the settings of this {@link CacheBuilder}. */ + public Cache build() { + if (weakKeys && !weakValues && maximumSize == UNSET) { + return new WeakLockFreeCache<>(); + } + Caffeine caffeine = Caffeine.newBuilder(); + if (weakKeys) { + caffeine.weakKeys(); + } + if (weakValues) { + caffeine.weakValues(); + } + if (maximumSize != UNSET) { + caffeine.maximumSize(maximumSize); + } + if (executor != null) { + caffeine.executor(executor); + } else { + caffeine.executor(Runnable::run); + } + @SuppressWarnings("unchecked") + com.github.benmanes.caffeine.cache.Cache delegate = + (com.github.benmanes.caffeine.cache.Cache) caffeine.build(); + return new CaffeineCache<>(delegate); + } + + CacheBuilder() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/CaffeineCache.java b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/CaffeineCache.java new file mode 100644 index 000000000..33aa741e9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/CaffeineCache.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.caching; + +import java.util.Set; +import java.util.function.Function; + +final class CaffeineCache implements Cache { + + private final com.github.benmanes.caffeine.cache.Cache delegate; + + CaffeineCache(com.github.benmanes.caffeine.cache.Cache delegate) { + this.delegate = delegate; + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return delegate.get(key, mappingFunction); + } + + @Override + public V get(K key) { + return delegate.getIfPresent(key); + } + + @Override + public void put(K key, V value) { + delegate.put(key, value); + } + + @Override + public void remove(K key) { + delegate.invalidate(key); + } + + // Visible for testing + Set keySet() { + return delegate.asMap().keySet(); + } + + // Visible for testing + void cleanup() { + delegate.cleanUp(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/WeakLockFreeCache.java b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/WeakLockFreeCache.java new file mode 100644 index 000000000..692177a9c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/main/java/io/opentelemetry/instrumentation/api/caching/WeakLockFreeCache.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.caching; + +import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; +import java.util.function.Function; + +final class WeakLockFreeCache implements Cache { + + private final WeakConcurrentMap delegate; + + WeakLockFreeCache() { + this.delegate = new WeakConcurrentMap.WithInlinedExpunction<>(); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + V value = get(key); + if (value != null) { + return value; + } + // Best we can do, we don't expect high contention with this implementation. Note, this + // prevents executing mappingFunction twice but it does not prevent executing mappingFunction + // if there is a concurrent put operation as would be the case for ConcurrentHashMap. However, + // we would never expect an order guarantee in this case anyways so it still has the same + // safety. + synchronized (delegate) { + value = get(key); + if (value != null) { + return value; + } + value = mappingFunction.apply(key); + V previous = delegate.putIfAbsent(key, value); + if (previous != null) { + return previous; + } + return value; + } + } + + @Override + public V get(K key) { + return delegate.getIfPresent(key); + } + + @Override + public void put(K key, V value) { + delegate.put(key, value); + } + + @Override + public void remove(K key) { + delegate.remove(key); + } + + // Visible for testing + int size() { + return delegate.approximateSize(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api-caching/src/test/README.md b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/test/README.md new file mode 100644 index 000000000..865db385e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api-caching/src/test/README.md @@ -0,0 +1 @@ +Tests for this module are in the instrumentation-api project to verify against the shaded artifact. diff --git a/opentelemetry-java-instrumentation/instrumentation-api/instrumentation-api.gradle b/opentelemetry-java-instrumentation/instrumentation-api/instrumentation-api.gradle new file mode 100644 index 000000000..60a4b82ac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/instrumentation-api.gradle @@ -0,0 +1,53 @@ +plugins { + id 'org.xbib.gradle.plugin.jflex' version '1.5.0' +} + +group = 'io.opentelemetry.instrumentation' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.jacoco-conventions" +apply plugin: "otel.publish-conventions" + +def jflexTargetDir = file"${project.buildDir}/generated/jflex/sql" + +def sqlSanitizerJflex = tasks.register("sqlSanitizerJflex", org.xbib.gradle.plugin.JFlexTask) { + group = 'jflex' + description = 'Generate SqlSanitizer' + source = [file("${project.projectDir}/src/main/jflex/SqlSanitizer.flex")] + target = jflexTargetDir +} + +tasks.named("compileJava").configure { + dependsOn(sqlSanitizerJflex) +} + +tasks.named("javadoc").configure { + dependsOn(sqlSanitizerJflex) +} + +tasks.named("sourcesJar").configure { + dependsOn(sqlSanitizerJflex) +} + +sourceSets.main.java.srcDir(jflexTargetDir) + +dependencies { + api project(":instrumentation-api-caching") + + api "run.mone:opentelemetry-api" + api "run.mone:opentelemetry-semconv" + + implementation "run.mone:opentelemetry-api-metrics" + implementation "org.slf4j:slf4j-api" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + testImplementation project(':testing-common') + testImplementation "org.mockito:mockito-core" + testImplementation "org.mockito:mockito-junit-jupiter" + testImplementation "org.assertj:assertj-core" + testImplementation "org.awaitility:awaitility" + testImplementation "run.mone:opentelemetry-sdk-metrics" + testImplementation "run.mone:opentelemetry-sdk-testing" +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/InstrumentationVersion.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/InstrumentationVersion.java new file mode 100644 index 000000000..4b539b335 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/InstrumentationVersion.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api; + +public final class InstrumentationVersion { + public static final String VERSION = + InstrumentationVersion.class.getPackage().getImplementationVersion(); + + private InstrumentationVersion() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/annotations/UnstableApi.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/annotations/UnstableApi.java new file mode 100644 index 000000000..8b15a9daa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/annotations/UnstableApi.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * A marker for public classes and methods that are not part of the stable API exposed by an + * artifact. Even if the artifact itself is stable (i.e., it has a version number with no suffix + * such as {@code -alpha}), the marked API has no stability guarantees. It may be changed in a + * backwards incompatible manner, such as changing its signature, or removed entirely without any + * prior warning or period of deprecation. Using the API may also require additional dependency + * declarations on unstable artifacts. + * + *

Only use an API marked with {@link UnstableApi} if you are comfortable keeping up with + * breaking changes. In particular, DO NOT use it in a library that itself has a guarantee of + * stability, there is no valid use case for it. + */ +@Target({ + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.METHOD, + ElementType.TYPE +}) +@Documented +@UnstableApi +public @interface UnstableApi {} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/CollectionParsers.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/CollectionParsers.java new file mode 100644 index 000000000..0af51a43e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/CollectionParsers.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.config; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class CollectionParsers { + private static final Logger log = LoggerFactory.getLogger(CollectionParsers.class); + + static List parseList(String value) { + String[] tokens = value.split(",", -1); + // Remove whitespace from each item. + for (int i = 0; i < tokens.length; i++) { + tokens[i] = tokens[i].trim(); + } + return Collections.unmodifiableList(Arrays.asList(tokens)); + } + + static Map parseMap(String value) { + Map result = new LinkedHashMap<>(); + for (String token : value.split(",", -1)) { + token = token.trim(); + String[] parts = token.split("=", -1); + if (parts.length != 2) { + log.warn("Invalid map config part, should be formatted key1=value1,key2=value2: {}", value); + return Collections.emptyMap(); + } + result.put(parts[0], parts[1]); + } + return Collections.unmodifiableMap(result); + } + + private CollectionParsers() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/Config.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/Config.java new file mode 100644 index 000000000..a1d0ce644 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/Config.java @@ -0,0 +1,320 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.config; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Objects.requireNonNull; + +import com.google.auto.value.AutoValue; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents the global agent configuration consisting of system properties, environment variables, + * contents of the agent configuration file and properties defined by the {@code + * ConfigPropertySource} SPI (see {@code ConfigInitializer} and {@link ConfigBuilder}). + * + *

In case any {@code get*()} method variant gets called for the same property more than once + * (e.g. each time an advice class executes) it is suggested to cache the result instead of + * repeatedly calling {@link Config}. Agent configuration does not change during the runtime so + * retrieving the property once and storing its result in e.g. static final field allows JIT to do + * its magic and remove some code branches. + */ +@AutoValue +public abstract class Config { + private static final Logger logger = LoggerFactory.getLogger(Config.class); + + // lazy initialized, so that javaagent can set it, and library instrumentation can fall back and + // read system properties + @Nullable private static volatile Config instance = null; + + /** Start building a new {@link Config} instance. */ + public static ConfigBuilder newBuilder() { + return new ConfigBuilder(); + } + + static Config create(Map allProperties) { + return new AutoValue_Config(allProperties); + } + + // package protected constructor to make extending this class impossible + Config() {} + + /** + * Sets the agent configuration singleton. This method is only supposed to be called once, from + * the agent classloader just before the first instrumentation is loaded (and before {@link + * Config#get()} is used for the first time). + */ + public static void internalInitializeConfig(Config config) { + if (instance != null) { + logger.warn("Config#INSTANCE was already set earlier"); + return; + } + instance = requireNonNull(config); + } + + /** Returns the global agent configuration. */ + public static Config get() { + if (instance == null) { + // this should only happen in library instrumentation + // + // no need to synchronize because worst case is creating instance more than once + instance = newBuilder().readEnvironmentVariables().readSystemProperties().build(); + } + return instance; + } + + /** Returns all properties stored in this instance. The returned map is unmodifiable. */ + public abstract Map getAllProperties(); + + /** + * Returns a string-valued configuration property or {@code null} if a property with name {@code + * name} has not been configured. + */ + @Nullable + public String getString(String name) { + return getRawProperty(name, null); + } + + /** + * Returns a string-valued configuration property or {@code defaultValue} if a property with name + * {@code name} has not been configured. + */ + public String getString(String name, String defaultValue) { + return getRawProperty(name, defaultValue); + } + + /** + * Returns a boolean-valued configuration property or {@code null} if a property with name {@code + * name} has not been configured. + */ + @Nullable + public Boolean getBoolean(String name) { + return getTypedProperty(name, ConfigValueParsers::parseBoolean); + } + + /** + * Returns a boolean-valued configuration property or {@code defaultValue} if a property with name + * {@code name} has not been configured. + */ + public boolean getBoolean(String name, boolean defaultValue) { + return safeGetTypedProperty(name, ConfigValueParsers::parseBoolean, defaultValue); + } + + /** + * Returns a integer-valued configuration property or {@code null} if a property with name {@code + * name} has not been configured. + * + * @throws ConfigParsingException if the property is not a valid integer. + */ + @Nullable + public Integer getInt(String name) { + return getTypedProperty(name, ConfigValueParsers::parseInt); + } + + /** + * Returns a integer-valued configuration property or {@code defaultValue} if a property with name + * {@code name} has not been configured or when parsing has failed. This is the safe variant of + * {@link #getInt(String)}. + */ + public int getInt(String name, int defaultValue) { + return safeGetTypedProperty(name, ConfigValueParsers::parseInt, defaultValue); + } + + /** + * Returns a long-valued configuration property or {@code null} if a property with name {@code + * name} has not been configured. + * + * @throws ConfigParsingException if the property is not a valid long. + */ + @Nullable + public Long getLong(String name) { + return getTypedProperty(name, ConfigValueParsers::parseLong); + } + + /** + * Returns a long-valued configuration property or {@code defaultValue} if a property with name + * {@code name} has not been configured or when parsing has failed. This is the safe variant of + * {@link #getLong(String)}. + */ + public long getLong(String name, long defaultValue) { + return safeGetTypedProperty(name, ConfigValueParsers::parseLong, defaultValue); + } + + /** + * Returns a double-valued configuration property or {@code null} if a property with name {@code + * name} has not been configured. + * + * @throws ConfigParsingException if the property is not a valid long. + */ + @Nullable + public Double getDouble(String name) { + return getTypedProperty(name, ConfigValueParsers::parseDouble); + } + + /** + * Returns a double-valued configuration property or {@code defaultValue} if a property with name + * {@code name} has not been configured or when parsing has failed. This is the safe variant of + * {@link #getDouble(String)}. + */ + public double getDouble(String name, double defaultValue) { + return safeGetTypedProperty(name, ConfigValueParsers::parseDouble, defaultValue); + } + + /** + * Returns a duration-valued configuration property or {@code null} if a property with name {@code + * name} has not been configured. + * + *

Durations can be of the form "{number}{unit}", where unit is one of: + * + *

    + *
  • ms + *
  • s + *
  • m + *
  • h + *
  • d + *
+ * + *

If no unit is specified, milliseconds is the assumed duration unit. + * + * @throws ConfigParsingException if the property is not a valid long. + */ + @Nullable + public Duration getDuration(String name) { + return getTypedProperty(name, ConfigValueParsers::parseDuration); + } + + /** + * Returns a duration-valued configuration property or {@code defaultValue} if a property with + * name {@code name} has not been configured or when parsing has failed. This is the safe variant + * of {@link #getDuration(String)}. + * + *

Durations can be of the form "{number}{unit}", where unit is one of: + * + *

    + *
  • ms + *
  • s + *
  • m + *
  • h + *
  • d + *
+ * + *

If no unit is specified, milliseconds is the assumed duration unit. + */ + public Duration getDuration(String name, Duration defaultValue) { + return safeGetTypedProperty(name, ConfigValueParsers::parseDuration, defaultValue); + } + + /** + * Returns a list-valued configuration property or an empty list if a property with name {@code + * name} has not been configured. The format of the original value must be comma-separated, e.g. + * {@code one,two,three}. + */ + public List getList(String name) { + List list = getTypedProperty(name, ConfigValueParsers::parseList); + return list == null ? emptyList() : list; + } + + /** + * Returns a list-valued configuration property or {@code defaultValue} if a property with name + * {@code name} has not been configured. The format of the original value must be comma-separated, + * e.g. {@code one,two,three}. + */ + public List getList(String name, List defaultValue) { + return safeGetTypedProperty(name, ConfigValueParsers::parseList, defaultValue); + } + + /** + * Returns a map-valued configuration property or an empty map if a property with name {@code + * name} has not been configured. The format of the original value must be comma-separated for + * each key, with an '=' separating the key and value, e.g. {@code + * key=value,anotherKey=anotherValue}. + * + * @throws ConfigParsingException if the property is not a valid long. + */ + public Map getMap(String name) { + Map map = getTypedProperty(name, ConfigValueParsers::parseMap); + return map == null ? emptyMap() : map; + } + + /** + * Returns a map-valued configuration property or {@code defaultValue} if a property with name + * {@code name} has not been configured or when parsing has failed. This is the safe variant of + * {@link #getMap(String)}. The format of the original value must be comma-separated for each key, + * with an '=' separating the key and value, e.g. {@code key=value,anotherKey=anotherValue}. + */ + public Map getMap(String name, Map defaultValue) { + return safeGetTypedProperty(name, ConfigValueParsers::parseMap, defaultValue); + } + + private T safeGetTypedProperty(String name, ConfigValueParser parser, T defaultValue) { + try { + T value = getTypedProperty(name, parser); + return value == null ? defaultValue : value; + } catch (RuntimeException t) { + logger.debug("Error occurred during parsing: {}", t.getMessage(), t); + return defaultValue; + } + } + + @Nullable + private T getTypedProperty(String name, ConfigValueParser parser) { + String value = getRawProperty(name, null); + if (value == null || value.trim().isEmpty()) { + return null; + } + return parser.parse(name, value); + } + + private String getRawProperty(String name, String defaultValue) { + return getAllProperties().getOrDefault(NamingConvention.DOT.normalize(name), defaultValue); + } + + public boolean isInstrumentationEnabled( + Iterable instrumentationNames, boolean defaultEnabled) { + return isInstrumentationPropertyEnabled(instrumentationNames, "enabled", defaultEnabled); + } + + public boolean isInstrumentationPropertyEnabled( + Iterable instrumentationNames, String suffix, boolean defaultEnabled) { + // If default is enabled, we want to enable individually, + // if default is disabled, we want to disable individually. + boolean anyEnabled = defaultEnabled; + for (String name : instrumentationNames) { + String propertyName = "otel.instrumentation." + name + '.' + suffix; + boolean enabled = getBoolean(propertyName, defaultEnabled); + + if (defaultEnabled) { + anyEnabled &= enabled; + } else { + anyEnabled |= enabled; + } + } + return anyEnabled; + } + + public boolean isAgentDebugEnabled() { + return getBoolean("otel.javaagent.debug", false); + } + + /** + * Converts this config instance to Java {@link Properties}. + * + * @deprecated Use {@link #getAllProperties()} instead. + */ + @Deprecated + public Properties asJavaProperties() { + Properties properties = new Properties(); + properties.putAll(getAllProperties()); + return properties; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigBuilder.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigBuilder.java new file mode 100644 index 000000000..04bc1bffb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigBuilder.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public final class ConfigBuilder { + + private final Map allProperties = new HashMap<>(); + + public ConfigBuilder addProperty(String name, String value) { + allProperties.put(NamingConvention.DOT.normalize(name), value); + return this; + } + + public ConfigBuilder readProperties(Properties properties) { + for (String name : properties.stringPropertyNames()) { + allProperties.put(NamingConvention.DOT.normalize(name), properties.getProperty(name)); + } + return this; + } + + public ConfigBuilder readProperties(Map properties) { + return fromConfigMap(properties, NamingConvention.DOT); + } + + /** Sets the configuration values from environment variables. */ + public ConfigBuilder readEnvironmentVariables() { + return fromConfigMap(System.getenv(), NamingConvention.ENV_VAR); + } + + /** Sets the configuration values from system properties. */ + public ConfigBuilder readSystemProperties() { + return readProperties(System.getProperties()); + } + + private ConfigBuilder fromConfigMap( + Map configMap, NamingConvention namingConvention) { + for (Map.Entry entry : configMap.entrySet()) { + allProperties.put(namingConvention.normalize(entry.getKey()), entry.getValue()); + } + return this; + } + + public Config build() { + return Config.create(Collections.unmodifiableMap(new HashMap<>(allProperties))); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigParsingException.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigParsingException.java new file mode 100644 index 000000000..055082767 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigParsingException.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.config; + +public class ConfigParsingException extends RuntimeException { + public ConfigParsingException(String message) { + super(message); + } + + public ConfigParsingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigValueParser.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigValueParser.java new file mode 100644 index 000000000..78e9cba3d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigValueParser.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.config; + +@FunctionalInterface +interface ConfigValueParser { + T parse(String propertyName, String rawValue); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigValueParsers.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigValueParsers.java new file mode 100644 index 000000000..e01a92836 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/ConfigValueParsers.java @@ -0,0 +1,150 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.config; + +import java.time.Duration; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +// most of the parsing code copied from +// https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/DefaultConfigProperties.java +@SuppressWarnings("UnusedException") +final class ConfigValueParsers { + + static boolean parseBoolean(@SuppressWarnings("unused") String propertyName, String value) { + return Boolean.parseBoolean(value); + } + + static int parseInt(String propertyName, String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw newInvalidPropertyException(propertyName, value, "integer"); + } + } + + static long parseLong(String propertyName, String value) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + throw newInvalidPropertyException(propertyName, value, "long"); + } + } + + static double parseDouble(String propertyName, String value) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + throw newInvalidPropertyException(propertyName, value, "double"); + } + } + + private static ConfigParsingException newInvalidPropertyException( + String name, String value, String type) { + throw new ConfigParsingException( + "Invalid value for property " + name + "=" + value + ". Must be a " + type + "."); + } + + static List parseList(@SuppressWarnings("unused") String propertyName, String value) { + return Collections.unmodifiableList(filterBlanks(value.split(","))); + } + + static Map parseMap(String propertyName, String value) { + return parseList(propertyName, value).stream() + .map(keyValuePair -> trim(keyValuePair.split("=", 2))) + .map( + splitKeyValuePairs -> { + if (splitKeyValuePairs.size() != 2 || splitKeyValuePairs.get(0).isEmpty()) { + throw new ConfigParsingException( + "Invalid map property: " + propertyName + "=" + value); + } + return new AbstractMap.SimpleImmutableEntry<>( + splitKeyValuePairs.get(0), splitKeyValuePairs.get(1)); + }) + // If duplicate keys, prioritize later ones similar to duplicate system properties on a + // Java command line. + .collect( + Collectors.toMap( + Map.Entry::getKey, Map.Entry::getValue, (first, next) -> next, LinkedHashMap::new)); + } + + private static List filterBlanks(String[] values) { + return Arrays.stream(values) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + } + + private static List trim(String[] values) { + return Arrays.stream(values).map(String::trim).collect(Collectors.toList()); + } + + static Duration parseDuration(String propertyName, String value) { + String unitString = getUnitString(value); + String numberString = value.substring(0, value.length() - unitString.length()); + try { + long rawNumber = Long.parseLong(numberString.trim()); + TimeUnit unit = getDurationUnit(unitString.trim()); + return Duration.ofMillis(TimeUnit.MILLISECONDS.convert(rawNumber, unit)); + } catch (NumberFormatException e) { + throw new ConfigParsingException( + "Invalid duration property " + + propertyName + + "=" + + value + + ". Expected number, found: " + + numberString); + } catch (ConfigParsingException ex) { + throw new ConfigParsingException( + "Invalid duration property " + propertyName + "=" + value + ". " + ex.getMessage()); + } + } + + /** Returns the TimeUnit associated with a unit string. Defaults to milliseconds. */ + private static TimeUnit getDurationUnit(String unitString) { + switch (unitString) { + case "": // Fallthrough expected + case "ms": + return TimeUnit.MILLISECONDS; + case "s": + return TimeUnit.SECONDS; + case "m": + return TimeUnit.MINUTES; + case "h": + return TimeUnit.HOURS; + case "d": + return TimeUnit.DAYS; + default: + throw new ConfigParsingException("Invalid duration string, found: " + unitString); + } + } + + /** + * Fragments the 'units' portion of a config value from the 'value' portion. + * + *

E.g. "1ms" would return the string "ms". + */ + private static String getUnitString(String rawValue) { + int lastDigitIndex = rawValue.length() - 1; + while (lastDigitIndex >= 0) { + char c = rawValue.charAt(lastDigitIndex); + if (Character.isDigit(c)) { + break; + } + lastDigitIndex -= 1; + } + // Pull everything after the last digit. + return rawValue.substring(lastDigitIndex + 1); + } + + private ConfigValueParsers() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/NamingConvention.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/NamingConvention.java new file mode 100644 index 000000000..a962b7148 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/NamingConvention.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.config; + +import java.util.Locale; + +// config property names are normalized to dot separated lowercase words +enum NamingConvention { + DOT { + @Override + public String normalize(String key) { + // many instrumentation names have dashes ('-') + return key.toLowerCase(Locale.ROOT).replace('-', '.'); + } + }, + ENV_VAR { + @Override + public String normalize(String key) { + return key.toLowerCase(Locale.ROOT).replace('_', '.'); + } + }; + + abstract String normalize(String key); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizer.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizer.java new file mode 100644 index 000000000..1f59f804b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizer.java @@ -0,0 +1,428 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.db; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableMap; + +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer.CommandSanitizer.CommandAndNumArgs; +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer.CommandSanitizer.Eval; +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer.CommandSanitizer.KeepAllArgs; +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer.CommandSanitizer.MultiKeyValue; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * This class is responsible for masking potentially sensitive data in Redis commands. + * + *

Examples: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Raw commandNormalized command
{@code AUTH password}{@code AUTH ?}
{@code HMSET hash creditcard 1234567887654321 address asdf}{@code HMSET hash creditcard ? address ?}
+ */ +public final class RedisCommandSanitizer { + + private static final Map SANITIZERS; + private static final CommandSanitizer DEFAULT = new CommandAndNumArgs(0); + + static { + Map sanitizers = new HashMap<>(); + + CommandSanitizer keepOneArg = new CommandAndNumArgs(1); + CommandSanitizer keepTwoArgs = new CommandAndNumArgs(2); + CommandSanitizer setMultiHashField = new MultiKeyValue(1); + CommandSanitizer setMultiField = new MultiKeyValue(0); + + // Cluster + for (String command : asList("CLUSTER", "READONLY", "READWRITE")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Connection + sanitizers.put("AUTH", DEFAULT); + // HELLO can contain AUTH data + sanitizers.put("HELLO", keepTwoArgs); + for (String command : asList("CLIENT", "ECHO", "PING", "QUIT", "SELECT")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Geo + for (String command : + asList("GEOADD", "GEODIST", "GEOHASH", "GEOPOS", "GEORADIUS", "GEORADIUSBYMEMBER")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Hashes + sanitizers.put("HMSET", setMultiHashField); + sanitizers.put("HSET", setMultiHashField); + sanitizers.put("HSETNX", keepTwoArgs); + for (String command : + asList( + "HDEL", + "HEXISTS", + "HGET", + "HGETALL", + "HINCRBY", + "HINCRBYFLOAT", + "HKEYS", + "HLEN", + "HMGET", + "HSCAN", + "HSTRLEN", + "HVALS")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // HyperLogLog + sanitizers.put("PFADD", keepOneArg); + for (String command : asList("PFCOUNT", "PFMERGE")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Keys + // MIGRATE can contain AUTH data + sanitizers.put("MIGRATE", new CommandAndNumArgs(6)); + sanitizers.put("RESTORE", keepTwoArgs); + for (String command : + asList( + "DEL", + "DUMP", + "EXISTS", + "EXPIRE", + "EXPIREAT", + "KEYS", + "MOVE", + "OBJECT", + "PERSIST", + "PEXPIRE", + "PEXPIREAT", + "PTTL", + "RANDOMKEY", + "RENAME", + "RENAMENX", + "SCAN", + "SORT", + "TOUCH", + "TTL", + "TYPE", + "UNLINK", + "WAIT")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Lists + sanitizers.put("LINSERT", keepTwoArgs); + sanitizers.put("LPOS", keepOneArg); + sanitizers.put("LPUSH", keepOneArg); + sanitizers.put("LPUSHX", keepOneArg); + sanitizers.put("LREM", keepOneArg); + sanitizers.put("LSET", keepOneArg); + sanitizers.put("RPUSH", keepOneArg); + sanitizers.put("RPUSHX", keepOneArg); + for (String command : + asList( + "BLMOVE", + "BLPOP", + "BRPOP", + "BRPOPLPUSH", + "LINDEX", + "LLEN", + "LMOVE", + "LPOP", + "LRANGE", + "LTRIM", + "RPOP", + "RPOPLPUSH")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Pub/Sub + sanitizers.put("PUBLISH", keepOneArg); + for (String command : + asList("PSUBSCRIBE", "PUBSUB", "PUNSUBSCRIBE", "SUBSCRIBE", "UNSUBSCRIBE")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Scripting + sanitizers.put("EVAL", Eval.INSTANCE); + sanitizers.put("EVALSHA", Eval.INSTANCE); + sanitizers.put("SCRIPT", KeepAllArgs.INSTANCE); + + // Server + // CONFIG SET can set any property, including the master password + sanitizers.put("CONFIG", keepTwoArgs); + for (String command : + asList( + "ACL", + "BGREWRITEAOF", + "BGSAVE", + "COMMAND", + "DBSIZE", + "DEBUG", + "FLUSHALL", + "FLUSHDB", + "INFO", + "LASTSAVE", + "LATENCY", + "LOLWUT", + "MEMORY", + "MODULE", + "MONITOR", + "PSYNC", + "REPLICAOF", + "ROLE", + "SAVE", + "SHUTDOWN", + "SLAVEOF", + "SLOWLOG", + "SWAPDB", + "SYNC", + "TIME")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Sets + sanitizers.put("SADD", keepOneArg); + sanitizers.put("SISMEMBER", keepOneArg); + sanitizers.put("SMISMEMBER", keepOneArg); + sanitizers.put("SMOVE", keepTwoArgs); + sanitizers.put("SREM", keepOneArg); + for (String command : + asList( + "SCARD", + "SDIFF", + "SDIFFSTORE", + "SINTER", + "SINTERSTORE", + "SMEMBERS", + "SPOP", + "SRANDMEMBER", + "SSCAN", + "SUNION", + "SUNIONSTORE")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Sorted Sets + sanitizers.put("ZADD", keepOneArg); + sanitizers.put("ZCOUNT", keepOneArg); + sanitizers.put("ZINCRBY", keepOneArg); + sanitizers.put("ZLEXCOUNT", keepOneArg); + sanitizers.put("ZMSCORE", keepOneArg); + sanitizers.put("ZRANGEBYLEX", keepOneArg); + sanitizers.put("ZRANGEBYSCORE", keepOneArg); + sanitizers.put("ZRANK", keepOneArg); + sanitizers.put("ZREM", keepOneArg); + sanitizers.put("ZREMRANGEBYLEX", keepOneArg); + sanitizers.put("ZREMRANGEBYSCORE", keepOneArg); + sanitizers.put("ZREVRANGEBYLEX", keepOneArg); + sanitizers.put("ZREVRANGEBYSCORE", keepOneArg); + sanitizers.put("ZREVRANK", keepOneArg); + sanitizers.put("ZSCORE", keepOneArg); + for (String command : + asList( + "BZPOPMAX", + "BZPOPMIN", + "ZCARD", + "ZINTER", + "ZINTERSTORE", + "ZPOPMAX", + "ZPOPMIN", + "ZRANGE", + "ZREMRANGEBYRANK", + "ZREVRANGE", + "ZSCAN", + "ZUNION", + "ZUNIONSTORE")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Streams + sanitizers.put("XADD", new MultiKeyValue(2)); + for (String command : + asList( + "XACK", + "XCLAIM", + "XDEL", + "XGROUP", + "XINFO", + "XLEN", + "XPENDING", + "XRANGE", + "XREAD", + "XREADGROUP", + "XREVRANGE", + "XTRIM")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Strings + sanitizers.put("APPEND", keepOneArg); + sanitizers.put("GETSET", keepOneArg); + sanitizers.put("MSET", setMultiField); + sanitizers.put("MSETNX", setMultiField); + sanitizers.put("PSETEX", keepTwoArgs); + sanitizers.put("SET", keepOneArg); + sanitizers.put("SETEX", keepTwoArgs); + sanitizers.put("SETNX", keepOneArg); + sanitizers.put("SETRANGE", keepOneArg); + for (String command : + asList( + "BITCOUNT", + "BITFIELD", + "BITOP", + "BITPOS", + "DECR", + "DECRBY", + "GET", + "GETBIT", + "GETRANGE", + "INCR", + "INCRBY", + "INCRBYFLOAT", + "MGET", + "SETBIT", + "STRALGO", + "STRLEN")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + // Transactions + for (String command : asList("DISCARD", "EXEC", "MULTI", "UNWATCH", "WATCH")) { + sanitizers.put(command, KeepAllArgs.INSTANCE); + } + + SANITIZERS = unmodifiableMap(sanitizers); + } + + public static String sanitize(String command, List args) { + if (!StatementSanitizationConfig.isStatementSanitizationEnabled()) { + return KeepAllArgs.INSTANCE.sanitize(command, args); + } + return SANITIZERS + .getOrDefault(command.toUpperCase(Locale.ROOT), DEFAULT) + .sanitize(command, args); + } + + public interface CommandSanitizer { + String sanitize(String command, List args); + + static String argToString(Object arg) { + if (arg instanceof byte[]) { + return new String((byte[]) arg, StandardCharsets.UTF_8); + } else { + return arg.toString(); + } + } + + enum KeepAllArgs implements CommandSanitizer { + INSTANCE; + + @Override + public String sanitize(String command, List args) { + StringBuilder sanitized = new StringBuilder(command); + for (Object arg : args) { + sanitized.append(" ").append(argToString(arg)); + } + return sanitized.toString(); + } + } + + // keeps only a chosen number of arguments + // example for num=2: CMD arg1 arg2 ? ? + class CommandAndNumArgs implements CommandSanitizer { + private final int numOfArgsToKeep; + + public CommandAndNumArgs(int numOfArgsToKeep) { + this.numOfArgsToKeep = numOfArgsToKeep; + } + + @Override + public String sanitize(String command, List args) { + StringBuilder sanitized = new StringBuilder(command); + for (int i = 0; i < numOfArgsToKeep && i < args.size(); ++i) { + sanitized.append(" ").append(argToString(args.get(i))); + } + for (int i = numOfArgsToKeep; i < args.size(); ++i) { + sanitized.append(" ?"); + } + return sanitized.toString(); + } + } + + // keeps only chosen number of arguments and then every second one + // example for num=2: CMD arg1 arg2 key1 ? key2 ? + class MultiKeyValue implements CommandSanitizer { + private final int numOfArgsBeforeKeyValue; + + public MultiKeyValue(int numOfArgsBeforeKeyValue) { + this.numOfArgsBeforeKeyValue = numOfArgsBeforeKeyValue; + } + + @Override + public String sanitize(String command, List args) { + StringBuilder sanitized = new StringBuilder(command); + // append all "initial" arguments before key-value pairs start + for (int i = 0; i < numOfArgsBeforeKeyValue && i < args.size(); ++i) { + sanitized.append(" ").append(argToString(args.get(i))); + } + + // loop over keys only + for (int i = numOfArgsBeforeKeyValue; i < args.size(); i += 2) { + sanitized.append(" ").append(argToString(args.get(i))).append(" ?"); + } + return sanitized.toString(); + } + } + + enum Eval implements CommandSanitizer { + INSTANCE; + + @Override + public String sanitize(String command, List args) { + StringBuilder sanitized = new StringBuilder(command); + + // get the number of keys passed from the command itself (second arg) + int numberOfKeys = 0; + if (args.size() > 2) { + try { + numberOfKeys = Integer.parseInt(argToString(args.get(1))); + } catch (NumberFormatException ignored) { + // Ignore + } + } + + int i = 0; + // log the script, number of keys and all keys + for (; i < (numberOfKeys + 2) && i < args.size(); ++i) { + sanitized.append(" ").append(argToString(args.get(i))); + } + // mask the rest + for (; i < args.size(); ++i) { + sanitized.append(" ?"); + } + return sanitized.toString(); + } + } + } + + private RedisCommandSanitizer() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandUtil.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandUtil.java new file mode 100644 index 000000000..1a987a838 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandUtil.java @@ -0,0 +1,31 @@ +package io.opentelemetry.instrumentation.api.db; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@SuppressWarnings("SystemOut") +public final class RedisCommandUtil { + // don't perform the span export command + public static final String REDIS_EXCLUDE_COMMAND = "PING|AUTH"; + + // The operation quantity of these commands is too large, it needs to be skipped. + private static final Set SKIP_END_NAME = Stream.of("hget", "mget", "mset", "hmget", "hgetall", "get").collect(Collectors.toSet()); + + // Retain spans that exceed this duration threshold, unit: ms. + private static final int DURATION_THRESHOLD = 1000; + + // If it's in skipName and there are no errors, skip it directly.(issue #16) + public static boolean skipEnd(String operationName, @Nullable Throwable error, long startTime) { + return (null != operationName) && (SKIP_END_NAME.contains(operationName.toLowerCase()) && (null == error) && (redisDuration(startTime) < DURATION_THRESHOLD)); + } + + private static long redisDuration(long startTime){ + System.out.println("start time is : "+startTime); + long l = System.currentTimeMillis() - startTime; + System.out.println("duration is : "+l); + return l; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementInfo.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementInfo.java new file mode 100644 index 000000000..753ea7981 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementInfo.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.db; + +import com.google.auto.value.AutoValue; +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.Nullable; + +@AutoValue +public abstract class SqlStatementInfo { + + public static SqlStatementInfo create( + @Nullable String fullStatement, @Nullable String operation, @Nullable String table) { + return new AutoValue_SqlStatementInfo(fullStatement, operation, table); + } + + public SqlStatementInfo mapTable(Function mapper) { + return SqlStatementInfo.create(getFullStatement(), getOperation(), mapper.apply(getTable())); + } + + @Nullable + public abstract String getFullStatement(); + + @Nullable + public abstract String getOperation(); + + @Nullable + public abstract String getTable(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizer.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizer.java new file mode 100644 index 000000000..5d553d1e0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizer.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.db; + +import static io.opentelemetry.instrumentation.api.db.StatementSanitizationConfig.isStatementSanitizationEnabled; +import static io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics.CounterNames.SQL_STATEMENT_SANITIZER_CACHE_MISS; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics; + +/** + * This class is responsible for masking potentially sensitive parameters in SQL (and SQL-like) + * statements and queries. + */ +public final class SqlStatementSanitizer { + private static final SupportabilityMetrics supportability = SupportabilityMetrics.instance(); + + private static final Cache sqlToStatementInfoCache = + Cache.newBuilder().setMaximumSize(1000).build(); + + public static SqlStatementInfo sanitize(String statement) { + if (!isStatementSanitizationEnabled() || statement == null) { + return SqlStatementInfo.create(statement, null, null); + } + return sqlToStatementInfoCache.computeIfAbsent( + statement, + k -> { + supportability.incrementCounter(SQL_STATEMENT_SANITIZER_CACHE_MISS); + return AutoSqlSanitizer.sanitize(statement); + }); + } + + private SqlStatementSanitizer() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/StatementSanitizationConfig.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/StatementSanitizationConfig.java new file mode 100644 index 000000000..3499e8a06 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/db/StatementSanitizationConfig.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.db; + +import io.opentelemetry.instrumentation.api.config.Config; + +/** DB statement sanitization is always enabled by default, you have to manually disable it. */ +final class StatementSanitizationConfig { + + private static final boolean STATEMENT_SANITIZATION_ENABLED = + Config.get() + .getBoolean("otel.instrumentation.common.db-statement-sanitizer.enabled", true); + + static boolean isStatementSanitizationEnabled() { + return STATEMENT_SANITIZATION_ENABLED; + } + + private StatementSanitizationConfig() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractor.java new file mode 100644 index 000000000..8ab2a1aaf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractor.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of {@link io.opentelemetry.api.common.Attributes} for a given request and response. + * Will be called {@linkplain #onStart(AttributesBuilder, Object) on start} with just the {@link + * REQUEST} and again {@linkplain #onEnd(AttributesBuilder, Object, Object) on end} with both {@link + * REQUEST} and {@link RESPONSE} to allow populating attributes at each stage of a request's + * lifecycle. It is best to populate as much as possible in {@link #onStart(AttributesBuilder, + * Object)} to have it available during sampling. + * + * @see DbAttributesExtractor + * @see HttpAttributesExtractor + * @see NetAttributesExtractor + */ +public abstract class AttributesExtractor { + /** + * Extracts attributes from the {@link REQUEST} into the {@link AttributesBuilder} at the + * beginning of a request. + */ + protected abstract void onStart(AttributesBuilder attributes, REQUEST request); + + /** + * Extracts attributes from the {@link REQUEST} and {@link RESPONSE} into the {@link + * AttributesBuilder} at the end of a request. + */ + protected abstract void onEnd( + AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response); + + /** + * Sets the {@code value} with the given {@code key} to the {@link AttributesBuilder} if {@code + * value} is not {@code null}. + */ + protected static void set(AttributesBuilder attributes, AttributeKey key, T value) { + if (value != null) { + attributes.put(key, value); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ClientInstrumenter.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ClientInstrumenter.java new file mode 100644 index 000000000..c5549ad3f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ClientInstrumenter.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapSetter; + +final class ClientInstrumenter extends Instrumenter { + + private final ContextPropagators propagators; + private final TextMapSetter setter; + + ClientInstrumenter( + InstrumenterBuilder builder, TextMapSetter setter) { + super(builder); + this.propagators = builder.openTelemetry.getPropagators(); + this.setter = setter; + } + + @Override + public Context start(Context parentContext, REQUEST request) { + Context newContext = super.start(parentContext, request); + propagators.getTextMapPropagator().inject(newContext, request, setter); + return newContext; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/DefaultSpanStatusExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/DefaultSpanStatusExtractor.java new file mode 100644 index 000000000..38f2db152 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/DefaultSpanStatusExtractor.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class DefaultSpanStatusExtractor + implements SpanStatusExtractor { + + static final SpanStatusExtractor INSTANCE = new DefaultSpanStatusExtractor<>(); + + @Override + public StatusCode extract( + REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error, SpanKind spanKind) { + if (error != null) { + return StatusCode.ERROR; + } + return StatusCode.UNSET; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/EndTimeExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/EndTimeExtractor.java new file mode 100644 index 000000000..db1fabe1b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/EndTimeExtractor.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.time.Instant; + +/** + * Extractor of the end time of response processing. An {@link EndTimeExtractor} should always use + * the same timestamp source as the corresponding {@link StartTimeExtractor} - extracted timestamps + * must be comparable. + */ +@FunctionalInterface +public interface EndTimeExtractor { + + /** Returns the timestamp marking the end of the response processing. */ + Instant extract(RESPONSE response); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ErrorCauseExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ErrorCauseExtractor.java new file mode 100644 index 000000000..f895d26e3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ErrorCauseExtractor.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +/** + * Extractor of the root cause of a {@link Throwable}. When instrumenting a library which wraps user + * exceptions with a framework exception, generally for propagating checked exceptions across + * unchecked boundaries, it is recommended to override this to unwrap back to the user exception. + */ +public interface ErrorCauseExtractor { + Throwable extractCause(Throwable error); + + /** + * Returns a {@link ErrorCauseExtractor} which unwraps common standard library wrapping + * exceptions. + */ + static ErrorCauseExtractor jdk() { + return JdkErrorCauseExtractor.INSTANCE; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java new file mode 100644 index 000000000..f0523570d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java @@ -0,0 +1,195 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.InstrumentationVersion; +import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics; +import io.opentelemetry.instrumentation.api.tracer.ClientSpan; +import io.opentelemetry.instrumentation.api.tracer.ConsumerSpan; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +// TODO(anuraaga): Need to define what are actually useful knobs, perhaps even providing a +// base-class +// for instrumentation library builders. +/** + * An instrumenter of the start and end of a request/response lifecycle. Almost all instrumentation + * of libraries falls into modeling start and end, generating observability signals from these such + * as a tracing {@link Span}, or metrics such as the duration taken, active requests, etc. When + * instrumenting a library, there will generally be four steps. + * + *

    + *
  • Create an {@link Instrumenter} using {@link InstrumenterBuilder}. Use the builder to + * configure any library-specific customizations, and also expose useful knobs to your user. + *
  • Call {@link Instrumenter#shouldStart(Context, Object)} and do not proceed if {@code false}. + *
  • Call {@link Instrumenter#start(Context, Object)} at the beginning of a request. + *
  • Call {@link Instrumenter#end(Context, Object, Object, Throwable)} at the end of a request. + *
+ */ +public class Instrumenter { + + /** Returns a new {@link InstrumenterBuilder}. */ + public static InstrumenterBuilder newBuilder( + OpenTelemetry openTelemetry, + String instrumentationName, + SpanNameExtractor spanNameExtractor) { + return new InstrumenterBuilder<>(openTelemetry, instrumentationName, spanNameExtractor); + } + + private static final SupportabilityMetrics supportability = SupportabilityMetrics.instance(); + + private final String instrumentationName; + private final Tracer tracer; + private final SpanNameExtractor spanNameExtractor; + private final SpanKindExtractor spanKindExtractor; + private final SpanStatusExtractor spanStatusExtractor; + private final List> + attributesExtractors; + private final List> spanLinkExtractors; + private final List requestListeners; + private final ErrorCauseExtractor errorCauseExtractor; + @Nullable private final StartTimeExtractor startTimeExtractor; + @Nullable private final EndTimeExtractor endTimeExtractor; + + Instrumenter(InstrumenterBuilder builder) { + this.instrumentationName = builder.instrumentationName; + this.tracer = + builder.openTelemetry.getTracer(instrumentationName, InstrumentationVersion.VERSION); + this.spanNameExtractor = builder.spanNameExtractor; + this.spanKindExtractor = builder.spanKindExtractor; + this.spanStatusExtractor = builder.spanStatusExtractor; + this.attributesExtractors = new ArrayList<>(builder.attributesExtractors); + this.spanLinkExtractors = new ArrayList<>(builder.spanLinkExtractors); + this.requestListeners = new ArrayList<>(builder.requestListeners); + this.errorCauseExtractor = builder.errorCauseExtractor; + this.startTimeExtractor = builder.startTimeExtractor; + this.endTimeExtractor = builder.endTimeExtractor; + } + + /** + * Returns whether instrumentation should be applied for the {@link REQUEST}. If {@code true}, + * call {@link #start(Context, Object)} and {@link #end(Context, Object, Object, Throwable)} + * around the operation being instrumented, or if {@code false} execute the operation directly + * without calling those methods. + */ + public boolean shouldStart(Context parentContext, REQUEST request) { + boolean suppressed = false; + SpanKind spanKind = spanKindExtractor.extract(request); + switch (spanKind) { + case SERVER: + case CONSUMER: + suppressed = ServerSpan.exists(parentContext) || ConsumerSpan.exists(parentContext); + break; + case CLIENT: + suppressed = ClientSpan.exists(parentContext); + break; + default: + break; + } + if (suppressed) { + supportability.recordSuppressedSpan(spanKind, instrumentationName); + } + return !suppressed; + } + + /** + * Starts a new operation to be instrumented. The {@code parentContext} is the parent of the + * resulting instrumented operation and should usually be {@code Context.current()}. The {@code + * request} is the request object of this operation. The returned {@link Context} should be + * propagated along with the operation and passed to {@link #end(Context, Object, Object, + * Throwable)} when it is finished. + */ + public Context start(Context parentContext, REQUEST request) { + SpanKind spanKind = spanKindExtractor.extract(request); + SpanBuilder spanBuilder = + tracer + .spanBuilder(spanNameExtractor.extract(request)) + .setSpanKind(spanKind) + .setParent(parentContext); + + if (startTimeExtractor != null) { + spanBuilder.setStartTimestamp(startTimeExtractor.extract(request)); + } + + for (SpanLinkExtractor extractor : spanLinkExtractors) { + spanBuilder.addLink(extractor.extract(parentContext, request)); + } + + UnsafeAttributes attributesBuilder = new UnsafeAttributes(); + for (AttributesExtractor extractor : attributesExtractors) { + extractor.onStart(attributesBuilder, request); + } + Attributes attributes = attributesBuilder; + + Context context = parentContext; + + for (RequestListener requestListener : requestListeners) { + context = requestListener.start(context, attributes); + } + + spanBuilder.setAllAttributes(attributes); + Span span = spanBuilder.startSpan(); + context = context.with(span); + switch (spanKind) { + case SERVER: + return ServerSpan.with(context, span); + case CLIENT: + return ClientSpan.with(context, span); + case CONSUMER: + return ConsumerSpan.with(context, span); + default: + return context; + } + } + + /** + * Ends an instrumented operation. The {@link Context} must be what was returned from {@link + * #start(Context, Object)}. {@code request} is the request object of the operation, {@code + * response} is the response object of the operation, and {@code error} is an exception that was + * thrown by the operation, or {@code null} if none was thrown. + */ + public void end(Context context, REQUEST request, RESPONSE response, @Nullable Throwable error) { + Span span = Span.fromContext(context); + + UnsafeAttributes attributesBuilder = new UnsafeAttributes(); + for (AttributesExtractor extractor : attributesExtractors) { + extractor.onEnd(attributesBuilder, request, response); + } + Attributes attributes = attributesBuilder; + + for (RequestListener requestListener : requestListeners) { + requestListener.end(context, attributes); + } + + span.setAllAttributes(attributes); + + if (error != null) { + error = errorCauseExtractor.extractCause(error); + span.recordException(error); + } + + StatusCode statusCode = spanStatusExtractor.extract(request, response, error, spanKindExtractor.extract(request)); + if (statusCode != StatusCode.UNSET) { + span.setStatus(statusCode); + } + + if (endTimeExtractor != null) { + span.end(endTimeExtractor.extract(response)); + } else { + span.end(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java new file mode 100644 index 000000000..2bad44b27 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -0,0 +1,209 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.annotations.UnstableApi; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A builder of {@link Instrumenter}. Instrumentation libraries should generally expose their own + * builder with controls that are appropriate for that library and delegate to this to create the + * {@link Instrumenter}. + */ +public final class InstrumenterBuilder { + + final OpenTelemetry openTelemetry; + final Meter meter; + final String instrumentationName; + final SpanNameExtractor spanNameExtractor; + + final List> attributesExtractors = + new ArrayList<>(); + final List> spanLinkExtractors = new ArrayList<>(); + final List requestListeners = new ArrayList<>(); + + SpanKindExtractor spanKindExtractor = SpanKindExtractor.alwaysInternal(); + SpanStatusExtractor spanStatusExtractor = + SpanStatusExtractor.getDefault(); + ErrorCauseExtractor errorCauseExtractor = ErrorCauseExtractor.jdk(); + @Nullable StartTimeExtractor startTimeExtractor = null; + @Nullable EndTimeExtractor endTimeExtractor = null; + + InstrumenterBuilder( + OpenTelemetry openTelemetry, + String instrumentationName, + SpanNameExtractor spanNameExtractor) { + this.openTelemetry = openTelemetry; + // TODO(anuraaga): Retrieve from openTelemetry when not alpha anymore. + this.meter = GlobalMeterProvider.get().get(instrumentationName); + this.instrumentationName = instrumentationName; + this.spanNameExtractor = spanNameExtractor; + } + + /** + * Sets the {@link SpanStatusExtractor} to use to determine the {@link StatusCode} for a response. + */ + public InstrumenterBuilder setSpanStatusExtractor( + SpanStatusExtractor spanStatusExtractor) { + this.spanStatusExtractor = spanStatusExtractor; + return this; + } + + /** Adds a {@link AttributesExtractor} to extract attributes from requests and responses. */ + public InstrumenterBuilder addAttributesExtractor( + AttributesExtractor attributesExtractor) { + this.attributesExtractors.add(attributesExtractor); + return this; + } + + /** Adds {@link AttributesExtractor}s to extract attributes from requests and responses. */ + public InstrumenterBuilder addAttributesExtractors( + Iterable> + attributesExtractors) { + attributesExtractors.forEach(this.attributesExtractors::add); + return this; + } + + /** Adds {@link AttributesExtractor}s to extract attributes from requests and responses. */ + public InstrumenterBuilder addAttributesExtractors( + AttributesExtractor... attributesExtractors) { + return addAttributesExtractors(Arrays.asList(attributesExtractors)); + } + + /** Adds a {@link SpanLinkExtractor} to extract span link from requests. */ + public InstrumenterBuilder addSpanLinkExtractor( + SpanLinkExtractor spanLinkExtractor) { + spanLinkExtractors.add(spanLinkExtractor); + return this; + } + + /** Adds a {@link RequestMetrics} whose metrics will be recorded for request start and stop. */ + @UnstableApi + public InstrumenterBuilder addRequestMetrics(RequestMetrics factory) { + requestListeners.add(factory.create(meter)); + return this; + } + + /** + * Sets the {@link ErrorCauseExtractor} to extract the root cause from an exception handling the + * request. + */ + public InstrumenterBuilder setErrorCauseExtractor( + ErrorCauseExtractor errorCauseExtractor) { + this.errorCauseExtractor = errorCauseExtractor; + return this; + } + + /** + * Sets the {@link StartTimeExtractor} and the {@link EndTimeExtractor} to extract the timestamp + * marking the start and end of processing. If unset, the constructed instrumenter will defer + * determining start and end timestamps to the OpenTelemetry SDK. + */ + public InstrumenterBuilder setTimeExtractors( + StartTimeExtractor startTimeExtractor, EndTimeExtractor endTimeExtractor) { + this.startTimeExtractor = requireNonNull(startTimeExtractor); + this.endTimeExtractor = requireNonNull(endTimeExtractor); + return this; + } + + /** + * Returns a new {@link Instrumenter} which will create client spans and inject context into + * requests. + */ + public Instrumenter newClientInstrumenter(TextMapSetter setter) { + return newInstrumenter( + InstrumenterConstructor.propagatingToDownstream(setter), SpanKindExtractor.alwaysClient()); + } + + /** + * Returns a new {@link Instrumenter} which will create server spans and extract context from + * requests. + */ + public Instrumenter newServerInstrumenter(TextMapGetter getter) { + return newUpstreamPropagatingInstrumenter(SpanKindExtractor.alwaysServer(), getter); + } + + /** + * Returns a new {@link Instrumenter} which will create producer spans and inject context into + * requests. + */ + public Instrumenter newProducerInstrumenter(TextMapSetter setter) { + return newInstrumenter( + InstrumenterConstructor.propagatingToDownstream(setter), + SpanKindExtractor.alwaysProducer()); + } + + /** + * Returns a new {@link Instrumenter} which will create consumer spans and extract context from + * requests. + */ + public Instrumenter newConsumerInstrumenter(TextMapGetter getter) { + return newUpstreamPropagatingInstrumenter(SpanKindExtractor.alwaysConsumer(), getter); + } + + /** + * Returns a new {@link Instrumenter} which will create spans with kind determined by the passed + * {@code spanKindExtractor} and extract context from requests. + */ + public Instrumenter newUpstreamPropagatingInstrumenter( + SpanKindExtractor spanKindExtractor, TextMapGetter getter) { + return newInstrumenter( + InstrumenterConstructor.propagatingFromUpstream(getter), spanKindExtractor); + } + + /** + * Returns a new {@link Instrumenter} which will create internal spans and do no context + * propagation. + */ + public Instrumenter newInstrumenter() { + return newInstrumenter(InstrumenterConstructor.internal(), SpanKindExtractor.alwaysInternal()); + } + + /** + * Returns a new {@link Instrumenter} which will create spans with kind determined by the passed + * {@code spanKindExtractor} and do no context propagation. + */ + public Instrumenter newInstrumenter( + SpanKindExtractor spanKindExtractor) { + return newInstrumenter(InstrumenterConstructor.internal(), spanKindExtractor); + } + + private Instrumenter newInstrumenter( + InstrumenterConstructor constructor, + SpanKindExtractor spanKindExtractor) { + this.spanKindExtractor = spanKindExtractor; + return constructor.create(this); + } + + private interface InstrumenterConstructor { + Instrumenter create(InstrumenterBuilder builder); + + static InstrumenterConstructor internal() { + return Instrumenter::new; + } + + static InstrumenterConstructor propagatingToDownstream( + TextMapSetter setter) { + return builder -> new ClientInstrumenter<>(builder, setter); + } + + static InstrumenterConstructor propagatingFromUpstream( + TextMapGetter getter) { + return builder -> new ServerInstrumenter<>(builder, getter); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/JdkErrorCauseExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/JdkErrorCauseExtractor.java new file mode 100644 index 000000000..cb90193d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/JdkErrorCauseExtractor.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; + +final class JdkErrorCauseExtractor implements ErrorCauseExtractor { + static final ErrorCauseExtractor INSTANCE = new JdkErrorCauseExtractor(); + + @Override + public Throwable extractCause(Throwable error) { + if (error.getCause() != null + && (error instanceof ExecutionException + || error instanceof CompletionException + || error instanceof InvocationTargetException + || error instanceof UndeclaredThrowableException)) { + return extractCause(error.getCause()); + } + return error; + } + + private JdkErrorCauseExtractor() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/PropagatorBasedSpanLinkExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/PropagatorBasedSpanLinkExtractor.java new file mode 100644 index 000000000..f0fabf11c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/PropagatorBasedSpanLinkExtractor.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; + +final class PropagatorBasedSpanLinkExtractor implements SpanLinkExtractor { + private final ContextPropagators propagators; + private final TextMapGetter getter; + + PropagatorBasedSpanLinkExtractor(ContextPropagators propagators, TextMapGetter getter) { + this.propagators = propagators; + this.getter = getter; + } + + @Override + public SpanContext extract(Context parentContext, REQUEST request) { + Context extracted = propagators.getTextMapPropagator().extract(parentContext, request, getter); + return Span.fromContext(extracted).getSpanContext(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/RequestListener.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/RequestListener.java new file mode 100644 index 000000000..ef8468ed7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/RequestListener.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; + +/** + * A listener of the start and end of a request. Instrumented libraries will call {@link + * #start(Context, Attributes)} as early as possible in the processing of a request and {@link + * #end(Context, Attributes)} as late as possible when finishing the request. These correspond to + * the start and end of a span when tracing. + */ +public interface RequestListener { + + /** + * Listener method that is called at the start of a request. If any state needs to be kept between + * the start and end of the request, e.g., an in-progress span, it should be added to the passed + * in {@link Context} and returned. + */ + Context start(Context context, Attributes requestAttributes); + + /** Listener method that is called at the end of a request. */ + void end(Context context, Attributes responseAttributes); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/RequestMetrics.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/RequestMetrics.java new file mode 100644 index 000000000..126c6e031 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/RequestMetrics.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.api.annotations.UnstableApi; + +/** A factory for a {@link RequestListener} for recording metrics using a {@link Meter}. */ +@FunctionalInterface +@UnstableApi +public interface RequestMetrics { + /** Returns a {@link RequestListener} for recording metrics using the given {@link Meter}. */ + RequestListener create(Meter meter); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ServerInstrumenter.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ServerInstrumenter.java new file mode 100644 index 000000000..f26a2e7e6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/ServerInstrumenter.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug; + +final class ServerInstrumenter extends Instrumenter { + + private final ContextPropagators propagators; + private final TextMapGetter getter; + + ServerInstrumenter( + InstrumenterBuilder builder, TextMapGetter getter) { + super(builder); + this.propagators = builder.openTelemetry.getPropagators(); + this.getter = getter; + } + + @Override + public Context start(Context parentContext, REQUEST request) { + ContextPropagationDebug.debugContextLeakIfEnabled(); + + Context extracted = propagators.getTextMapPropagator().extract(parentContext, request, getter); + return super.start(extracted, request); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanKindExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanKindExtractor.java new file mode 100644 index 000000000..fb65461de --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanKindExtractor.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.trace.SpanKind; + +/** + * Extractor or {@link SpanKind}. In most cases, the span kind will be constant for server or client + * requests, but you may need to implement a custom {@link SpanKindExtractor} if a framework can + * generate different kinds of spans, for example both HTTP and messaging spans. + */ +@FunctionalInterface +public interface SpanKindExtractor { + + /** Returns a {@link SpanNameExtractor} which always returns {@link SpanKind#INTERNAL}. */ + static SpanKindExtractor alwaysInternal() { + return request -> SpanKind.INTERNAL; + } + + /** Returns a {@link SpanNameExtractor} which always returns {@link SpanKind#CLIENT}. */ + static SpanKindExtractor alwaysClient() { + return request -> SpanKind.CLIENT; + } + + /** Returns a {@link SpanNameExtractor} which always returns {@link SpanKind#SERVER}. */ + static SpanKindExtractor alwaysServer() { + return request -> SpanKind.SERVER; + } + + /** Returns a {@link SpanNameExtractor} which always returns {@link SpanKind#PRODUCER}. */ + static SpanKindExtractor alwaysProducer() { + return request -> SpanKind.PRODUCER; + } + + /** Returns a {@link SpanNameExtractor} which always returns {@link SpanKind#CONSUMER}. */ + static SpanKindExtractor alwaysConsumer() { + return request -> SpanKind.CONSUMER; + } + + /** Returns the {@link SpanKind} corresponding to the {@link REQUEST}. */ + SpanKind extract(REQUEST request); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanLinkExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanLinkExtractor.java new file mode 100644 index 000000000..91020e94f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanLinkExtractor.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; + +/** Extractor of a span link for a request. */ +@FunctionalInterface +public interface SpanLinkExtractor { + /** + * Extract a {@link SpanContext} that should be linked to the newly created span. Returning {@code + * SpanContext.getInvalid()} will not add any link to the span. + */ + SpanContext extract(Context parentContext, REQUEST request); + + /** + * Returns a new {@link SpanLinkExtractor} that will extract a {@link SpanContext} from the + * request using configured {@code propagators}. + */ + static SpanLinkExtractor fromUpstreamRequest( + ContextPropagators propagators, TextMapGetter getter) { + return new PropagatorBasedSpanLinkExtractor<>(propagators, getter); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanNameExtractor.java new file mode 100644 index 000000000..801d90039 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanNameExtractor.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +/** + * Extractor of the span name for a request. Where possible, an extractor based on semantic + * conventions returned from the factories in this class should be used. The most common reason to + * provide a custom implementation would be to apply the extractor for a particular semantic + * convention by first determining which convention applies to the request. + */ +@FunctionalInterface +public interface SpanNameExtractor { + + /** Returns the span name. */ + String extract(REQUEST request); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanStatusExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanStatusExtractor.java new file mode 100644 index 000000000..d45fb59cb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanStatusExtractor.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of {@link StatusCode}, which will be called after a request and response is completed + * to determine its final status. + */ +@FunctionalInterface +public interface SpanStatusExtractor { + + /** + * Returns the default {@link SpanStatusExtractor}, which returns {@link StatusCode#ERROR} if the + * framework returned an unhandled exception, or {@link StatusCode#UNSET} otherwise. + */ + @SuppressWarnings("unchecked") + static SpanStatusExtractor getDefault() { + return (SpanStatusExtractor) DefaultSpanStatusExtractor.INSTANCE; + } + + /** Returns the {@link StatusCode}. */ + StatusCode extract(REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error, SpanKind spanKind); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/StartTimeExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/StartTimeExtractor.java new file mode 100644 index 000000000..c013a3300 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/StartTimeExtractor.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import java.time.Instant; + +/** + * Extractor of the start time of request processing. A {@link StartTimeExtractor} should always use + * the same timestamp source as the corresponding {@link EndTimeExtractor} - extracted timestamps + * must be comparable. + */ +@FunctionalInterface +public interface StartTimeExtractor { + + /** Returns the timestamp marking the start of the request processing. */ + Instant extract(REQUEST request); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributes.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributes.java new file mode 100644 index 000000000..07995752a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributes.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import java.util.HashMap; +import java.util.Map; + +/** + * The {@link AttributesBuilder} and {@link Attributes} used by the instrumentation API. We are able + * to take advantage of the fact that we know our attributes builder cannot be reused to create + * multiple Attributes instances. So we use just one storage for both the builder and attributes. A + * couple of methods still require copying to satisfy the interface contracts, but in practice + * should never be called by user code even though they can. + */ +final class UnsafeAttributes extends HashMap, Object> + implements Attributes, AttributesBuilder { + + // Attributes + + @SuppressWarnings("unchecked") + @Override + public T get(AttributeKey key) { + return (T) super.get(key); + } + + @Override + public Map, Object> asMap() { + return this; + } + + // This can be called by user code in a RequestListener so copy. In practice, it should not be + // called as there is no real use case. + @Override + public AttributesBuilder toBuilder() { + return Attributes.builder().putAll(this); + } + + // AttributesBuilder + + // This can be called by user code in an AttributesExtractor so copy. In practice, it should not + // be called as there is no real use case. + @Override + public Attributes build() { + return toBuilder().build(); + } + + @Override + public AttributesBuilder put(AttributeKey key, int value) { + return put(key, (long) value); + } + + @Override + public AttributesBuilder put(AttributeKey key, T value) { + super.put(key, value); + return this; + } + + @Override + public AttributesBuilder putAll(Attributes attributes) { + attributes.forEach(this::put); + return this; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractor.java new file mode 100644 index 000000000..06df0d279 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractor.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.code; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of source + * code attributes. + */ +public abstract class CodeAttributesExtractor + extends AttributesExtractor { + + @Override + protected final void onStart(AttributesBuilder attributes, REQUEST request) { + Class cls = codeClass(request); + if (cls != null) { + set(attributes, SemanticAttributes.CODE_NAMESPACE, cls.getName()); + } + set(attributes, SemanticAttributes.CODE_FUNCTION, methodName(request)); + set(attributes, SemanticAttributes.CODE_FILEPATH, filePath(request)); + set(attributes, SemanticAttributes.CODE_LINENO, lineNumber(request)); + } + + @Override + protected final void onEnd( + AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) {} + + @Nullable + protected abstract Class codeClass(REQUEST request); + + @Nullable + protected abstract String methodName(REQUEST request); + + @Nullable + protected abstract String filePath(REQUEST request); + + @Nullable + protected abstract Long lineNumber(REQUEST request); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractor.java new file mode 100644 index 000000000..ac0cc835c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractor.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.code; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.tracer.ClassNames; + +/** + * A helper {@link SpanNameExtractor} implementation for instrumentations that target specific Java + * classes/methods. + */ +public final class CodeSpanNameExtractor implements SpanNameExtractor { + + /** + * Returns a {@link SpanNameExtractor} that constructs the span name according to the following + * pattern: {@code .}. + */ + public static SpanNameExtractor create( + CodeAttributesExtractor attributesExtractor) { + return new CodeSpanNameExtractor<>(attributesExtractor); + } + + private final CodeAttributesExtractor attributesExtractor; + + private CodeSpanNameExtractor(CodeAttributesExtractor attributesExtractor) { + this.attributesExtractor = attributesExtractor; + } + + @Override + public String extract(REQUEST request) { + Class cls = attributesExtractor.codeClass(request); + String className = cls != null ? ClassNames.simpleName(cls) : ""; + String methodName = defaultString(attributesExtractor.methodName(request)); + return className + "." + methodName; + } + + private static String defaultString(String s) { + return s == null ? "" : s; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbAttributesExtractor.java new file mode 100644 index 000000000..c05e5957a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbAttributesExtractor.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.db; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of database + * attributes. Instrumentations of database libraries should extend this class, defining {@link + * REQUEST} with the actual request type of the instrumented library. If an attribute is not + * available in this library, it is appropriate to return {@code null} from the protected attribute + * methods, but implement as many as possible for best compliance with the OpenTelemetry + * specification. + */ +public abstract class DbAttributesExtractor + extends AttributesExtractor { + @Override + protected void onStart(AttributesBuilder attributes, REQUEST request) { + set(attributes, SemanticAttributes.DB_SYSTEM, system(request)); + set(attributes, SemanticAttributes.DB_USER, user(request)); + set(attributes, SemanticAttributes.DB_NAME, name(request)); + set(attributes, SemanticAttributes.DB_CONNECTION_STRING, connectionString(request)); + set(attributes, SemanticAttributes.DB_STATEMENT, statement(request)); + set(attributes, SemanticAttributes.DB_OPERATION, operation(request)); + } + + @Override + protected final void onEnd( + AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) {} + + @Nullable + protected abstract String system(REQUEST request); + + @Nullable + protected abstract String user(REQUEST request); + + @Nullable + protected abstract String name(REQUEST request); + + @Nullable + protected abstract String connectionString(REQUEST request); + + @Nullable + protected abstract String statement(REQUEST request); + + @Nullable + protected abstract String operation(REQUEST request); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbSpanNameExtractor.java new file mode 100644 index 000000000..0fa523e37 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbSpanNameExtractor.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.db; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class DbSpanNameExtractor implements SpanNameExtractor { + /** + * Returns a {@link SpanNameExtractor} that constructs the span name according to DB semantic + * conventions: {@code .}. + * + * @see DbAttributesExtractor#operation(Object) used to extract {@code }. + * @see DbAttributesExtractor#name(Object) used to extract {@code }. + * @see SqlAttributesExtractor#table(Object) used to extract {@code }. + */ + public static SpanNameExtractor create( + DbAttributesExtractor attributesExtractor) { + return new DbSpanNameExtractor<>(attributesExtractor); + } + + private static final String DEFAULT_SPAN_NAME = "DB Query"; + + private final DbAttributesExtractor attributesExtractor; + + private DbSpanNameExtractor(DbAttributesExtractor attributesExtractor) { + this.attributesExtractor = attributesExtractor; + } + + @Override + public String extract(REQUEST request) { + String operation = attributesExtractor.operation(request); + String dbName = attributesExtractor.name(request); + if (operation == null) { + return dbName == null ? DEFAULT_SPAN_NAME : dbName; + } + + String table = getTableName(request); + StringBuilder name = new StringBuilder(operation); + if (dbName != null || table != null) { + name.append(' '); + } + // skip db name if table already has a db name prefixed to it + if (dbName != null && (table == null || table.indexOf('.') == -1)) { + name.append(dbName); + if (table != null) { + name.append('.'); + } + } + if (table != null) { + name.append(table); + } + return name.toString(); + } + + @Nullable + private String getTableName(REQUEST request) { + if (attributesExtractor instanceof SqlAttributesExtractor) { + return ((SqlAttributesExtractor) attributesExtractor).table(request); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlAttributesExtractor.java new file mode 100644 index 000000000..a145faecb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlAttributesExtractor.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.db; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of database + * attributes. This class is designed with SQL (or SQL-like) database clients in mind. Aside + * from adding the same attributes as {@link DbAttributesExtractor}, it has two more features: + * + *
    + *
  • It sanitizes the raw SQL query and removes all parameters; + *
  • It enables adding the table name extracted by the sanitizer as a parameter. + *
+ */ +public abstract class SqlAttributesExtractor + extends DbAttributesExtractor { + + @Override + protected final void onStart(AttributesBuilder attributes, REQUEST request) { + super.onStart(attributes, request); + AttributeKey dbTable = dbTableAttribute(); + if (dbTable != null) { + set(attributes, dbTable, table(request)); + } + } + + @Nullable + @Override + protected final String statement(REQUEST request) { + return sanitize(request).getFullStatement(); + } + + @Nullable + @Override + protected final String operation(REQUEST request) { + return sanitize(request).getOperation(); + } + + @Nullable + protected final String table(REQUEST request) { + return sanitize(request).getTable(); + } + + private SqlStatementInfo sanitize(REQUEST request) { + // sanitized statement is cached + return SqlStatementSanitizer.sanitize(rawStatement(request)); + } + + @Nullable + protected abstract AttributeKey dbTableAttribute(); + + @Nullable + protected abstract String rawStatement(REQUEST request); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributesExtractor.java new file mode 100644 index 000000000..793d2e233 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributesExtractor.java @@ -0,0 +1,165 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of HTTP + * attributes. Instrumentation of HTTP server or client frameworks should extend this class, + * defining {@link REQUEST} and {@link RESPONSE} with the actual request / response types of the + * instrumented library. If an attribute is not available in this library, it is appropriate to + * return {@code null} from the protected attribute methods, but implement as many as possible for + * best compliance with the OpenTelemetry specification. + */ +public abstract class HttpAttributesExtractor + extends AttributesExtractor { + + @Override + protected final void onStart(AttributesBuilder attributes, REQUEST request) { + set(attributes, SemanticAttributes.HTTP_METHOD, method(request)); + set(attributes, SemanticAttributes.HTTP_URL, url(request)); + set(attributes, SemanticAttributes.HTTP_TARGET, target(request)); + set(attributes, SemanticAttributes.HTTP_HOST, host(request)); + set(attributes, SemanticAttributes.HTTP_ROUTE, route(request)); + set(attributes, SemanticAttributes.HTTP_SCHEME, scheme(request)); + set(attributes, SemanticAttributes.HTTP_USER_AGENT, userAgent(request)); + } + + @Override + protected final void onEnd( + AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) { + set( + attributes, + SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, + requestContentLength(request, response)); + set( + attributes, + SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED, + requestContentLengthUncompressed(request, response)); + set(attributes, SemanticAttributes.HTTP_FLAVOR, flavor(request, response)); + set(attributes, SemanticAttributes.HTTP_SERVER_NAME, serverName(request, response)); + set(attributes, SemanticAttributes.HTTP_CLIENT_IP, clientIp(request, response)); + if (response != null) { + Integer statusCode = statusCode(request, response); + if (statusCode != null) { + set(attributes, SemanticAttributes.HTTP_STATUS_CODE, (long) statusCode); + } + set( + attributes, + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + responseContentLength(request, response)); + set( + attributes, + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED, + responseContentLengthUncompressed(request, response)); + } + } + + // Attributes that always exist in a request + + @Nullable + protected abstract String method(REQUEST request); + + @Nullable + protected abstract String url(REQUEST request); + + @Nullable + protected abstract String target(REQUEST request); + + @Nullable + protected abstract String host(REQUEST request); + + @Nullable + protected abstract String route(REQUEST request); + + @Nullable + protected abstract String scheme(REQUEST request); + + @Nullable + protected abstract String userAgent(REQUEST request); + + // Attributes which are not always available when the request is ready. + + /** + * Extracts the {@code http.request_content_length} span attribute. + * + *

This is called from {@link Instrumenter#end(Context, Object, Object, Throwable)}, whether + * {@code response} is {@code null} or not. + */ + @Nullable + protected abstract Long requestContentLength(REQUEST request, @Nullable RESPONSE response); + + /** + * Extracts the {@code http.request_content_length_uncompressed} span attribute. + * + *

This is called from {@link Instrumenter#end(Context, Object, Object, Throwable)}, whether + * {@code response} is {@code null} or not. + */ + @Nullable + protected abstract Long requestContentLengthUncompressed( + REQUEST request, @Nullable RESPONSE response); + + /** + * Extracts the {@code http.flavor} span attribute. + * + *

This is called from {@link Instrumenter#end(Context, Object, Object, Throwable)}, whether + * {@code response} is {@code null} or not. + */ + @Nullable + protected abstract String flavor(REQUEST request, @Nullable RESPONSE response); + + /** + * Extracts the {@code http.server_name} span attribute. + * + *

This is called from {@link Instrumenter#end(Context, Object, Object, Throwable)}, whether + * {@code response} is {@code null} or not. + */ + @Nullable + protected abstract String serverName(REQUEST request, @Nullable RESPONSE response); + + /** + * Extracts the {@code http.client_ip} span attribute. + * + *

This is called from {@link Instrumenter#end(Context, Object, Object, Throwable)}, whether + * {@code response} is {@code null} or not. + */ + @Nullable + protected abstract String clientIp(REQUEST request, @Nullable RESPONSE response); + + /** + * Extracts the {@code http.status_code} span attribute. + * + *

This is called from {@link Instrumenter#end(Context, Object, Object, Throwable)}, only when + * {@code response} is non-{@code null}. + */ + @Nullable + protected abstract Integer statusCode(REQUEST request, RESPONSE response); + + /** + * Extracts the {@code http.response_content_length} span attribute. + * + *

This is called from {@link Instrumenter#end(Context, Object, Object, Throwable)}, only when + * {@code response} is non-{@code null}. + */ + @Nullable + protected abstract Long responseContentLength(REQUEST request, RESPONSE response); + + /** + * Extracts the {@code http.response_content_length_uncompressed} span attribute. + * + *

This is called from {@link Instrumenter#end(Context, Object, Object, Throwable)}, only when + * {@code response} is non-{@code null}. + */ + @Nullable + protected abstract Long responseContentLengthUncompressed(REQUEST request, RESPONSE response); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java new file mode 100644 index 000000000..f776be5bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.AttributeType; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleValueRecorder; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.common.LabelsBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.annotations.UnstableApi; +import io.opentelemetry.instrumentation.api.instrumenter.RequestListener; +import io.opentelemetry.instrumentation.api.instrumenter.RequestMetrics; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link RequestListener} which keeps track of HTTP + * server metrics. + * + *

To use this class, you may need to add the {@code opentelemetry-api-metrics} artifact to your + * dependencies. + */ +@UnstableApi +public final class HttpServerMetrics implements RequestListener { + + private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1); + + private static final ContextKey HTTP_SERVER_REQUEST_METRICS_STATE = + ContextKey.named("http-server-request-metrics-state"); + + private static final Logger logger = LoggerFactory.getLogger(HttpServerMetrics.class); + + /** + * Returns a {@link RequestMetrics} which can be used to enable recording of {@link + * HttpServerMetrics} on an {@link + * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. + */ + @UnstableApi + public static RequestMetrics get() { + return HttpServerMetrics::new; + } + + private final LongUpDownCounter activeRequests; + private final DoubleValueRecorder duration; + + private HttpServerMetrics(Meter meter) { + activeRequests = + meter + .longUpDownCounterBuilder("http.server.active_requests") + .setUnit("requests") + .setDescription("The number of concurrent HTTP requests that are currently in-flight") + .build(); + + duration = + meter + .doubleValueRecorderBuilder("http.server.duration") + .setUnit("milliseconds") + .setDescription("The duration of the inbound HTTP request") + .build(); + } + + @Override + public Context start(Context context, Attributes requestAttributes) { + long startTimeNanos = System.nanoTime(); + Labels activeRequestLabels = activeRequestLabels(requestAttributes); + Labels durationLabels = durationLabels(requestAttributes); + activeRequests.add(1, activeRequestLabels); + + return context.with( + HTTP_SERVER_REQUEST_METRICS_STATE, + new AutoValue_HttpServerMetrics_State(activeRequestLabels, durationLabels, startTimeNanos)); + } + + @Override + public void end(Context context, Attributes responseAttributes) { + State state = context.get(HTTP_SERVER_REQUEST_METRICS_STATE); + if (state == null) { + logger.debug( + "No state present when ending context {}. Cannot reset HTTP request metrics.", context); + return; + } + activeRequests.add(-1, state.activeRequestLabels()); + duration.record( + (System.nanoTime() - state.startTimeNanos()) / NANOS_PER_MS, state.durationLabels()); + } + + private static Labels activeRequestLabels(Attributes attributes) { + LabelsBuilder labels = Labels.builder(); + attributes.forEach( + (key, value) -> { + if (key.getType() != AttributeType.STRING) { + return; + } + switch (key.getKey()) { + case "http.method": + case "http.host": + case "http.scheme": + case "http.flavor": + case "http.server_name": + labels.put(key.getKey(), (String) value); + break; + default: + // fall through + } + }); + return labels.build(); + } + + private static Labels durationLabels(Attributes attributes) { + LabelsBuilder labels = Labels.builder(); + attributes.forEach( + (key, value) -> { + switch (key.getKey()) { + case "http.method": + case "http.host": + case "http.scheme": + case "http.flavor": + case "http.server_name": + case "net.host.name": + if (value instanceof String) { + labels.put(key.getKey(), (String) value); + } + break; + case "http.status_code": + case "net.host.port": + if (value instanceof Long) { + labels.put(key.getKey(), Long.toString((long) value)); + } + break; + default: + // fall through + } + }); + return labels.build(); + } + + @AutoValue + abstract static class State { + + abstract Labels activeRequestLabels(); + + abstract Labels durationLabels(); + + abstract long startTimeNanos(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractor.java new file mode 100644 index 000000000..949dfb976 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractor.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +/** + * Extractor of the HTTP + * span name. Instrumentation of HTTP server or client frameworks should use this class to + * comply with OpenTelemetry HTTP semantic conventions. + */ +public final class HttpSpanNameExtractor implements SpanNameExtractor { + + /** + * Returns a {@link SpanNameExtractor} which should be used for HTTP requests. HTTP attributes + * will be examined to determine the name of the span. + */ + public static SpanNameExtractor create( + HttpAttributesExtractor attributesExtractor) { + return new HttpSpanNameExtractor<>(attributesExtractor); + } + + private final HttpAttributesExtractor attributesExtractor; + + private HttpSpanNameExtractor(HttpAttributesExtractor attributesExtractor) { + this.attributesExtractor = attributesExtractor; + } + + @Override + public String extract(REQUEST request) { + String route = attributesExtractor.route(request); + if (route != null) { + return route; + } + String method = attributesExtractor.method(request); + if (method != null) { + return "HTTP " + method; + } + return "HTTP request"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractor.java new file mode 100644 index 000000000..f848c5335 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractor.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.tracer.HttpStatusConverter; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of the HTTP + * span status. Instrumentation of HTTP server or client frameworks should use this class to + * comply with OpenTelemetry HTTP semantic conventions. + */ +public final class HttpSpanStatusExtractor + implements SpanStatusExtractor { + + /** + * Returns the {@link SpanStatusExtractor} for HTTP requests, which will use the HTTP status code + * to determine the {@link StatusCode} if available or fallback to {@linkplain #getDefault() the + * default status} otherwise. + */ + public static SpanStatusExtractor create( + HttpAttributesExtractor attributesExtractor) { + return new HttpSpanStatusExtractor<>(attributesExtractor); + } + + private final HttpAttributesExtractor attributesExtractor; + + private HttpSpanStatusExtractor(HttpAttributesExtractor attributesExtractor) { + this.attributesExtractor = attributesExtractor; + } + + @Override + public StatusCode extract(REQUEST request, @Nullable RESPONSE response, Throwable error, SpanKind spanKind) { + if (response != null) { + Integer statusCode = attributesExtractor.statusCode(request, response); + if (statusCode != null) { + return HttpStatusConverter.statusFromHttpStatus(statusCode, spanKind); + } + } + return SpanStatusExtractor.getDefault().extract(request, response, error, spanKind); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessageOperation.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessageOperation.java new file mode 100644 index 000000000..9c1ab7b1a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessageOperation.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.messaging; + +import java.util.Locale; + +/** + * Represents type of operations + * that may be used in a messaging system. + */ +public enum MessageOperation { + SEND, + RECEIVE, + PROCESS; + + /** + * Returns the operation name as defined in the + * specification. + */ + public String operationName() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractor.java new file mode 100644 index 000000000..78cce0731 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractor.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.messaging; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of messaging + * attributes. Instrumentation of messaging frameworks/libraries should extend this class, + * defining {@link REQUEST} and {@link RESPONSE} with the actual request / response types of the + * instrumented library. If an attribute is not available in this library, it is appropriate to + * return {@code null} from the protected attribute methods, but implement as many as possible for + * best compliance with the OpenTelemetry specification. + */ +public abstract class MessagingAttributesExtractor + extends AttributesExtractor { + public static final String TEMP_DESTINATION_NAME = "(temporary)"; + + @Override + protected final void onStart(AttributesBuilder attributes, REQUEST request) { + set(attributes, SemanticAttributes.MESSAGING_SYSTEM, system(request)); + set(attributes, SemanticAttributes.MESSAGING_DESTINATION_KIND, destinationKind(request)); + boolean isTemporaryDestination = temporaryDestination(request); + if (isTemporaryDestination) { + set(attributes, SemanticAttributes.MESSAGING_TEMP_DESTINATION, true); + set(attributes, SemanticAttributes.MESSAGING_DESTINATION, TEMP_DESTINATION_NAME); + } else { + set(attributes, SemanticAttributes.MESSAGING_DESTINATION, destination(request)); + } + set(attributes, SemanticAttributes.MESSAGING_PROTOCOL, protocol(request)); + set(attributes, SemanticAttributes.MESSAGING_PROTOCOL_VERSION, protocolVersion(request)); + set(attributes, SemanticAttributes.MESSAGING_URL, url(request)); + set(attributes, SemanticAttributes.MESSAGING_CONVERSATION_ID, conversationId(request)); + set( + attributes, + SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + messagePayloadSize(request)); + set( + attributes, + SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_COMPRESSED_SIZE_BYTES, + messagePayloadCompressedSize(request)); + MessageOperation operation = operation(request); + if (operation == MessageOperation.RECEIVE || operation == MessageOperation.PROCESS) { + set(attributes, SemanticAttributes.MESSAGING_OPERATION, operation.operationName()); + } + } + + @Override + protected final void onEnd( + AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) { + set(attributes, SemanticAttributes.MESSAGING_MESSAGE_ID, messageId(request, response)); + } + + @Nullable + protected abstract String system(REQUEST request); + + @Nullable + protected abstract String destinationKind(REQUEST request); + + @Nullable + protected abstract String destination(REQUEST request); + + protected abstract boolean temporaryDestination(REQUEST request); + + @Nullable + protected abstract String protocol(REQUEST request); + + @Nullable + protected abstract String protocolVersion(REQUEST request); + + @Nullable + protected abstract String url(REQUEST request); + + @Nullable + protected abstract String conversationId(REQUEST request); + + @Nullable + protected abstract Long messagePayloadSize(REQUEST request); + + @Nullable + protected abstract Long messagePayloadCompressedSize(REQUEST request); + + @Nullable + protected abstract MessageOperation operation(REQUEST request); + + @Nullable + protected abstract String messageId(REQUEST request, @Nullable RESPONSE response); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractor.java new file mode 100644 index 000000000..180735437 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractor.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.messaging; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +public final class MessagingSpanNameExtractor implements SpanNameExtractor { + + /** + * Returns a {@link SpanNameExtractor} that constructs the span name according to + * messaging semantic conventions: {@code }. + * + * @see MessagingAttributesExtractor#destination(Object) used to extract {@code }. + * @see MessagingAttributesExtractor#operation(Object) used to extract {@code }. + */ + public static SpanNameExtractor create( + MessagingAttributesExtractor attributesExtractor) { + return new MessagingSpanNameExtractor<>(attributesExtractor); + } + + private final MessagingAttributesExtractor attributesExtractor; + + private MessagingSpanNameExtractor(MessagingAttributesExtractor attributesExtractor) { + this.attributesExtractor = attributesExtractor; + } + + @Override + public String extract(REQUEST request) { + String destinationName = + attributesExtractor.temporaryDestination(request) + ? MessagingAttributesExtractor.TEMP_DESTINATION_NAME + : attributesExtractor.destination(request); + if (destinationName == null) { + destinationName = "unknown"; + } + + MessageOperation operation = attributesExtractor.operation(request); + return operation == null ? destinationName : destinationName + " " + operation.operationName(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetAttributesExtractor.java new file mode 100644 index 000000000..2047b0239 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetAttributesExtractor.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.net; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of Network + * attributes from a {@link InetSocketAddress}. Most network libraries will provide access to a + * {@link InetSocketAddress} so this is a convenient alternative to {@link NetAttributesExtractor}. + * There is no meaning to implement both in the same instrumentation. + */ +public abstract class InetSocketAddressNetAttributesExtractor + extends NetAttributesExtractor { + + /** + * This method will be called twice: both when the request starts ({@code response} is always null + * then) and when the response ends. This way it is possible to capture net attributes in both + * phases of processing. + */ + @Nullable + public abstract InetSocketAddress getAddress(REQUEST request, @Nullable RESPONSE response); + + @Override + @Nullable + public final String peerName(REQUEST request, @Nullable RESPONSE response) { + InetSocketAddress address = getAddress(request, response); + if (address == null) { + return null; + } + if (address.getAddress() != null) { + return address.getAddress().getHostName(); + } + return address.getHostString(); + } + + @Override + @Nullable + public final Integer peerPort(REQUEST request, @Nullable RESPONSE response) { + InetSocketAddress address = getAddress(request, response); + if (address == null) { + return null; + } + return address.getPort(); + } + + @Override + @Nullable + public final String peerIp(REQUEST request, @Nullable RESPONSE response) { + InetSocketAddress address = getAddress(request, response); + if (address == null) { + return null; + } + InetAddress remoteAddress = address.getAddress(); + if (remoteAddress != null) { + return remoteAddress.getHostAddress(); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetAttributesExtractor.java new file mode 100644 index 000000000..9fb03c66f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetAttributesExtractor.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.net; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of Network + * attributes. It is common to have access to {@link java.net.InetSocketAddress}, in which case + * it is more convenient to use {@link InetSocketAddressNetAttributesExtractor}. + */ +public abstract class NetAttributesExtractor + extends AttributesExtractor { + + @Override + protected final void onStart(AttributesBuilder attributes, REQUEST request) { + set(attributes, SemanticAttributes.NET_TRANSPORT, transport(request)); + set(attributes, SemanticAttributes.NET_PEER_IP, peerIp(request, null)); + set(attributes, SemanticAttributes.NET_PEER_NAME, peerName(request, null)); + Integer peerPort = peerPort(request, null); + if (peerPort != null) { + set(attributes, SemanticAttributes.NET_PEER_PORT, (long) peerPort); + } + } + + @Override + protected final void onEnd( + AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) { + set(attributes, SemanticAttributes.NET_PEER_IP, peerIp(request, response)); + set(attributes, SemanticAttributes.NET_PEER_NAME, peerName(request, response)); + Integer peerPort = peerPort(request, response); + if (peerPort != null) { + set(attributes, SemanticAttributes.NET_PEER_PORT, (long) peerPort); + } + } + + @Nullable + public abstract String transport(REQUEST request); + + /** + * This method will be called twice: both when the request starts ({@code response} is always null + * then) and when the response ends. This way it is possible to capture net attributes in both + * phases of processing. + */ + @Nullable + public abstract String peerName(REQUEST request, @Nullable RESPONSE response); + + /** + * This method will be called twice: both when the request starts ({@code response} is always null + * then) and when the response ends. This way it is possible to capture net attributes in both + * phases of processing. + */ + @Nullable + public abstract Integer peerPort(REQUEST request, @Nullable RESPONSE response); + + /** + * This method will be called twice: both when the request starts ({@code response} is always null + * then) and when the response ends. This way it is possible to capture net attributes in both + * phases of processing. + */ + @Nullable + public abstract String peerIp(REQUEST request, @Nullable RESPONSE response); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractor.java new file mode 100644 index 000000000..1adc4f712 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractor.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of RPC + * attributes. Instrumentations of RPC libraries should extend this class, defining {@link + * REQUEST} with the actual request type of the instrumented library. If an attribute is not + * available in this library, it is appropriate to return {@code null} from the protected attribute + * methods, but implement as many as possible for best compliance with the OpenTelemetry + * specification. + */ +public abstract class RpcAttributesExtractor + extends AttributesExtractor { + + @Override + protected final void onStart(AttributesBuilder attributes, REQUEST request) { + set(attributes, SemanticAttributes.RPC_SYSTEM, system(request)); + set(attributes, SemanticAttributes.RPC_SERVICE, service(request)); + set(attributes, SemanticAttributes.RPC_METHOD, method(request)); + } + + @Override + protected final void onEnd(AttributesBuilder attributes, REQUEST request, RESPONSE response) { + // No response attributes + } + + @Nullable + protected abstract String system(REQUEST request); + + @Nullable + protected abstract String service(REQUEST request); + + @Nullable + protected abstract String method(REQUEST request); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractor.java new file mode 100644 index 000000000..691792ba8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractor.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +/** A {@link SpanNameExtractor} for RPC requests. */ +public final class RpcSpanNameExtractor implements SpanNameExtractor { + + /** + * Returns a {@link SpanNameExtractor} that constructs the span name according to RPC semantic + * conventions: {@code /}. + */ + public static SpanNameExtractor create( + RpcAttributesExtractor attributesExtractor) { + return new RpcSpanNameExtractor<>(attributesExtractor); + } + + private final RpcAttributesExtractor attributesExtractor; + + private RpcSpanNameExtractor(RpcAttributesExtractor attributesExtractor) { + this.attributesExtractor = attributesExtractor; + } + + @Override + public String extract(REQUEST request) { + String service = attributesExtractor.service(request); + String method = attributesExtractor.method(request); + if (service == null || method == null) { + return "RPC request"; + } + return service + '/' + method; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextPropagationDebug.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextPropagationDebug.java new file mode 100644 index 000000000..c37a5ba6a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextPropagationDebug.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.config.Config; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ContextPropagationDebug { + private static final Logger log = LoggerFactory.getLogger(ContextPropagationDebug.class); + + // locations where the context was propagated to another thread (tracking multiple steps is + // helpful in akka where there is so much recursive async spawning of new work) + private static final ContextKey> THREAD_PROPAGATION_LOCATIONS = + ContextKey.named("thread-propagation-locations"); + + private static final boolean THREAD_PROPAGATION_DEBUGGER = + Config.get() + .getBoolean( + "otel.javaagent.experimental.thread-propagation-debugger.enabled", + Config.get().isAgentDebugEnabled()); + + private static final boolean FAIL_ON_CONTEXT_LEAK = + Config.get().getBoolean("otel.javaagent.testing.fail-on-context-leak", false); + + public static boolean isThreadPropagationDebuggerEnabled() { + return THREAD_PROPAGATION_DEBUGGER; + } + + public static Context appendLocations( + Context context, StackTraceElement[] locations, Object carrier) { + List currentLocations = ContextPropagationDebug.getPropagations(context); + if (currentLocations == null) { + currentLocations = new CopyOnWriteArrayList<>(); + context = context.with(THREAD_PROPAGATION_LOCATIONS, currentLocations); + } + currentLocations.add(0, new Propagation(carrier.getClass().getName(), locations)); + return context; + } + + public static void debugContextLeakIfEnabled() { + if (!isThreadPropagationDebuggerEnabled()) { + return; + } + + Context current = Context.current(); + if (current != Context.root()) { + log.error("Unexpected non-root current context found when extracting remote context!"); + Span currentSpan = Span.fromContextOrNull(current); + if (currentSpan != null) { + log.error("It contains this span: {}", currentSpan); + } + + debugContextPropagation(current); + + if (FAIL_ON_CONTEXT_LEAK) { + throw new IllegalStateException("Context leak detected"); + } + } + } + + private static List getPropagations(Context context) { + return context.get(THREAD_PROPAGATION_LOCATIONS); + } + + private static void debugContextPropagation(Context context) { + List propagations = getPropagations(context); + if (propagations != null) { + StringBuilder sb = new StringBuilder(); + Iterator i = propagations.iterator(); + while (i.hasNext()) { + Propagation entry = i.next(); + sb.append("\ncarrier of type: ").append(entry.carrierClassName); + for (StackTraceElement ste : entry.location) { + sb.append("\n "); + sb.append(ste); + } + if (i.hasNext()) { + sb.append("\nwhich was propagated from:"); + } + } + log.error("a context leak was detected. it was propagated from:{}", sb); + } + } + + private static class Propagation { + public final String carrierClassName; + public final StackTraceElement[] location; + + public Propagation(String carrierClassName, StackTraceElement[] location) { + this.carrierClassName = carrierClassName; + this.location = location; + } + } + + private ContextPropagationDebug() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java new file mode 100644 index 000000000..26c424c1c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.config.Config; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class SupportabilityMetrics { + private static final Logger log = LoggerFactory.getLogger(SupportabilityMetrics.class); + private final boolean agentDebugEnabled; + private final Consumer reporter; + + private final ConcurrentMap suppressionCounters = new ConcurrentHashMap<>(); + private final ConcurrentMap counters = new ConcurrentHashMap<>(); + + private static final SupportabilityMetrics INSTANCE = + new SupportabilityMetrics(Config.get(), log::debug).start(); + + public static SupportabilityMetrics instance() { + return INSTANCE; + } + + // visible for testing + SupportabilityMetrics(Config config, Consumer reporter) { + agentDebugEnabled = config.isAgentDebugEnabled(); + this.reporter = reporter; + } + + public void recordSuppressedSpan(SpanKind kind, String instrumentationName) { + if (!agentDebugEnabled) { + return; + } + + suppressionCounters + .computeIfAbsent(instrumentationName, s -> new KindCounters()) + .increment(kind); + } + + public void incrementCounter(String counterName) { + if (!agentDebugEnabled) { + return; + } + + counters.computeIfAbsent(counterName, k -> new LongAdder()).increment(); + } + + // visible for testing + void report() { + suppressionCounters.forEach( + (instrumentationName, countsByKind) -> { + for (SpanKind kind : SpanKind.values()) { + long value = countsByKind.getAndReset(kind); + if (value > 0) { + reporter.accept( + "Suppressed Spans by '" + instrumentationName + "' (" + kind + ") : " + value); + } + } + }); + counters.forEach( + (counterName, counter) -> { + long value = counter.sumThenReset(); + if (value > 0) { + reporter.accept("Counter '" + counterName + "' : " + value); + } + }); + } + + SupportabilityMetrics start() { + if (agentDebugEnabled) { + Executors.newScheduledThreadPool( + 1, + runnable -> { + Thread result = new Thread(runnable, "supportability_metrics_reporter"); + result.setDaemon(true); + result.setContextClassLoader(null); + return result; + }) + .scheduleAtFixedRate(this::report, 5, 5, TimeUnit.SECONDS); + } + return this; + } + + public static final class CounterNames { + public static final String SQL_STATEMENT_SANITIZER_CACHE_MISS = + "SqlStatementSanitizer cache miss"; + + private CounterNames() {} + } + + // this class is threadsafe. + private static class KindCounters { + private final LongAdder server = new LongAdder(); + private final LongAdder client = new LongAdder(); + private final LongAdder internal = new LongAdder(); + private final LongAdder consumer = new LongAdder(); + private final LongAdder producer = new LongAdder(); + + void increment(SpanKind kind) { + switch (kind) { + case INTERNAL: + internal.increment(); + break; + case SERVER: + server.increment(); + break; + case CLIENT: + client.increment(); + break; + case PRODUCER: + producer.increment(); + break; + case CONSUMER: + consumer.increment(); + break; + } + } + + long getAndReset(SpanKind kind) { + switch (kind) { + case INTERNAL: + return internal.sumThenReset(); + case SERVER: + return server.sumThenReset(); + case CLIENT: + return client.sumThenReset(); + case PRODUCER: + return producer.sumThenReset(); + case CONSUMER: + return consumer.sumThenReset(); + } + return 0; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/log/LoggingContextConstants.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/log/LoggingContextConstants.java new file mode 100644 index 000000000..c679f6a18 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/log/LoggingContextConstants.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.log; + +import io.opentelemetry.api.trace.SpanContext; + +/** + * This class contains several constants used in logging libraries' Mapped Diagnostic Context + * instrumentations. + * + * @see org.slf4j.MDC + */ +public final class LoggingContextConstants { + /** + * Key under which the current trace id will be injected into the context data. + * + * @see SpanContext#getTraceId() + */ + public static final String TRACE_ID = "trace_id"; + /** + * Key under which the current span id will be injected into the context data. + * + * @see SpanContext#getSpanId() + */ + public static final String SPAN_ID = "span_id"; + /** + * Key under which the current trace flags will be injected into the context data. + * + * @see SpanContext#getTraceFlags() + */ + public static final String TRACE_FLAGS = "trace_flags"; + + private LoggingContextConstants() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/AppServerBridge.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/AppServerBridge.java new file mode 100644 index 000000000..95a8b986d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/AppServerBridge.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.servlet; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; + +/** + * Helper container for Context attributes for transferring certain information between servlet + * integration and app-server server handler integrations. + */ +public class AppServerBridge { + + private static final ContextKey CONTEXT_KEY = + ContextKey.named("opentelemetry-servlet-app-server-bridge"); + + /** + * Attach AppServerBridge to context. + * + * @param ctx server context + * @return new context with AppServerBridge attached. + */ + public static Context init(Context ctx) { + return init(ctx, /* shouldRecordException= */ true); + } + + /** + * Attach AppServerBridge to context. + * + * @param ctx server context + * @param shouldRecordException whether servlet integration should record exception thrown during + * servlet invocation in server span. Use false on servers where exceptions + * thrown during servlet invocation are propagated to the method where server span is closed + * and can be added to server span there and true otherwise. + * @return new context with AppServerBridge attached. + */ + public static Context init(Context ctx, boolean shouldRecordException) { + return ctx.with(AppServerBridge.CONTEXT_KEY, new AppServerBridge(shouldRecordException)); + } + + private final boolean servletShouldRecordException; + + private AppServerBridge(boolean shouldRecordException) { + servletShouldRecordException = shouldRecordException; + } + + /** + * Returns true, if servlet integration should record exception thrown during servlet invocation + * in server span. This method should return false on servers where exceptions thrown + * during servlet invocation are propagated to the method where server span is closed and can be + * added to server span there and true otherwise. + * + * @param ctx server context + * @return true, if servlet integration should record exception thrown during servlet + * invocation in server span, or false otherwise. + */ + public static boolean shouldRecordException(Context ctx) { + AppServerBridge appServerBridge = ctx.get(AppServerBridge.CONTEXT_KEY); + if (appServerBridge != null) { + return appServerBridge.servletShouldRecordException; + } + return true; + } + + /** + * Class used as key in CallDepthThreadLocalMap for counting servlet invocation depth in + * Servlet3Advice and Servlet2Advice. We can not use helper classes like Servlet3Advice and + * Servlet2Advice for determining call depth of server invocation because they can be injected + * into multiple class loaders. + * + * @return class used as a key in CallDepthThreadLocalMap for counting servlet invocation depth + */ + public static Class getCallDepthKey() { + class Key {} + + return Key.class; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/MappingResolver.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/MappingResolver.java new file mode 100644 index 000000000..e771ce7d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/MappingResolver.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.servlet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Helper class for finding a mapping that matches current request from a collection of mappings. + */ +public final class MappingResolver { + private final Set exactMatches; + private final List wildcardMatchers; + private final boolean hasDefault; + + private MappingResolver( + Set exactMatches, List wildcardMatchers, boolean hasDefault) { + this.exactMatches = exactMatches.isEmpty() ? Collections.emptySet() : exactMatches; + this.wildcardMatchers = wildcardMatchers.isEmpty() ? Collections.emptyList() : wildcardMatchers; + this.hasDefault = hasDefault; + } + + public static MappingResolver build(Collection mappings) { + List wildcardMatchers = new ArrayList<>(); + Set exactMatches = new HashSet<>(); + boolean hasDefault = false; + for (String mapping : mappings) { + if (mapping.equals("")) { + exactMatches.add("/"); + } else if (mapping.equals("/") || mapping.equals("/*")) { + hasDefault = true; + } else if (mapping.startsWith("*.") && mapping.length() > 2) { + wildcardMatchers.add(new SuffixMatcher("/" + mapping, mapping.substring(1))); + } else if (mapping.endsWith("/*")) { + wildcardMatchers.add( + new PrefixMatcher(mapping, mapping.substring(0, mapping.length() - 2))); + } else { + exactMatches.add(mapping); + } + } + + // wildfly has empty mappings for default servlet + if (mappings.isEmpty()) { + hasDefault = true; + } + + return new MappingResolver(exactMatches, wildcardMatchers, hasDefault); + } + + /** Find mapping for requested path. */ + public String resolve(String servletPath, String pathInfo) { + if (servletPath == null) { + return null; + } + + // get full path inside context + String path = servletPath; + if (pathInfo != null) { + path += pathInfo; + } + // trim trailing / + if (path.endsWith("/") && !path.equals("/")) { + path = path.substring(0, path.length() - 1); + } + + if (exactMatches.contains(path)) { + return path; + } + + for (WildcardMatcher matcher : wildcardMatchers) { + if (matcher.match(path)) { + String mapping = matcher.getMapping(); + // for jsp return servlet path + if ("/*.jsp".equals(mapping) || "/*.jspx".equals(mapping)) { + return servletPath; + } + return mapping; + } + } + + if (hasDefault) { + return path.equals("/") ? "/" : "/*"; + } + + return null; + } + + private interface WildcardMatcher { + boolean match(String path); + + String getMapping(); + } + + private static class PrefixMatcher implements WildcardMatcher { + private final String mapping; + private final String prefix; + + private PrefixMatcher(String mapping, String prefix) { + this.mapping = mapping; + this.prefix = prefix; + } + + @Override + public boolean match(String path) { + return path.equals(prefix) || path.startsWith(prefix + "/"); + } + + @Override + public String getMapping() { + return mapping; + } + } + + private static class SuffixMatcher implements WildcardMatcher { + private final String mapping; + private final String suffix; + + private SuffixMatcher(String mapping, String suffix) { + this.mapping = mapping; + this.suffix = suffix; + } + + @Override + public boolean match(String path) { + return path.endsWith(suffix); + } + + @Override + public String getMapping() { + return mapping; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/ServerSpanNaming.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/ServerSpanNaming.java new file mode 100644 index 000000000..0ccf9623e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/ServerSpanNaming.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.servlet; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import java.util.function.Supplier; + +/** Helper container for tracking whether instrumentation should update server span name or not. */ +public final class ServerSpanNaming { + + private static final ContextKey CONTEXT_KEY = + ContextKey.named("opentelemetry-servlet-span-naming-key"); + + public static Context init(Context context, Source initialSource) { + ServerSpanNaming serverSpanNaming = context.get(CONTEXT_KEY); + if (serverSpanNaming != null) { + // TODO (trask) does this ever happen? + serverSpanNaming.updatedBySource = initialSource; + return context; + } + return context.with(CONTEXT_KEY, new ServerSpanNaming(initialSource)); + } + + private volatile Source updatedBySource; + // Length of the currently set name. This is used when setting name from a servlet filter + // to pick the most descriptive (longest) name. + private volatile int nameLength; + + private ServerSpanNaming(Source initialSource) { + this.updatedBySource = initialSource; + } + + /** + * If there is a server span in the context, and {@link #init(Context, Source)} has been called to + * populate a {@code ServerSpanName} into the context, then this method will update the server + * span name using the provided {@link Supplier} if and only if the last {@link Source} to update + * the span name using this method has strictly lower priority than the provided {@link Source}, + * and the value returned from the {@link Supplier} is non-null. + * + *

If there is a server span in the context, and {@link #init(Context, Source)} has NOT been + * called to populate a {@code ServerSpanName} into the context, then this method will update the + * server span name using the provided {@link Supplier} if the value returned from it is non-null. + */ + public static void updateServerSpanName( + Context context, Source source, Supplier serverSpanName) { + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + ServerSpanNaming serverSpanNaming = context.get(CONTEXT_KEY); + if (serverSpanNaming == null) { + String name = serverSpanName.get(); + if (name != null && !name.isEmpty()) { + serverSpan.updateName(name); + } + return; + } + // special case for servlet filters, even when we have a name from previous filter see whether + // the new name is better and if so use it instead + boolean onlyIfBetterName = + !source.useFirst && source.order == serverSpanNaming.updatedBySource.order; + if (source.order > serverSpanNaming.updatedBySource.order || onlyIfBetterName) { + String name = serverSpanName.get(); + if (name != null + && !name.isEmpty() + && (!onlyIfBetterName || serverSpanNaming.isBetterName(name))) { + serverSpan.updateName(name); + serverSpanNaming.updatedBySource = source; + serverSpanNaming.nameLength = name.length(); + } + } + } + + private boolean isBetterName(String name) { + return name.length() > nameLength; + } + + // TODO (trask) migrate the one usage (ServletHttpServerTracer) to ServerSpanNaming.init() once we + // migrate to new Instrumenters (see + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2814#discussion_r617351334 + // for the challenge with doing this now in the current Tracer structure, at least without some + // bigger changes, which we want to avoid in the Tracers as they are already deprecated) + @Deprecated + public static void updateSource(Context context, Source source) { + ServerSpanNaming serverSpanNaming = context.get(CONTEXT_KEY); + if (serverSpanNaming != null && source.order > serverSpanNaming.updatedBySource.order) { + serverSpanNaming.updatedBySource = source; + } + } + + public enum Source { + CONTAINER(1), + // for servlet filters we try to find the best name which isn't necessarily from the first + // filter that is called + FILTER(2, /* useFirst= */ false), + SERVLET(3), + CONTROLLER(4); + + private final int order; + private final boolean useFirst; + + Source(int order) { + this(order, /* useFirst= */ true); + } + + Source(int order, boolean useFirst) { + this.order = order; + this.useFirst = useFirst; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/ServletContextPath.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/ServletContextPath.java new file mode 100644 index 000000000..7af5f2cbd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/servlet/ServletContextPath.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.servlet; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; + +/** + * The context key here is used to propagate the servlet context path throughout the request, so + * that routing framework instrumentation that updates the span name with a more specific route can + * prepend the servlet context path in front of that route. + * + *

This needs to be in the instrumentation-api module, instead of injected as a helper class into + * the different modules that need it, in order to make sure that there is only a single instance of + * the context key, since otherwise instrumentation across different class loaders would use + * different context keys and not be able to share the servlet context path. + */ +public final class ServletContextPath { + + // Keeps track of the servlet context path that needs to be prepended to the route when updating + // the span name + public static final ContextKey CONTEXT_KEY = + ContextKey.named("opentelemetry-servlet-context-path-key"); + + public static String prepend(Context context, String spanName) { + String value = context.get(CONTEXT_KEY); + // checking isEmpty just to avoid unnecessary string concat / allocation + if (value != null && !value.isEmpty()) { + return value + spanName; + } else { + return spanName; + } + } + + private ServletContextPath() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/AttributeSetter.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/AttributeSetter.java new file mode 100644 index 000000000..108266a52 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/AttributeSetter.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; + +/** This helper interface allows setting attributes on both {@link Span} and {@link SpanBuilder}. */ +@FunctionalInterface +public interface AttributeSetter { + void setAttribute(AttributeKey key, T value); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/BaseTracer.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/BaseTracer.java new file mode 100644 index 000000000..dd91d7960 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/BaseTracer.java @@ -0,0 +1,284 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.InstrumentationVersion; +import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug; +import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * Base class for all instrumentation specific tracer implementations. + * + *

Tracers should not use {@link Span} directly in their public APIs: ideally all lifecycle + * methods (ex. start/end methods) should return/accept {@link Context}. By convention, {@link + * Context} should be passed to all methods as the first parameter. + * + *

The {@link BaseTracer} offers several {@code startSpan()} utility methods for creating bare + * spans without any attributes. If you want to provide some additional attributes on span start + * please consider writing your own specific {@code startSpan()} method in your tracer. + * + *

A {@link Context} returned by any {@code startSpan()} method will always contain a new + * span. If there is a need to suppress span creation {@link #shouldStartSpan(Context, SpanKind)} + * should be called before {@code startSpan()}. + * + *

When constructing {@link Span}s tracers should set all attributes available during + * construction on a {@link SpanBuilder} instead of a {@link Span}. This way {@code SpanProcessor}s + * are able to see those attributes in the {@code onStart()} method and can freely read/modify them. + */ +public abstract class BaseTracer { + private static final SupportabilityMetrics supportability = SupportabilityMetrics.instance(); + + private final Tracer tracer; + private final ContextPropagators propagators; + + /** + * Instead of using this always pass an OpenTelemetry instance; javaagent tracers should + * explicitly pass {@code GlobalOpenTelemetry.get()} to the constructor. + * + * @deprecated always pass an OpenTelemetry instance. + */ + @Deprecated + protected BaseTracer() { + this(GlobalOpenTelemetry.get()); + } + + protected BaseTracer(OpenTelemetry openTelemetry) { + this.tracer = openTelemetry.getTracer(getInstrumentationName(), getVersion()); + this.propagators = openTelemetry.getPropagators(); + } + + /** + * The name of the instrumentation library, not the name of the instrument*ed* library. The value + * returned by this method should uniquely identify the instrumentation library so that during + * troubleshooting it's possible to pinpoint what tracer produced problematic telemetry. + * + *

In this project we use a convention to encode the version of the instrument*ed* library into + * the instrumentation name, for example {@code io.opentelemetry.javaagent.apache-httpclient-4.0}. + * This way, if there are different instrumentations for different library versions it's easy to + * find out which instrumentations produced the telemetry data. + * + * @see io.opentelemetry.api.trace.TracerProvider#get(String, String) + */ + protected abstract String getInstrumentationName(); + + /** + * The version of the instrumentation library - defaults to the value of JAR manifest attribute + * {@code Implementation-Version}. + */ + protected String getVersion() { + return InstrumentationVersion.VERSION; + } + + /** + * Returns true if a new span of the {@code proposedKind} should be suppressed. + * + *

If the passed {@code context} contains a {@link SpanKind#SERVER} span the instrumentation + * must not create another {@code SERVER} span. The same is true for a {@link SpanKind#CLIENT} + * span: if one {@code CLIENT} span is already present in the passed {@code context} then another + * one must not be started. + * + * @see #withClientSpan(Context, Span) + * @see #withServerSpan(Context, Span) + */ + public final boolean shouldStartSpan(Context context, SpanKind proposedKind) { + boolean suppressed = false; + switch (proposedKind) { + case CLIENT: + suppressed = ClientSpan.exists(context); + break; + case SERVER: + case CONSUMER: + suppressed = ServerSpan.exists(context) || ConsumerSpan.exists(context); + break; + default: + break; + } + if (suppressed) { + supportability.recordSuppressedSpan(proposedKind, getInstrumentationName()); + } + return !suppressed; + } + + /** + * Returns a {@link Context} inheriting from {@code Context.current()} that contains a new span + * with name {@code spanName} and kind {@link SpanKind#INTERNAL}. + */ + public Context startSpan(String spanName) { + return startSpan(spanName, SpanKind.INTERNAL); + } + + /** + * Returns a {@link Context} inheriting from {@code Context.current()} that contains a new span + * with name {@code spanName} and kind {@code kind}. + */ + public Context startSpan(String spanName, SpanKind kind) { + return startSpan(Context.current(), spanName, kind); + } + + /** + * Returns a {@link Context} inheriting from {@code parentContext} that contains a new span with + * name {@code spanName} and kind {@code kind}. + */ + public Context startSpan(Context parentContext, String spanName, SpanKind kind) { + Span span = spanBuilder(parentContext, spanName, kind).startSpan(); + return parentContext.with(span); + } + + /** Returns a {@link SpanBuilder} to create and start a new {@link Span}. */ + protected final SpanBuilder spanBuilder(Context parentContext, String spanName, SpanKind kind) { + return tracer.spanBuilder(spanName).setSpanKind(kind).setParent(parentContext); + } + + /** + * Returns a {@link Context} containing the passed {@code span} marked as the current {@link + * SpanKind#CLIENT} span. + * + * @see #shouldStartSpan(Context, SpanKind) + */ + protected final Context withClientSpan(Context parentContext, Span span) { + return ClientSpan.with(parentContext.with(span), span); + } + + /** + * Returns a {@link Context} containing the passed {@code span} marked as the current {@link + * SpanKind#SERVER} span. + * + * @see #shouldStartSpan(Context, SpanKind) + */ + protected final Context withServerSpan(Context parentContext, Span span) { + return ServerSpan.with(parentContext.with(span), span); + } + + /** + * Returns a {@link Context} containing the passed {@code span} marked as the current {@link + * SpanKind#CONSUMER} span. + * + * @see #shouldStartSpan(Context, SpanKind) + */ + protected final Context withConsumerSpan(Context parentContext, Span span) { + return ConsumerSpan.with(parentContext.with(span), span); + } + + /** Ends the execution of a span stored in the passed {@code context}. */ + public void end(Context context) { + end(context, -1); + } + + /** + * Ends the execution of a span stored in the passed {@code context}. + * + * @param endTimeNanos Explicit nanoseconds timestamp from the epoch. + */ + public void end(Context context, long endTimeNanos) { + Span span = Span.fromContext(context); + if (endTimeNanos > 0) { + span.end(endTimeNanos, TimeUnit.NANOSECONDS); + } else { + span.end(); + } + } + + /** + * Records the {@code throwable} in the span stored in the passed {@code context} and marks the + * end of the span's execution. + * + * @see #onException(Context, Throwable) + * @see #end(Context) + */ + public void endExceptionally(Context context, Throwable throwable) { + endExceptionally(context, throwable, -1); + } + + /** + * Records the {@code throwable} in the span stored in the passed {@code context} and marks the + * end of the span's execution. + * + * @param endTimeNanos Explicit nanoseconds timestamp from the epoch. + * @see #onException(Context, Throwable) + * @see #end(Context, long) + */ + public void endExceptionally(Context context, Throwable throwable, long endTimeNanos) { + onException(context, throwable); + end(context, endTimeNanos); + } + + /** + * Records the {@code throwable} in the span stored in the passed {@code context} and sets the + * span's status to {@link StatusCode#ERROR}. The throwable is unwrapped ({@link + * #unwrapThrowable(Throwable)}) before being added to the span. + */ + public void onException(Context context, Throwable throwable) { + Span span = Span.fromContext(context); + span.setStatus(StatusCode.ERROR); + span.recordException(unwrapThrowable(throwable)); + } + + /** + * Extracts the actual cause by unwrapping passed {@code throwable} from known wrapper exceptions, + * e.g {@link ExecutionException}. + */ + protected Throwable unwrapThrowable(Throwable throwable) { + if (throwable.getCause() != null + && (throwable instanceof ExecutionException + || throwable instanceof CompletionException + || throwable instanceof InvocationTargetException + || throwable instanceof UndeclaredThrowableException)) { + return unwrapThrowable(throwable.getCause()); + } + return throwable; + } + + /** + * Extracts a {@link Context} from {@code carrier} using the propagator embedded in this tracer. + * This method can be used to propagate {@link Context} passed from upstream services. + * + * @see TextMapPropagator#extract(Context, Object, TextMapGetter) + */ + public Context extract(C carrier, TextMapGetter getter) { + ContextPropagationDebug.debugContextLeakIfEnabled(); + + Context parent = Context.current(); + if (Span.fromContextOrNull(parent) != null) { + // A span has leaked from another thread. + // We want either span context extracted from the carrier or invalid one. + // We DO NOT want any span context potentially lingering in the current context. + // We reset to the root context, which may not always be appropriate (e.g., a framework added + // an item to the context before we create a span) but it is safer than removing all the + // possible spans that instrumentation may have added and such frameworks as of now do not + // have leaks. + parent = Context.root(); + } + + return propagators.getTextMapPropagator().extract(parent, carrier, getter); + } + + /** + * Injects {@code context} data into {@code carrier} using the propagator embedded in this tracer. + * This method can be used to propagate passed {@code context} to downstream services. + * + * @see TextMapPropagator#inject(Context, Object, TextMapSetter) + */ + public void inject(Context context, C carrier, TextMapSetter setter) { + propagators.getTextMapPropagator().inject(context, carrier, setter); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ClassNames.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ClassNames.java new file mode 100644 index 000000000..8e3865fb3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ClassNames.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +public final class ClassNames { + + private static final ClassValue simpleNames = + new ClassValue() { + @Override + protected String computeValue(Class type) { + if (!type.isAnonymousClass()) { + return type.getSimpleName(); + } + String className = type.getName(); + if (type.getPackage() != null) { + String pkgName = type.getPackage().getName(); + if (!pkgName.isEmpty()) { + className = className.substring(pkgName.length() + 1); + } + } + return className; + } + }; + + /** + * This method is used to generate a simple name based on a given class reference, e.g. for use in + * span names and span attributes. Anonymous classes are named based on their parent. + */ + public static String simpleName(Class type) { + return simpleNames.get(type); + } + + private ClassNames() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ClientSpan.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ClientSpan.java new file mode 100644 index 000000000..f6f46d2f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ClientSpan.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This class encapsulates the context key for storing the current {@link SpanKind#CLIENT} span in + * the {@link Context}. + */ +public final class ClientSpan { + // Keeps track of the client span in a subtree corresponding to a client request. + private static final ContextKey KEY = + ContextKey.named("opentelemetry-traces-client-span-key"); + + /** Returns true when a {@link SpanKind#CLIENT} span is present in the passed {@code context}. */ + public static boolean exists(Context context) { + return fromContextOrNull(context) != null; + } + + /** + * Returns span of type {@link SpanKind#CLIENT} from the given context or {@code null} if not + * found. + */ + @Nullable + public static Span fromContextOrNull(Context context) { + return context.get(KEY); + } + + public static Context with(Context context, Span clientSpan) { + return context.with(KEY, clientSpan); + } + + private ClientSpan() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ConsumerSpan.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ConsumerSpan.java new file mode 100644 index 000000000..668d9c68d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ConsumerSpan.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This class encapsulates the context key for storing the current {@link SpanKind#CONSUMER} span in + * the {@link Context}. + */ +public final class ConsumerSpan { + // Keeps track of the consumer span for the current trace. + private static final ContextKey KEY = + ContextKey.named("opentelemetry-traces-consumer-span-key"); + + /** + * Returns true when a {@link SpanKind#CONSUMER} span is present in the passed {@code context}. + */ + public static boolean exists(Context context) { + return fromContextOrNull(context) != null; + } + + /** + * Returns span of type {@link SpanKind#CONSUMER} from the given context or {@code null} if not + * found. + */ + @Nullable + public static Span fromContextOrNull(Context context) { + return context.get(KEY); + } + + public static Context with(Context context, Span consumerSpan) { + return context.with(KEY, consumerSpan); + } + + private ConsumerSpan() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/DatabaseClientTracer.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/DatabaseClientTracer.java new file mode 100644 index 000000000..718a642bd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/DatabaseClientTracer.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Base class for implementing Tracers for database clients. + * + * @param type of the database connection. + * @param type of the database statement being executed. + * @param type of the database statement after sanitization. + */ +public abstract class DatabaseClientTracer + extends BaseTracer { + private static final String DB_QUERY = "DB Query"; + + protected final NetPeerAttributes netPeerAttributes; + + protected DatabaseClientTracer(NetPeerAttributes netPeerAttributes) { + this.netPeerAttributes = netPeerAttributes; + } + + protected DatabaseClientTracer(OpenTelemetry openTelemetry, NetPeerAttributes netPeerAttributes) { + super(openTelemetry); + this.netPeerAttributes = netPeerAttributes; + } + + public boolean shouldStartSpan(Context parentContext) { + return shouldStartSpan(parentContext, CLIENT); + } + + public Context startSpan(Context parentContext, CONNECTION connection, STATEMENT statement) { + SANITIZEDSTATEMENT sanitizedStatement = sanitizeStatement(statement); + + SpanBuilder span = + spanBuilder(parentContext, spanName(connection, statement, sanitizedStatement), CLIENT) + .setAttribute(SemanticAttributes.DB_SYSTEM, dbSystem(connection)); + + if (connection != null) { + onConnection(span, connection); + setNetSemanticConvention(span, connection); + } + onStatement(span, connection, statement, sanitizedStatement); + + return withClientSpan(parentContext, span.startSpan()); + } + + protected abstract SANITIZEDSTATEMENT sanitizeStatement(STATEMENT statement); + + protected String spanName( + CONNECTION connection, STATEMENT statement, SANITIZEDSTATEMENT sanitizedStatement) { + return conventionSpanName( + dbName(connection), dbOperation(connection, statement, sanitizedStatement), null); + } + + /** + * A helper method for constructing the span name formatting according to DB semantic conventions: + * {@code

}. + */ + public static String conventionSpanName( + @Nullable String dbName, @Nullable String operation, @Nullable String table) { + return conventionSpanName(dbName, operation, table, DB_QUERY); + } + + /** + * A helper method for constructing the span name formatting according to DB semantic conventions: + * {@code
}. If {@code dbName} and {@code operation} are not + * provided then {@code defaultValue} is returned. + */ + public static String conventionSpanName( + @Nullable String dbName, + @Nullable String operation, + @Nullable String table, + String defaultValue) { + if (operation == null) { + return dbName == null ? defaultValue : dbName; + } + + StringBuilder name = new StringBuilder(operation); + if (dbName != null || table != null) { + name.append(' '); + } + if (dbName != null) { + name.append(dbName); + if (table != null) { + name.append('.'); + } + } + if (table != null) { + name.append(table); + } + return name.toString(); + } + + protected abstract String dbSystem(CONNECTION connection); + + /** This should be called when the connection is being used, not when it's created. */ + protected void onConnection(SpanBuilder span, CONNECTION connection) { + span.setAttribute(SemanticAttributes.DB_USER, dbUser(connection)); + span.setAttribute(SemanticAttributes.DB_NAME, dbName(connection)); + span.setAttribute(SemanticAttributes.DB_CONNECTION_STRING, dbConnectionString(connection)); + } + + protected String dbUser(CONNECTION connection) { + return null; + } + + protected String dbName(CONNECTION connection) { + return null; + } + + @Nullable + protected String dbConnectionString(CONNECTION connection) { + return null; + } + + protected void setNetSemanticConvention(SpanBuilder span, CONNECTION connection) { + netPeerAttributes.setNetPeer(span, peerAddress(connection)); + } + + @Nullable + protected abstract InetSocketAddress peerAddress(CONNECTION connection); + + protected void onStatement( + SpanBuilder span, + CONNECTION connection, + STATEMENT statement, + SANITIZEDSTATEMENT sanitizedStatement) { + span.setAttribute( + SemanticAttributes.DB_STATEMENT, dbStatement(connection, statement, sanitizedStatement)); + span.setAttribute( + SemanticAttributes.DB_OPERATION, dbOperation(connection, statement, sanitizedStatement)); + } + + protected String dbStatement( + CONNECTION connection, STATEMENT statement, SANITIZEDSTATEMENT sanitizedStatement) { + return null; + } + + protected String dbOperation( + CONNECTION connection, STATEMENT statement, SANITIZEDSTATEMENT sanitizedStatement) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpClientTracer.java new file mode 100644 index 000000000..cd5fa8a13 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpClientTracer.java @@ -0,0 +1,229 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.TimeUnit; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class HttpClientTracer extends BaseTracer { + + private static final Logger log = LoggerFactory.getLogger(HttpClientTracer.class); + + public static final String DEFAULT_SPAN_NAME = "HTTP request"; + + protected static final String USER_AGENT = "User-Agent"; + + protected final NetPeerAttributes netPeerAttributes; + + protected HttpClientTracer(NetPeerAttributes netPeerAttributes) { + super(); + this.netPeerAttributes = netPeerAttributes; + } + + protected HttpClientTracer(OpenTelemetry openTelemetry, NetPeerAttributes netPeerAttributes) { + super(openTelemetry); + this.netPeerAttributes = netPeerAttributes; + } + + protected abstract String method(REQUEST request); + + @Nullable + protected abstract URI url(REQUEST request) throws URISyntaxException; + + @Nullable + protected String flavor(REQUEST request) { + // This is de facto standard nowadays, so let us use it, unless overridden + return "1.1"; + } + + @Nullable + protected abstract Integer status(RESPONSE response); + + @Nullable + protected abstract String requestHeader(REQUEST request, String name); + + @Nullable + protected abstract String responseHeader(RESPONSE response, String name); + + protected abstract TextMapSetter getSetter(); + + public boolean shouldStartSpan(Context parentContext) { + return shouldStartSpan(parentContext, CLIENT); + } + + public Context startSpan(Context parentContext, REQUEST request, CARRIER carrier) { + return startSpan(parentContext, request, carrier, -1); + } + + public Context startSpan( + SpanKind kind, Context parentContext, REQUEST request, CARRIER carrier, long startTimeNanos) { + Span span = + internalStartSpan( + kind, parentContext, request, spanNameForRequest(request), startTimeNanos); + Context context = withClientSpan(parentContext, span); + inject(context, carrier); + return context; + } + + public Context startSpan( + Context parentContext, REQUEST request, CARRIER carrier, long startTimeNanos) { + return startSpan(SpanKind.CLIENT, parentContext, request, carrier, startTimeNanos); + } + + protected void inject(Context context, CARRIER carrier) { + TextMapSetter setter = getSetter(); + if (setter == null) { + throw new IllegalStateException( + "getSetter() not defined but calling inject(), either getSetter must be implemented or the scope should be setup manually"); + } + inject(context, carrier, setter); + } + + public void end(Context context, RESPONSE response) { + end(context, response, -1); + } + + public void end(Context context, RESPONSE response, long endTimeNanos) { + Span span = Span.fromContext(context); + onResponse(span, response); + super.end(context, endTimeNanos); + } + + public void endExceptionally(Context context, RESPONSE response, Throwable throwable) { + endExceptionally(context, response, throwable, -1); + } + + public void endExceptionally( + Context context, RESPONSE response, Throwable throwable, long endTimeNanos) { + Span span = Span.fromContext(context); + onResponse(span, response); + super.endExceptionally(context, throwable, endTimeNanos); + } + + // TODO (trask) see if we can reduce the number of end..() variants + // see https://github.com/open-telemetry/opentelemetry-java-instrumentation + // /pull/1893#discussion_r542111699 + public void endMaybeExceptionally( + Context context, RESPONSE response, @Nullable Throwable throwable) { + if (throwable != null) { + endExceptionally(context, throwable); + } else { + end(context, response); + } + } + + private Span internalStartSpan( + SpanKind kind, Context parentContext, REQUEST request, String name, long startTimeNanos) { + SpanBuilder spanBuilder = spanBuilder(parentContext, name, kind); + if (startTimeNanos > 0) { + spanBuilder.setStartTimestamp(startTimeNanos, TimeUnit.NANOSECONDS); + } + onRequest(spanBuilder, request); + return spanBuilder.startSpan(); + } + + protected void onRequest(SpanBuilder spanBuilder, REQUEST request) { + onRequest(spanBuilder::setAttribute, request); + } + + /** + * This method should only be used when the request is not yet available when {@link #startSpan} + * is called. Otherwise {@link #onRequest(SpanBuilder, Object)} should be used. + */ + protected void onRequest(Span span, REQUEST request) { + onRequest(span::setAttribute, request); + } + + private void onRequest(AttributeSetter setter, REQUEST request) { + assert setter != null; + if (request != null) { + setter.setAttribute(SemanticAttributes.NET_TRANSPORT, IP_TCP); + setter.setAttribute(SemanticAttributes.HTTP_METHOD, method(request)); + setter.setAttribute(SemanticAttributes.HTTP_USER_AGENT, requestHeader(request, USER_AGENT)); + + setFlavor(setter, request); + setUrl(setter, request); + } + } + + private void setFlavor(AttributeSetter setter, REQUEST request) { + String flavor = flavor(request); + if (flavor == null) { + return; + } + + String httpProtocolPrefix = "HTTP/"; + if (flavor.startsWith(httpProtocolPrefix)) { + flavor = flavor.substring(httpProtocolPrefix.length()); + } + + setter.setAttribute(SemanticAttributes.HTTP_FLAVOR, flavor); + } + + private void setUrl(AttributeSetter setter, REQUEST request) { + try { + URI url = url(request); + if (url != null) { + netPeerAttributes.setNetPeer(setter, url.getHost(), null, url.getPort()); + final URI sanitized; + if (url.getUserInfo() != null) { + sanitized = + new URI( + url.getScheme(), + null, + url.getHost(), + url.getPort(), + url.getPath(), + url.getQuery(), + url.getFragment()); + } else { + sanitized = url; + } + setter.setAttribute(SemanticAttributes.HTTP_URL, sanitized.toString()); + } + } catch (Exception e) { + log.debug("Error tagging url", e); + } + } + + protected void onResponse(Span span, RESPONSE response) { + assert span != null; + if (response != null) { + Integer status = status(response); + if (status != null) { + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, (long) status); + StatusCode statusCode = HttpStatusConverter.statusFromHttpStatus(status, CLIENT); + if (statusCode != StatusCode.UNSET) { + span.setStatus(statusCode); + } + } + } + } + + protected String spanNameForRequest(REQUEST request) { + if (request == null) { + return DEFAULT_SPAN_NAME; + } + String method = method(request); + return method != null ? "HTTP " + method : DEFAULT_SPAN_NAME; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpServerTracer.java new file mode 100644 index 000000000..afa5438b3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpServerTracer.java @@ -0,0 +1,303 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import static io.opentelemetry.api.trace.SpanKind.SERVER; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; +import org.checkerframework.checker.nullness.qual.Nullable; + +// TODO In search for a better home package + +/** + * Base class for implementing Tracers for HTTP servers. It has 3 types that must be specified by + * subclasses: + * + * @param - The specific type for HTTP requests + * @param - The specific type for HTTP responses + * @param - The specific type of HTTP connection, used to get peer address information + * and HTTP flavor. + * @param - Implementation specific storage type for attaching/getting the server context. + * Use Void if your subclass does not have an implementation specific storage need. + */ +public abstract class HttpServerTracer extends BaseTracer { + + // the class name is part of the attribute name, so that it will be shaded when used in javaagent + // instrumentation, and won't conflict with usage outside javaagent instrumentation + public static final String CONTEXT_ATTRIBUTE = HttpServerTracer.class.getName() + ".Context"; + + protected static final String USER_AGENT = "User-Agent"; + + protected HttpServerTracer() { + super(); + } + + protected HttpServerTracer(OpenTelemetry openTelemetry) { + super(openTelemetry); + } + + public Context startSpan(REQUEST request, CONNECTION connection, STORAGE storage, Method origin) { + String spanName = SpanNames.fromMethod(origin); + return startSpan(request, connection, storage, spanName); + } + + public Context startSpan( + REQUEST request, CONNECTION connection, STORAGE storage, String spanName) { + return startSpan(request, connection, storage, spanName, -1); + } + + public Context startSpan( + REQUEST request, + CONNECTION connection, + @Nullable STORAGE storage, + String spanName, + long startTimestamp) { + + // not checking if inside of nested SERVER span because of concerns about context leaking + // and so always starting with a clean context here + + // also we can't conditionally start a span in this method, because the caller won't know + // whether to call end() or not on the Span in the returned Context + + Context parentContext = extract(request, getGetter()); + SpanBuilder spanBuilder = spanBuilder(parentContext, spanName, SERVER); + + if (startTimestamp >= 0) { + spanBuilder.setStartTimestamp(startTimestamp, TimeUnit.NANOSECONDS); + } + + onConnection(spanBuilder, connection); + onRequest(spanBuilder, request); + onConnectionAndRequest(spanBuilder, connection, request); + + Context context = withServerSpan(parentContext, spanBuilder.startSpan()); + context = customizeContext(context, request); + attachServerContext(context, storage); + + return context; + } + + /** Override in subclass to customize context that is returned by {@code startSpan}. */ + protected Context customizeContext(Context context, REQUEST request) { + return context; + } + + /** + * Convenience method. Delegates to {@link #end(Context, Object, long)}, passing {@code timestamp} + * value of {@code -1}. + */ + // TODO should end methods remove SPAN attribute from request as well? + public void end(Context context, RESPONSE response) { + end(context, response, -1); + } + + // TODO should end methods remove SPAN attribute from request as well? + public void end(Context context, RESPONSE response, long timestamp) { + Span span = Span.fromContext(context); + setStatus(span, responseStatus(response), bussinessStatus(response), bussinessMessage(response)); + end(context, timestamp); + } + + /** + * Convenience method. Delegates to {@link #endExceptionally(Context, Throwable, Object)}, passing + * {@code response} value of {@code null}. + */ + @Override + public void endExceptionally(Context context, Throwable throwable) { + endExceptionally(context, throwable, null); + } + + /** + * Convenience method. Delegates to {@link #endExceptionally(Context, Throwable, Object, long)}, + * passing {@code timestamp} value of {@code -1}. + */ + public void endExceptionally(Context context, Throwable throwable, RESPONSE response) { + endExceptionally(context, throwable, response, -1); + } + + /** + * If {@code response} is {@code null}, the {@code http.status_code} will be set to {@code 500} + * and the {@link Span} status will be set to {@link io.opentelemetry.api.trace.StatusCode#ERROR}. + */ + public void endExceptionally( + Context context, Throwable throwable, RESPONSE response, long timestamp) { + onException(context, throwable); + Span span = Span.fromContext(context); + if (response == null) { + setStatus(span, 500, bussinessStatus(response), bussinessMessage(response)); + } else { + setStatus(span, responseStatus(response), bussinessStatus(response), bussinessMessage(response)); + } + end(context, timestamp); + } + + public Span getServerSpan(STORAGE storage) { + Context attachedContext = getServerContext(storage); + return attachedContext == null ? null : ServerSpan.fromContextOrNull(attachedContext); + } + + /** + * Returns context stored to the given request-response-loop storage by {@link + * #attachServerContext(Context, Object)}. + */ + @Nullable + public abstract Context getServerContext(STORAGE storage); + + protected void onConnection(SpanBuilder spanBuilder, CONNECTION connection) { + // TODO: use NetPeerAttributes here + spanBuilder.setAttribute(SemanticAttributes.NET_PEER_IP, peerHostIP(connection)); + Integer port = peerPort(connection); + // Negative or Zero ports might represent an unset/null value for an int type. Skip setting. + if (port != null && port > 0) { + spanBuilder.setAttribute(SemanticAttributes.NET_PEER_PORT, (long) port); + } + } + + protected void onRequest(SpanBuilder spanBuilder, REQUEST request) { + spanBuilder.setAttribute(SemanticAttributes.HTTP_METHOD, method(request)); + spanBuilder.setAttribute( + SemanticAttributes.HTTP_USER_AGENT, requestHeader(request, USER_AGENT)); + + setUrl(spanBuilder, request); + + // TODO set resource name from URL. + } + + /* + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md + + HTTP semantic convention recommends setting http.scheme, http.host, http.target attributes + instead of http.url because it "is usually not readily available on the server side but would have + to be assembled in a cumbersome and sometimes lossy process from other information". + + But in Java world there is no standard way to access "The full request target as passed in a HTTP request line or equivalent" + which is the recommended value for http.target attribute. Therefore we cannot use any of the + recommended combinations of attributes and are forced to use http.url. + */ + private void setUrl(SpanBuilder spanBuilder, REQUEST request) { + spanBuilder.setAttribute(SemanticAttributes.HTTP_URL, url(request)); + } + + protected void onConnectionAndRequest( + SpanBuilder spanBuilder, CONNECTION connection, REQUEST request) { + String flavor = flavor(connection, request); + if (flavor != null) { + // remove HTTP/ prefix to comply with semantic conventions + if (flavor.startsWith("HTTP/")) { + flavor = flavor.substring("HTTP/".length()); + } + spanBuilder.setAttribute(SemanticAttributes.HTTP_FLAVOR, flavor); + } + spanBuilder.setAttribute(SemanticAttributes.HTTP_CLIENT_IP, clientIP(connection, request)); + } + + private String clientIP(CONNECTION connection, REQUEST request) { + // try Forwarded + String forwarded = requestHeader(request, "Forwarded"); + if (forwarded != null) { + forwarded = extractForwardedFor(forwarded); + if (forwarded != null) { + return forwarded; + } + } + + // try X-Forwarded-For + forwarded = requestHeader(request, "X-Forwarded-For"); + if (forwarded != null) { + // may be split by , + int endIndex = forwarded.indexOf(','); + if (endIndex > 0) { + forwarded = forwarded.substring(0, endIndex); + } + if (!forwarded.isEmpty()) { + return forwarded; + } + } + + // fallback to peer IP if there are no proxy headers + return peerHostIP(connection); + } + + // VisibleForTesting + static String extractForwardedFor(String forwarded) { + int start = forwarded.toLowerCase().indexOf("for="); + if (start < 0) { + return null; + } + start += 4; // start is now the index after for= + if (start >= forwarded.length() - 1) { // the value after for= must not be empty + return null; + } + for (int i = start; i < forwarded.length() - 1; i++) { + char c = forwarded.charAt(i); + if (c == ',' || c == ';') { + if (i == start) { // empty string + return null; + } + return forwarded.substring(start, i); + } + } + return forwarded.substring(start); + } + + private static void setStatus(Span span, int status, String bussinessCode, String message) { + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, (long) status); + StatusCode statusCode = HttpStatusConverter.statusFromHttpStatus(status, SERVER); + if (statusCode != StatusCode.UNSET) { + span.setStatus(statusCode); + return; + } + // check bussinessCode + if(!StringUtils.isNullOrEmpty(bussinessCode) && bussinessCode.startsWith("5")){ + span.setStatus(StatusCode.ERROR); + span.recordException(new HttpServletException("bussiness code: "+bussinessCode+", message: "+message)); + } + } + + @Nullable + protected abstract Integer peerPort(CONNECTION connection); + + @Nullable + protected abstract String peerHostIP(CONNECTION connection); + + protected abstract String flavor(CONNECTION connection, REQUEST request); + + protected abstract TextMapGetter getGetter(); + + protected abstract String url(REQUEST request); + + protected abstract String method(REQUEST request); + + @Nullable + protected abstract String requestHeader(REQUEST request, String name); + + protected abstract int responseStatus(RESPONSE response); + + protected abstract String bussinessStatus(RESPONSE response); + + protected abstract String bussinessMessage(RESPONSE response); + + /** + * Stores given context in the given request-response-loop storage in implementation specific way. + */ + protected abstract void attachServerContext(Context context, STORAGE storage); + + /* + We are making quite simple check by just verifying the presence of schema. + */ + protected boolean isRelativeUrl(String url) { + return !(url.startsWith("http://") || url.startsWith("https://")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpServletException.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpServletException.java new file mode 100644 index 000000000..d1f2f361e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpServletException.java @@ -0,0 +1,7 @@ +package io.opentelemetry.instrumentation.api.tracer; + +public class HttpServletException extends RuntimeException{ + public HttpServletException(String message){ + super(message); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpStatusConverter.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpStatusConverter.java new file mode 100644 index 000000000..b71b814bf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/HttpStatusConverter.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; + +public final class HttpStatusConverter { + + // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status + public static StatusCode statusFromHttpStatus(int httpStatus, SpanKind spanKind) { + if(spanKind.equals(SpanKind.CLIENT)) { + if (httpStatus >= 100 && httpStatus < 400) { + return StatusCode.UNSET; + } + return StatusCode.ERROR; + } + if(spanKind.equals(SpanKind.SERVER)){ + if (httpStatus >= 100 && httpStatus < 500) { + return StatusCode.UNSET; + } + return StatusCode.ERROR; + } + return StatusCode.ERROR; + } + + private HttpStatusConverter() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/RpcClientTracer.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/RpcClientTracer.java new file mode 100644 index 000000000..102742154 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/RpcClientTracer.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import io.opentelemetry.api.OpenTelemetry; + +public abstract class RpcClientTracer extends BaseTracer { + protected RpcClientTracer() {} + + protected RpcClientTracer(OpenTelemetry openTelemetry) { + super(openTelemetry); + } + + protected abstract String getRpcSystem(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/RpcServerTracer.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/RpcServerTracer.java new file mode 100644 index 000000000..e80f942e3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/RpcServerTracer.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.TextMapGetter; + +public abstract class RpcServerTracer extends BaseTracer { + + protected RpcServerTracer() {} + + protected RpcServerTracer(OpenTelemetry openTelemetry) { + super(openTelemetry); + } + + protected abstract TextMapGetter getGetter(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ServerSpan.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ServerSpan.java new file mode 100644 index 000000000..1d19b971c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/ServerSpan.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This class encapsulates the context key for storing the current {@link SpanKind#SERVER} span in + * the {@link Context}. + */ +public final class ServerSpan { + // Keeps track of the server span for the current trace. + private static final ContextKey KEY = + ContextKey.named("opentelemetry-traces-server-span-key"); + + /** Returns true when a {@link SpanKind#SERVER} span is present in the passed {@code context}. */ + public static boolean exists(Context context) { + return fromContextOrNull(context) != null; + } + + /** + * Returns span of type {@link SpanKind#SERVER} from the given context or {@code null} if not + * found. + */ + @Nullable + public static Span fromContextOrNull(Context context) { + return context.get(KEY); + } + + public static Context with(Context context, Span serverSpan) { + return context.with(KEY, serverSpan); + } + + private ServerSpan() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/SpanNames.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/SpanNames.java new file mode 100644 index 000000000..dad2a2bd3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/SpanNames.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import java.lang.reflect.Method; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class SpanNames { + /** + * This method is used to generate a span name based on a method. Anonymous classes are named + * based on their parent. + */ + public static String fromMethod(Method method) { + return fromMethod(method.getDeclaringClass(), method.getName()); + } + + /** + * This method is used to generate a span name based on a method. Anonymous classes are named + * based on their parent. + */ + public static String fromMethod(Class clazz, @Nullable Method method) { + return fromMethod(clazz, method == null ? "" : method.getName()); + } + + /** + * This method is used to generate a span name based on a method. Anonymous classes are named + * based on their parent. + */ + public static String fromMethod(Class cl, String methodName) { + return ClassNames.simpleName(cl) + "." + methodName; + } + + private SpanNames() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/AsyncSpanEndStrategies.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/AsyncSpanEndStrategies.java new file mode 100644 index 000000000..9d7a0dd88 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/AsyncSpanEndStrategies.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer.async; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Registry of {@link AsyncSpanEndStrategy} implementations for tracing the asynchronous operations + * represented by the return type of a traced method. + */ +public class AsyncSpanEndStrategies { + private static final AsyncSpanEndStrategies instance = new AsyncSpanEndStrategies(); + + public static AsyncSpanEndStrategies getInstance() { + return instance; + } + + private final List strategies = new CopyOnWriteArrayList<>(); + + private AsyncSpanEndStrategies() { + strategies.add(Jdk8AsyncSpanEndStrategy.INSTANCE); + } + + public void registerStrategy(AsyncSpanEndStrategy strategy) { + Objects.requireNonNull(strategy); + strategies.add(strategy); + } + + public void unregisterStrategy(AsyncSpanEndStrategy strategy) { + strategies.remove(strategy); + } + + public void unregisterStrategy(Class strategyClass) { + strategies.removeIf(strategy -> strategy.getClass() == strategyClass); + } + + @Nullable + public AsyncSpanEndStrategy resolveStrategy(Class returnType) { + for (AsyncSpanEndStrategy strategy : strategies) { + if (strategy.supports(returnType)) { + return strategy; + } + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/AsyncSpanEndStrategy.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/AsyncSpanEndStrategy.java new file mode 100644 index 000000000..5981da300 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/AsyncSpanEndStrategy.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer.async; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; + +/** + * Represents an implementation of a strategy for composing over the return value of an asynchronous + * traced method which can compose or register for notification of completion at which point the + * span representing the invocation of the method will be ended. + */ +public interface AsyncSpanEndStrategy { + boolean supports(Class returnType); + + /** + * Denotes the end of the invocation of the traced method with a successful result which will end + * the span stored in the passed {@code context}. The span will remain open until the asynchronous + * operation has completed. + * + * @param tracer {@link BaseTracer} tracer to be used to end the span stored in the {@code + * context}. + * @param returnValue Return value from the traced method. Must be an instance of a {@code + * returnType} for which {@link #supports(Class)} returned true (in particular it must not be + * {@code null}). + * @return Either {@code returnValue} or a value composing over {@code returnValue} for + * notification of completion. + */ + Object end(BaseTracer tracer, Context context, Object returnValue); +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/Jdk8AsyncSpanEndStrategy.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/Jdk8AsyncSpanEndStrategy.java new file mode 100644 index 000000000..e0af4b7d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/Jdk8AsyncSpanEndStrategy.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer.async; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +enum Jdk8AsyncSpanEndStrategy implements AsyncSpanEndStrategy { + INSTANCE; + + @Override + public boolean supports(Class returnType) { + return returnType == CompletionStage.class || returnType == CompletableFuture.class; + } + + @Override + public Object end(BaseTracer tracer, Context context, Object returnValue) { + if (returnValue instanceof CompletableFuture) { + CompletableFuture future = (CompletableFuture) returnValue; + if (endSynchronously(future, tracer, context)) { + return future; + } + return endWhenComplete(future, tracer, context); + } + CompletionStage stage = (CompletionStage) returnValue; + return endWhenComplete(stage, tracer, context); + } + + /** + * Checks to see if the {@link CompletableFuture} has already been completed and if so + * synchronously ends the span to avoid additional allocations and overhead registering for + * notification of completion. + */ + private static boolean endSynchronously( + CompletableFuture future, BaseTracer tracer, Context context) { + + if (!future.isDone()) { + return false; + } + + if (future.isCompletedExceptionally()) { + // If the future completed exceptionally then join to catch the exception + // so that it can be recorded to the span + try { + future.join(); + } catch (Throwable t) { + tracer.endExceptionally(context, t); + return true; + } + } + tracer.end(context); + return true; + } + + /** + * Registers for notification of the completion of the {@link CompletionStage} at which time the + * span will be ended. + */ + private CompletionStage endWhenComplete( + CompletionStage stage, BaseTracer tracer, Context context) { + return stage.whenComplete( + (result, exception) -> { + if (exception != null) { + tracer.endExceptionally(context, exception); + } else { + tracer.end(context); + } + }); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/package-info.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/package-info.java new file mode 100644 index 000000000..e48560a54 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/async/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides implementations of strategies for tracing methods that return asynchronous and reactive + * values so that the span can be ended when the asynchronous operation completes. + */ +package io.opentelemetry.instrumentation.api.tracer.async; diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/net/NetPeerAttributes.java b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/net/NetPeerAttributes.java new file mode 100644 index 000000000..6ae40a6cb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/tracer/net/NetPeerAttributes.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer.net; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.AttributeSetter; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class NetPeerAttributes { + + // TODO: this should only be used by the javaagent; move to javaagent-api after removing all + // library usages + public static final NetPeerAttributes INSTANCE = + new NetPeerAttributes( + Config.get().getMap("otel.instrumentation.common.peer-service-mapping")); + + private final Map peerServiceMapping; + + public NetPeerAttributes() { + this(Collections.emptyMap()); + } + + public NetPeerAttributes(Map peerServiceMapping) { + this.peerServiceMapping = peerServiceMapping; + } + + public void setNetPeer(Span span, @Nullable InetSocketAddress remoteConnection) { + setNetPeer(span::setAttribute, remoteConnection); + } + + public void setNetPeer(SpanBuilder span, @Nullable InetSocketAddress remoteConnection) { + setNetPeer(span::setAttribute, remoteConnection); + } + + public void setNetPeer(AttributeSetter span, @Nullable InetSocketAddress remoteConnection) { + if (remoteConnection != null) { + InetAddress remoteAddress = remoteConnection.getAddress(); + if (remoteAddress != null) { + setNetPeer( + span, + remoteAddress.getHostName(), + remoteAddress.getHostAddress(), + remoteConnection.getPort()); + } else { + // Failed DNS lookup, the host string is the name. + setNetPeer(span, remoteConnection.getHostString(), null, remoteConnection.getPort()); + } + } + } + + public void setNetPeer(SpanBuilder span, InetAddress remoteAddress, int port) { + setNetPeer( + span::setAttribute, remoteAddress.getHostName(), remoteAddress.getHostAddress(), port); + } + + public void setNetPeer(Span span, String peerName, String peerIp) { + setNetPeer(span::setAttribute, peerName, peerIp, -1); + } + + public void setNetPeer(Span span, String peerName, String peerIp, int port) { + setNetPeer(span::setAttribute, peerName, peerIp, port); + } + + public void setNetPeer( + AttributeSetter span, @Nullable String peerName, @Nullable String peerIp, int port) { + if (peerName != null && !peerName.equals(peerIp)) { + span.setAttribute(SemanticAttributes.NET_PEER_NAME, peerName); + } + if (peerIp != null) { + span.setAttribute(SemanticAttributes.NET_PEER_IP, peerIp); + } + + String peerService = mapToPeerService(peerName); + if (peerService == null) { + peerService = mapToPeerService(peerIp); + } + if (peerService != null) { + span.setAttribute(SemanticAttributes.PEER_SERVICE, peerService); + } + if (port > 0) { + span.setAttribute(SemanticAttributes.NET_PEER_PORT, (long) port); + } + } + + private String mapToPeerService(String endpoint) { + if (endpoint == null) { + return null; + } + + return peerServiceMapping.get(endpoint); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/main/jflex/SqlSanitizer.flex b/opentelemetry-java-instrumentation/instrumentation-api/src/main/jflex/SqlSanitizer.flex new file mode 100644 index 000000000..759659107 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/main/jflex/SqlSanitizer.flex @@ -0,0 +1,377 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.db; + +%% + +%final +%class AutoSqlSanitizer +%apiprivate +%int +%buffer 2048 + +%unicode +%ignorecase + +COMMA = "," +OPEN_PAREN = "(" +CLOSE_PAREN = ")" +OPEN_COMMENT = "/*" +CLOSE_COMMENT = "*/" +IDENTIFIER = ([:letter:] | "_") ([:letter:] | [0-9] | [_.])* +BASIC_NUM = [.+-]* [0-9] ([0-9] | [eE.+-])* +HEX_NUM = "0x" ([a-f] | [A-F] | [0-9])+ +QUOTED_STR = "'" ("''" | [^'])* "'" +DOUBLE_QUOTED_STR = "\"" ("\"\"" | [^\"])* "\"" +DOLLAR_QUOTED_STR = "$$" [^$]* "$$" +WHITESPACE = [ \t\r\n]+ + +%{ + static SqlStatementInfo sanitize(String statement) { + AutoSqlSanitizer sanitizer = new AutoSqlSanitizer(new java.io.StringReader(statement)); + try { + while (!sanitizer.yyatEOF()) { + int token = sanitizer.yylex(); + // YYEOF token may be used to stop processing + if (token == YYEOF) { + break; + } + } + return sanitizer.getResult(); + } catch (java.io.IOException e) { + // should never happen + return SqlStatementInfo.create(null, null, null); + } + } + + // max length of the sanitized statement - SQLs longer than this will be trimmed + private static final int LIMIT = 32 * 1024; + + private final StringBuilder builder = new StringBuilder(); + + private void appendCurrentFragment() { + builder.append(zzBuffer, zzStartRead, zzMarkedPos - zzStartRead); + } + + private boolean isOverLimit() { + return builder.length() > LIMIT; + } + + // you can reference a table in the FROM clause in one of the following ways: + // table + // table t + // table as t + // in other words, you need max 3 identifiers to reference a table + private static final int FROM_TABLE_REF_MAX_IDENTIFIERS = 3; + + private int parenLevel = 0; + private boolean insideComment = false; + private Operation operation = NoOp.INSTANCE; + private boolean extractionDone = false; + + private void setOperation(Operation operation) { + if (this.operation == NoOp.INSTANCE) { + this.operation = operation; + } + } + + private static abstract class Operation { + String mainTable = null; + + /** @return true if all statement info is gathered */ + boolean handleFrom() { + return false; + } + + /** @return true if all statement info is gathered */ + boolean handleInto() { + return false; + } + + /** @return true if all statement info is gathered */ + boolean handleJoin() { + return false; + } + + /** @return true if all statement info is gathered */ + boolean handleIdentifier() { + return false; + } + + /** @return true if all statement info is gathered */ + boolean handleComma() { + return false; + } + + SqlStatementInfo getResult(String fullStatement) { + return SqlStatementInfo.create(fullStatement, getClass().getSimpleName().toUpperCase(java.util.Locale.ROOT), mainTable); + } + } + + private static class NoOp extends Operation { + static final Operation INSTANCE = new NoOp(); + + SqlStatementInfo getResult(String fullStatement) { + return SqlStatementInfo.create(fullStatement, null, null); + } + } + + private class Select extends Operation { + // you can reference a table in the FROM clause in one of the following ways: + // table + // table t + // table as t + // in other words, you need max 3 identifiers to reference a table + private static final int FROM_TABLE_REF_MAX_IDENTIFIERS = 3; + + boolean expectingTableName = false; + boolean mainTableSetAlready = false; + int identifiersAfterMainFromClause = 0; + + boolean handleFrom() { + if (parenLevel == 0) { + // main query FROM clause + expectingTableName = true; + return false; + } + + // subquery in WITH or SELECT clause, before main FROM clause; skipping + mainTable = null; + return true; + } + + boolean handleJoin() { + // for SELECT statements with joined tables there's no main table + mainTable = null; + return true; + } + + boolean handleIdentifier() { + if (identifiersAfterMainFromClause > 0) { + ++identifiersAfterMainFromClause; + } + + if (!expectingTableName) { + return false; + } + + // SELECT FROM (subquery) case + if (parenLevel != 0) { + mainTable = null; + return true; + } + + // whenever >1 table is used there is no main table (e.g. unions) + if (mainTableSetAlready) { + mainTable = null; + return true; + } + + mainTable = yytext(); + mainTableSetAlready = true; + expectingTableName = false; + // start counting identifiers after encountering main from clause + identifiersAfterMainFromClause = 1; + + // continue scanning the query, there may be more than one table (e.g. joins) + return false; + } + + boolean handleComma() { + // comma was encountered in the FROM clause, i.e. implicit join + // (if less than 3 identifiers have appeared before first comma then it means that it's a table list; + // any other list that can appear later needs at least 4 idents) + if (identifiersAfterMainFromClause > 0 + && identifiersAfterMainFromClause <= FROM_TABLE_REF_MAX_IDENTIFIERS) { + mainTable = null; + return true; + } + return false; + } + } + + private class Insert extends Operation { + boolean expectingTableName = false; + + boolean handleInto() { + expectingTableName = true; + return false; + } + + boolean handleIdentifier() { + if (!expectingTableName) { + return false; + } + + mainTable = yytext(); + return true; + } + } + + private class Delete extends Operation { + boolean expectingTableName = false; + + boolean handleFrom() { + expectingTableName = true; + return false; + } + + boolean handleIdentifier() { + if (!expectingTableName) { + return false; + } + + mainTable = yytext(); + return true; + } + } + + private class Update extends Operation { + boolean handleIdentifier() { + mainTable = yytext(); + return true; + } + } + + private class Merge extends Operation { + boolean handleIdentifier() { + mainTable = yytext(); + return true; + } + } + + private SqlStatementInfo getResult() { + if (builder.length() > LIMIT) { + builder.delete(LIMIT, builder.length()); + } + String fullStatement = builder.toString(); + return operation.getResult(fullStatement); + } + +%} + +%% + + { + + "SELECT" { + if (!insideComment) { + setOperation(new Select()); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "INSERT" { + if (!insideComment) { + setOperation(new Insert()); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "DELETE" { + if (!insideComment) { + setOperation(new Delete()); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "UPDATE" { + if (!insideComment) { + setOperation(new Update()); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "MERGE" { + if (!insideComment) { + setOperation(new Merge()); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + + "FROM" { + if (!insideComment && !extractionDone) { + if (operation == NoOp.INSTANCE) { + // hql/jpql queries may skip SELECT and start with FROM clause + // treat such queries as SELECT queries + setOperation(new Select()); + } + extractionDone = operation.handleFrom(); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "INTO" { + if (!insideComment && !extractionDone) { + extractionDone = operation.handleInto(); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "JOIN" { + if (!insideComment && !extractionDone) { + extractionDone = operation.handleJoin(); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + {COMMA} { + if (!insideComment && !extractionDone) { + extractionDone = operation.handleComma(); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + {IDENTIFIER} { + if (!insideComment && !extractionDone) { + extractionDone = operation.handleIdentifier(); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + + {OPEN_PAREN} { + if (!insideComment) { + parenLevel += 1; + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + {CLOSE_PAREN} { + if (!insideComment) { + parenLevel -= 1; + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + + {OPEN_COMMENT} { + insideComment = true; + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + {CLOSE_COMMENT} { + insideComment = false; + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + + // here is where the actual sanitization happens + {BASIC_NUM} | {HEX_NUM} | {QUOTED_STR} | {DOUBLE_QUOTED_STR} | {DOLLAR_QUOTED_STR} { + builder.append('?'); + if (isOverLimit()) return YYEOF; + } + + {WHITESPACE} { + builder.append(' '); + if (isOverLimit()) return YYEOF; + } + [^] { + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/config/ConfigTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/config/ConfigTest.groovy new file mode 100644 index 000000000..b8899e668 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/config/ConfigTest.groovy @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.config + +import spock.lang.Specification + +class ConfigTest extends Specification { + def "verify instrumentation config"() { + setup: + def config = new ConfigBuilder().readProperties([ + "otel.instrumentation.order.enabled" : "true", + "otel.instrumentation.test-prop.enabled" : "true", + "otel.instrumentation.disabled-prop.enabled": "false", + "otel.instrumentation.test-env.enabled" : "true", + "otel.instrumentation.disabled-env.enabled" : "false" + ]).build() + + expect: + config.isInstrumentationEnabled(instrumentationNames, defaultEnabled) == expected + + where: + names | defaultEnabled | expected + [] | true | true + [] | false | false + ["invalid"] | true | true + ["invalid"] | false | false + ["test-prop"] | false | true + ["test-env"] | false | true + ["disabled-prop"] | true | false + ["disabled-env"] | true | false + ["other", "test-prop"] | false | true + ["other", "test-env"] | false | true + ["order"] | false | true + ["test-prop", "disabled-prop"] | false | true + ["disabled-env", "test-env"] | false | true + ["test-prop", "disabled-prop"] | true | false + ["disabled-env", "test-env"] | true | false + + instrumentationNames = new TreeSet(names) + } + + def "should get string property"() { + given: + def config = new ConfigBuilder().readProperties([ + "property.string": "whatever" + ]).build() + + expect: + config.getProperty("property.string") == "whatever" + config.getProperty("property.string", "default") == "whatever" + config.getProperty("does-not-exist") == null + config.getProperty("does-not-exist", "default") == "default" + } + + def "should get boolean property"() { + given: + def config = new ConfigBuilder().readProperties([ + "property.bool": "false" + ]).build() + + expect: + !config.getBoolean("property.bool", true) + config.getBoolean("does-not-exist", true) + } + + def "should get list property"() { + given: + def config = new ConfigBuilder().readProperties([ + "property.list": "one, two, three" + ]).build() + + expect: + config.getList("property.list") == ["one", "two", "three"] + config.getList("property.list", ["four"]) == ["one", "two", "three"] + config.getList("does-not-exist") == [] + config.getList("does-not-exist", ["four"]) == ["four"] + } + + def "should get map property"() { + given: + def config = new ConfigBuilder().readProperties([ + "property.map": "one=1, two=2" + ]).build() + + expect: + config.getMapProperty("property.map") == ["one": "1", "two": "2"] + config.getMapProperty("does-not-exist").isEmpty() + } + + def "should return empty map when map property value is invalid"() { + given: + def config = new ConfigBuilder().readProperties([ + "property.map": "one=1, broken!" + ]).build() + + expect: + config.getMapProperty("property.map").isEmpty() + } + + def "should expose if agent debug is enabled"() { + given: + def config = new ConfigBuilder().readProperties([ + "otel.javaagent.debug": value + ]).build() + + expect: + config.isAgentDebugEnabled() == result + + where: + value | result + "true" | true + "blather" | false + null | false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizerTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizerTest.groovy new file mode 100644 index 000000000..9f110e1a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizerTest.groovy @@ -0,0 +1,148 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.db + +import spock.lang.Specification +import spock.lang.Unroll + +class RedisCommandSanitizerTest extends Specification { + @Unroll + def "should sanitize #expected"() { + when: + def sanitized = RedisCommandSanitizer.sanitize(command, args) + + then: + sanitized == expected + + where: + command | args | expected + // Connection + "AUTH" | ["password"] | "AUTH ?" + "HELLO" | ["3", "AUTH", "username", "password"] | "HELLO 3 AUTH ? ?" + "HELLO" | ["3"] | "HELLO 3" + // Hashes + "HMSET" | ["hash", "key1", "value1", "key2", "value2"] | "HMSET hash key1 ? key2 ?" + "HSET" | ["hash", "key1", "value1", "key2", "value2"] | "HSET hash key1 ? key2 ?" + "HSETNX" | ["hash", "key", "value"] | "HSETNX hash key ?" + // HyperLogLog + "PFADD" | ["hll", "a", "b", "c"] | "PFADD hll ? ? ?" + // Keys + "MIGRATE" | ["127.0.0.1", "4242", "key", "0", "5000", "AUTH", "password"] | "MIGRATE 127.0.0.1 4242 key 0 5000 AUTH ?" + "RESTORE" | ["key", "42", "value"] | "RESTORE key 42 ?" + // Lists + "LINSERT" | ["list", "BEFORE", "value1", "value2"] | "LINSERT list BEFORE ? ?" + "LPOS" | ["list", "value"] | "LPOS list ?" + "LPUSH" | ["list", "value1", "value2"] | "LPUSH list ? ?" + "LPUSHX" | ["list", "value1", "value2"] | "LPUSHX list ? ?" + "LREM" | ["list", "2", "value"] | "LREM list ? ?" + "LSET" | ["list", "2", "value"] | "LSET list ? ?" + "RPUSH" | ["list", "value1", "value2"] | "RPUSH list ? ?" + "RPUSHX" | ["list", "value1", "value2"] | "RPUSHX list ? ?" + // Pub/Sub + "PUBLISH" | ["channel", "message"] | "PUBLISH channel ?" + // Scripting + "EVAL" | ["script", "2", "key1", "key2", "value"] | "EVAL script 2 key1 key2 ?" + "EVALSHA" | ["sha1", "0", "value1", "value2"] | "EVALSHA sha1 0 ? ?" + // Sets + "SADD" | ["set", "value1", "value2"] | "SADD set ? ?" + "SISMEMBER" | ["set", "value"] | "SISMEMBER set ?" + "SMISMEMBER" | ["set", "value1", "value2"] | "SMISMEMBER set ? ?" + "SMOVE" | ["set1", "set2", "value"] | "SMOVE set1 set2 ?" + "SREM" | ["set", "value1", "value2"] | "SREM set ? ?" + // Server + "CONFIG" | ["SET", "masterpassword", "password"] | "CONFIG SET masterpassword ?" + // Sorted Sets + "ZADD" | ["sset", "1", "value1", "2", "value2"] | "ZADD sset ? ? ? ?" + "ZCOUNT" | ["sset", "1", "10"] | "ZCOUNT sset ? ?" + "ZINCRBY" | ["sset", "1", "value"] | "ZINCRBY sset ? ?" + "ZLEXCOUNT" | ["sset", "1", "10"] | "ZLEXCOUNT sset ? ?" + "ZMSCORE" | ["sset", "value1", "value2"] | "ZMSCORE sset ? ?" + "ZRANGEBYLEX" | ["sset", "1", "10"] | "ZRANGEBYLEX sset ? ?" + "ZRANGEBYSCORE" | ["sset", "1", "10"] | "ZRANGEBYSCORE sset ? ?" + "ZRANK" | ["sset", "value"] | "ZRANK sset ?" + "ZREM" | ["sset", "value1", "value2"] | "ZREM sset ? ?" + "ZREMRANGEBYLEX" | ["sset", "1", "10"] | "ZREMRANGEBYLEX sset ? ?" + "ZREMRANGEBYSCORE" | ["sset", "1", "10"] | "ZREMRANGEBYSCORE sset ? ?" + "ZREVRANGEBYLEX" | ["sset", "1", "10"] | "ZREVRANGEBYLEX sset ? ?" + "ZREVRANGEBYSCORE" | ["sset", "1", "10"] | "ZREVRANGEBYSCORE sset ? ?" + "ZREVRANK" | ["sset", "value"] | "ZREVRANK sset ?" + "ZSCORE" | ["sset", "value"] | "ZSCORE sset ?" + // Streams + "XADD" | ["stream", "*", "key1", "value1", "key2", "value2"] | "XADD stream * key1 ? key2 ?" + // Strings + "APPEND" | ["key", "value"] | "APPEND key ?" + "GETSET" | ["key", "value"] | "GETSET key ?" + "MSET" | ["key1", "value1", "key2", "value2"] | "MSET key1 ? key2 ?" + "MSETNX" | ["key1", "value1", "key2", "value2"] | "MSETNX key1 ? key2 ?" + "PSETEX" | ["key", "10000", "value"] | "PSETEX key 10000 ?" + "SET" | ["key", "value"] | "SET key ?" + "SETEX" | ["key", "10", "value"] | "SETEX key 10 ?" + "SETNX" | ["key", "value"] | "SETNX key ?" + "SETRANGE" | ["key", "42", "value"] | "SETRANGE key ? ?" + } + + @Unroll + def "should keep all arguments of #command"() { + given: + def args = ["arg1", "arg 2"] + + when: + def sanitized = RedisCommandSanitizer.sanitize(command, args) + + then: + sanitized == command + " " + args.join(" ") + + where: + command << [ + // Cluster + "CLUSTER", "READONLY", "READWRITE", + // Connection + "CLIENT", "ECHO", "PING", "QUIT", "SELECT", + // Geo + "GEOADD", "GEODIST", "GEOHASH", "GEOPOS", "GEORADIUS", "GEORADIUSBYMEMBER", + // Hashes + "HDEL", "HEXISTS", "HGET", "HGETALL", "HINCRBY", "HINCRBYFLOAT", "HKEYS", "HLEN", "HMGET", + "HSCAN", "HSTRLEN", "HVALS", + // HyperLogLog + "PFCOUNT", "PFMERGE", + // Keys + "DEL", "DUMP", "EXISTS", "EXPIRE", "EXPIREAT", "KEYS", "MOVE", "OBJECT", "PERSIST", "PEXPIRE", + "PEXPIREAT", "PTTL", "RANDOMKEY", "RENAME", "RENAMENX", "RESTORE", "SCAN", "SORT", "TOUCH", + "TTL", "TYPE", "UNLINK", "WAIT", + // Lists + "BLMOVE", "BLPOP", "BRPOP", "BRPOPLPUSH", "LINDEX", "LLEN", "LMOVE", "LPOP", "LRANGE", + "LTRIM", "RPOP", "RPOPLPUSH", + // Pub/Sub + "PSUBSCRIBE", "PUBSUB", "PUNSUBSCRIBE", "SUBSCRIBE", "UNSUBSCRIBE", + // Server + "ACL", "BGREWRITEAOF", "BGSAVE", "COMMAND", "DBSIZE", "DEBUG", "FLUSHALL", "FLUSHDB", "INFO", + "LASTSAVE", "LATENCY", "LOLWUT", "MEMORY", "MODULE", "MONITOR", "PSYNC", "REPLICAOF", "ROLE", + "SAVE", "SHUTDOWN", "SLAVEOF", "SLOWLOG", "SWAPDB", "SYNC", "TIME", + // Sets + "SCARD", "SDIFF", "SDIFFSTORE", "SINTER", "SINTERSTORE", "SMEMBERS", "SPOP", "SRANDMEMBER", + "SSCAN", "SUNION", "SUNIONSTORE", + // Sorted Sets + "BZPOPMAX", "BZPOPMIN", "ZCARD", "ZINTER", "ZINTERSTORE", "ZPOPMAX", "ZPOPMIN", "ZRANGE", + "ZREMRANGEBYRANK", "ZREVRANGE", "ZSCAN", "ZUNION", "ZUNIONSTORE", + // Streams + "XACK", "XCLAIM", "XDEL", "XGROUP", "XINFO", "XLEN", "XPENDING", "XRANGE", "XREAD", + "XREADGROUP", "XREVRANGE", "XTRIM", + // Strings + "BITCOUNT", "BITFIELD", "BITOP", "BITPOS", "DECR", "DECRBY", "GET", "GETBIT", "GETRANGE", + "INCR", "INCRBY", "INCRBYFLOAT", "MGET", "SETBIT", "STRALGO", "STRLEN", + // Transactions + "DISCARD", "EXEC", "MULTI", "UNWATCH", "WATCH" + ] + } + + def "should mask all arguments of an unknown command"() { + when: + def sanitized = RedisCommandSanitizer.sanitize("NEWAUTH", ["password", "secret"]) + + then: + sanitized == "NEWAUTH ? ?" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizerTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizerTest.groovy new file mode 100644 index 000000000..991198e44 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizerTest.groovy @@ -0,0 +1,204 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.db + +import spock.lang.Specification +import spock.lang.Unroll + +class SqlStatementSanitizerTest extends Specification { + + def "normalize #originalSql"() { + setup: + def actualSanitized = SqlStatementSanitizer.sanitize(originalSql) + + expect: + actualSanitized.getFullStatement() == sanitizedSql + + where: + originalSql | sanitizedSql + // Numbers + "SELECT * FROM TABLE WHERE FIELD=1234" | "SELECT * FROM TABLE WHERE FIELD=?" + "SELECT * FROM TABLE WHERE FIELD = 1234" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD>=-1234" | "SELECT * FROM TABLE WHERE FIELD>=?" + "SELECT * FROM TABLE WHERE FIELD<-1234" | "SELECT * FROM TABLE WHERE FIELD7" | "SELECT FIELD2 FROM TABLE_123 WHERE X<>?" + + // Semi-nonsensical almost-numbers to elide or not + "SELECT --83--...--8e+76e3E-1" | "SELECT ?" + "SELECT DEADBEEF" | "SELECT DEADBEEF" + "SELECT 123-45-6789" | "SELECT ?" + "SELECT 1/2/34" | "SELECT ?/?/?" + + // Basic ' strings + "SELECT * FROM TABLE WHERE FIELD = ''" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = 'words and spaces'" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = ' an escaped '' quote mark inside'" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = '\\\\'" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = '\"inside doubles\"'" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = '\"\$\$\$\$\"'" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = 'a single \" doublequote inside'" | "SELECT * FROM TABLE WHERE FIELD = ?" + + // Some databases support/encourage " instead of ' with same escape rules + "SELECT * FROM TABLE WHERE FIELD = \"\"" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \"words and spaces'\"" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \" an escaped \"\" quote mark inside\"" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \"\\\\\"" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \"'inside singles'\"" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \"'\$\$\$\$'\"" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \"a single ' singlequote inside\"" | "SELECT * FROM TABLE WHERE FIELD = ?" + + // Some databases allow using dollar-quoted strings + "SELECT * FROM TABLE WHERE FIELD = \$\$\$\$" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \$\$words and spaces\$\$" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \$\$quotes '\" inside\$\$" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \$\$\"''\"\$\$" | "SELECT * FROM TABLE WHERE FIELD = ?" + "SELECT * FROM TABLE WHERE FIELD = \$\$\\\\\$\$" | "SELECT * FROM TABLE WHERE FIELD = ?" + + // Unicode, including a unicode identifier with a trailing number + "SELECT * FROM TABLE\u09137 WHERE FIELD = '\u0194'" | "SELECT * FROM TABLE\u09137 WHERE FIELD = ?" + + // whitespace normalization + "SELECT * \t\r\nFROM TABLE WHERE FIELD1 = 12344 AND FIELD2 = 5678" | "SELECT * FROM TABLE WHERE FIELD1 = ? AND FIELD2 = ?" + + // hibernate/jpa query language + "FROM TABLE WHERE FIELD=1234" | "FROM TABLE WHERE FIELD=?" + } + + @Unroll + def "should simplify #sql"() { + expect: + SqlStatementSanitizer.sanitize(sql) == expected + + where: + sql | expected + // Select + 'SELECT x, y, z FROM schema.table' | SqlStatementInfo.create(sql, 'SELECT', 'schema.table') + 'WITH subquery as (select a from b) SELECT x, y, z FROM table' | SqlStatementInfo.create(sql, 'SELECT', null) + 'SELECT x, y, (select a from b) as z FROM table' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select delete, insert into, merge, update from table' | SqlStatementInfo.create(sql, 'SELECT', 'table') + 'select col /* from table2 */ from table' | SqlStatementInfo.create(sql, 'SELECT', 'table') + 'select col from table join anotherTable' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select col from (select * from anotherTable)' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select col from (select * from anotherTable) alias' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select col from table1 union select col from table2' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select col from table where col in (select * from anotherTable)' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select col from table1, table2' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select col from table1 t1, table2 t2' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select col from table1 as t1, table2 as t2' | SqlStatementInfo.create(sql, 'SELECT', null) + 'select col from table where col in (1, 2, 3)' | SqlStatementInfo.create('select col from table where col in (?, ?, ?)', 'SELECT', 'table') + 'select col from table order by col, col2' | SqlStatementInfo.create(sql, 'SELECT', 'table') + 'select ąś∂ń© from źćļńĶ order by col, col2' | SqlStatementInfo.create(sql, 'SELECT', 'źćļńĶ') + 'select 12345678' | SqlStatementInfo.create('select ?', 'SELECT', null) + '/* update comment */ select * from table1' | SqlStatementInfo.create(sql, 'SELECT', 'table1') + 'select /*((*/abc from table' | SqlStatementInfo.create(sql, 'SELECT', 'table') + 'SeLeCT * FrOm TAblE' | SqlStatementInfo.create(sql, 'SELECT', 'TAblE') + // hibernate/jpa + 'FROM schema.table' | SqlStatementInfo.create(sql, 'SELECT', 'schema.table') + '/* update comment */ from table1' | SqlStatementInfo.create(sql, 'SELECT', 'table1') + // Insert + ' insert into table where lalala' | SqlStatementInfo.create(sql, 'INSERT', 'table') + 'insert insert into table where lalala' | SqlStatementInfo.create(sql, 'INSERT', 'table') + 'insert into db.table where lalala' | SqlStatementInfo.create(sql, 'INSERT', 'db.table') + 'insert without i-n-t-o' | SqlStatementInfo.create(sql, 'INSERT', null) + // Delete + 'delete from table where something something' | SqlStatementInfo.create(sql, 'DELETE', 'table') + 'delete from 12345678' | SqlStatementInfo.create('delete from ?', 'DELETE', null) + 'delete (((' | SqlStatementInfo.create('delete (((', 'DELETE', null) + // Update + 'update table set answer=42' | SqlStatementInfo.create('update table set answer=?', 'UPDATE', 'table') + 'update /*table' | SqlStatementInfo.create(sql, 'UPDATE', null) + // Merge + 'merge into table' | SqlStatementInfo.create(sql, 'MERGE', 'table') + 'merge table (into is optional in some dbs)' | SqlStatementInfo.create(sql, 'MERGE', 'table') + 'merge (into )))' | SqlStatementInfo.create(sql, 'MERGE', null) + // Unknown operation + 'and now for something completely different' | SqlStatementInfo.create(sql, null, null) + '' | SqlStatementInfo.create(sql, null, null) + null | SqlStatementInfo.create(sql, null, null) + } + + def "very long SELECT statements don't cause problems"() { + given: + def sb = new StringBuilder("SELECT * FROM table WHERE") + for (int i = 0; i < 2000; i++) { + sb.append(" column").append(i).append("=123 and") + } + def query = sb.toString() + + expect: + def sanitizedQuery = query.replace('=123', '=?').substring(0, AutoSqlSanitizer.LIMIT) + SqlStatementSanitizer.sanitize(query) == SqlStatementInfo.create(sanitizedQuery, "SELECT", "table") + } + + def "lots and lots of ticks don't cause stack overflow or long runtimes"() { + setup: + String s = "'" + for (int i = 0; i < 10000; i++) { + assert SqlStatementSanitizer.sanitize(s) != null + s += "'" + } + } + + def "very long numbers don't cause a problem"() { + setup: + String s = "" + for (int i = 0; i < 10000; i++) { + s += String.valueOf(i) + } + assert "?" == SqlStatementSanitizer.sanitize(s).getFullStatement() + } + + def "very long numbers at end of table name don't cause problem"() { + setup: + String s = "A" + for (int i = 0; i < 10000; i++) { + s += String.valueOf(i) + } + assert s.substring(0, AutoSqlSanitizer.LIMIT) == SqlStatementSanitizer.sanitize(s).getFullStatement() + } + + def "test 32k truncation"() { + setup: + StringBuffer s = new StringBuffer() + for (int i = 0; i < 10000; i++) { + s.append("SELECT * FROM TABLE WHERE FIELD = 1234 AND ") + } + String sanitized = SqlStatementSanitizer.sanitize(s.toString()).getFullStatement() + System.out.println(sanitized.length()) + assert sanitized.length() <= AutoSqlSanitizer.LIMIT + assert !sanitized.contains("1234") + } + + def "random bytes don't cause exceptions or timeouts"() { + setup: + Random r = new Random(0) + for (int i = 0; i < 1000; i++) { + StringBuffer sb = new StringBuffer() + for (int c = 0; c < 1000; c++) { + sb.append((char) r.nextInt((int) Character.MAX_VALUE)) + } + SqlStatementSanitizer.sanitize(sb.toString()) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/BaseTracerTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/BaseTracerTest.groovy new file mode 100644 index 000000000..98ee71be3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/BaseTracerTest.groovy @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.context.Context +import spock.lang.Shared +import spock.lang.Specification + +// TODO add tests for BaseTracer +class BaseTracerTest extends Specification { + @Shared + def tracer = newTracer() + + def span = Mock(Span) + + @Shared + def root = Context.root() + + @Shared + def existingSpan = Span.getInvalid() + + def newTracer() { + return new BaseTracer() { + @Override + protected String getInstrumentationName() { + return "BaseTracerTest" + } + } + } + + def "test shouldStartSpan"() { + when: + boolean result = tracer.shouldStartSpan(context, kind) + + then: + result == expected + + where: + kind | context | expected + SpanKind.CLIENT | root | true + SpanKind.SERVER | root | true + SpanKind.INTERNAL | root | true + SpanKind.PRODUCER | root | true + SpanKind.CONSUMER | root | true + SpanKind.CLIENT | tracer.withClientSpan(root, existingSpan) | false + SpanKind.SERVER | tracer.withClientSpan(root, existingSpan) | true + SpanKind.INTERNAL | tracer.withClientSpan(root, existingSpan) | true + SpanKind.CONSUMER | tracer.withClientSpan(root, existingSpan) | true + SpanKind.PRODUCER | tracer.withClientSpan(root, existingSpan) | true + SpanKind.SERVER | tracer.withServerSpan(root, existingSpan) | false + SpanKind.INTERNAL | tracer.withServerSpan(root, existingSpan) | true + SpanKind.CONSUMER | tracer.withServerSpan(root, existingSpan) | false + SpanKind.PRODUCER | tracer.withServerSpan(root, existingSpan) | true + SpanKind.CLIENT | tracer.withServerSpan(root, existingSpan) | true + } + + + class SomeInnerClass implements Runnable { + void run() { + } + } + + static class SomeNestedClass implements Runnable { + void run() { + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/ClassNamesTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/ClassNamesTest.groovy new file mode 100644 index 000000000..c8718fd5e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/ClassNamesTest.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer + +import spock.lang.Specification + +class ClassNamesTest extends Specification { + + def "test simpleName"() { + when: + String result = ClassNames.simpleName(clazz) + + then: + result == expected + + where: + clazz | expected + ClassNamesTest | "ClassNamesTest" + ClassNames | "ClassNames" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/HttpClientTracerTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/HttpClientTracerTest.groovy new file mode 100644 index 000000000..08e3d0eba --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/HttpClientTracerTest.groovy @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer + +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.context.propagation.TextMapSetter +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import spock.lang.Shared + +class HttpClientTracerTest extends BaseTracerTest { + + @Shared + def testUrl = new URI("http://myhost:123/somepath") + + @Shared + def testUrlMapped = new URI("http://dogs.com:123/somepath") + + @Shared + def testUserAgent = "Apache HttpClient" + + def "test onRequest"() { + setup: + def tracer = newTracer() + + when: + tracer.onRequest(span, req) + + then: + if (req) { + 1 * span.setAttribute(SemanticAttributes.NET_TRANSPORT, IP_TCP) + 1 * span.setAttribute(SemanticAttributes.HTTP_METHOD, req.method) + 1 * span.setAttribute(SemanticAttributes.HTTP_URL, "$req.url") + 1 * span.setAttribute(SemanticAttributes.NET_PEER_NAME, req.url.host) + 1 * span.setAttribute(SemanticAttributes.NET_PEER_PORT, req.url.port) + 1 * span.setAttribute(SemanticAttributes.HTTP_USER_AGENT, req["User-Agent"]) + 1 * span.setAttribute(SemanticAttributes.HTTP_FLAVOR, "1.1") + } + 0 * _ + + where: + req << [ + null, + [method: "test-method", url: testUrl, "User-Agent": testUserAgent] + ] + } + + def "test onRequest with mapped peer"() { + setup: + def tracer = newTracer() + def req = [method: "test-method", url: testUrlMapped, "User-Agent": testUserAgent] + + when: + tracer.onRequest(span, req) + + then: + if (req) { + 1 * span.setAttribute(SemanticAttributes.NET_TRANSPORT, IP_TCP) + 1 * span.setAttribute(SemanticAttributes.HTTP_METHOD, req.method) + 1 * span.setAttribute(SemanticAttributes.HTTP_URL, "$req.url") + 1 * span.setAttribute(SemanticAttributes.NET_PEER_NAME, req.url.host) + 1 * span.setAttribute(SemanticAttributes.NET_PEER_PORT, req.url.port) + 1 * span.setAttribute(SemanticAttributes.PEER_SERVICE, "dogsservice") + 1 * span.setAttribute(SemanticAttributes.HTTP_USER_AGENT, req["User-Agent"]) + 1 * span.setAttribute(SemanticAttributes.HTTP_FLAVOR, "1.1") + } + 0 * _ + } + + def "test url handling for #url"() { + setup: + def tracer = newTracer() + + when: + tracer.onRequest(span, req) + + then: + 1 * span.setAttribute(SemanticAttributes.NET_TRANSPORT, IP_TCP) + if (expectedUrl != null) { + 1 * span.setAttribute(SemanticAttributes.HTTP_URL, expectedUrl) + } + 1 * span.setAttribute(SemanticAttributes.HTTP_METHOD, null) + 1 * span.setAttribute(SemanticAttributes.HTTP_FLAVOR, "1.1") + 1 * span.setAttribute(SemanticAttributes.HTTP_USER_AGENT, null) + if (hostname) { + 1 * span.setAttribute(SemanticAttributes.NET_PEER_NAME, hostname) + } + if (port) { + 1 * span.setAttribute(SemanticAttributes.NET_PEER_PORT, port) + } + 0 * _ + + where: + tagQueryString | url | expectedUrl | expectedQuery | expectedFragment | hostname | port + false | null | null | null | null | null | null + false | "" | "" | "" | null | null | null + false | "/path?query" | "/path?query" | "" | null | null | null + false | "https://host:0" | "https://host:0" | "" | null | "host" | null + false | "https://host/path" | "https://host/path" | "" | null | "host" | null + false | "http://host:99/path?query#fragment" | "http://host:99/path?query#fragment" | "" | null | "host" | 99 + false | "https://usr:pswd@host/path" | "https://host/path" | "" | null | "host" | null + + req = [url: url == null ? null : new URI(url)] + } + + def "test onResponse"() { + setup: + def tracer = newTracer() + def statusCode = status != null ? HttpStatusConverter.statusFromHttpStatus(status) : null + + when: + tracer.onResponse(span, resp) + + then: + if (status) { + 1 * span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, status) + } + if (statusCode != null && statusCode != StatusCode.UNSET) { + 1 * span.setStatus(statusCode) + } + 0 * _ + + where: + status | resp + 200 | [status: 200] + 399 | [status: 399] + 400 | [status: 400] + 499 | [status: 499] + 500 | [status: 500] + 500 | [status: 500] + 600 | [status: 600] + null | [status: null] + null | null + } + + @Override + def newTracer() { + def netPeerAttributes = new NetPeerAttributes([ + "1.2.3.4": "catservice", "dogs.com": "dogsservice" + ]) + return new HttpClientTracer(netPeerAttributes) { + + @Override + protected String method(Map m) { + return m.method + } + + @Override + protected URI url(Map m) { + return m.url + } + + @Override + protected Integer status(Map m) { + return m.status + } + + @Override + protected String requestHeader(Map m, String name) { + return m[name] + } + + @Override + protected String responseHeader(Map m, String name) { + return m[name] + } + + @Override + protected TextMapSetter getSetter() { + return null + } + + @Override + protected String getInstrumentationName() { + return "HttpClientTracerTest" + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/HttpStatusConverterTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/HttpStatusConverterTest.groovy new file mode 100644 index 000000000..7b2b37aba --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/HttpStatusConverterTest.groovy @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer + +import io.opentelemetry.api.trace.StatusCode +import spock.lang.Specification + +class HttpStatusConverterTest extends Specification { + + def "test HTTP #httpStatus to OTel #expectedStatus"() { + when: + def status = HttpStatusConverter.statusFromHttpStatus(httpStatus) + + then: + status == expectedStatus + + // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + where: + httpStatus | expectedStatus + 100 | StatusCode.UNSET + 101 | StatusCode.UNSET + 102 | StatusCode.UNSET + 103 | StatusCode.UNSET + + 200 | StatusCode.UNSET + 201 | StatusCode.UNSET + 202 | StatusCode.UNSET + 203 | StatusCode.UNSET + 204 | StatusCode.UNSET + 205 | StatusCode.UNSET + 206 | StatusCode.UNSET + 207 | StatusCode.UNSET + 208 | StatusCode.UNSET + 226 | StatusCode.UNSET + + 300 | StatusCode.UNSET + 301 | StatusCode.UNSET + 302 | StatusCode.UNSET + 303 | StatusCode.UNSET + 304 | StatusCode.UNSET + 305 | StatusCode.UNSET + 306 | StatusCode.UNSET + 307 | StatusCode.UNSET + 308 | StatusCode.UNSET + + 400 | StatusCode.ERROR + 401 | StatusCode.ERROR + 403 | StatusCode.ERROR + 404 | StatusCode.ERROR + 405 | StatusCode.ERROR + 406 | StatusCode.ERROR + 407 | StatusCode.ERROR + 408 | StatusCode.ERROR + 409 | StatusCode.ERROR + 410 | StatusCode.ERROR + 411 | StatusCode.ERROR + 412 | StatusCode.ERROR + 413 | StatusCode.ERROR + 414 | StatusCode.ERROR + 415 | StatusCode.ERROR + 416 | StatusCode.ERROR + 417 | StatusCode.ERROR + 418 | StatusCode.ERROR + 421 | StatusCode.ERROR + 422 | StatusCode.ERROR + 423 | StatusCode.ERROR + 424 | StatusCode.ERROR + 425 | StatusCode.ERROR + 426 | StatusCode.ERROR + 428 | StatusCode.ERROR + 429 | StatusCode.ERROR + 431 | StatusCode.ERROR + 451 | StatusCode.ERROR + + 500 | StatusCode.ERROR + 501 | StatusCode.ERROR + 502 | StatusCode.ERROR + 503 | StatusCode.ERROR + 504 | StatusCode.ERROR + 505 | StatusCode.ERROR + 506 | StatusCode.ERROR + 507 | StatusCode.ERROR + 508 | StatusCode.ERROR + 510 | StatusCode.ERROR + 511 | StatusCode.ERROR + + // Don't exist + 99 | StatusCode.ERROR + 600 | StatusCode.ERROR + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/SpanNamesTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/SpanNamesTest.groovy new file mode 100644 index 000000000..a835a189f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/SpanNamesTest.groovy @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer + +import org.spockframework.util.ReflectionUtil +import spock.lang.Specification + +class SpanNamesTest extends Specification { + + def "test fromMethod"() { + when: + String result = SpanNames.fromMethod(method) + + then: + result == expected + + where: + method | expected + ReflectionUtil.getMethodByName(SpanNames, "fromMethod") | "SpanNames.fromMethod" + ReflectionUtil.getMethodByName(String, "length") | "String.length" + } + + def "test fromMethod with class and method ref"() { + when: + String result = SpanNames.fromMethod(clazz, method) + + then: + result == expected + + where: + clazz = SpanNames + method = ReflectionUtil.getMethodByName(SpanNames, "fromMethod") + expected = "SpanNames.fromMethod" + } + + def "test fromMethod with class and method name"() { + when: + String result = SpanNames.fromMethod(clazz, method) + + then: + result == expected + + where: + clazz = SpanNames + method = "test" + expected = "SpanNames.test" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/async/Jdk8AsyncSpanEndStrategyTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/async/Jdk8AsyncSpanEndStrategyTest.groovy new file mode 100644 index 000000000..bb52b09f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/async/Jdk8AsyncSpanEndStrategyTest.groovy @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer.async + +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.api.tracer.BaseTracer +import spock.lang.Specification + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException + +class Jdk8AsyncSpanEndStrategyTest extends Specification { + BaseTracer tracer + + Context context + + def underTest = Jdk8AsyncSpanEndStrategy.INSTANCE + + void setup() { + tracer = Mock() + context = Mock() + } + + def "ends span on completed future"() { + when: + underTest.end(tracer, context, CompletableFuture.completedFuture("completed")) + + then: + 1 * tracer.end(context) + } + + def "ends span exceptionally on failed future"() { + given: + def exception = new CompletionException() + def future = new CompletableFuture() + future.completeExceptionally(exception) + + when: + underTest.end(tracer, context, future) + + then: + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span on future when complete"() { + def future = new CompletableFuture() + + when: + underTest.end(tracer, context, future) + + then: + 0 * tracer._ + + when: + future.complete("completed") + + then: + 1 * tracer.end(context) + } + + def "ends span exceptionally on future when completed exceptionally"() { + def future = new CompletableFuture() + def exception = new Exception() + + when: + underTest.end(tracer, context, future) + + then: + 0 * tracer._ + + when: + future.completeExceptionally(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/net/NetPeerAttributesTest.groovy b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/net/NetPeerAttributesTest.groovy new file mode 100644 index 000000000..2f4f4a261 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/groovy/io/opentelemetry/instrumentation/api/tracer/net/NetPeerAttributesTest.groovy @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer.net + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import spock.lang.Shared +import spock.lang.Specification + +class NetPeerAttributesTest extends Specification { + + @Shared + def resolvedAddress = new InetSocketAddress("github.com", 999) + + def span = Mock(Span) + + def "test setAttributes"() { + setup: + def utils = new NetPeerAttributes([:]) + + when: + utils.setNetPeer(span, connection) + + then: + if (expectedPeerName) { + 1 * span.setAttribute(SemanticAttributes.NET_PEER_NAME, expectedPeerName) + } + if (expectedPeerIp) { + 1 * span.setAttribute(SemanticAttributes.NET_PEER_IP, expectedPeerIp) + } + 1 * span.setAttribute(SemanticAttributes.NET_PEER_PORT, connection.port) + 0 * _ + + where: + connection | expectedPeerName | expectedPeerIp + new InetSocketAddress("localhost", 888) | "localhost" | "127.0.0.1" + new InetSocketAddress("1.2.1.2", 888) | null | "1.2.1.2" + resolvedAddress | "github.com" | resolvedAddress.address.hostAddress + new InetSocketAddress("bad.address.local", 999) | "bad.address.local" | null + } + + def "test setAttributes with mapped peer"() { + setup: + def utils = new NetPeerAttributes([ + "1.2.3.4": "catservice", "dogs.com": "dogsservice" + ]) + + when: + utils.setNetPeer(span, connection) + + then: + if (expectedPeerService) { + 1 * span.setAttribute(SemanticAttributes.PEER_SERVICE, expectedPeerService) + } else { + 0 * span.setAttribute(SemanticAttributes.PEER_SERVICE, _) + } + + where: + connection | expectedPeerService + new InetSocketAddress("1.2.3.4", 888) | "catservice" + new InetSocketAddress("2.3.4.5", 888) | null + new InetSocketAddress("dogs.com", 999) | "dogsservice" + new InetSocketAddress("github.com", 999) | null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/caching/CacheTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/caching/CacheTest.java new file mode 100644 index 000000000..acd8c483c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/caching/CacheTest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.caching; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CacheTest { + + @Nested + @SuppressWarnings("ClassCanBeStatic") + class StrongKeys { + @Test + void unbounded() { + Cache cache = Cache.newBuilder().build(); + + assertThat(cache.computeIfAbsent("bear", unused -> "roar")).isEqualTo("roar"); + cache.remove("bear"); + + CaffeineCache caffeineCache = ((CaffeineCache) cache); + assertThat(cache.computeIfAbsent("cat", unused -> "meow")).isEqualTo("meow"); + assertThat(caffeineCache.keySet()).hasSize(1); + + assertThat(cache.computeIfAbsent("cat", unused -> "bark")).isEqualTo("meow"); + assertThat(caffeineCache.keySet()).hasSize(1); + + cache.put("dog", "bark"); + assertThat(cache.get("dog")).isEqualTo("bark"); + assertThat(cache.get("cat")).isEqualTo("meow"); + assertThat(cache.get("bear")).isNull(); + assertThat(caffeineCache.keySet()).hasSize(2); + assertThat(cache.computeIfAbsent("cat", unused -> "meow")).isEqualTo("meow"); + } + + @Test + void bounded() { + Cache cache = Cache.newBuilder().setMaximumSize(1).build(); + + assertThat(cache.computeIfAbsent("bear", unused -> "roar")).isEqualTo("roar"); + cache.remove("bear"); + + CaffeineCache caffeineCache = ((CaffeineCache) cache); + assertThat(cache.computeIfAbsent("cat", unused -> "meow")).isEqualTo("meow"); + assertThat(caffeineCache.keySet()).hasSize(1); + + assertThat(cache.computeIfAbsent("cat", unused -> "bark")).isEqualTo("meow"); + assertThat(caffeineCache.keySet()).hasSize(1); + + cache.put("dog", "bark"); + assertThat(cache.get("dog")).isEqualTo("bark"); + caffeineCache.cleanup(); + assertThat(caffeineCache.keySet()).hasSize(1); + assertThat(cache.computeIfAbsent("cat", unused -> "purr")).isEqualTo("purr"); + } + } + + @Nested + @SuppressWarnings("ClassCanBeStatic") + class WeakKeys { + @Test + void unbounded() { + Cache cache = Cache.newBuilder().setWeakKeys().build(); + + assertThat(cache.computeIfAbsent("bear", unused -> "roar")).isEqualTo("roar"); + cache.remove("bear"); + + WeakLockFreeCache weakLockFreeCache = ((WeakLockFreeCache) cache); + String cat = new String("cat"); + String dog = new String("dog"); + assertThat(cache.computeIfAbsent(cat, unused -> "meow")).isEqualTo("meow"); + assertThat(weakLockFreeCache.size()).isEqualTo(1); + + assertThat(cache.computeIfAbsent(cat, unused -> "bark")).isEqualTo("meow"); + assertThat(weakLockFreeCache.size()).isEqualTo(1); + + cache.put(dog, "bark"); + assertThat(cache.get(dog)).isEqualTo("bark"); + assertThat(cache.get(cat)).isEqualTo("meow"); + assertThat(cache.get(new String("dog"))).isNull(); + assertThat(weakLockFreeCache.size()).isEqualTo(2); + assertThat(cache.computeIfAbsent(cat, unused -> "meow")).isEqualTo("meow"); + + cat = null; + System.gc(); + // Wait for GC to be reflected. + await().untilAsserted(() -> assertThat(weakLockFreeCache.size()).isEqualTo(1)); + assertThat(cache.computeIfAbsent(dog, unused -> "bark")).isEqualTo("bark"); + dog = null; + System.gc(); + // Wait for GC to be reflected. + await().untilAsserted(() -> assertThat(weakLockFreeCache.size()).isEqualTo(0)); + } + + @Test + void bounded() { + Cache cache = Cache.newBuilder().setWeakKeys().setMaximumSize(1).build(); + + assertThat(cache.computeIfAbsent("bear", unused -> "roar")).isEqualTo("roar"); + cache.remove("bear"); + + CaffeineCache caffeineCache = ((CaffeineCache) cache); + + String cat = new String("cat"); + String dog = new String("dog"); + assertThat(cache.computeIfAbsent(cat, unused -> "meow")).isEqualTo("meow"); + assertThat(cache.get(cat)).isEqualTo("meow"); + assertThat(cache.get(new String("cat"))).isNull(); + assertThat(caffeineCache.keySet()).hasSize(1); + + assertThat(cache.computeIfAbsent(cat, unused -> "bark")).isEqualTo("meow"); + assertThat(caffeineCache.keySet()).hasSize(1); + + cache.put(dog, "bark"); + assertThat(cache.get(dog)).isEqualTo("bark"); + assertThat(cache.get(new String("dog"))).isNull(); + caffeineCache.cleanup(); + assertThat(caffeineCache.keySet()).hasSize(1); + dog = null; + System.gc(); + // Wait for GC to be reflected. + await() + .untilAsserted( + () -> { + caffeineCache.cleanup(); + assertThat(caffeineCache.keySet()).isEmpty(); + }); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorTest.java new file mode 100644 index 000000000..e07eab2e6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/AttributesExtractorTest.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class AttributesExtractorTest { + + static class TestAttributesExtractor + extends AttributesExtractor, Map> { + + @Override + protected void onStart(AttributesBuilder attributes, Map request) { + set(attributes, AttributeKey.stringKey("animal"), request.get("animal")); + set(attributes, AttributeKey.stringKey("country"), request.get("country")); + } + + @Override + protected void onEnd( + AttributesBuilder attributes, Map request, Map response) { + set(attributes, AttributeKey.stringKey("food"), response.get("food")); + set(attributes, AttributeKey.stringKey("number"), request.get("number")); + } + } + + @Test + void normal() { + TestAttributesExtractor extractor = new TestAttributesExtractor(); + Map request = new HashMap<>(); + request.put("animal", "cat"); + Map response = new HashMap<>(); + response.put("food", "pizza"); + AttributesBuilder attributesBuilder = Attributes.builder(); + extractor.onStart(attributesBuilder, request); + extractor.onEnd(attributesBuilder, request, response); + assertThat(attributesBuilder.build()) + .containsOnly(attributeEntry("animal", "cat"), attributeEntry("food", "pizza")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/DefaultSpanStatusExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/DefaultSpanStatusExtractorTest.java new file mode 100644 index 000000000..a097c30ea --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/DefaultSpanStatusExtractorTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.trace.StatusCode; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class DefaultSpanStatusExtractorTest { + + @Test + void noException() { + assertThat( + SpanStatusExtractor.getDefault() + .extract(Collections.emptyMap(), Collections.emptyMap(), null)) + .isEqualTo(StatusCode.UNSET); + } + + @Test + void exception() { + assertThat( + SpanStatusExtractor.getDefault() + .extract( + Collections.emptyMap(), + Collections.emptyMap(), + new IllegalStateException("test"))) + .isEqualTo(StatusCode.ERROR); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java new file mode 100644 index 000000000..b82e9aa37 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java @@ -0,0 +1,392 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class InstrumenterTest { + private static final String LINK_TRACE_ID = TraceId.fromLongs(0, 42); + private static final String LINK_SPAN_ID = SpanId.fromLong(123); + + private static final Map REQUEST = + Collections.unmodifiableMap( + Stream.of( + entry("req1", "req1_value"), + entry("req2", "req2_value"), + entry("req2_2", "req2_2_value"), + entry("req3", "req3_value"), + entry("linkTraceId", LINK_TRACE_ID), + entry("linkSpanId", LINK_SPAN_ID)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + + private static final Map RESPONSE = + Collections.unmodifiableMap( + Stream.of( + entry("resp1", "resp1_value"), + entry("resp2", "resp2_value"), + entry("resp2_2", "resp2_2_value"), + entry("resp3", "resp3_value")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + + static class AttributesExtractor1 + extends AttributesExtractor, Map> { + + @Override + protected void onStart(AttributesBuilder attributes, Map request) { + attributes.put("req1", request.get("req1")); + attributes.put("req2", request.get("req2")); + } + + @Override + protected void onEnd( + AttributesBuilder attributes, Map request, Map response) { + attributes.put("resp1", response.get("resp1")); + attributes.put("resp2", response.get("resp2")); + } + } + + static class AttributesExtractor2 + extends AttributesExtractor, Map> { + + @Override + protected void onStart(AttributesBuilder attributes, Map request) { + attributes.put("req3", request.get("req3")); + attributes.put("req2", request.get("req2_2")); + } + + @Override + protected void onEnd( + AttributesBuilder attributes, Map request, Map response) { + attributes.put("resp3", response.get("resp3")); + attributes.put("resp2", response.get("resp2_2")); + } + } + + static class LinkExtractor implements SpanLinkExtractor> { + @Override + public SpanContext extract(Context parentContext, Map request) { + return SpanContext.create( + request.get("linkTraceId"), + request.get("linkSpanId"), + TraceFlags.getSampled(), + TraceState.getDefault()); + } + } + + static class MapGetter implements TextMapGetter> { + + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + } + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + @Test + void server() { + Instrumenter, Map> instrumenter = + Instrumenter., Map>newBuilder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .addAttributesExtractors(new AttributesExtractor1(), new AttributesExtractor2()) + .addSpanLinkExtractor(new LinkExtractor()) + .newServerInstrumenter(new MapGetter()); + + Context context = instrumenter.start(Context.root(), REQUEST); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + + assertThat(spanContext.isValid()).isTrue(); + assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext); + + instrumenter.end(context, REQUEST, RESPONSE, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasKind(SpanKind.SERVER) + .hasInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("test", null)) + .hasTraceId(spanContext.getTraceId()) + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.unset()) + .hasLinks(expectedSpanLink()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + attributeEntry("req1", "req1_value"), + attributeEntry("req2", "req2_2_value"), + attributeEntry("req3", "req3_value"), + attributeEntry("resp1", "resp1_value"), + attributeEntry("resp2", "resp2_2_value"), + attributeEntry("resp3", "resp3_value"))))); + } + + @Test + void server_error() { + Instrumenter, Map> instrumenter = + Instrumenter., Map>newBuilder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .addAttributesExtractors(new AttributesExtractor1(), new AttributesExtractor2()) + .newServerInstrumenter(new MapGetter()); + + Context context = instrumenter.start(Context.root(), REQUEST); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + + assertThat(spanContext.isValid()).isTrue(); + assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext); + + instrumenter.end(context, REQUEST, RESPONSE, new IllegalStateException("test")); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("span").hasStatus(StatusData.error()))); + } + + @Test + void server_parent() { + Instrumenter, Map> instrumenter = + Instrumenter., Map>newBuilder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .addAttributesExtractors(new AttributesExtractor1(), new AttributesExtractor2()) + .newServerInstrumenter(new MapGetter()); + + Map request = new HashMap<>(REQUEST); + W3CTraceContextPropagator.getInstance() + .inject( + Context.root() + .with( + Span.wrap( + SpanContext.createFromRemoteParent( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()))), + request, + Map::put); + Context context = instrumenter.start(Context.root(), request); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + + assertThat(spanContext.isValid()).isTrue(); + assertThat(ServerSpan.fromContextOrNull(context).getSpanContext()).isEqualTo(spanContext); + + instrumenter.end(context, request, RESPONSE, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId("090a0b0c0d0e0f00"))); + } + + @Test + void client() { + Instrumenter, Map> instrumenter = + Instrumenter., Map>newBuilder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .addAttributesExtractors(new AttributesExtractor1(), new AttributesExtractor2()) + .addSpanLinkExtractor(new LinkExtractor()) + .newClientInstrumenter(Map::put); + + Map request = new HashMap<>(REQUEST); + Context context = instrumenter.start(Context.root(), request); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + + assertThat(spanContext.isValid()).isTrue(); + assertThat(request).containsKey("traceparent"); + + instrumenter.end(context, request, RESPONSE, null); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasKind(SpanKind.CLIENT) + .hasInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("test", null)) + .hasTraceId(spanContext.getTraceId()) + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.unset()) + .hasLinks(expectedSpanLink()) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsOnly( + attributeEntry("req1", "req1_value"), + attributeEntry("req2", "req2_2_value"), + attributeEntry("req3", "req3_value"), + attributeEntry("resp1", "resp1_value"), + attributeEntry("resp2", "resp2_2_value"), + attributeEntry("resp3", "resp3_value"))))); + } + + @Test + void client_error() { + Instrumenter, Map> instrumenter = + Instrumenter., Map>newBuilder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .addAttributesExtractors(new AttributesExtractor1(), new AttributesExtractor2()) + .newClientInstrumenter(Map::put); + + Map request = new HashMap<>(REQUEST); + Context context = instrumenter.start(Context.root(), request); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + + assertThat(spanContext.isValid()).isTrue(); + assertThat(request).containsKey("traceparent"); + + instrumenter.end(context, request, RESPONSE, new IllegalStateException("test")); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("span").hasStatus(StatusData.error()))); + } + + @Test + void client_parent() { + Instrumenter, Map> instrumenter = + Instrumenter., Map>newBuilder( + otelTesting.getOpenTelemetry(), "test", unused -> "span") + .addAttributesExtractors(new AttributesExtractor1(), new AttributesExtractor2()) + .newClientInstrumenter(Map::put); + + Context parent = + Context.root() + .with( + Span.wrap( + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()))); + + Map request = new HashMap<>(REQUEST); + Context context = instrumenter.start(parent, request); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + + assertThat(spanContext.isValid()).isTrue(); + assertThat(request).containsKey("traceparent"); + + instrumenter.end(context, request, RESPONSE, new IllegalStateException("test")); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("span") + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId(spanContext.getSpanId()) + .hasParentSpanId("090a0b0c0d0e0f00"))); + } + + @Test + void shouldStartSpanWithGivenStartTime() { + // given + Instrumenter instrumenter = + Instrumenter.newBuilder( + otelTesting.getOpenTelemetry(), "test", request -> "test span") + .setTimeExtractors(request -> request, response -> response) + .newInstrumenter(); + + Instant startTime = Instant.ofEpochSecond(100); + Instant endTime = Instant.ofEpochSecond(123); + + // when + Context context = instrumenter.start(Context.root(), startTime); + instrumenter.end(context, startTime, endTime, null); + + // then + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test span").startsAt(startTime).endsAt(endTime))); + } + + @Test + void shouldNotAddInvalidLink() { + // given + Instrumenter instrumenter = + Instrumenter.newBuilder( + otelTesting.getOpenTelemetry(), "test", request -> "test span") + .addSpanLinkExtractor((parentContext, request) -> SpanContext.getInvalid()) + .newInstrumenter(); + + // when + Context context = instrumenter.start(Context.root(), "request"); + instrumenter.end(context, "request", "response", null); + + // then + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test span").hasTotalRecordedLinks(0))); + } + + private static LinkData expectedSpanLink() { + return LinkData.create( + SpanContext.create( + LINK_TRACE_ID, LINK_SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/JdkErrorCauseExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/JdkErrorCauseExtractorTest.java new file mode 100644 index 000000000..a3656a872 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/JdkErrorCauseExtractorTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class JdkErrorCauseExtractorTest { + + @ParameterizedTest + @ValueSource( + classes = { + ExecutionException.class, + CompletionException.class, + InvocationTargetException.class, + UndeclaredThrowableException.class + }) + void unwraps(Class exceptionClass) throws Exception { + Exception exception = + exceptionClass + .getConstructor(Throwable.class) + .newInstance(new IllegalArgumentException("test")); + + assertThat(ErrorCauseExtractor.jdk().extractCause(exception)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("test"); + } + + @Test + void multipleUnwraps() { + assertThat( + ErrorCauseExtractor.jdk() + .extractCause( + new ExecutionException( + new UndeclaredThrowableException(new IllegalArgumentException("test"))))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("test"); + } + + @Test + void notWrapped() { + assertThat(ErrorCauseExtractor.jdk().extractCause(new IllegalArgumentException("test"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("test"); + assertThat( + ErrorCauseExtractor.jdk() + .extractCause( + new IllegalArgumentException("test", new IllegalStateException("state")))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("test"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/PropagatorBasedSpanLinkExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/PropagatorBasedSpanLinkExtractorTest.java new file mode 100644 index 000000000..2b6d64469 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/PropagatorBasedSpanLinkExtractorTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class PropagatorBasedSpanLinkExtractorTest { + private static final String TRACE_ID = TraceId.fromLongs(0, 123); + private static final String SPAN_ID = SpanId.fromLong(456); + + @Test + void shouldExtractSpanLink() { + // given + ContextPropagators propagators = + ContextPropagators.create(W3CTraceContextPropagator.getInstance()); + + SpanLinkExtractor> underTest = + SpanLinkExtractor.fromUpstreamRequest(propagators, new MapGetter()); + + Map request = + singletonMap("traceparent", String.format("00-%s-%s-01", TRACE_ID, SPAN_ID)); + + // when + SpanContext link = underTest.extract(Context.root(), request); + + // then + assertEquals( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + link); + } + + static final class MapGetter implements TextMapGetter> { + + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributesTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributesTest.java new file mode 100644 index 000000000..8e66b746a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributesTest.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import org.junit.jupiter.api.Test; + +class UnsafeAttributesTest { + + @Test + void buildAndUse() { + Attributes previous = + new UnsafeAttributes().put("world", "earth").put("country", "japan").build(); + + UnsafeAttributes attributes = new UnsafeAttributes(); + attributes.put(AttributeKey.stringKey("animal"), "cat"); + attributes.put("needs_catnip", false); + // Overwrites + attributes.put("needs_catnip", true); + attributes.put(AttributeKey.longKey("lives"), 9); + attributes.putAll(previous); + + assertThat((Attributes) attributes) + .containsOnly( + attributeEntry("world", "earth"), + attributeEntry("country", "japan"), + attributeEntry("animal", "cat"), + attributeEntry("needs_catnip", true), + attributeEntry("lives", 9L)); + + Attributes built = attributes.build(); + assertThat(built) + .containsOnly( + attributeEntry("world", "earth"), + attributeEntry("country", "japan"), + attributeEntry("animal", "cat"), + attributeEntry("needs_catnip", true), + attributeEntry("lives", 9L)); + + attributes.put("clothes", "fur"); + assertThat((Attributes) attributes) + .containsOnly( + attributeEntry("world", "earth"), + attributeEntry("country", "japan"), + attributeEntry("animal", "cat"), + attributeEntry("needs_catnip", true), + attributeEntry("lives", 9L), + attributeEntry("clothes", "fur")); + + // Unmodified + assertThat(built) + .containsOnly( + attributeEntry("world", "earth"), + attributeEntry("country", "japan"), + attributeEntry("animal", "cat"), + attributeEntry("needs_catnip", true), + attributeEntry("lives", 9L)); + + Attributes modified = attributes.toBuilder().put("country", "us").build(); + assertThat(modified) + .containsOnly( + attributeEntry("world", "earth"), + attributeEntry("country", "us"), + attributeEntry("animal", "cat"), + attributeEntry("needs_catnip", true), + attributeEntry("lives", 9L), + attributeEntry("clothes", "fur")); + + // Unmodified + assertThat((Attributes) attributes) + .containsOnly( + attributeEntry("world", "earth"), + attributeEntry("country", "japan"), + attributeEntry("animal", "cat"), + attributeEntry("needs_catnip", true), + attributeEntry("lives", 9L), + attributeEntry("clothes", "fur")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractorTest.java new file mode 100644 index 000000000..6c68bb927 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractorTest.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.code; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class CodeAttributesExtractorTest { + + static final CodeAttributesExtractor, Void> underTest = + new CodeAttributesExtractor, Void>() { + @Override + protected Class codeClass(Map request) { + try { + String className = request.get("class"); + return className == null ? null : Class.forName(className); + } catch (ClassNotFoundException e) { + throw new AssertionError(e); + } + } + + @Override + protected String methodName(Map request) { + return request.get("methodName"); + } + + @Override + protected String filePath(Map request) { + return request.get("filePath"); + } + + @Override + protected Long lineNumber(Map request) { + String lineNo = request.get("lineNo"); + return lineNo == null ? null : Long.parseLong(lineNo); + } + }; + + @Test + void shouldExtractAllAttributes() { + // given + Map request = new HashMap<>(); + request.put("class", TestClass.class.getName()); + request.put("methodName", "doSomething"); + request.put("filePath", "/tmp/TestClass.java"); + request.put("lineNo", "42"); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, request); + + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, request, null); + + // then + assertThat(startAttributes.build()) + .containsOnly( + entry(SemanticAttributes.CODE_NAMESPACE, TestClass.class.getName()), + entry(SemanticAttributes.CODE_FUNCTION, "doSomething"), + entry(SemanticAttributes.CODE_FILEPATH, "/tmp/TestClass.java"), + entry(SemanticAttributes.CODE_LINENO, 42L)); + + assertThat(endAttributes.build().isEmpty()).isTrue(); + } + + @Test + void shouldExtractNoAttributesIfNoneAreAvailable() { + // when + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, Collections.emptyMap()); + + // then + assertThat(attributes.build().isEmpty()).isTrue(); + } + + static class TestClass {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractorTest.java new file mode 100644 index 000000000..c595f2d17 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractorTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.code; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.willReturn; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CodeSpanNameExtractorTest { + @Mock CodeAttributesExtractor attributesExtractor; + + @Test + void shouldExtractFullSpanName() { + // given + Object request = new Object(); + + willReturn(TestClass.class).given(attributesExtractor).codeClass(request); + willReturn("doSomething").given(attributesExtractor).methodName(request); + + SpanNameExtractor underTest = CodeSpanNameExtractor.create(attributesExtractor); + + // when + String spanName = underTest.extract(request); + + // then + assertEquals("TestClass.doSomething", spanName); + } + + @Test + void shouldExtractFullSpanNameForAnonymousClass() { + // given + AnonymousBaseClass anon = new AnonymousBaseClass() {}; + Object request = new Object(); + + willReturn(anon.getClass()).given(attributesExtractor).codeClass(request); + willReturn("doSomething").given(attributesExtractor).methodName(request); + + SpanNameExtractor underTest = CodeSpanNameExtractor.create(attributesExtractor); + + // when + String spanName = underTest.extract(request); + + // then + assertEquals(getClass().getSimpleName() + "$1.doSomething", spanName); + } + + static class TestClass {} + + static class AnonymousBaseClass {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbAttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbAttributesExtractorTest.java new file mode 100644 index 000000000..1ef4ec938 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbAttributesExtractorTest.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.db; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class DbAttributesExtractorTest { + static final DbAttributesExtractor, Void> underTest = + new DbAttributesExtractor, Void>() { + @Override + protected String system(Map map) { + return map.get("db.system"); + } + + @Override + protected String user(Map map) { + return map.get("db.user"); + } + + @Override + protected String name(Map map) { + return map.get("db.name"); + } + + @Override + protected String connectionString(Map map) { + return map.get("db.connection_string"); + } + + @Override + protected String statement(Map map) { + return map.get("db.statement"); + } + + @Override + protected String operation(Map map) { + return map.get("db.operation"); + } + }; + + @Test + void shouldExtractAllAvailableAttributes() { + // given + Map request = new HashMap<>(); + request.put("db.system", "myDb"); + request.put("db.user", "username"); + request.put("db.name", "potatoes"); + request.put("db.connection_string", "mydb:///potatoes"); + request.put("db.statement", "SELECT * FROM potato"); + request.put("db.operation", "SELECT"); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, request); + + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, request, null); + + // then + assertThat(startAttributes.build()) + .containsOnly( + entry(SemanticAttributes.DB_SYSTEM, "myDb"), + entry(SemanticAttributes.DB_USER, "username"), + entry(SemanticAttributes.DB_NAME, "potatoes"), + entry(SemanticAttributes.DB_CONNECTION_STRING, "mydb:///potatoes"), + entry(SemanticAttributes.DB_STATEMENT, "SELECT * FROM potato"), + entry(SemanticAttributes.DB_OPERATION, "SELECT")); + + assertThat(endAttributes.build().isEmpty()).isTrue(); + } + + @Test + void shouldExtractNoAttributesIfNoneAreAvailable() { + // when + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, Collections.emptyMap()); + + // then + assertThat(attributes.build().isEmpty()).isTrue(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbSpanNameExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbSpanNameExtractorTest.java new file mode 100644 index 000000000..c289b9258 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbSpanNameExtractorTest.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.db; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DbSpanNameExtractorTest { + @Mock DbAttributesExtractor dbAttributesExtractor; + @Mock SqlAttributesExtractor sqlAttributesExtractor; + + @Test + void shouldExtractFullSpanName() { + // given + DbRequest dbRequest = new DbRequest(); + + // cannot stub dbOperation() and dbTable() because they're final + given(sqlAttributesExtractor.rawStatement(dbRequest)).willReturn("SELECT * FROM table"); + given(sqlAttributesExtractor.name(dbRequest)).willReturn("database"); + + SpanNameExtractor underTest = DbSpanNameExtractor.create(sqlAttributesExtractor); + + // when + String spanName = underTest.extract(dbRequest); + + // then + assertEquals("SELECT database.table", spanName); + } + + @Test + void shouldSkipDbNameIfTableAlreadyHasDbNamePrefix() { + // given + DbRequest dbRequest = new DbRequest(); + + // cannot stub dbOperation() and dbTable() because they're final + given(sqlAttributesExtractor.rawStatement(dbRequest)).willReturn("SELECT * FROM another.table"); + given(sqlAttributesExtractor.name(dbRequest)).willReturn("database"); + + SpanNameExtractor underTest = DbSpanNameExtractor.create(sqlAttributesExtractor); + + // when + String spanName = underTest.extract(dbRequest); + + // then + assertEquals("SELECT another.table", spanName); + } + + @Test + void shouldExtractOperationAndTable() { + // given + DbRequest dbRequest = new DbRequest(); + + // cannot stub dbOperation() and dbTable() because they're final + given(sqlAttributesExtractor.rawStatement(dbRequest)).willReturn("SELECT * FROM table"); + + SpanNameExtractor underTest = DbSpanNameExtractor.create(sqlAttributesExtractor); + + // when + String spanName = underTest.extract(dbRequest); + + // then + assertEquals("SELECT table", spanName); + } + + @Test + void shouldExtractOperationAndName() { + // given + DbRequest dbRequest = new DbRequest(); + + given(dbAttributesExtractor.operation(dbRequest)).willReturn("SELECT"); + given(dbAttributesExtractor.name(dbRequest)).willReturn("database"); + + SpanNameExtractor underTest = DbSpanNameExtractor.create(dbAttributesExtractor); + + // when + String spanName = underTest.extract(dbRequest); + + // then + assertEquals("SELECT database", spanName); + } + + @Test + void shouldExtractOperation() { + // given + DbRequest dbRequest = new DbRequest(); + + given(dbAttributesExtractor.operation(dbRequest)).willReturn("SELECT"); + + SpanNameExtractor underTest = DbSpanNameExtractor.create(dbAttributesExtractor); + + // when + String spanName = underTest.extract(dbRequest); + + // then + assertEquals("SELECT", spanName); + } + + @Test + void shouldExtractDbName() { + // given + DbRequest dbRequest = new DbRequest(); + + given(dbAttributesExtractor.name(dbRequest)).willReturn("database"); + + SpanNameExtractor underTest = DbSpanNameExtractor.create(dbAttributesExtractor); + + // when + String spanName = underTest.extract(dbRequest); + + // then + assertEquals("database", spanName); + } + + @Test + void shouldFallBackToDefaultSpanName() { + // given + DbRequest dbRequest = new DbRequest(); + + SpanNameExtractor underTest = DbSpanNameExtractor.create(dbAttributesExtractor); + + // when + String spanName = underTest.extract(dbRequest); + + // then + assertEquals("DB Query", spanName); + } + + static class DbRequest {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlAttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlAttributesExtractorTest.java new file mode 100644 index 000000000..8c4d3acb7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlAttributesExtractorTest.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.db; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class SqlAttributesExtractorTest { + AttributeKey dbTableAttribute; + final SqlAttributesExtractor, Void> underTest = + new SqlAttributesExtractor, Void>() { + + @Override + protected AttributeKey dbTableAttribute() { + return dbTableAttribute; + } + + @Override + protected String rawStatement(Map map) { + return map.get("db.statement"); + } + + @Override + protected String system(Map map) { + return map.get("db.system"); + } + + @Override + protected String user(Map map) { + return map.get("db.user"); + } + + @Override + protected String name(Map map) { + return map.get("db.name"); + } + + @Override + protected String connectionString(Map map) { + return map.get("db.connection_string"); + } + }; + + @Test + void shouldExtractAllAttributes() { + // given + Map request = new HashMap<>(); + request.put("db.system", "myDb"); + request.put("db.user", "username"); + request.put("db.name", "potatoes"); + request.put("db.connection_string", "mydb:///potatoes"); + request.put("db.statement", "SELECT * FROM potato WHERE id=12345"); + + dbTableAttribute = SemanticAttributes.DB_SQL_TABLE; + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, request); + + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, request, null); + + // then + assertThat(startAttributes.build()) + .containsOnly( + entry(SemanticAttributes.DB_SYSTEM, "myDb"), + entry(SemanticAttributes.DB_USER, "username"), + entry(SemanticAttributes.DB_NAME, "potatoes"), + entry(SemanticAttributes.DB_CONNECTION_STRING, "mydb:///potatoes"), + entry(SemanticAttributes.DB_STATEMENT, "SELECT * FROM potato WHERE id=?"), + entry(SemanticAttributes.DB_OPERATION, "SELECT"), + entry(SemanticAttributes.DB_SQL_TABLE, "potato")); + + assertThat(endAttributes.build().isEmpty()).isTrue(); + } + + @Test + void shouldNotExtractTableIfAttributeIsNotSet() { + // given + Map request = new HashMap<>(); + request.put("db.statement", "SELECT * FROM potato WHERE id=12345"); + + dbTableAttribute = null; + + // when + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, request); + + // then + assertThat(attributes.build()) + .containsOnly( + entry(SemanticAttributes.DB_STATEMENT, "SELECT * FROM potato WHERE id=?"), + entry(SemanticAttributes.DB_OPERATION, "SELECT")); + } + + @Test + void shouldExtractNoAttributesIfNoneAreAvailable() { + // when + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, Collections.emptyMap()); + + // then + assertThat(attributes.build().isEmpty()).isTrue(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributesExtractorTest.java new file mode 100644 index 000000000..34efdbb67 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributesExtractorTest.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class HttpAttributesExtractorTest { + + static class TestHttpAttributesExtractor + extends HttpAttributesExtractor, Map> { + + @Override + protected String method(Map request) { + return request.get("method"); + } + + @Override + protected String url(Map request) { + return request.get("url"); + } + + @Override + protected String target(Map request) { + return request.get("target"); + } + + @Override + protected String host(Map request) { + return request.get("host"); + } + + @Override + protected String route(Map request) { + return request.get("route"); + } + + @Override + protected String scheme(Map request) { + return request.get("scheme"); + } + + @Override + protected String userAgent(Map request) { + return request.get("userAgent"); + } + + @Override + protected Long requestContentLength(Map request, Map response) { + return Long.parseLong(request.get("requestContentLength")); + } + + @Override + protected Long requestContentLengthUncompressed( + Map request, Map response) { + return Long.parseLong(request.get("requestContentLengthUncompressed")); + } + + @Override + protected Integer statusCode(Map request, Map response) { + return Integer.parseInt(response.get("statusCode")); + } + + @Override + protected String flavor(Map request, Map response) { + return request.get("flavor"); + } + + @Override + protected Long responseContentLength( + Map request, Map response) { + return Long.parseLong(response.get("responseContentLength")); + } + + @Override + protected Long responseContentLengthUncompressed( + Map request, Map response) { + return Long.parseLong(response.get("responseContentLengthUncompressed")); + } + + @Override + protected String serverName(Map request, Map response) { + return request.get("serverName"); + } + + @Override + protected String clientIp(Map request, Map response) { + return request.get("clientIp"); + } + } + + @Test + void normal() { + Map request = new HashMap<>(); + request.put("method", "POST"); + request.put("url", "http://github.com"); + request.put("target", "github.com"); + request.put("host", "github.com:80"); + request.put("route", "/repositories/{id}"); + request.put("scheme", "https"); + request.put("userAgent", "okhttp 3.x"); + request.put("requestContentLength", "10"); + request.put("requestContentLengthUncompressed", "11"); + request.put("flavor", "http/2"); + request.put("serverName", "server"); + request.put("clientIp", "1.2.3.4"); + + Map response = new HashMap<>(); + response.put("statusCode", "202"); + response.put("responseContentLength", "20"); + response.put("responseContentLengthUncompressed", "21"); + + TestHttpAttributesExtractor extractor = new TestHttpAttributesExtractor(); + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, request); + assertThat(attributes.build()) + .containsOnly( + entry(SemanticAttributes.HTTP_METHOD, "POST"), + entry(SemanticAttributes.HTTP_URL, "http://github.com"), + entry(SemanticAttributes.HTTP_TARGET, "github.com"), + entry(SemanticAttributes.HTTP_HOST, "github.com:80"), + entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{id}"), + entry(SemanticAttributes.HTTP_SCHEME, "https"), + entry(SemanticAttributes.HTTP_USER_AGENT, "okhttp 3.x")); + + extractor.onEnd(attributes, request, response); + assertThat(attributes.build()) + .containsOnly( + entry(SemanticAttributes.HTTP_METHOD, "POST"), + entry(SemanticAttributes.HTTP_URL, "http://github.com"), + entry(SemanticAttributes.HTTP_TARGET, "github.com"), + entry(SemanticAttributes.HTTP_HOST, "github.com:80"), + entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{id}"), + entry(SemanticAttributes.HTTP_SCHEME, "https"), + entry(SemanticAttributes.HTTP_USER_AGENT, "okhttp 3.x"), + entry(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, 10L), + entry(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED, 11L), + entry(SemanticAttributes.HTTP_FLAVOR, "http/2"), + entry(SemanticAttributes.HTTP_SERVER_NAME, "server"), + entry(SemanticAttributes.HTTP_CLIENT_IP, "1.2.3.4"), + entry(SemanticAttributes.HTTP_STATUS_CODE, 202L), + entry(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 20L), + entry(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED, 21L)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsTest.java new file mode 100644 index 000000000..92e3e2daa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsTest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.RequestListener; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import java.util.Collection; +import org.junit.jupiter.api.Test; + +class HttpServerMetricsTest { + + @Test + void collectsMetrics() { + SdkMeterProvider meterProvider = SdkMeterProvider.builder().build(); + + RequestListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put("http.method", "GET") + .put("http.host", "host") + .put("http.scheme", "https") + .put("net.host.name", "localhost") + .put("net.host.port", 1234) + .put("rpc.service", "unused") + .put("rpc.method", "unused") + .build(); + + // Currently ignored. + Attributes responseAttributes = + Attributes.builder() + .put("http.flavor", "2.0") + .put("http.server_name", "server") + .put("http.status_code", 200) + .build(); + + Context context1 = listener.start(Context.current(), requestAttributes); + + Collection metrics = meterProvider.collectAllMetrics(); + assertThat(metrics).hasSize(1); + assertThat(metrics) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("http.server.active_requests"); + assertThat(metric.getDescription()) + .isEqualTo("The number of concurrent HTTP requests that are currently in-flight"); + assertThat(metric.getUnit()).isEqualTo("requests"); + assertThat(metric.getType()).isEqualTo(MetricDataType.LONG_SUM); + assertThat(metric.getLongSumData().getPoints()).hasSize(1); + LongPointData data = metric.getLongSumData().getPoints().stream().findFirst().get(); + assertThat(data.getLabels().asMap()) + .containsOnly( + entry("http.host", "host"), + entry("http.method", "GET"), + entry("http.scheme", "https")); + assertThat(data.getValue()).isEqualTo(1); + }); + + Context context2 = listener.start(Context.current(), requestAttributes); + + metrics = meterProvider.collectAllMetrics(); + assertThat(metrics).hasSize(1); + assertThat(metrics) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("http.server.active_requests"); + assertThat(metric.getLongSumData().getPoints()).hasSize(1); + LongPointData data = metric.getLongSumData().getPoints().stream().findFirst().get(); + assertThat(data.getValue()).isEqualTo(2); + }); + + listener.end(context1, responseAttributes); + + metrics = meterProvider.collectAllMetrics(); + assertThat(metrics).hasSize(2); + assertThat(metrics) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("http.server.active_requests"); + assertThat(metric.getLongSumData().getPoints()).hasSize(1); + LongPointData data = metric.getLongSumData().getPoints().stream().findFirst().get(); + assertThat(data.getValue()).isEqualTo(1); + }); + assertThat(metrics) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("http.server.duration"); + assertThat(metric.getDoubleSummaryData().getPoints()).hasSize(1); + DoubleSummaryPointData data = + metric.getDoubleSummaryData().getPoints().stream().findFirst().get(); + assertThat(data.getLabels().asMap()) + .containsOnly( + entry("http.host", "host"), + entry("http.method", "GET"), + entry("http.scheme", "https"), + entry("net.host.name", "localhost"), + entry("net.host.port", "1234")); + assertThat(data.getPercentileValues()).isNotEmpty(); + }); + + listener.end(context2, responseAttributes); + + metrics = meterProvider.collectAllMetrics(); + assertThat(metrics).hasSize(2); + assertThat(metrics) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("http.server.active_requests"); + assertThat(metric.getLongSumData().getPoints()).hasSize(1); + LongPointData data = metric.getLongSumData().getPoints().stream().findFirst().get(); + assertThat(data.getValue()).isEqualTo(0); + }); + assertThat(metrics) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("http.server.duration"); + assertThat(metric.getDoubleSummaryData().getPoints()).hasSize(1); + DoubleSummaryPointData data = + metric.getDoubleSummaryData().getPoints().stream().findFirst().get(); + assertThat(data.getPercentileValues()).isNotEmpty(); + }); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractorTest.java new file mode 100644 index 000000000..7abe98c69 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractorTest.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class HttpSpanNameExtractorTest { + + @Mock private HttpAttributesExtractor, Map> extractor; + + @Test + void routeAndMethod() { + when(extractor.route(anyMap())).thenReturn("/cats/{id}"); + when(extractor.method(anyMap())).thenReturn("GET"); + assertThat(HttpSpanNameExtractor.create(extractor).extract(Collections.emptyMap())) + .isEqualTo("/cats/{id}"); + } + + @Test + void method() { + when(extractor.method(anyMap())).thenReturn("GET"); + assertThat(HttpSpanNameExtractor.create(extractor).extract(Collections.emptyMap())) + .isEqualTo("HTTP GET"); + } + + @Test + void nothing() { + assertThat(HttpSpanNameExtractor.create(extractor).extract(Collections.emptyMap())) + .isEqualTo("HTTP request"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractorTest.java new file mode 100644 index 000000000..8201c82ca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractorTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.http; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.tracer.HttpStatusConverter; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HttpSpanStatusExtractorTest { + @Mock private HttpAttributesExtractor, Map> extractor; + + @ParameterizedTest + @ValueSource(ints = {1, 100, 101, 200, 201, 300, 301, 400, 401, 500, 501, 600, 601}) + void hasStatus(int statusCode) { + when(extractor.statusCode(anyMap(), anyMap())).thenReturn(statusCode); + + assertThat( + HttpSpanStatusExtractor.create(extractor) + .extract(Collections.emptyMap(), Collections.emptyMap(), null, SpanKind.CLIENT)) + .isEqualTo(HttpStatusConverter.statusFromHttpStatus(statusCode, SpanKind.CLIENT)); + assertThat( + HttpSpanStatusExtractor.create(extractor) + .extract(Collections.emptyMap(), Collections.emptyMap(), null, SpanKind.SERVER)) + .isEqualTo(HttpStatusConverter.statusFromHttpStatus(statusCode, SpanKind.SERVER)); + } + + @ParameterizedTest + @ValueSource(ints = {1, 100, 101, 200, 201, 300, 301, 400, 401, 500, 501, 600, 601}) + void hasStatus_ignoresException(int statusCode) { + when(extractor.statusCode(anyMap(), anyMap())).thenReturn(statusCode); + + // Presence of exception has no effect. + assertThat( + HttpSpanStatusExtractor.create(extractor) + .extract( + Collections.emptyMap(), Collections.emptyMap(), new IllegalStateException(), SpanKind.CLIENT)) + .isEqualTo(HttpStatusConverter.statusFromHttpStatus(statusCode, SpanKind.CLIENT)); + assertThat( + HttpSpanStatusExtractor.create(extractor) + .extract( + Collections.emptyMap(), Collections.emptyMap(), new IllegalStateException(), SpanKind.SERVER)) + .isEqualTo(HttpStatusConverter.statusFromHttpStatus(statusCode, SpanKind.SERVER)); + } + + @Test + void fallsBackToDefault_unset() { + when(extractor.statusCode(anyMap(), anyMap())).thenReturn(null); + + assertThat( + HttpSpanStatusExtractor.create(extractor) + .extract(Collections.emptyMap(), Collections.emptyMap(), null, SpanKind.CLIENT)) + .isEqualTo(StatusCode.UNSET); + assertThat( + HttpSpanStatusExtractor.create(extractor) + .extract(Collections.emptyMap(), Collections.emptyMap(), null, SpanKind.SERVER)) + .isEqualTo(StatusCode.UNSET); + } + + @Test + void fallsBackToDefault_error() { + when(extractor.statusCode(anyMap(), anyMap())).thenReturn(null); + + assertThat( + HttpSpanStatusExtractor.create(extractor) + .extract( + Collections.emptyMap(), Collections.emptyMap(), new IllegalStateException(), SpanKind.CLIENT)) + .isEqualTo(StatusCode.ERROR); + assertThat( + HttpSpanStatusExtractor.create(extractor) + .extract( + Collections.emptyMap(), Collections.emptyMap(), new IllegalStateException(), SpanKind.SERVER)) + .isEqualTo(StatusCode.ERROR); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorTest.java new file mode 100644 index 000000000..8e33a25c7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorTest.java @@ -0,0 +1,178 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.messaging; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.assertj.core.data.MapEntry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class MessagingAttributesExtractorTest { + static final MessagingAttributesExtractor, String> underTest = + new MessagingAttributesExtractor, String>() { + @Override + protected String system(Map request) { + return request.get("system"); + } + + @Override + protected String destinationKind(Map request) { + return request.get("destinationKind"); + } + + @Override + protected String destination(Map request) { + return request.get("destination"); + } + + @Override + protected boolean temporaryDestination(Map request) { + return request.containsKey("temporaryDestination"); + } + + @Override + protected String protocol(Map request) { + return request.get("protocol"); + } + + @Override + protected String protocolVersion(Map request) { + return request.get("protocolVersion"); + } + + @Override + protected String url(Map request) { + return request.get("url"); + } + + @Override + protected String conversationId(Map request) { + return request.get("conversationId"); + } + + @Override + protected Long messagePayloadSize(Map request) { + String payloadSize = request.get("payloadSize"); + return payloadSize == null ? null : Long.valueOf(payloadSize); + } + + @Override + protected Long messagePayloadCompressedSize(Map request) { + String payloadSize = request.get("payloadCompressedSize"); + return payloadSize == null ? null : Long.valueOf(payloadSize); + } + + @Override + protected MessageOperation operation(Map request) { + String operation = request.get("operation"); + return operation == null ? null : MessageOperation.valueOf(operation); + } + + @Override + protected String messageId(Map request, String response) { + return response; + } + }; + + @ParameterizedTest + @MethodSource("destinations") + void shouldExtractAllAvailableAttributes( + boolean temporary, + String destination, + MessageOperation operation, + String expectedDestination) { + // given + Map request = new HashMap<>(); + request.put("system", "myQueue"); + request.put("destinationKind", "topic"); + request.put("destination", destination); + if (temporary) { + request.put("temporaryDestination", "y"); + } + request.put("protocol", "AMQP"); + request.put("protocolVersion", "1.0.0"); + request.put("url", "http://broker/topic"); + request.put("conversationId", "42"); + request.put("payloadSize", "100"); + request.put("payloadCompressedSize", "10"); + request.put("operation", operation.name()); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, request); + + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, request, "42"); + + // then + List, Object>> expectedEntries = new ArrayList<>(); + expectedEntries.add(entry(SemanticAttributes.MESSAGING_SYSTEM, "myQueue")); + expectedEntries.add(entry(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic")); + expectedEntries.add(entry(SemanticAttributes.MESSAGING_DESTINATION, expectedDestination)); + if (temporary) { + expectedEntries.add(entry(SemanticAttributes.MESSAGING_TEMP_DESTINATION, true)); + } + expectedEntries.add(entry(SemanticAttributes.MESSAGING_PROTOCOL, "AMQP")); + expectedEntries.add(entry(SemanticAttributes.MESSAGING_PROTOCOL_VERSION, "1.0.0")); + expectedEntries.add(entry(SemanticAttributes.MESSAGING_URL, "http://broker/topic")); + expectedEntries.add(entry(SemanticAttributes.MESSAGING_CONVERSATION_ID, "42")); + expectedEntries.add(entry(SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, 100L)); + expectedEntries.add( + entry(SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_COMPRESSED_SIZE_BYTES, 10L)); + expectedEntries.add(entry(SemanticAttributes.MESSAGING_OPERATION, operation.operationName())); + + assertThat(startAttributes.build()).containsOnly(expectedEntries.toArray(new MapEntry[0])); + + assertThat(endAttributes.build()) + .containsOnly(entry(SemanticAttributes.MESSAGING_MESSAGE_ID, "42")); + } + + static Stream destinations() { + return Stream.of( + Arguments.of(false, "destination", MessageOperation.RECEIVE, "destination"), + Arguments.of(true, null, MessageOperation.PROCESS, "(temporary)")); + } + + @Test + void shouldNotSetSendOperation() { + // when + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, singletonMap("operation", MessageOperation.SEND.name())); + + // then + assertThat(attributes.build().isEmpty()).isTrue(); + } + + @Test + void shouldExtractNoAttributesIfNoneAreAvailable() { + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, Collections.emptyMap()); + + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, Collections.emptyMap(), null); + + // then + assertThat(startAttributes.build().isEmpty()).isTrue(); + + assertThat(endAttributes.build().isEmpty()).isTrue(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractorTest.java new file mode 100644 index 000000000..d28f8a8f2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractorTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.messaging; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MessagingSpanNameExtractorTest { + @Mock MessagingAttributesExtractor attributesExtractor; + + @ParameterizedTest + @MethodSource("spanNameParams") + void shouldExtractSpanName( + boolean isTemporaryQueue, + String destinationName, + MessageOperation operation, + String expectedSpanName) { + // given + Message message = new Message(); + + if (isTemporaryQueue) { + given(attributesExtractor.temporaryDestination(message)).willReturn(true); + } else { + given(attributesExtractor.destination(message)).willReturn(destinationName); + } + given(attributesExtractor.operation(message)).willReturn(operation); + + SpanNameExtractor underTest = MessagingSpanNameExtractor.create(attributesExtractor); + + // when + String spanName = underTest.extract(message); + + // then + assertEquals(expectedSpanName, spanName); + } + + static Stream spanNameParams() { + return Stream.of( + Arguments.of(false, "destination", MessageOperation.SEND, "destination send"), + Arguments.of(true, null, MessageOperation.PROCESS, "(temporary) process"), + Arguments.of(false, null, MessageOperation.RECEIVE, "unknown receive"), + Arguments.of(false, "destination", null, "destination")); + } + + static class Message {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetAttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetAttributesExtractorTest.java new file mode 100644 index 000000000..292441d22 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetAttributesExtractorTest.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.net; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class InetSocketAddressNetAttributesExtractorTest { + + private final InetSocketAddressNetAttributesExtractor + extractor = + new InetSocketAddressNetAttributesExtractor() { + @Override + public InetSocketAddress getAddress( + InetSocketAddress request, InetSocketAddress response) { + return response != null ? response : request; + } + + @Override + public String transport(InetSocketAddress inetSocketAddress) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + }; + + @Test + void noInetSocketAddress() { + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, null); + extractor.onEnd(attributes, null, null); + assertThat(attributes.build()) + .containsOnly( + entry(SemanticAttributes.NET_TRANSPORT, SemanticAttributes.NetTransportValues.IP_TCP)); + } + + @Test + void fullAddress() { + // given + InetSocketAddress address = new InetSocketAddress("github.com", 123); + assertThat(address.getAddress().getHostAddress()).isNotNull(); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, address); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, null, address); + + // then + assertThat(startAttributes.build()) + .containsOnly( + entry(SemanticAttributes.NET_TRANSPORT, SemanticAttributes.NetTransportValues.IP_TCP), + entry(SemanticAttributes.NET_PEER_IP, address.getAddress().getHostAddress()), + entry(SemanticAttributes.NET_PEER_NAME, "github.com"), + entry(SemanticAttributes.NET_PEER_PORT, 123L)); + + assertThat(endAttributes.build()) + .containsOnly( + entry(SemanticAttributes.NET_PEER_IP, address.getAddress().getHostAddress()), + entry(SemanticAttributes.NET_PEER_NAME, "github.com"), + entry(SemanticAttributes.NET_PEER_PORT, 123L)); + } + + @Test + void unresolved() { + // given + InetSocketAddress address = InetSocketAddress.createUnresolved("github.com", 123); + assertThat(address.getAddress()).isNull(); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, address); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, null, address); + + // then + assertThat(startAttributes.build()) + .containsOnly( + entry(SemanticAttributes.NET_TRANSPORT, SemanticAttributes.NetTransportValues.IP_TCP), + entry(SemanticAttributes.NET_PEER_NAME, "github.com"), + entry(SemanticAttributes.NET_PEER_PORT, 123L)); + + assertThat(endAttributes.build()) + .containsOnly( + entry(SemanticAttributes.NET_PEER_NAME, "github.com"), + entry(SemanticAttributes.NET_PEER_PORT, 123L)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetAttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetAttributesExtractorTest.java new file mode 100644 index 000000000..a81a2e5ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetAttributesExtractorTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.net; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class NetAttributesExtractorTest { + + static class TestNetAttributesExtractor + extends NetAttributesExtractor, Map> { + + @Override + public String transport(Map request) { + return request.get("transport"); + } + + @Override + public String peerName(Map request, Map response) { + if (response != null) { + return response.get("peerName"); + } + return request.get("peerName"); + } + + @Override + public Integer peerPort(Map request, Map response) { + if (response != null) { + return Integer.valueOf(response.get("peerPort")); + } + return Integer.valueOf(request.get("peerPort")); + } + + @Override + public String peerIp(Map request, Map response) { + if (response != null) { + return response.get("peerIp"); + } + return request.get("peerIp"); + } + } + + @Test + void normal() { + // given + Map request = new HashMap<>(); + request.put("transport", "TCP"); + request.put("peerName", "github.com"); + request.put("peerPort", "123"); + request.put("peerIp", "1.2.3.4"); + + Map response = new HashMap<>(); + response.put("peerName", "opentelemetry.io"); + response.put("peerPort", "42"); + response.put("peerIp", "4.3.2.1"); + + TestNetAttributesExtractor extractor = new TestNetAttributesExtractor(); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, request); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, request, response); + + // then + assertThat(startAttributes.build()) + .containsOnly( + entry(SemanticAttributes.NET_TRANSPORT, "TCP"), + entry(SemanticAttributes.NET_PEER_NAME, "github.com"), + entry(SemanticAttributes.NET_PEER_PORT, 123L), + entry(SemanticAttributes.NET_PEER_IP, "1.2.3.4")); + + assertThat(endAttributes.build()) + .containsOnly( + entry(SemanticAttributes.NET_PEER_NAME, "opentelemetry.io"), + entry(SemanticAttributes.NET_PEER_PORT, 42L), + entry(SemanticAttributes.NET_PEER_IP, "4.3.2.1")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractorTest.java new file mode 100644 index 000000000..0050c9c27 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractorTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class RpcAttributesExtractorTest { + + static class TestExtractor extends RpcAttributesExtractor, Void> { + + @Override + protected String system(Map request) { + return "test"; + } + + @Override + protected String service(Map request) { + return request.get("service"); + } + + @Override + protected String method(Map request) { + return request.get("method"); + } + } + + @Test + void normal() { + Map request = new HashMap<>(); + request.put("service", "my.Service"); + request.put("method", "Method"); + + TestExtractor extractor = new TestExtractor(); + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, request); + assertThat(attributes.build()) + .containsOnly( + entry(SemanticAttributes.RPC_SYSTEM, "test"), + entry(SemanticAttributes.RPC_SERVICE, "my.Service"), + entry(SemanticAttributes.RPC_METHOD, "Method")); + extractor.onEnd(attributes, request, null); + assertThat(attributes.build()) + .containsOnly( + entry(SemanticAttributes.RPC_SYSTEM, "test"), + entry(SemanticAttributes.RPC_SERVICE, "my.Service"), + entry(SemanticAttributes.RPC_METHOD, "Method")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractorTest.java new file mode 100644 index 000000000..901111857 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractorTest.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.rpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class RpcSpanNameExtractorTest { + + @Mock RpcAttributesExtractor attributesExtractor; + + @Test + void normal() { + RpcRequest request = new RpcRequest(); + + when(attributesExtractor.service(request)).thenReturn("my.Service"); + when(attributesExtractor.method(request)).thenReturn("Method"); + + SpanNameExtractor extractor = RpcSpanNameExtractor.create(attributesExtractor); + assertThat(extractor.extract(request)).isEqualTo("my.Service/Method"); + } + + @Test + void serviceNull() { + RpcRequest request = new RpcRequest(); + + when(attributesExtractor.method(request)).thenReturn("Method"); + + SpanNameExtractor extractor = RpcSpanNameExtractor.create(attributesExtractor); + assertThat(extractor.extract(request)).isEqualTo("RPC request"); + } + + @Test + void methodNull() { + RpcRequest request = new RpcRequest(); + + when(attributesExtractor.service(request)).thenReturn("my.Service"); + + SpanNameExtractor extractor = RpcSpanNameExtractor.create(attributesExtractor); + assertThat(extractor.extract(request)).isEqualTo("RPC request"); + } + + static class RpcRequest {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetricsTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetricsTest.java new file mode 100644 index 000000000..0b65b7d45 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetricsTest.java @@ -0,0 +1,82 @@ +///* +// * Copyright The OpenTelemetry Authors +// * SPDX-License-Identifier: Apache-2.0 +// */ +// +//package io.opentelemetry.instrumentation.api.internal; +// +//import static org.assertj.core.api.Assertions.assertThat; +// +//import io.opentelemetry.api.trace.SpanKind; +//import io.opentelemetry.instrumentation.api.config.Config; +//import java.util.ArrayList; +//import java.util.Collections; +//import java.util.List; +//import org.junit.jupiter.api.Test; +// +//class SupportabilityMetricsTest { +// @Test +// void disabled() { +// List reports = new ArrayList<>(); +// SupportabilityMetrics metrics = +// new SupportabilityMetrics( +// Config.create(Collections.singletonMap("otel.javaagent.debug", "false")), reports::add); +// +// metrics.recordSuppressedSpan(SpanKind.CLIENT, "favoriteInstrumentation"); +// metrics.recordSuppressedSpan(SpanKind.SERVER, "favoriteInstrumentation"); +// metrics.recordSuppressedSpan(SpanKind.CLIENT, "favoriteInstrumentation"); +// metrics.recordSuppressedSpan(SpanKind.INTERNAL, "otherInstrumentation"); +// metrics.incrementCounter("some counter"); +// metrics.incrementCounter("another counter"); +// metrics.incrementCounter("some counter"); +// +// metrics.report(); +// +// assertThat(reports).isEmpty(); +// } +// +// @Test +// void reportsMetrics() { +// List reports = new ArrayList<>(); +// SupportabilityMetrics metrics = +// new SupportabilityMetrics( +// Config.create(Collections.singletonMap("otel.javaagent.debug", "true")), reports::add); +// +// metrics.recordSuppressedSpan(SpanKind.CLIENT, "favoriteInstrumentation"); +// metrics.recordSuppressedSpan(SpanKind.SERVER, "favoriteInstrumentation"); +// metrics.recordSuppressedSpan(SpanKind.CLIENT, "favoriteInstrumentation"); +// metrics.recordSuppressedSpan(SpanKind.INTERNAL, "otherInstrumentation"); +// metrics.incrementCounter("some counter"); +// metrics.incrementCounter("another counter"); +// metrics.incrementCounter("some counter"); +// +// metrics.report(); +// +// assertThat(reports) +// .containsExactlyInAnyOrder( +// "Suppressed Spans by 'favoriteInstrumentation' (CLIENT) : 2", +// "Suppressed Spans by 'favoriteInstrumentation' (SERVER) : 1", +// "Suppressed Spans by 'otherInstrumentation' (INTERNAL) : 1", +// "Counter 'some counter' : 2", +// "Counter 'another counter' : 1"); +// } +// +// @Test +// void resetsCountsEachReport() { +// List reports = new ArrayList<>(); +// SupportabilityMetrics metrics = +// new SupportabilityMetrics( +// Config.create(Collections.singletonMap("otel.javaagent.debug", "true")), reports::add); +// +// metrics.recordSuppressedSpan(SpanKind.CLIENT, "favoriteInstrumentation"); +// metrics.incrementCounter("some counter"); +// +// metrics.report(); +// metrics.report(); +// +// assertThat(reports) +// .containsExactlyInAnyOrder( +// "Suppressed Spans by 'favoriteInstrumentation' (CLIENT) : 1", +// "Counter 'some counter' : 1"); +// } +//} diff --git a/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/tracer/HttpServerTracerTest.java b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/tracer/HttpServerTracerTest.java new file mode 100644 index 000000000..fb27162af --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/tracer/HttpServerTracerTest.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.tracer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class HttpServerTracerTest { + @Test + public void extractForwardedFor() { + assertEquals("1.1.1.1", HttpServerTracer.extractForwardedFor("for=1.1.1.1")); + } + + @Test + public void extractForwardedForCaps() { + assertEquals("1.1.1.1", HttpServerTracer.extractForwardedFor("For=1.1.1.1")); + } + + @Test + public void extractForwardedForMalformed() { + assertNull(HttpServerTracer.extractForwardedFor("for=;for=1.1.1.1")); + } + + @Test + public void extractForwardedForEmpty() { + assertNull(HttpServerTracer.extractForwardedFor("")); + } + + @Test + public void extractForwardedForEmptyValue() { + assertNull(HttpServerTracer.extractForwardedFor("for=")); + } + + @Test + public void extractForwardedForEmptyValueWithSemicolon() { + assertNull(HttpServerTracer.extractForwardedFor("for=;")); + } + + @Test + public void extractForwardedForNoFor() { + assertNull(HttpServerTracer.extractForwardedFor("by=1.1.1.1;test=1.1.1.1")); + } + + @Test + public void extractForwardedForMultiple() { + assertEquals("1.1.1.1", HttpServerTracer.extractForwardedFor("for=1.1.1.1;for=1.2.3.4")); + } + + @Test + public void extractForwardedForMixedSplitter() { + assertEquals( + "1.1.1.1", + HttpServerTracer.extractForwardedFor("test=abcd; by=1.2.3.4, for=1.1.1.1;for=1.2.3.4")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/akka-actor-2.5-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/akka-actor-2.5-javaagent.gradle new file mode 100644 index 000000000..8070c11b4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/akka-actor-2.5-javaagent.gradle @@ -0,0 +1,34 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: "otel.scala-conventions" + +muzzle { + pass { + group = 'com.typesafe.akka' + module = 'akka-actor_2.11' + versions = "[2.5.0,)" + } + pass { + group = 'com.typesafe.akka' + module = 'akka-actor_2.12' + versions = "[2.5.0,)" + } + pass { + group = 'com.typesafe.akka' + module = 'akka-actor_2.13' + versions = "(,)" + } +} + +dependencies { + compileOnly "com.typesafe.akka:akka-actor_2.11:2.5.0" + testImplementation "com.typesafe.akka:akka-actor_2.11:2.5.0" + + latestDepTestLibrary "com.typesafe.akka:akka-actor_2.13:+" +} + +if (findProperty('testLatestDeps')) { + configurations { + // akka artifact name is different for regular and latest tests + testImplementation.exclude group: 'com.typesafe.akka', module: 'akka-actor_2.11' + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorCellInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorCellInstrumentation.java new file mode 100644 index 000000000..cfb8a7271 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorCellInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkaactor; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import akka.dispatch.Envelope; +import akka.dispatch.sysmsg.SystemMessage; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.AdviceUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AkkaActorCellInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("akka.actor.ActorCell"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invoke").and(takesArgument(0, named("akka.dispatch.Envelope"))), + AkkaActorCellInstrumentation.class.getName() + "$InvokeAdvice"); + transformer.applyAdviceToMethod( + named("systemInvoke").and(takesArgument(0, named("akka.dispatch.sysmsg.SystemMessage"))), + AkkaActorCellInstrumentation.class.getName() + "$SystemInvokeAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter(@Advice.Argument(0) Envelope envelope) { + ContextStore contextStore = + InstrumentationContext.get(Envelope.class, State.class); + return AdviceUtils.startTaskScope(contextStore, envelope); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } + + @SuppressWarnings("unused") + public static class SystemInvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter(@Advice.Argument(0) SystemMessage systemMessage) { + ContextStore contextStore = + InstrumentationContext.get(SystemMessage.class, State.class); + return AdviceUtils.startTaskScope(contextStore, systemMessage); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorInstrumentationModule.java new file mode 100644 index 000000000..b58a2be90 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkaactor; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class AkkaActorInstrumentationModule extends InstrumentationModule { + public AkkaActorInstrumentationModule() { + super("akka-actor", "akka-actor-2.5"); + } + + @Override + public List typeInstrumentations() { + return asList( + new AkkaDispatcherInstrumentation(), + new AkkaActorCellInstrumentation(), + new AkkaDefaultSystemMessageQueueInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaDefaultSystemMessageQueueInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaDefaultSystemMessageQueueInstrumentation.java new file mode 100644 index 000000000..0cf9bd59f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaDefaultSystemMessageQueueInstrumentation.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkaactor; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import akka.dispatch.sysmsg.SystemMessage; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AkkaDefaultSystemMessageQueueInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("akka.dispatch.DefaultSystemMessageQueue")); + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("akka.dispatch.DefaultSystemMessageQueue"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("systemEnqueue") + .and(takesArgument(0, named("akka.actor.ActorRef"))) + .and(takesArgument(1, named("akka.dispatch.sysmsg.SystemMessage"))), + AkkaDefaultSystemMessageQueueInstrumentation.class.getName() + "$DispatchSystemAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchSystemAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enter(@Advice.Argument(1) SystemMessage systemMessage) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(systemMessage)) { + ContextStore contextStore = + InstrumentationContext.get(SystemMessage.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, systemMessage, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaDispatcherInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaDispatcherInstrumentation.java new file mode 100644 index 000000000..96cafba63 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaDispatcherInstrumentation.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkaactor; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import akka.dispatch.Envelope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AkkaDispatcherInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("akka.dispatch.Dispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("dispatch") + .and(takesArgument(0, named("akka.actor.ActorCell"))) + .and(takesArgument(1, named("akka.dispatch.Envelope"))), + AkkaDispatcherInstrumentation.class.getName() + "$DispatchEnvelopeAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchEnvelopeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterDispatch(@Advice.Argument(1) Envelope envelope) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(envelope)) { + ContextStore contextStore = + InstrumentationContext.get(Envelope.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, envelope, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitDispatch(@Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/test/groovy/AkkaActorTest.groovy b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/test/groovy/AkkaActorTest.groovy new file mode 100644 index 000000000..d117260f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/test/groovy/AkkaActorTest.groovy @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +class AkkaActorTest extends AgentInstrumentationSpecification { + + def "akka #testMethod #count"() { + setup: + AkkaActors akkaTester = new AkkaActors() + count.times { + akkaTester."$testMethod"() + } + + expect: + assertTraces(count) { + count.times { + trace(it, 2) { + span(0) { + name "parent" + attributes { + } + } + span(1) { + name "$expectedGreeting, Akka" + childOf span(0) + attributes { + } + } + } + } + } + + where: + testMethod | expectedGreeting | count + "basicTell" | "Howdy" | 1 + "basicAsk" | "Howdy" | 1 + "basicForward" | "Hello" | 1 + "basicTell" | "Howdy" | 150 + "basicAsk" | "Howdy" | 150 + "basicForward" | "Hello" | 150 + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/test/scala/AkkaActors.scala b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/test/scala/AkkaActors.scala new file mode 100644 index 000000000..d4fce2f9a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-2.5/javaagent/src/test/scala/AkkaActors.scala @@ -0,0 +1,138 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props} +import akka.pattern.ask +import akka.util.Timeout +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.javaagent.testing.common.Java8BytecodeBridge + +import scala.concurrent.duration._ + +// ! == send-message +object AkkaActors { + val tracer: Tracer = GlobalOpenTelemetry.getTracer("test") + + val system: ActorSystem = ActorSystem("helloAkka") + + val printer: ActorRef = system.actorOf(Receiver.props, "receiverActor") + + val howdyGreeter: ActorRef = + system.actorOf(Greeter.props("Howdy", printer), "howdyGreeter") + + val forwarder: ActorRef = + system.actorOf(Forwarder.props(printer), "forwarderActor") + val helloGreeter: ActorRef = + system.actorOf(Greeter.props("Hello", forwarder), "helloGreeter") + + def tracedChild(opName: String): Unit = { + tracer.spanBuilder(opName).startSpan().end() + } +} + +class AkkaActors { + + import AkkaActors._ + import Greeter._ + + implicit val timeout: Timeout = 5.minutes + + def basicTell(): Unit = { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = + Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent() + try { + howdyGreeter ! WhoToGreet("Akka") + howdyGreeter ! Greet + } finally { + parentSpan.end() + parentScope.close() + } + } + + def basicAsk(): Unit = { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = + Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent() + try { + howdyGreeter ! WhoToGreet("Akka") + howdyGreeter ? Greet + } finally { + parentSpan.end() + parentScope.close() + } + } + + def basicForward(): Unit = { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = + Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent() + try { + helloGreeter ! WhoToGreet("Akka") + helloGreeter ? Greet + } finally { + parentSpan.end() + parentScope.close() + } + } +} + +object Greeter { + def props(message: String, receiverActor: ActorRef): Props = + Props(new Greeter(message, receiverActor)) + + final case class WhoToGreet(who: String) + + case object Greet + +} + +class Greeter(message: String, receiverActor: ActorRef) extends Actor { + + import Greeter._ + import Receiver._ + + var greeting = "" + + def receive = { + case WhoToGreet(who) => + greeting = s"$message, $who" + case Greet => + receiverActor ! Greeting(greeting) + } +} + +object Receiver { + def props: Props = Props[Receiver] + + final case class Greeting(greeting: String) + +} + +class Receiver extends Actor with ActorLogging { + + import Receiver._ + + def receive = { + case Greeting(greeting) => { + AkkaActors.tracedChild(greeting) + } + + } +} + +object Forwarder { + def props(receiverActor: ActorRef): Props = + Props(new Forwarder(receiverActor)) +} + +class Forwarder(receiverActor: ActorRef) extends Actor with ActorLogging { + def receive = { + case msg => { + receiverActor forward msg + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/akka-actor-fork-join-2.5-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/akka-actor-fork-join-2.5-javaagent.gradle new file mode 100644 index 000000000..d18debe27 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/akka-actor-fork-join-2.5-javaagent.gradle @@ -0,0 +1,14 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: "otel.scala-conventions" + +muzzle { + pass { + group = 'com.typesafe.akka' + module = 'akka-actor_2.11' + versions = "[2.5.0,)" + } +} + +dependencies { + library "com.typesafe.akka:akka-actor_2.11:2.5.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorForkJoinInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorForkJoinInstrumentationModule.java new file mode 100644 index 000000000..fa7552ae9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaActorForkJoinInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkaactor; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class AkkaActorForkJoinInstrumentationModule extends InstrumentationModule { + public AkkaActorForkJoinInstrumentationModule() { + super("akka-actor", "akka-actor-fork-join", "akka-actor-2.5", "akka-actor-fork-join-2.5"); + } + + @Override + public List typeInstrumentations() { + return asList(new AkkaForkJoinPoolInstrumentation(), new AkkaForkJoinTaskInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinPoolInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinPoolInstrumentation.java new file mode 100644 index 000000000..e9c640900 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinPoolInstrumentation.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkaactor; + +import static net.bytebuddy.matcher.ElementMatchers.nameMatches; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import akka.dispatch.forkjoin.ForkJoinTask; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AkkaForkJoinPoolInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + // This might need to be an extendsClass matcher... + return named("akka.dispatch.forkjoin.ForkJoinPool"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute") + .and(takesArgument(0, named(AkkaForkJoinTaskInstrumentation.TASK_CLASS_NAME))), + AkkaForkJoinPoolInstrumentation.class.getName() + "$SetAkkaForkJoinStateAdvice"); + transformer.applyAdviceToMethod( + named("submit") + .and(takesArgument(0, named(AkkaForkJoinTaskInstrumentation.TASK_CLASS_NAME))), + AkkaForkJoinPoolInstrumentation.class.getName() + "$SetAkkaForkJoinStateAdvice"); + transformer.applyAdviceToMethod( + nameMatches("invoke") + .and(takesArgument(0, named(AkkaForkJoinTaskInstrumentation.TASK_CLASS_NAME))), + AkkaForkJoinPoolInstrumentation.class.getName() + "$SetAkkaForkJoinStateAdvice"); + } + + @SuppressWarnings("unused") + public static class SetAkkaForkJoinStateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterJobSubmit( + @Advice.Argument(value = 0, readOnly = false) ForkJoinTask task) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(task)) { + ContextStore contextStore = + InstrumentationContext.get(ForkJoinTask.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, task, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitJobSubmit( + @Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinTaskInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinTaskInstrumentation.java new file mode 100644 index 000000000..14b3fe3a2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinTaskInstrumentation.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkaactor; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import akka.dispatch.forkjoin.ForkJoinPool; +import akka.dispatch.forkjoin.ForkJoinTask; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.AdviceUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import java.util.concurrent.Callable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Instrument {@link ForkJoinTask}. + * + *

Note: There are quite a few separate implementations of {@code ForkJoinTask}/{@code + * ForkJoinPool}: JVM, Akka, Scala, Netty to name a few. This class handles Akka version. + */ +public class AkkaForkJoinTaskInstrumentation implements TypeInstrumentation { + static final String TASK_CLASS_NAME = "akka.dispatch.forkjoin.ForkJoinTask"; + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed(TASK_CLASS_NAME); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named(TASK_CLASS_NAME)); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("exec").and(takesArguments(0)).and(not(isAbstract())), + AkkaForkJoinTaskInstrumentation.class.getName() + "$ForkJoinTaskAdvice"); + } + + @SuppressWarnings("unused") + public static class ForkJoinTaskAdvice { + + /** + * When {@link ForkJoinTask} object is submitted to {@link ForkJoinPool} as {@link Runnable} or + * {@link Callable} it will not get wrapped, instead it will be casted to {@code ForkJoinTask} + * directly. This means state is still stored in {@code Runnable} or {@code Callable} and we + * need to use that state. + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter(@Advice.This ForkJoinTask thiz) { + ContextStore, State> contextStore = + InstrumentationContext.get(ForkJoinTask.class, State.class); + Scope scope = AdviceUtils.startTaskScope(contextStore, thiz); + if (thiz instanceof Runnable) { + ContextStore runnableContextStore = + InstrumentationContext.get(Runnable.class, State.class); + Scope newScope = AdviceUtils.startTaskScope(runnableContextStore, (Runnable) thiz); + if (null != newScope) { + if (null != scope) { + newScope.close(); + } else { + scope = newScope; + } + } + } + if (thiz instanceof Callable) { + ContextStore, State> callableContextStore = + InstrumentationContext.get(Callable.class, State.class); + Scope newScope = AdviceUtils.startTaskScope(callableContextStore, (Callable) thiz); + if (null != newScope) { + if (null != scope) { + newScope.close(); + } else { + scope = newScope; + } + } + } + return scope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/test/groovy/AkkaExecutorInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/test/groovy/AkkaExecutorInstrumentationTest.groovy new file mode 100644 index 000000000..83c461e04 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/test/groovy/AkkaExecutorInstrumentationTest.groovy @@ -0,0 +1,143 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import akka.dispatch.forkjoin.ForkJoinPool +import akka.dispatch.forkjoin.ForkJoinTask +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.lang.reflect.InvocationTargetException +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.Callable +import java.util.concurrent.Future +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import spock.lang.Shared + +/** + * Test executor instrumentation for Akka specific classes. + * This is to large extent a copy of ExecutorInstrumentationTest. + */ +class AkkaExecutorInstrumentationTest extends AgentInstrumentationSpecification { + + @Shared + def executeRunnable = { e, c -> e.execute((Runnable) c) } + @Shared + def akkaExecuteForkJoinTask = { e, c -> e.execute((ForkJoinTask) c) } + @Shared + def submitRunnable = { e, c -> e.submit((Runnable) c) } + @Shared + def submitCallable = { e, c -> e.submit((Callable) c) } + @Shared + def akkaSubmitForkJoinTask = { e, c -> e.submit((ForkJoinTask) c) } + @Shared + def akkaInvokeForkJoinTask = { e, c -> e.invoke((ForkJoinTask) c) } + + def "#poolName '#name' propagates"() { + setup: + def pool = poolImpl + def m = method + + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + // this child will have a span + def child1 = new AkkaAsyncChild() + // this child won't + def child2 = new AkkaAsyncChild(false, false) + m(pool, child1) + m(pool, child2) + child1.waitForCompletion() + child2.waitForCompletion() + } + } + }.run() + + expect: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "asyncChild", span(0)) + } + } + + cleanup: + pool?.shutdown() + pool?.awaitTermination(10, TimeUnit.SECONDS) + + // Unfortunately, there's no simple way to test the cross product of methods/pools. + where: + name | method | poolImpl + "execute Runnable" | executeRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "submit Runnable" | submitRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "submit Callable" | submitCallable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + + // ForkJoinPool has additional set of method overloads for ForkJoinTask to deal with + "execute Runnable" | executeRunnable | new ForkJoinPool() + "execute ForkJoinTask" | akkaExecuteForkJoinTask | new ForkJoinPool() + "submit Runnable" | submitRunnable | new ForkJoinPool() + "submit Callable" | submitCallable | new ForkJoinPool() + "submit ForkJoinTask" | akkaSubmitForkJoinTask | new ForkJoinPool() + "invoke ForkJoinTask" | akkaInvokeForkJoinTask | new ForkJoinPool() + poolName = poolImpl.class.name + } + + def "ForkJoinPool '#name' reports after canceled jobs"() { + setup: + def pool = poolImpl + def m = method + List children = new ArrayList<>() + List jobFutures = new ArrayList<>() + + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + try { + for (int i = 0; i < 20; ++i) { + // Our current instrumentation instrumentation does not behave very well + // if we try to reuse Callable/Runnable. Namely we would be getting 'orphaned' + // child traces sometimes since state can contain only one parent span - and + // we do not really have a good way for attributing work to correct parent span + // if we reuse Callable/Runnable. + // Solution for now is to never reuse a Callable/Runnable. + AkkaAsyncChild child = new AkkaAsyncChild(false, true) + children.add(child) + try { + Future f = m(pool, child) + jobFutures.add(f) + } catch (InvocationTargetException e) { + throw e.getCause() + } + } + } catch (RejectedExecutionException ignored) { + } + + for (Future f : jobFutures) { + f.cancel(false) + } + for (AkkaAsyncChild child : children) { + child.unblock() + } + } + } + }.run() + + expect: + waitForTraces(1).size() == 1 + + cleanup: + pool?.shutdown() + pool?.awaitTermination(10, TimeUnit.SECONDS) + + where: + name | method | poolImpl + "submit Runnable" | submitRunnable | new ForkJoinPool() + "submit Callable" | submitCallable | new ForkJoinPool() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/test/java/AkkaAsyncChild.java b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/test/java/AkkaAsyncChild.java new file mode 100644 index 000000000..81b0f5ce0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-actor-fork-join-2.5/javaagent/src/test/java/AkkaAsyncChild.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import akka.dispatch.forkjoin.ForkJoinTask; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +public class AkkaAsyncChild extends ForkJoinTask implements Runnable, Callable { + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + private final AtomicBoolean blockThread; + private final boolean doTraceableWork; + private final CountDownLatch latch = new CountDownLatch(1); + + public AkkaAsyncChild() { + this(/* doTraceableWork= */ true, /* blockThread= */ false); + } + + public AkkaAsyncChild(boolean doTraceableWork, boolean blockThread) { + this.doTraceableWork = doTraceableWork; + this.blockThread = new AtomicBoolean(blockThread); + } + + @Override + public Object getRawResult() { + return null; + } + + @Override + protected void setRawResult(Object value) {} + + @Override + protected boolean exec() { + runImpl(); + return true; + } + + public void unblock() { + blockThread.set(false); + } + + @Override + public void run() { + runImpl(); + } + + @Override + public Object call() { + runImpl(); + return null; + } + + public void waitForCompletion() throws InterruptedException { + latch.await(); + } + + private void runImpl() { + while (blockThread.get()) { + // busy-wait to block thread + } + if (doTraceableWork) { + asyncChild(); + } + latch.countDown(); + } + + private static void asyncChild() { + tracer.spanBuilder("asyncChild").startSpan().end(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/akka-http-10.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/akka-http-10.0-javaagent.gradle new file mode 100644 index 000000000..c30a34dbf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/akka-http-10.0-javaagent.gradle @@ -0,0 +1,51 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: "otel.scala-conventions" + +muzzle { + pass { + group = 'com.typesafe.akka' + module = 'akka-http_2.11' + versions = "[10.0.0,10.1.0)" + // later versions of akka-http expect streams to be provided + extraDependency 'com.typesafe.akka:akka-stream_2.11:2.4.14' + } + pass { + group = 'com.typesafe.akka' + module = 'akka-http_2.12' + versions = "[10.0.0,10.1.0)" + // later versions of akka-http expect streams to be provided + extraDependency 'com.typesafe.akka:akka-stream_2.12:2.4.14' + } + pass { + group = 'com.typesafe.akka' + module = 'akka-http_2.11' + versions = "[10.1.0,)" + // later versions of akka-http expect streams to be provided + extraDependency 'com.typesafe.akka:akka-stream_2.11:2.5.11' + } + pass { + group = 'com.typesafe.akka' + module = 'akka-http_2.12' + versions = "[10.1.0,)" + // later versions of akka-http expect streams to be provided + extraDependency 'com.typesafe.akka:akka-stream_2.12:2.5.11' + } + //There is no akka-http 10.0.x series for scala 2.13 + pass { + group = 'com.typesafe.akka' + module = 'akka-http_2.13' + versions = "[10.1.8,)" + // later versions of akka-http expect streams to be provided + extraDependency 'com.typesafe.akka:akka-stream_2.13:2.5.23' + } +} + +dependencies { + library "com.typesafe.akka:akka-http_2.11:10.0.0" + library "com.typesafe.akka:akka-stream_2.11:2.4.14" + + // these instrumentations are not needed for the tests to pass + // they are here to test for context leaks + testInstrumentation project(':instrumentation:akka-actor-2.5:javaagent') + testInstrumentation project(':instrumentation:akka-actor-fork-join-2.5:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientInstrumentationModule.java new file mode 100644 index 000000000..600ee5dd7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class AkkaHttpClientInstrumentationModule extends InstrumentationModule { + public AkkaHttpClientInstrumentationModule() { + super("akka-http", "akka-http-client"); + } + + @Override + public List typeInstrumentations() { + return asList(new HttpExtClientInstrumentation(), new PoolMasterActorInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientTracer.java new file mode 100644 index 000000000..21b55ce16 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientTracer.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.HttpHeaderSetter.SETTER; + +import akka.http.javadsl.model.HttpHeader; +import akka.http.scaladsl.model.HttpRequest; +import akka.http.scaladsl.model.HttpResponse; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import java.net.URISyntaxException; + +public class AkkaHttpClientTracer + extends HttpClientTracer { + private static final AkkaHttpClientTracer TRACER = new AkkaHttpClientTracer(); + + private AkkaHttpClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static AkkaHttpClientTracer tracer() { + return TRACER; + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.method().value(); + } + + @Override + protected URI url(HttpRequest httpRequest) throws URISyntaxException { + return new URI(httpRequest.uri().toString()); + } + + @Override + protected String flavor(HttpRequest httpRequest) { + return httpRequest.protocol().value(); + } + + @Override + protected Integer status(HttpResponse httpResponse) { + return httpResponse.status().intValue(); + } + + @Override + protected String requestHeader(HttpRequest httpRequest, String name) { + return httpRequest.getHeader(name).map(HttpHeader::value).orElse(null); + } + + @Override + protected String responseHeader(HttpResponse httpResponse, String name) { + return httpResponse.getHeader(name).map(HttpHeader::value).orElse(null); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.akka-http-10.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpHeaders.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpHeaders.java new file mode 100644 index 000000000..cccdb184e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpHeaders.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import akka.http.scaladsl.model.HttpRequest; + +public class AkkaHttpHeaders { + private HttpRequest request; + + public AkkaHttpHeaders(HttpRequest request) { + this.request = request; + } + + public HttpRequest getRequest() { + return request; + } + + public void setRequest(HttpRequest request) { + this.request = request; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/FutureWrapper.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/FutureWrapper.java new file mode 100644 index 000000000..47e2ba30f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/FutureWrapper.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import scala.concurrent.ExecutionContext; +import scala.concurrent.Future; +import scala.concurrent.impl.Promise; +import scala.runtime.AbstractFunction1; +import scala.util.Try; + +public class FutureWrapper { + public static Future wrap( + Future future, ExecutionContext executionContext, Context context) { + Promise.DefaultPromise promise = new Promise.DefaultPromise<>(); + future.onComplete( + new AbstractFunction1, Object>() { + + @Override + public Object apply(Try result) { + try (Scope ignored = context.makeCurrent()) { + return promise.complete(result); + } + } + }, + executionContext); + + return promise; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java new file mode 100644 index 000000000..056d1ef29 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.AkkaHttpClientTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import akka.http.scaladsl.HttpExt; +import akka.http.scaladsl.model.HttpRequest; +import akka.http.scaladsl.model.HttpResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import scala.concurrent.Future; + +public class HttpExtClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("akka.http.scaladsl.HttpExt"); + } + + @Override + public void transform(TypeTransformer transformer) { + // This is mainly for compatibility with 10.0 + transformer.applyAdviceToMethod( + named("singleRequest").and(takesArgument(0, named("akka.http.scaladsl.model.HttpRequest"))), + this.getClass().getName() + "$SingleRequestAdvice"); + // This is for 10.1+ + transformer.applyAdviceToMethod( + named("singleRequestImpl") + .and(takesArgument(0, named("akka.http.scaladsl.model.HttpRequest"))), + this.getClass().getName() + "$SingleRequestAdvice"); + } + + @SuppressWarnings("unused") + public static class SingleRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(value = 0, readOnly = false) HttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + /* + Versions 10.0 and 10.1 have slightly different structure that is hard to distinguish so here + we cast 'wider net' and avoid instrumenting twice. + In the future we may want to separate these, but since lots of code is reused we would need to come up + with way of continuing to reusing it. + */ + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + // Request is immutable, so we have to assign new value once we update headers + AkkaHttpHeaders headers = new AkkaHttpHeaders(request); + context = tracer().startSpan(parentContext, request, headers); + scope = context.makeCurrent(); + request = headers.getRequest(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) HttpRequest request, + @Advice.This HttpExt thiz, + @Advice.Return(readOnly = false) Future responseFuture, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable == null) { + responseFuture.onComplete(new OnCompleteHandler(context), thiz.system().dispatcher()); + } else { + tracer().endExceptionally(context, throwable); + } + if (responseFuture != null) { + responseFuture = + FutureWrapper.wrap(responseFuture, thiz.system().dispatcher(), currentContext()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpHeaderSetter.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpHeaderSetter.java new file mode 100644 index 000000000..2ed718526 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpHeaderSetter.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import akka.http.javadsl.model.headers.RawHeader; +import akka.http.scaladsl.model.HttpRequest; +import io.opentelemetry.context.propagation.TextMapSetter; + +public class HttpHeaderSetter implements TextMapSetter { + + public static final HttpHeaderSetter SETTER = new HttpHeaderSetter(); + + @Override + public void set(AkkaHttpHeaders carrier, String key, String value) { + HttpRequest request = carrier.getRequest(); + if (request != null) { + // It looks like this cast is only needed in Java, Scala would have figured it out + carrier.setRequest( + (HttpRequest) request.removeHeader(key).addHeader(RawHeader.create(key, value))); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/OnCompleteHandler.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/OnCompleteHandler.java new file mode 100644 index 000000000..e690c9aeb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/OnCompleteHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.AkkaHttpClientTracer.tracer; + +import akka.http.scaladsl.model.HttpResponse; +import io.opentelemetry.context.Context; +import scala.runtime.AbstractFunction1; +import scala.util.Try; + +public class OnCompleteHandler extends AbstractFunction1, Void> { + private final Context context; + + public OnCompleteHandler(Context context) { + this.context = context; + } + + @Override + public Void apply(Try result) { + if (result.isSuccess()) { + tracer().end(context, result.get()); + } else { + tracer().endExceptionally(context, result.failed().get()); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/PoolMasterActorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/PoolMasterActorInstrumentation.java new file mode 100644 index 000000000..7c105ab08 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/PoolMasterActorInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class PoolMasterActorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("akka.http.impl.engine.client.PoolMasterActor"); + } + + @Override + public void transform(TypeTransformer transformer) { + // scala compiler mangles method names + transformer.applyAdviceToMethod( + named("akka$http$impl$engine$client$PoolMasterActor$$startPoolInterface") + .or(named("akka$http$impl$engine$client$PoolMasterActor$$startPoolInterfaceActor")), + ClearContextAdvice.class.getName()); + } + + @SuppressWarnings("unused") + public static class ClearContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter() { + return Java8BytecodeBridge.rootContext().makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerHeaders.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerHeaders.java new file mode 100644 index 000000000..e2f9cb606 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerHeaders.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server; + +import akka.http.javadsl.model.HttpHeader; +import akka.http.scaladsl.model.HttpRequest; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class AkkaHttpServerHeaders implements TextMapGetter { + + public static final AkkaHttpServerHeaders GETTER = new AkkaHttpServerHeaders(); + + @Override + public Iterable keys(HttpRequest httpRequest) { + return StreamSupport.stream(httpRequest.getHeaders().spliterator(), false) + .map(HttpHeader::lowercaseName) + .collect(Collectors.toList()); + } + + @Override + public String get(HttpRequest carrier, String key) { + Optional header = carrier.getHeader(key); + return header.map(HttpHeader::value).orElse(null); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerInstrumentationModule.java new file mode 100644 index 000000000..4e4b3a5fc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerInstrumentationModule.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server; + +import static io.opentelemetry.javaagent.instrumentation.akkahttp.server.AkkaHttpServerTracer.tracer; +import static java.util.Collections.singletonList; + +import akka.http.scaladsl.model.HttpRequest; +import akka.http.scaladsl.model.HttpResponse; +import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import scala.Function1; +import scala.concurrent.ExecutionContext; +import scala.concurrent.Future; +import scala.runtime.AbstractFunction1; + +@AutoService(InstrumentationModule.class) +public class AkkaHttpServerInstrumentationModule extends InstrumentationModule { + public AkkaHttpServerInstrumentationModule() { + super("akka-http", "akka-http-server"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpExtServerInstrumentation()); + } + + public static class SyncWrapper extends AbstractFunction1 { + private final Function1 userHandler; + + public SyncWrapper(Function1 userHandler) { + this.userHandler = userHandler; + } + + @Override + public HttpResponse apply(HttpRequest request) { + Context ctx = tracer().startSpan(request, request, null, "akka.request"); + try (Scope ignored = ctx.makeCurrent()) { + HttpResponse response = userHandler.apply(request); + tracer().end(ctx, response); + return response; + } catch (Throwable t) { + tracer().endExceptionally(ctx, t); + throw t; + } + } + } + + public static class AsyncWrapper extends AbstractFunction1> { + private final Function1> userHandler; + private final ExecutionContext executionContext; + + public AsyncWrapper( + Function1> userHandler, + ExecutionContext executionContext) { + this.userHandler = userHandler; + this.executionContext = executionContext; + } + + @Override + public Future apply(HttpRequest request) { + Context ctx = tracer().startSpan(request, request, null, "akka.request"); + try (Scope ignored = ctx.makeCurrent()) { + return userHandler + .apply(request) + .transform( + new AbstractFunction1() { + @Override + public HttpResponse apply(HttpResponse response) { + tracer().end(ctx, response); + return response; + } + }, + new AbstractFunction1() { + @Override + public Throwable apply(Throwable t) { + tracer().endExceptionally(ctx, t); + return t; + } + }, + executionContext); + } catch (Throwable t) { + tracer().endExceptionally(ctx, t); + throw t; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerTracer.java new file mode 100644 index 000000000..a0a76dbe6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerTracer.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server; + +import akka.http.javadsl.model.HttpHeader; +import akka.http.scaladsl.model.HttpRequest; +import akka.http.scaladsl.model.HttpResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; + +public class AkkaHttpServerTracer + extends HttpServerTracer { + private static final AkkaHttpServerTracer TRACER = new AkkaHttpServerTracer(); + + public static AkkaHttpServerTracer tracer() { + return TRACER; + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.method().value(); + } + + @Override + protected String requestHeader(HttpRequest httpRequest, String name) { + return httpRequest.getHeader(name).map(HttpHeader::value).orElse(null); + } + + @Override + protected int responseStatus(HttpResponse httpResponse) { + return httpResponse.status().intValue(); + } + + @Override + protected void attachServerContext(Context context, Void none) {} + + @Override + public Context getServerContext(Void none) { + return null; + } + + @Override + protected String url(HttpRequest httpRequest) { + return httpRequest.uri().toString(); + } + + @Override + protected String peerHostIP(HttpRequest httpRequest) { + return null; + } + + @Override + protected String flavor(HttpRequest connection, HttpRequest request) { + return connection.protocol().value(); + } + + @Override + protected TextMapGetter getGetter() { + return AkkaHttpServerHeaders.GETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.akka-http-10.0"; + } + + @Override + protected Integer peerPort(HttpRequest httpRequest) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/HttpExtServerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/HttpExtServerInstrumentation.java new file mode 100644 index 000000000..06d727306 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/HttpExtServerInstrumentation.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import akka.http.scaladsl.model.HttpRequest; +import akka.http.scaladsl.model.HttpResponse; +import akka.stream.Materializer; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import scala.Function1; +import scala.concurrent.Future; + +public class HttpExtServerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("akka.http.scaladsl.HttpExt"); + } + + @Override + public void transform(TypeTransformer transformer) { + // Instrumenting akka-streams bindAndHandle api was previously attempted. + // This proved difficult as there was no clean way to close the async scope + // in the graph logic after the user's request handler completes. + // + // Instead, we're instrumenting the bindAndHandle function helpers by + // wrapping the scala functions with our own handlers. + transformer.applyAdviceToMethod( + named("bindAndHandleSync").and(takesArgument(0, named("scala.Function1"))), + this.getClass().getName() + "$AkkaHttpSyncAdvice"); + transformer.applyAdviceToMethod( + named("bindAndHandleAsync").and(takesArgument(0, named("scala.Function1"))), + this.getClass().getName() + "$AkkaHttpAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class AkkaHttpSyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.Argument(value = 0, readOnly = false) + Function1 handler) { + handler = new AkkaHttpServerInstrumentationModule.SyncWrapper(handler); + } + } + + @SuppressWarnings("unused") + public static class AkkaHttpAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.Argument(value = 0, readOnly = false) + Function1> handler, + @Advice.Argument(7) Materializer materializer) { + handler = + new AkkaHttpServerInstrumentationModule.AsyncWrapper( + handler, materializer.executionContext()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy new file mode 100644 index 000000000..b21daaac4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import akka.actor.ActorSystem +import akka.http.javadsl.Http +import akka.http.javadsl.model.HttpMethods +import akka.http.javadsl.model.HttpRequest +import akka.http.javadsl.model.HttpResponse +import akka.http.javadsl.model.headers.RawHeader +import akka.stream.ActorMaterializer +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import java.util.concurrent.TimeUnit +import spock.lang.Shared + +class AkkaHttpClientInstrumentationTest extends HttpClientTest implements AgentTestTrait { + + @Shared + ActorSystem system = ActorSystem.create() + @Shared + ActorMaterializer materializer = ActorMaterializer.create(system) + + @Override + HttpRequest buildRequest(String method, URI uri, Map headers) { + return HttpRequest.create(uri.toString()) + .withMethod(HttpMethods.lookup(method).get()) + .addHeaders(headers.collect { RawHeader.create(it.key, it.value) }) + } + + @Override + int sendRequest(HttpRequest request, String method, URI uri, Map headers) { + HttpResponse response = Http.get(system) + .singleRequest(request, materializer) + .toCompletableFuture() + .get(10, TimeUnit.SECONDS) + + response.discardEntityBytes(materializer) + + return response.status().intValue() + } + + @Override + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + Http.get(system).singleRequest(request, materializer).whenComplete {response, throwable -> + if (throwable == null) { + response.discardEntityBytes(materializer) + } + requestResult.complete({ response.status().intValue() }, throwable) + } + } + + @Override + boolean testRedirects() { + false + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + // singleConnection test would require instrumentation to support requests made through pools + // (newHostConnectionPool, superPool, etc), which is currently not supported. + return null + } + + def "singleRequest exception trace"() { + when: + // Passing null causes NPE in singleRequest + Http.get(system).singleRequest(null, materializer) + + then: + def e = thrown NullPointerException + assertTraces(1) { + trace(0, 1) { + span(0) { + hasNoParent() + name "HTTP request" + kind CLIENT + status ERROR + errorEvent(NullPointerException, e.getMessage()) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpServerInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpServerInstrumentationTest.groovy new file mode 100644 index 000000000..044c43c90 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpServerInstrumentationTest.groovy @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest + +abstract class AkkaHttpServerInstrumentationTest extends HttpServerTest implements AgentTestTrait { + +// FIXME: This doesn't work because we don't support bindAndHandle. +// @Override +// def startServer(int port) { +// AkkaHttpTestWebServer.start(port) +// } +// +// @Override +// void stopServer(Object ignore) { +// AkkaHttpTestWebServer.stop() +// } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "akka.request" + } + + @Override + boolean testConcurrency() { + return true + } +} + +class AkkaHttpServerInstrumentationTestSync extends AkkaHttpServerInstrumentationTest { + @Override + def startServer(int port) { + AkkaHttpTestSyncWebServer.start(port) + } + + @Override + void stopServer(Object ignore) { + AkkaHttpTestSyncWebServer.stop() + } +} + +class AkkaHttpServerInstrumentationTestAsync extends AkkaHttpServerInstrumentationTest { + @Override + def startServer(int port) { + AkkaHttpTestAsyncWebServer.start(port) + } + + @Override + void stopServer(Object ignore) { + AkkaHttpTestAsyncWebServer.stop() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/resources/application.conf b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/resources/application.conf new file mode 100644 index 000000000..e2391d6a4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/resources/application.conf @@ -0,0 +1,11 @@ +akka.http { + host-connection-pool { + // Limit maximum http backoff for tests + max-connection-backoff = 100ms + max-open-requests = 1024 + max-retries = 0 + client { + connecting-timeout = 5s + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestAsyncWebServer.scala b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestAsyncWebServer.scala new file mode 100644 index 000000000..bdebd6b47 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestAsyncWebServer.scala @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.Http.ServerBinding +import akka.http.scaladsl.model.HttpMethods.GET +import akka.http.scaladsl.model._ +import akka.stream.ActorMaterializer +import groovy.lang.Closure +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint._ + +import scala.concurrent.{Await, ExecutionContextExecutor, Future} + +object AkkaHttpTestAsyncWebServer { + implicit val system: ActorSystem = ActorSystem("my-system") + implicit val materializer: ActorMaterializer = ActorMaterializer() + // needed for the future flatMap/onComplete in the end + implicit val executionContext: ExecutionContextExecutor = system.dispatcher + val asyncHandler: HttpRequest => Future[HttpResponse] = { + case HttpRequest(GET, uri: Uri, _, _, _) => + Future { + val endpoint = + HttpServerTest.ServerEndpoint.forPath(uri.path.toString()) + HttpServerTest.controller( + endpoint, + new Closure[HttpResponse](()) { + def doCall(): HttpResponse = { + val resp = HttpResponse(status = endpoint.getStatus) //.withHeaders(headers.Type)resp.contentType = "text/plain" + endpoint match { + case SUCCESS => resp.withEntity(endpoint.getBody) + case INDEXED_CHILD => + INDEXED_CHILD.collectSpanAttributes(new UrlParameterProvider { + override def getParameter(name: String): String = + uri.query().get(name).orNull + }) + resp.withEntity("") + case QUERY_PARAM => resp.withEntity(uri.queryString().orNull) + case REDIRECT => + resp.withHeaders(headers.Location(endpoint.getBody)) + case ERROR => resp.withEntity(endpoint.getBody) + case EXCEPTION => throw new Exception(endpoint.getBody) + case _ => + HttpResponse(status = NOT_FOUND.getStatus) + .withEntity(NOT_FOUND.getBody) + } + } + } + ) + } + } + + private var binding: ServerBinding = _ + + def start(port: Int): Unit = synchronized { + if (null == binding) { + import scala.concurrent.duration._ + binding = Await.result( + Http().bindAndHandleAsync(asyncHandler, "localhost", port), + 10 seconds + ) + } + } + + def stop(): Unit = synchronized { + if (null != binding) { + binding.unbind() + system.terminate() + binding = null + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestSyncWebServer.scala b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestSyncWebServer.scala new file mode 100644 index 000000000..bae57cd00 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestSyncWebServer.scala @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.Http.ServerBinding +import akka.http.scaladsl.model.HttpMethods.GET +import akka.http.scaladsl.model._ +import akka.stream.ActorMaterializer +import groovy.lang.Closure +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint._ + +import scala.concurrent.Await + +object AkkaHttpTestSyncWebServer { + implicit val system = ActorSystem("my-system") + implicit val materializer = ActorMaterializer() + // needed for the future flatMap/onComplete in the end + implicit val executionContext = system.dispatcher + val syncHandler: HttpRequest => HttpResponse = { + case HttpRequest(GET, uri: Uri, _, _, _) => { + val endpoint = HttpServerTest.ServerEndpoint.forPath(uri.path.toString()) + HttpServerTest.controller( + endpoint, + new Closure[HttpResponse](()) { + def doCall(): HttpResponse = { + val resp = HttpResponse(status = endpoint.getStatus) + endpoint match { + case SUCCESS => resp.withEntity(endpoint.getBody) + case INDEXED_CHILD => + INDEXED_CHILD.collectSpanAttributes(new UrlParameterProvider { + override def getParameter(name: String): String = + uri.query().get(name).orNull + }) + resp.withEntity("") + case QUERY_PARAM => resp.withEntity(uri.queryString().orNull) + case REDIRECT => + resp.withHeaders(headers.Location(endpoint.getBody)) + case ERROR => resp.withEntity(endpoint.getBody) + case EXCEPTION => throw new Exception(endpoint.getBody) + case _ => + HttpResponse(status = NOT_FOUND.getStatus) + .withEntity(NOT_FOUND.getBody) + } + } + } + ) + } + } + + private var binding: ServerBinding = null + + def start(port: Int): Unit = synchronized { + if (null == binding) { + import scala.concurrent.duration._ + binding = Await.result( + Http().bindAndHandleSync(syncHandler, "localhost", port), + 10 seconds + ) + } + } + + def stop(): Unit = synchronized { + if (null != binding) { + binding.unbind() + system.terminate() + binding = null + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestWebServer.scala b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestWebServer.scala new file mode 100644 index 000000000..a96f4e72c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/akka-http-10.0/javaagent/src/test/scala/AkkaHttpTestWebServer.scala @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.Http.ServerBinding +import akka.http.scaladsl.model._ +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.ExceptionHandler +import akka.stream.ActorMaterializer +import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint._ + +import scala.concurrent.Await + +// FIXME: This doesn't work because we don't support bindAndHandle. +object AkkaHttpTestWebServer { + implicit val system = ActorSystem("my-system") + implicit val materializer = ActorMaterializer() + // needed for the future flatMap/onComplete in the end + implicit val executionContext = system.dispatcher + + val exceptionHandler = ExceptionHandler { + case ex: Exception => + complete( + HttpResponse(status = EXCEPTION.getStatus).withEntity(ex.getMessage) + ) + } + + val route = { //handleExceptions(exceptionHandler) { + path(SUCCESS.rawPath) { + complete( + HttpResponse(status = SUCCESS.getStatus).withEntity(SUCCESS.getBody) + ) + } ~ path(QUERY_PARAM.rawPath) { + complete( + HttpResponse(status = QUERY_PARAM.getStatus).withEntity(SUCCESS.getBody) + ) + } ~ path(REDIRECT.rawPath) { + redirect(Uri(REDIRECT.getBody), StatusCodes.Found) + } ~ path(ERROR.rawPath) { + complete(HttpResponse(status = ERROR.getStatus).withEntity(ERROR.getBody)) + } ~ path(EXCEPTION.rawPath) { + failWith(new Exception(EXCEPTION.getBody)) + } + } + + private var binding: ServerBinding = null + + def start(port: Int): Unit = synchronized { + if (null == binding) { + import scala.concurrent.duration._ + binding = + Await.result(Http().bindAndHandle(route, "localhost", port), 10 seconds) + } + } + + def stop(): Unit = synchronized { + if (null != binding) { + binding.unbind() + system.terminate() + binding = null + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent-unit-tests/apache-camel-2.20-javaagent-unit-tests.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent-unit-tests/apache-camel-2.20-javaagent-unit-tests.gradle new file mode 100644 index 000000000..8a24b1f60 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent-unit-tests/apache-camel-2.20-javaagent-unit-tests.gradle @@ -0,0 +1,12 @@ +apply plugin: "otel.java-conventions" + +dependencies { + testImplementation project(':instrumentation:apache-camel-2.20:javaagent') + testImplementation "org.apache.camel:camel-core:2.20.1" + testImplementation "org.apache.camel:camel-aws:2.20.1" + testImplementation "org.apache.camel:camel-http:2.20.1" + + testImplementation "io.opentelemetry:opentelemetry-extension-trace-propagators" + testImplementation "io.opentelemetry:opentelemetry-extension-aws" + testImplementation "org.assertj:assertj-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtilTest.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtilTest.java new file mode 100644 index 000000000..38dfafead --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtilTest.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.extension.trace.propagation.JaegerPropagator; +import java.net.URI; +import java.util.Collections; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.component.aws.sqs.SqsComponent; +import org.apache.camel.component.aws.sqs.SqsConfiguration; +import org.apache.camel.component.aws.sqs.SqsEndpoint; +import org.apache.camel.component.http.HttpComponent; +import org.apache.camel.component.http.HttpEndpoint; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class CamelPropagationUtilTest { + + @BeforeAll + public static void setUp() { + GlobalOpenTelemetry.set( + OpenTelemetry.propagating(ContextPropagators.create(JaegerPropagator.getInstance()))); + } + + @Test + public void shouldExtractHttpParentForHttpEndpoint() throws Exception { + + // given + Endpoint endpoint = new HttpEndpoint("", new HttpComponent(), URI.create("")); + Map exchangeHeaders = + Collections.singletonMap( + "uber-trace-id", "1f7f8dab3f0043b1b9cf0a75caf57510:a13825abcb764bd3:0:1"); + + // when + Context parent = CamelPropagationUtil.extractParent(exchangeHeaders, endpoint); + + // then + Span parentSpan = Span.fromContext(parent); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + assertThat(parentSpanContext.getTraceId()).isEqualTo("1f7f8dab3f0043b1b9cf0a75caf57510"); + assertThat(parentSpanContext.getSpanId()).isEqualTo("a13825abcb764bd3"); + } + + @Test + public void shouldNotFailExtractingNullHttpParentForHttpEndpoint() throws Exception { + + // given + Endpoint endpoint = new HttpEndpoint("", new HttpComponent(), URI.create("")); + Map exchangeHeaders = Collections.singletonMap("uber-trace-id", null); + + // when + Context parent = CamelPropagationUtil.extractParent(exchangeHeaders, endpoint); + + // then + Span parentSpan = Span.fromContext(parent); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + assertThat(parentSpanContext.isValid()).isEqualTo(false); + } + + @Test + public void shouldNotFailExtractingNullAwsParentForSqsEndpoint() { + + // given + Endpoint endpoint = new SqsEndpoint("", new SqsComponent(), new SqsConfiguration()); + Map exchangeHeaders = Collections.singletonMap("AWSTraceHeader", null); + + // when + Context parent = CamelPropagationUtil.extractParent(exchangeHeaders, endpoint); + + // then + Span parentSpan = Span.fromContext(parent); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + assertThat(parentSpanContext.isValid()).isEqualTo(false); + } + + @Test + public void shouldExtractAwsParentForSqsEndpoint() { + + // given + Endpoint endpoint = new SqsEndpoint("", new SqsComponent(), new SqsConfiguration()); + Map exchangeHeaders = + Collections.singletonMap( + "AWSTraceHeader", + "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\n"); + + // when + Context parent = CamelPropagationUtil.extractParent(exchangeHeaders, endpoint); + + // then + Span parentSpan = Span.fromContext(parent); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + assertThat(parentSpanContext.getTraceId()).isEqualTo("5759e988bd862e3fe1be46a994272793"); + assertThat(parentSpanContext.getSpanId()).isEqualTo("53995c3f42cd8ad8"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/apache-camel-2.20-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/apache-camel-2.20-javaagent.gradle new file mode 100644 index 000000000..29fbbd341 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/apache-camel-2.20-javaagent.gradle @@ -0,0 +1,63 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.camel" + module = "camel-core" + versions = "[2.20.1,3)" + } +} + +ext { + camelversion = '2.20.1' +} + +dependencies { + library "org.apache.camel:camel-core:$camelversion" + implementation "io.opentelemetry:opentelemetry-extension-aws" + + testInstrumentation project(':instrumentation:apache-httpclient:apache-httpclient-2.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:aws-sdk:aws-sdk-1.11:javaagent') + + testLibrary "org.apache.camel:camel-spring-boot-starter:$camelversion" + testLibrary "org.apache.camel:camel-jetty-starter:$camelversion" + testLibrary "org.apache.camel:camel-http-starter:$camelversion" + testLibrary "org.apache.camel:camel-jaxb-starter:$camelversion" + testLibrary "org.apache.camel:camel-undertow:$camelversion" + testLibrary "org.apache.camel:camel-aws:$camelversion" + + testImplementation "org.springframework.boot:spring-boot-starter-test:1.5.17.RELEASE" + testImplementation "org.springframework.boot:spring-boot-starter:1.5.17.RELEASE" + + testImplementation "org.spockframework:spock-spring:${versions["org.spockframework"]}" + testImplementation 'javax.xml.bind:jaxb-api:2.3.1' + testImplementation "org.elasticmq:elasticmq-rest-sqs_2.12:1.0.0" + + testImplementation "org.testcontainers:localstack:${versions["org.testcontainers"]}" + + latestDepTestLibrary "org.apache.camel:camel-core:2.+" + latestDepTestLibrary "org.apache.camel:camel-spring-boot-starter:2.+" + latestDepTestLibrary "org.apache.camel:camel-jetty-starter:2.+" + latestDepTestLibrary "org.apache.camel:camel-http-starter:2.+" + latestDepTestLibrary "org.apache.camel:camel-jaxb-starter:2.+" + latestDepTestLibrary "org.apache.camel:camel-undertow:2.+" + latestDepTestLibrary "org.apache.camel:camel-aws:2.+" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.apache-camel.experimental-span-attributes=true" + jvmArgs "-Dotel.instrumentation.aws-sdk.experimental-span-attributes=true" +} + +javadoc { + dependencies { + // without adding this dependency, javadoc fails: + // warning: unknown enum constant XmlAccessType.PROPERTY + // reason: class file for javax.xml.bind.annotation.XmlAccessType not found + // due to usage of org.apache.camel.model.RouteDefinition in CamelTracingService + // which has jaxb class-level annotations + compileOnly 'javax.xml.bind:jaxb-api:2.3.1' + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ActiveSpanManager.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ActiveSpanManager.java new file mode 100644 index 000000000..d4dd378ff --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ActiveSpanManager.java @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Scope; +import org.apache.camel.Exchange; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Utility class for managing active spans as a stack associated with an exchange. */ +class ActiveSpanManager { + + private static final String ACTIVE_SPAN_PROPERTY = "OpenTelemetry.activeSpan"; + + private static final Logger LOG = LoggerFactory.getLogger(ActiveSpanManager.class); + + private ActiveSpanManager() {} + + public static Span getSpan(Exchange exchange) { + SpanWithScope spanWithScope = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class); + if (spanWithScope != null) { + return spanWithScope.getSpan(); + } + return null; + } + + /** + * This method activates the supplied span for the supplied exchange. If an existing span is found + * for the exchange, this will be pushed onto a stack. + * + * @param exchange The exchange + * @param span The span + */ + public static void activate(Exchange exchange, Span span, SpanKind spanKind) { + + SpanWithScope parent = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class); + SpanWithScope spanWithScope = SpanWithScope.activate(span, parent, spanKind); + exchange.setProperty(ACTIVE_SPAN_PROPERTY, spanWithScope); + LOG.debug("Activated a span: {}", spanWithScope); + } + + /** + * This method deactivates an existing active span associated with the supplied exchange. Once + * deactivated, if a parent span is found associated with the stack for the exchange, it will be + * restored as the current span for that exchange. + * + * @param exchange The exchange + */ + public static void deactivate(Exchange exchange) { + + SpanWithScope spanWithScope = exchange.getProperty(ACTIVE_SPAN_PROPERTY, SpanWithScope.class); + + if (spanWithScope != null) { + spanWithScope.deactivate(); + exchange.setProperty(ACTIVE_SPAN_PROPERTY, spanWithScope.getParent()); + LOG.debug("Deactivated span: {}", spanWithScope); + } + } + + public static class SpanWithScope { + @Nullable private final SpanWithScope parent; + private final Span span; + private final Scope scope; + + public SpanWithScope(SpanWithScope parent, Span span, Scope scope) { + this.parent = parent; + this.span = span; + this.scope = scope; + } + + public static SpanWithScope activate(Span span, SpanWithScope parent, SpanKind spanKind) { + Scope scope = null; + if (isClientSpan(spanKind)) { + scope = CamelTracer.TRACER.startClientScope(span); + } else { + scope = span.makeCurrent(); + } + + return new SpanWithScope(parent, span, scope); + } + + private static boolean isClientSpan(SpanKind kind) { + return (SpanKind.CLIENT.equals(kind) || SpanKind.PRODUCER.equals(kind)); + } + + public SpanWithScope getParent() { + return parent; + } + + public Span getSpan() { + return span; + } + + public void deactivate() { + span.end(); + scope.close(); + } + + @Override + public String toString() { + return "SpanWithScope [span=" + span + ", scope=" + scope + "]"; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ApacheCamelInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ApacheCamelInstrumentationModule.java new file mode 100644 index 000000000..b6ab18ed0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/ApacheCamelInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ApacheCamelInstrumentationModule extends InstrumentationModule { + + public ApacheCamelInstrumentationModule() { + super("apache-camel", "apache-camel-2.20"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new CamelContextInstrumentation()); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.extension.aws."); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelContextInstrumentation.java new file mode 100644 index 000000000..9d75600c8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelContextInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.camel.CamelContext; + +public class CamelContextInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.camel.CamelContext"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.apache.camel.CamelContext")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("start").and(isPublic()).and(takesArguments(0)), + this.getClass().getName() + "$StartAdvice"); + } + + @SuppressWarnings("unused") + public static class StartAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onContextStart(@Advice.This CamelContext context) throws Exception { + + if (context.hasService(CamelTracingService.class) == null) { + // start this service eager so we init before Camel is starting up + context.addService(new CamelTracingService(context), true, true); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelDirection.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelDirection.java new file mode 100644 index 000000000..d6f3d7c3d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelDirection.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +public enum CamelDirection { + INBOUND, + OUTBOUND +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelEventNotifier.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelEventNotifier.java new file mode 100644 index 000000000..2d106b0b5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelEventNotifier.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import java.util.EventObject; +import org.apache.camel.management.event.ExchangeSendingEvent; +import org.apache.camel.management.event.ExchangeSentEvent; +import org.apache.camel.support.EventNotifierSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class CamelEventNotifier extends EventNotifierSupport { + + private static final Logger LOG = LoggerFactory.getLogger(CamelEventNotifier.class); + + @Override + public void notify(EventObject event) { + + try { + if (event instanceof ExchangeSendingEvent) { + onExchangeSending((ExchangeSendingEvent) event); + } else if (event instanceof ExchangeSentEvent) { + onExchangeSent((ExchangeSentEvent) event); + } + } catch (Throwable t) { + LOG.warn("Failed to capture tracing data", t); + } + } + + /** Camel about to send (outbound). */ + private static void onExchangeSending(ExchangeSendingEvent ese) { + SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(ese.getEndpoint()); + if (!sd.shouldStartNewSpan()) { + return; + } + + String name = + sd.getOperationName(ese.getExchange(), ese.getEndpoint(), CamelDirection.OUTBOUND); + Context context = CamelTracer.TRACER.startSpan(name, sd.getInitiatorSpanKind()); + Span span = Span.fromContext(context); + sd.pre(span, ese.getExchange(), ese.getEndpoint(), CamelDirection.OUTBOUND); + ActiveSpanManager.activate(ese.getExchange(), span, sd.getInitiatorSpanKind()); + CamelPropagationUtil.injectParent(context, ese.getExchange().getIn().getHeaders()); + + LOG.debug("[Exchange sending] Initiator span started: {}", span); + } + + /** Camel finished sending (outbound). Finish span and remove it from CAMEL holder. */ + private static void onExchangeSent(ExchangeSentEvent event) { + SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(event.getEndpoint()); + if (!sd.shouldStartNewSpan()) { + return; + } + + Span span = ActiveSpanManager.getSpan(event.getExchange()); + if (span != null) { + LOG.debug("[Exchange sent] Initiator span finished: {}", span); + sd.post(span, event.getExchange(), event.getEndpoint()); + ActiveSpanManager.deactivate(event.getExchange()); + } else { + LOG.warn("Could not find managed span for exchange: {}", event.getExchange()); + } + } + + @Override + public boolean isEnabled(EventObject event) { + return event instanceof ExchangeSendingEvent || event instanceof ExchangeSentEvent; + } + + @Override + public String toString() { + return "OpenTelemetryCamelEventNotifier"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtil.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtil.java new file mode 100644 index 000000000..8dc5cf96b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtil.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import java.util.Collections; +import java.util.Map; +import org.apache.camel.Endpoint; + +final class CamelPropagationUtil { + + private CamelPropagationUtil() {} + + static Context extractParent(Map exchangeHeaders, Endpoint endpoint) { + return (isAwsPropagated(endpoint) + ? extractAwsPropagationParent(exchangeHeaders) + : extractHttpPropagationParent(exchangeHeaders)); + } + + private static boolean isAwsPropagated(Endpoint endpoint) { + return endpoint.getClass().getName().endsWith("SqsEndpoint"); + } + + private static Context extractAwsPropagationParent(Map exchangeHeaders) { + return AwsXrayPropagator.getInstance() + .extract( + Context.current(), + Collections.singletonMap("X-Amzn-Trace-Id", exchangeHeaders.get("AWSTraceHeader")), + MapGetter.INSTANCE); + } + + private static Context extractHttpPropagationParent(Map exchangeHeaders) { + return GlobalOpenTelemetry.getPropagators() + .getTextMapPropagator() + .extract(Context.current(), exchangeHeaders, MapGetter.INSTANCE); + } + + static void injectParent(Context context, Map exchangeHeaders) { + GlobalOpenTelemetry.getPropagators() + .getTextMapPropagator() + .inject(context, exchangeHeaders, MapSetter.INSTANCE); + } + + private static class MapGetter implements TextMapGetter> { + + private static final MapGetter INSTANCE = new MapGetter(); + + @Override + public Iterable keys(Map map) { + return map.keySet(); + } + + @Override + public String get(Map map, String key) { + Object value = map.get(key); + return (value == null ? null : value.toString()); + } + } + + private static class MapSetter implements TextMapSetter> { + + private static final MapSetter INSTANCE = new MapSetter(); + + @Override + public void set(Map carrier, String key, String value) { + // Camel keys are internal ones + if (!key.startsWith("Camel")) { + carrier.put(key, value); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelRoutePolicy.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelRoutePolicy.java new file mode 100644 index 000000000..f60ddeee1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelRoutePolicy.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import org.apache.camel.Exchange; +import org.apache.camel.Route; +import org.apache.camel.support.RoutePolicySupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class CamelRoutePolicy extends RoutePolicySupport { + + private static final Logger LOG = LoggerFactory.getLogger(CamelRoutePolicy.class); + + private static Span spanOnExchangeBegin( + Route route, Exchange exchange, SpanDecorator sd, Context parentContext, SpanKind spanKind) { + Span activeSpan = Span.fromContext(parentContext); + if (!activeSpan.getSpanContext().isValid()) { + parentContext = + CamelPropagationUtil.extractParent(exchange.getIn().getHeaders(), route.getEndpoint()); + } + + String name = sd.getOperationName(exchange, route.getEndpoint(), CamelDirection.INBOUND); + Context context = CamelTracer.TRACER.startSpan(parentContext, name, spanKind); + return Span.fromContext(context); + } + + private static SpanKind spanKind(Context context, SpanDecorator sd) { + Span activeSpan = Span.fromContext(context); + // if there's an active span, this is not a root span which we always mark as INTERNAL + return (activeSpan.getSpanContext().isValid() ? SpanKind.INTERNAL : sd.getReceiverSpanKind()); + } + + /** + * Route exchange started, ie request could have been already captured by upper layer + * instrumentation. + */ + @Override + public void onExchangeBegin(Route route, Exchange exchange) { + try { + SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(route.getEndpoint()); + Context parentContext = Context.current(); + SpanKind spanKind = spanKind(parentContext, sd); + Span span = spanOnExchangeBegin(route, exchange, sd, parentContext, spanKind); + sd.pre(span, exchange, route.getEndpoint(), CamelDirection.INBOUND); + ActiveSpanManager.activate(exchange, span, spanKind); + LOG.debug("[Route start] Receiver span started {}", span); + } catch (Throwable t) { + LOG.warn("Failed to capture tracing data", t); + } + } + + /** Route exchange done. Get active CAMEL span, finish, remove from CAMEL holder. */ + @Override + public void onExchangeDone(Route route, Exchange exchange) { + try { + Span span = ActiveSpanManager.getSpan(exchange); + if (span != null) { + + LOG.debug("[Route finished] Receiver span finished {}", span); + SpanDecorator sd = CamelTracer.TRACER.getSpanDecorator(route.getEndpoint()); + sd.post(span, exchange, route.getEndpoint()); + ActiveSpanManager.deactivate(exchange); + } else { + LOG.warn("Could not find managed span for exchange={}", exchange); + } + } catch (Throwable t) { + LOG.warn("Failed to capture tracing data", t); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracer.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracer.java new file mode 100644 index 000000000..6ec39660c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracer.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.javaagent.instrumentation.apachecamel.decorators.DecoratorRegistry; +import org.apache.camel.Endpoint; +import org.apache.camel.util.StringHelper; + +class CamelTracer extends BaseTracer { + + public static final CamelTracer TRACER = new CamelTracer(); + + private final DecoratorRegistry registry = new DecoratorRegistry(); + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.apache-camel-2.20"; + } + + public Scope startClientScope(Span span) { + Context current = super.withClientSpan(Context.current(), span); + return current.makeCurrent(); + } + + public SpanDecorator getSpanDecorator(Endpoint endpoint) { + + String component = ""; + String uri = endpoint.getEndpointUri(); + String[] splitUri = StringHelper.splitOnCharacter(uri, ":", 2); + if (splitUri[1] != null) { + component = splitUri[0]; + } + return registry.forComponent(component); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracingService.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracingService.java new file mode 100644 index 000000000..0bbbbe165 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelTracingService.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import org.apache.camel.CamelContext; +import org.apache.camel.StaticService; +import org.apache.camel.model.RouteDefinition; +import org.apache.camel.spi.RoutePolicy; +import org.apache.camel.spi.RoutePolicyFactory; +import org.apache.camel.support.ServiceSupport; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.ServiceHelper; + +public class CamelTracingService extends ServiceSupport + implements RoutePolicyFactory, StaticService { + + private final CamelContext camelContext; + private final CamelEventNotifier eventNotifier = new CamelEventNotifier(); + private final CamelRoutePolicy routePolicy = new CamelRoutePolicy(); + + public CamelTracingService(CamelContext camelContext) { + ObjectHelper.notNull(camelContext, "CamelContext", this); + this.camelContext = camelContext; + } + + @Override + protected void doStart() throws Exception { + camelContext.getManagementStrategy().addEventNotifier(eventNotifier); + if (!camelContext.getRoutePolicyFactories().contains(this)) { + camelContext.addRoutePolicyFactory(this); + } + + ServiceHelper.startServices(eventNotifier); + } + + @Override + protected void doStop() throws Exception { + // stop event notifier + camelContext.getManagementStrategy().removeEventNotifier(eventNotifier); + ServiceHelper.stopService(eventNotifier); + + // remove route policy + camelContext.getRoutePolicyFactories().remove(this); + } + + @Override + public RoutePolicy createRoutePolicy( + CamelContext camelContext, String routeId, RouteDefinition route) { + return routePolicy; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SpanDecorator.java new file mode 100644 index 000000000..31379b5c5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SpanDecorator.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +/** This interface represents a decorator specific to the component/endpoint being instrumented. */ +public interface SpanDecorator { + + /** + * This method indicates whether the component associated with the SpanDecorator should result in + * a new span being created. + * + * @return Whether a new span should be created + */ + boolean shouldStartNewSpan(); + + /** + * Returns the operation name to use with the Span representing this exchange and endpoint. + * + * @param exchange The exchange + * @param endpoint The endpoint + * @return The operation name + */ + String getOperationName(Exchange exchange, Endpoint endpoint, CamelDirection camelDirection); + + /** + * This method adds appropriate details (tags/logs) to the supplied span based on the pre + * processing of the exchange. + * + * @param span The span + * @param exchange The exchange + * @param endpoint The endpoint + */ + void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection); + + /** + * This method adds appropriate details (tags/logs) to the supplied span based on the post + * processing of the exchange. + * + * @param span The span + * @param exchange The exchange + * @param endpoint The endpoint + */ + void post(Span span, Exchange exchange, Endpoint endpoint); + + /** Returns the 'span.kind' value for use when the component is initiating a communication. */ + SpanKind getInitiatorSpanKind(); + + /** Returns the 'span.kind' value for use when the component is receiving a communication. */ + SpanKind getReceiverSpanKind(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/BaseSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/BaseSpanDecorator.java new file mode 100644 index 000000000..457d879ed --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/BaseSpanDecorator.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import io.opentelemetry.javaagent.instrumentation.apachecamel.SpanDecorator; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.apache.camel.util.StringHelper; +import org.apache.camel.util.URISupport; + +/** An abstract base implementation of the {@link SpanDecorator} interface. */ +class BaseSpanDecorator implements SpanDecorator { + + static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty( + "otel.instrumentation.apache-camel.experimental-span-attributes", false); + + static final String DEFAULT_OPERATION_NAME = "CamelOperation"; + + /** + * This method removes the scheme, any leading slash characters and options from the supplied URI. + * This is intended to extract a meaningful name from the URI that can be used in situations, such + * as the operation name. + * + * @param endpoint The endpoint + * @return The stripped value from the URI + */ + public static String stripSchemeAndOptions(Endpoint endpoint) { + int start = endpoint.getEndpointUri().indexOf(':'); + start++; + // Remove any leading '/' + while (endpoint.getEndpointUri().charAt(start) == '/') { + start++; + } + int end = endpoint.getEndpointUri().indexOf('?'); + return end == -1 + ? endpoint.getEndpointUri().substring(start) + : endpoint.getEndpointUri().substring(start, end); + } + + public static Map toQueryParameters(String uri) { + int index = uri.indexOf('?'); + if (index != -1) { + String queryString = uri.substring(index + 1); + Map map = new HashMap<>(); + for (String param : queryString.split("&")) { + String[] parts = param.split("="); + if (parts.length == 2) { + map.put(parts[0], parts[1]); + } + } + return map; + } + return Collections.emptyMap(); + } + + @Override + public boolean shouldStartNewSpan() { + return true; + } + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + String[] splitUri = StringHelper.splitOnCharacter(endpoint.getEndpointUri(), ":", 2); + return (splitUri.length > 0 ? splitUri[0] : DEFAULT_OPERATION_NAME); + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("apache-camel.uri", URISupport.sanitizeUri(endpoint.getEndpointUri())); + } + } + + @Override + public void post(Span span, Exchange exchange, Endpoint endpoint) { + if (exchange.isFailed()) { + span.setStatus(StatusCode.ERROR); + if (exchange.getException() != null) { + span.recordException(exchange.getException()); + } + } + } + + @Override + public SpanKind getInitiatorSpanKind() { + return SpanKind.CLIENT; + } + + @Override + public SpanKind getReceiverSpanKind() { + return SpanKind.SERVER; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DbSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DbSpanDecorator.java new file mode 100644 index 000000000..cf43f5caf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DbSpanDecorator.java @@ -0,0 +1,130 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class DbSpanDecorator extends BaseSpanDecorator { + + private final String component; + private final String system; + + DbSpanDecorator(String component, String system) { + this.component = component; + this.system = system; + } + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + + switch (component) { + case "mongodb": + case "elasticsearch": + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + if (queryParameters.containsKey("operation")) { + return queryParameters.get("operation"); + } + return super.getOperationName(exchange, endpoint, camelDirection); + default: + return super.getOperationName(exchange, endpoint, camelDirection); + } + } + + private String getStatement(Exchange exchange, Endpoint endpoint) { + switch (component) { + case "mongodb": + Map mongoParameters = toQueryParameters(endpoint.getEndpointUri()); + return mongoParameters.toString(); + case "cql": + Object cqlObj = exchange.getIn().getHeader("CamelCqlQuery"); + if (cqlObj != null) { + return cqlObj.toString(); + } + Map cqlParameters = toQueryParameters(endpoint.getEndpointUri()); + if (cqlParameters.containsKey("cql")) { + return cqlParameters.get("cql"); + } + return null; + case "jdbc": + Object body = exchange.getIn().getBody(); + if (body instanceof String) { + return (String) body; + } + return null; + case "sql": + Object sqlquery = exchange.getIn().getHeader("CamelSqlQuery"); + if (sqlquery instanceof String) { + return (String) sqlquery; + } + return null; + default: + return null; + } + } + + private String getDbName(Endpoint endpoint) { + switch (component) { + case "mongodb": + Map mongoParameters = toQueryParameters(endpoint.getEndpointUri()); + return mongoParameters.get("database"); + case "cql": + URI uri = URI.create(endpoint.getEndpointUri()); + if (uri.getPath() != null && uri.getPath().length() > 0) { + // Strip leading '/' from path + return uri.getPath().substring(1); + } + return null; + case "elasticsearch": + Map elasticsearchParameters = toQueryParameters(endpoint.getEndpointUri()); + if (elasticsearchParameters.containsKey("indexName")) { + return elasticsearchParameters.get("indexName"); + } + return null; + default: + return null; + } + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + super.pre(span, exchange, endpoint, camelDirection); + + span.setAttribute(SemanticAttributes.DB_SYSTEM, system); + String statement = getStatement(exchange, endpoint); + if (statement != null) { + span.setAttribute(SemanticAttributes.DB_STATEMENT, statement); + } + String dbName = getDbName(endpoint); + if (dbName != null) { + span.setAttribute(SemanticAttributes.DB_NAME, dbName); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DecoratorRegistry.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DecoratorRegistry.java new file mode 100644 index 000000000..265f07c32 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/DecoratorRegistry.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.javaagent.instrumentation.apachecamel.SpanDecorator; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; +import java.util.HashMap; +import java.util.Map; + +public class DecoratorRegistry { + + private static final SpanDecorator DEFAULT = new BaseSpanDecorator(); + private static final Map DECORATORS = loadDecorators(); + + private static Map loadDecorators() { + + Map result = new HashMap<>(); + result.put("ahc", new HttpSpanDecorator()); + result.put("ampq", new MessagingSpanDecorator("ampq")); + result.put("aws-s3", new S3SpanDecorator()); + result.put("aws-sns", new MessagingSpanDecorator("aws-sns")); + result.put("aws-sqs", new MessagingSpanDecorator("aws-sqs")); + result.put("cometd", new MessagingSpanDecorator("cometd")); + result.put("cometds", new MessagingSpanDecorator("cometds")); + result.put("cql", new DbSpanDecorator("cql", DbSystemValues.CASSANDRA)); + result.put("direct", new InternalSpanDecorator()); + result.put("direct-vm", new InternalSpanDecorator()); + result.put("disruptor", new InternalSpanDecorator()); + result.put("disruptor-vm", new InternalSpanDecorator()); + result.put("elasticsearch", new DbSpanDecorator("elasticsearch", "elasticsearch")); + result.put("http4", new HttpSpanDecorator()); + result.put("http", new HttpSpanDecorator()); + result.put("ironmq", new MessagingSpanDecorator("ironmq")); + result.put("jdbc", new DbSpanDecorator("jdbc", DbSystemValues.OTHER_SQL)); + result.put("jetty", new HttpSpanDecorator()); + result.put("jms", new MessagingSpanDecorator("jms")); + result.put("kafka", new KafkaSpanDecorator()); + result.put("log", new LogSpanDecorator()); + result.put("mongodb", new DbSpanDecorator("mongodb", DbSystemValues.MONGODB)); + result.put("mqtt", new MessagingSpanDecorator("mqtt")); + result.put("netty-http4", new HttpSpanDecorator()); + result.put("netty-http", new HttpSpanDecorator()); + result.put("paho", new MessagingSpanDecorator("paho")); + result.put("rabbitmq", new MessagingSpanDecorator("rabbitmq")); + result.put("restlet", new HttpSpanDecorator()); + result.put("rest", new RestSpanDecorator()); + result.put("seda", new InternalSpanDecorator()); + result.put("servlet", new HttpSpanDecorator()); + result.put("sjms", new MessagingSpanDecorator("sjms")); + result.put("sql", new DbSpanDecorator("sql", DbSystemValues.OTHER_SQL)); + result.put("stomp", new MessagingSpanDecorator("stomp")); + result.put("timer", new TimerSpanDecorator()); + result.put("undertow", new HttpSpanDecorator()); + result.put("vm", new InternalSpanDecorator()); + return result; + } + + public SpanDecorator forComponent(String component) { + + return DECORATORS.getOrDefault(component, DEFAULT); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java new file mode 100644 index 000000000..d349eaec0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.MalformedURLException; +import java.net.URL; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.checkerframework.checker.nullness.qual.Nullable; + +class HttpSpanDecorator extends BaseSpanDecorator { + + private static final String POST_METHOD = "POST"; + private static final String GET_METHOD = "GET"; + + protected static String getHttpMethod(Exchange exchange, Endpoint endpoint) { + // 1. Use method provided in header. + Object method = exchange.getIn().getHeader(Exchange.HTTP_METHOD); + if (method instanceof String) { + return (String) method; + } + + // 2. GET if query string is provided in header. + if (exchange.getIn().getHeader(Exchange.HTTP_QUERY) != null) { + return GET_METHOD; + } + + // 3. GET if endpoint is configured with a query string. + if (endpoint.getEndpointUri().indexOf('?') != -1) { + return GET_METHOD; + } + + // 4. POST if there is data to send (body is not null). + if (exchange.getIn().getBody() != null) { + return POST_METHOD; + } + + // 5. GET otherwise. + return GET_METHOD; + } + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + // Based on HTTP component documentation: + String spanName = null; + if (shouldSetPathAsName(camelDirection)) { + spanName = getPath(exchange, endpoint); + } + return (spanName == null ? getHttpMethod(exchange, endpoint) : spanName); + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + super.pre(span, exchange, endpoint, camelDirection); + + String httpUrl = getHttpUrl(exchange, endpoint); + if (httpUrl != null) { + span.setAttribute(SemanticAttributes.HTTP_URL, httpUrl); + } + + span.setAttribute(SemanticAttributes.HTTP_METHOD, getHttpMethod(exchange, endpoint)); + + Span serverSpan = ServerSpan.fromContextOrNull(Context.current()); + if (shouldUpdateServerSpanName(serverSpan, camelDirection)) { + updateServerSpanName(serverSpan, exchange, endpoint); + } + } + + private static boolean shouldSetPathAsName(CamelDirection camelDirection) { + return CamelDirection.INBOUND.equals(camelDirection); + } + + @Nullable + protected String getPath(Exchange exchange, Endpoint endpoint) { + + String httpUrl = getHttpUrl(exchange, endpoint); + try { + URL url = new URL(httpUrl); + return url.getPath(); + } catch (MalformedURLException e) { + return null; + } + } + + private static boolean shouldUpdateServerSpanName( + Span serverSpan, CamelDirection camelDirection) { + return (serverSpan != null && shouldSetPathAsName(camelDirection)); + } + + private void updateServerSpanName(Span serverSpan, Exchange exchange, Endpoint endpoint) { + String path = getPath(exchange, endpoint); + if (path != null) { + serverSpan.updateName(path); + } + } + + protected String getHttpUrl(Exchange exchange, Endpoint endpoint) { + Object url = exchange.getIn().getHeader(Exchange.HTTP_URL); + if (url instanceof String) { + return (String) url; + } else { + Object uri = exchange.getIn().getHeader(Exchange.HTTP_URI); + if (uri instanceof String) { + return (String) uri; + } else { + // Try to obtain from endpoint + int index = endpoint.getEndpointUri().lastIndexOf("http:"); + if (index != -1) { + return endpoint.getEndpointUri().substring(index); + } + } + } + return null; + } + + @Override + public void post(Span span, Exchange exchange, Endpoint endpoint) { + super.post(span, exchange, endpoint); + + if (exchange.hasOut()) { + Object responseCode = exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE); + if (responseCode instanceof Integer) { + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, (Integer) responseCode); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/InternalSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/InternalSpanDecorator.java new file mode 100644 index 000000000..b41bbb14b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/InternalSpanDecorator.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class InternalSpanDecorator extends BaseSpanDecorator { + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + // Internal communications use descriptive names, so suitable + // as an operation name, but need to strip the scheme and any options + return stripSchemeAndOptions(endpoint); + } + + @Override + public boolean shouldStartNewSpan() { + return false; + } + + @Override + public SpanKind getReceiverSpanKind() { + return SpanKind.INTERNAL; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java new file mode 100644 index 000000000..52c14b027 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class KafkaSpanDecorator extends MessagingSpanDecorator { + + private static final String PARTITION_KEY = "kafka.PARTITION_KEY"; + private static final String PARTITION = "kafka.PARTITION"; + private static final String KEY = "kafka.KEY"; + private static final String TOPIC = "kafka.TOPIC"; + private static final String OFFSET = "kafka.OFFSET"; + + public KafkaSpanDecorator() { + super("kafka"); + } + + @Override + public String getDestination(Exchange exchange, Endpoint endpoint) { + String topic = (String) exchange.getIn().getHeader(TOPIC); + if (topic == null) { + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + topic = queryParameters.get("topic"); + } + return topic != null ? topic : super.getDestination(exchange, endpoint); + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + super.pre(span, exchange, endpoint, camelDirection); + + span.setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process"); + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic"); + + Integer partition = exchange.getIn().getHeader(PARTITION, Integer.class); + if (partition != null) { + span.setAttribute(SemanticAttributes.MESSAGING_KAFKA_PARTITION, partition); + } + + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + String partitionKey = (String) exchange.getIn().getHeader(PARTITION_KEY); + if (partitionKey != null) { + span.setAttribute("apache-camel.kafka.partitionKey", partitionKey); + } + + String key = (String) exchange.getIn().getHeader(KEY); + if (key != null) { + span.setAttribute("apache-camel.kafka.key", key); + } + + String offset = getValue(exchange, OFFSET, Long.class); + if (offset != null) { + span.setAttribute("apache-camel.kafka.offset", offset); + } + } + } + + /** + * Extracts header value from the exchange for given header. + * + * @param exchange the {@link Exchange} + * @param header the header name + * @param type the class type of the exchange header + */ + private static String getValue(Exchange exchange, String header, Class type) { + T value = exchange.getIn().getHeader(header, type); + return value != null ? String.valueOf(value) : exchange.getIn().getHeader(header, String.class); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/LogSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/LogSpanDecorator.java new file mode 100644 index 000000000..ddf60ea6d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/LogSpanDecorator.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +class LogSpanDecorator extends BaseSpanDecorator { + + @Override + public boolean shouldStartNewSpan() { + return false; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java new file mode 100644 index 000000000..e300400df --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.util.Map; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class MessagingSpanDecorator extends BaseSpanDecorator { + + private final String component; + + public MessagingSpanDecorator(String component) { + this.component = component; + } + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + + if ("mqtt".equals(component)) { + return stripSchemeAndOptions(endpoint); + } + return getDestination(exchange, endpoint); + } + + @Override + public void pre(Span span, Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + super.pre(span, exchange, endpoint, camelDirection); + + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION, getDestination(exchange, endpoint)); + + String messageId = getMessageId(exchange); + if (messageId != null) { + span.setAttribute(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId); + } + } + + /** + * This method identifies the destination from the supplied exchange and/or endpoint. + * + * @param exchange The exchange + * @param endpoint The endpoint + * @return The message bus destination + */ + protected String getDestination(Exchange exchange, Endpoint endpoint) { + switch (component) { + case "cometds": + case "cometd": + return URI.create(endpoint.getEndpointUri()).getPath().substring(1); + case "rabbitmq": + return (String) exchange.getIn().getHeader("rabbitmq.EXCHANGE_NAME"); + case "stomp": + String destination = stripSchemeAndOptions(endpoint); + if (destination.startsWith("queue:")) { + destination = destination.substring("queue:".length()); + } + return destination; + case "mqtt": + Map queryParameters = toQueryParameters(endpoint.getEndpointUri()); + return (queryParameters.containsKey("subscribeTopicNames") + ? queryParameters.get("subscribeTopicNames") + : queryParameters.get("publishTopicName")); + default: + return stripSchemeAndOptions(endpoint); + } + } + + @Override + public SpanKind getInitiatorSpanKind() { + switch (component) { + case "aws-sns": + case "aws-sqs": + return SpanKind.INTERNAL; + default: + return SpanKind.PRODUCER; + } + } + + @Override + public SpanKind getReceiverSpanKind() { + switch (component) { + case "aws-sns": + case "aws-sqs": + return SpanKind.INTERNAL; + default: + return SpanKind.CONSUMER; + } + } + + /** + * This method identifies the message id for the messaging exchange. + * + * @return The message id, or null if no id exists for the exchange + */ + protected String getMessageId(Exchange exchange) { + switch (component) { + case "aws-sns": + return (String) exchange.getIn().getHeader("CamelAwsSnsMessageId"); + case "aws-sqs": + return (String) exchange.getIn().getHeader("CamelAwsSqsMessageId"); + case "ironmq": + return (String) exchange.getIn().getHeader("CamelIronMQMessageId"); + case "jms": + return (String) exchange.getIn().getHeader("JMSMessageID"); + default: + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/RestSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/RestSpanDecorator.java new file mode 100644 index 000000000..9b4844d8f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/RestSpanDecorator.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class RestSpanDecorator extends HttpSpanDecorator { + + private static final Logger LOG = LoggerFactory.getLogger(RestSpanDecorator.class); + + @Override + protected String getPath(Exchange exchange, Endpoint endpoint) { + String endpointUri = endpoint.getEndpointUri(); + // Obtain the 'path' part of the URI format: rest://method:path[:uriTemplate]?[options] + String path = null; + int index = endpointUri.indexOf(':'); + if (index != -1) { + index = endpointUri.indexOf(':', index + 1); + if (index != -1) { + path = endpointUri.substring(index + 1); + index = path.indexOf('?'); + if (index != -1) { + path = path.substring(0, index); + } + path = path.replaceAll(":", ""); + try { + path = URLDecoder.decode(path, "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOG.debug("Failed to decode URL path '{}', ignoring exception", path, e); + } + } + } + return path; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/S3SpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/S3SpanDecorator.java new file mode 100644 index 000000000..00f3150ee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/S3SpanDecorator.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.api.trace.SpanKind; + +public class S3SpanDecorator extends BaseSpanDecorator { + + @Override + public SpanKind getInitiatorSpanKind() { + return SpanKind.INTERNAL; + } + + @Override + public SpanKind getReceiverSpanKind() { + return SpanKind.INTERNAL; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/TimerSpanDecorator.java b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/TimerSpanDecorator.java new file mode 100644 index 000000000..402fbb7d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/TimerSpanDecorator.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Apache Camel Opentracing Component + * + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; + +class TimerSpanDecorator extends BaseSpanDecorator { + + @Override + public String getOperationName( + Exchange exchange, Endpoint endpoint, CamelDirection camelDirection) { + Object name = exchange.getProperty(Exchange.TIMER_NAME); + if (name instanceof String) { + return (String) name; + } + + return super.getOperationName(exchange, endpoint, camelDirection); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.groovy new file mode 100644 index 000000000..4b4f20553 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.groovy @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class DirectCamelTest extends AgentInstrumentationSpecification { + + @Shared + ConfigurableApplicationContext server + + def setupSpec() { + def app = new SpringApplication(DirectConfig) + server = app.run() + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "simple direct to a single services"() { + setup: + def camelContext = server.getBean(CamelContext) + ProducerTemplate template = camelContext.createProducerTemplate() + + when: + template.sendBody("direct:input", "Example request") + + then: + assertTraces(1) { + trace(0, 2) { + def parent = it + it.span(0) { + name "input" + kind INTERNAL + hasNoParent() + attributes { + "apache-camel.uri" "direct://input" + } + } + it.span(1) { + name "receiver" + kind INTERNAL + parentSpanId parent.span(0).spanId + attributes { + "apache-camel.uri" "direct://receiver" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.groovy new file mode 100644 index 000000000..9ec2ee387 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.groovy @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class DirectConfig { + + @Bean + RouteBuilder receiverRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:receiver") + .log(LoggingLevel.INFO, "test", "RECEIVER got: \${body}") + .delay(simple("2000")) + .setBody(constant("result")) + } + } + } + + @Bean + RouteBuilder clientRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test", "SENDING request \${body}") + .to("direct:receiver") + .log(LoggingLevel.INFO, "test", "RECEIVED response \${body}") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.groovy new file mode 100644 index 000000000..e1f5bd587 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.groovy @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class MulticastConfig { + + @Bean + RouteBuilder firstServiceRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:first") + .log(LoggingLevel.INFO, "test", "FIRST request: \${body}") + .delay(simple("1000")) + .setBody(constant("first")) + } + } + } + + @Bean + RouteBuilder secondServiceRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:second") + .log(LoggingLevel.INFO, "test", "SECOND request: \${body}") + .delay(simple("2000")) + .setBody(constant("second")) + } + } + } + + @Bean + RouteBuilder clientServiceRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test", "SENDING request \${body}") + .multicast() + .parallelProcessing() + .to("direct:first", "direct:second") + .end() + .log(LoggingLevel.INFO, "test", "RECEIVED response \${body}") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.groovy new file mode 100644 index 000000000..2c088a7a6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.groovy @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class MulticastDirectCamelTest extends AgentInstrumentationSpecification { + + @Shared + ConfigurableApplicationContext server + + def setupSpec() { + def app = new SpringApplication(MulticastConfig) + server = app.run() + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "parallel multicast to two child services"() { + setup: + def camelContext = server.getBean(CamelContext) + ProducerTemplate template = camelContext.createProducerTemplate() + + when: + template.sendBody("direct:input", "Example request") + + then: + assertTraces(1) { + trace(0, 3) { + def parent = it + it.span(0) { + name "input" + kind INTERNAL + hasNoParent() + attributes { + "apache-camel.uri" "direct://input" + } + } + // there is no strict ordering of "first" and "second" span + def indexOfFirst = span(1).name == "first" ? 1 : 2 + def indexOfSecond = span(1).name == "second" ? 1 : 2 + it.span(indexOfFirst) { + name "first" + kind INTERNAL + parentSpanId parent.span(0).spanId + attributes { + "apache-camel.uri" "direct://first" + } + } + it.span(indexOfSecond) { + name "second" + kind INTERNAL + parentSpanId parent.span(0).spanId + attributes { + "apache-camel.uri" "direct://second" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.groovy new file mode 100644 index 000000000..2207c49ca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.groovy @@ -0,0 +1,121 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class RestCamelTest extends AgentInstrumentationSpecification implements RetryOnAddressAlreadyInUseTrait { + + @Shared + ConfigurableApplicationContext server + @Shared + int port + + def setupSpec() { + withRetryOnAddressAlreadyInUse({ + setupSpecUnderRetry() + }) + } + + def setupSpecUnderRetry() { + port = PortUtils.findOpenPort() + def app = new SpringApplication(RestConfig) + app.setDefaultProperties(["restServer.port": port]) + server = app.run() + println getClass().name + " http server started at: http://localhost:$port/" + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "rest component - server and client call with jetty backend"() { + setup: + def camelContext = server.getBean(CamelContext) + ProducerTemplate template = camelContext.createProducerTemplate() + + when: + // run client and server in separate threads to simulate "real" rest client/server call + new Thread(new Runnable() { + @Override + void run() { + template.sendBodyAndHeaders("direct:start", null, ["module": "firstModule", "unitId": "unitOne"]) + } + } + ).start() + + then: + assertTraces(1) { + trace(0, 5) { + it.span(0) { + name "start" + kind INTERNAL + attributes { + "apache-camel.uri" "direct://start" + } + } + it.span(1) { + name "GET" + kind CLIENT + parentSpanId(span(0).spanId) + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "GET" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "apache-camel.uri" "rest://get:api/%7Bmodule%7D/unit/%7BunitId%7D" + } + } + it.span(2) { + name "/api/{module}/unit/{unitId}" + kind SERVER + parentSpanId(span(1).spanId) + attributes { + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$port/api/firstModule/unit/unitOne" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$SemanticAttributes.HTTP_CLIENT_IP.key" "127.0.0.1" + "$SemanticAttributes.HTTP_USER_AGENT.key" String + "$SemanticAttributes.HTTP_FLAVOR.key" "1.1" + "$SemanticAttributes.HTTP_METHOD.key" "GET" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_PORT.key" Long + } + } + it.span(3) { + name "/api/{module}/unit/{unitId}" + kind INTERNAL + parentSpanId(span(2).spanId) + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "GET" + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$port/api/firstModule/unit/unitOne" + "apache-camel.uri" String + } + } + it.span(4) { + name "moduleUnit" + kind INTERNAL + parentSpanId(span(3).spanId) + attributes { + "apache-camel.uri" "direct://moduleUnit" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.groovy new file mode 100644 index 000000000..1345de861 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.groovy @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.model.rest.RestBindingMode +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class RestConfig { + + @Bean + RouteBuilder routes() { + return new RouteBuilder() { + @Override + void configure() throws Exception { + + restConfiguration() + .component("jetty") + .bindingMode(RestBindingMode.auto) + .host("localhost") + .port("{{restServer.port}}") + .producerComponent("http") + + rest("/api") + .get("/{module}/unit/{unitId}") + .to("direct:moduleUnit") + + from("direct:moduleUnit") + .transform().simple("\${header.unitId} of \${header.module}") + + // producer - client route + from("direct:start") + .log(LoggingLevel.INFO, "test", "SENDING request") + .to("rest:get:api/{module}/unit/{unitId}") + .log(LoggingLevel.INFO, "test", "RECEIVED response: '\${body}'") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.groovy new file mode 100644 index 000000000..c9d728178 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.groovy @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.client.WebClient +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class SingleServiceCamelTest extends AgentInstrumentationSpecification implements RetryOnAddressAlreadyInUseTrait { + + @Shared + ConfigurableApplicationContext server + @Shared + WebClient client = WebClient.of() + @Shared + int port + @Shared + URI address + + def setupSpec() { + withRetryOnAddressAlreadyInUse({ + setupSpecUnderRetry() + }) + } + + def setupSpecUnderRetry() { + port = PortUtils.findOpenPort() + address = new URI("http://localhost:$port/") + def app = new SpringApplication(SingleServiceConfig) + app.setDefaultProperties(["camelService.port": port]) + server = app.run() + println getClass().name + " http server started at: http://localhost:$port/" + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "single camel service span"() { + setup: + def requestUrl = address.resolve("/camelService") + + when: + client.post(requestUrl.toString(), "testContent").aggregate().join() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + kind SERVER + name "/camelService" + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "${address.resolve("/camelService")}" + "apache-camel.uri" "${address.resolve("/camelService")}".replace("localhost", "0.0.0.0") + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.groovy new file mode 100644 index 000000000..603a4e713 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class SingleServiceConfig { + + @Bean + RouteBuilder serviceRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + + from("undertow:http://0.0.0.0:{{camelService.port}}/camelService") + .routeId("camelService") + .streamCaching() + .log("CamelService request: \${body}") + .delay(simple("\${random(1000, 2000)}")) + .transform(simple("CamelService-\${body}")) + .log(LoggingLevel.INFO, "test", "CamelService response: \${body}") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.groovy new file mode 100644 index 000000000..8a979ec9c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.groovy @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import org.apache.camel.builder.RouteBuilder +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class TwoServicesConfig { + + @Bean + RouteBuilder serviceOneRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + + from("undertow:http://0.0.0.0:{{service.one.port}}/serviceOne") + .routeId("serviceOne") + .streamCaching() + .removeHeaders("CamelHttp*") + .log("Service One request: \${body}") + .delay(simple("\${random(1000,2000)}")) + .transform(simple("Service-One-\${body}")) + .to("http://127.0.0.1:{{service.two.port}}/serviceTwo") + .log("Service One response: \${body}") + } + } + } + + @Bean + RouteBuilder serviceTwoRoute() { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + + from("jetty:http://0.0.0.0:{{service.two.port}}/serviceTwo?arg=value") + .routeId("serviceTwo") + .streamCaching() + .log("Service Two request: \${body}") + .delay(simple("\${random(1000, 2000)}")) + .transform(simple("Service-Two-\${body}")) + .log("Service Two response: \${body}") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.groovy new file mode 100644 index 000000000..07dc5596f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.groovy @@ -0,0 +1,148 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.impl.DefaultCamelContext +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import spock.lang.Shared + +class TwoServicesWithDirectClientCamelTest extends AgentInstrumentationSpecification implements RetryOnAddressAlreadyInUseTrait { + + @Shared + int portOne + @Shared + int portTwo + @Shared + ConfigurableApplicationContext server + @Shared + CamelContext clientContext + + def setupSpec() { + withRetryOnAddressAlreadyInUse({ + setupSpecUnderRetry() + }) + } + + def setupSpecUnderRetry() { + portOne = PortUtils.findOpenPort() + portTwo = PortUtils.findOpenPort() + def app = new SpringApplication(TwoServicesConfig) + app.setDefaultProperties(["service.one.port": portOne, "service.two.port": portTwo]) + server = app.run() + } + + def createAndStartClient() { + clientContext = new DefaultCamelContext() + clientContext.addRoutes(new RouteBuilder() { + void configure() { + from("direct:input") + .log("SENT Client request") + .to("http://localhost:$portOne/serviceOne") + .log("RECEIVED Client response") + } + }) + clientContext.start() + } + + def cleanupSpec() { + if (server != null) { + server.close() + server = null + } + } + + def "two camel service spans"() { + setup: + createAndStartClient() + ProducerTemplate template = clientContext.createProducerTemplate() + + when: + template.sendBody("direct:input", "Example request") + + then: + assertTraces(1) { + trace(0, 6) { + it.span(0) { + name "input" + kind INTERNAL + attributes { + "apache-camel.uri" "direct://input" + } + } + it.span(1) { + name "POST" + kind CLIENT + parentSpanId(span(0).spanId) + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$portOne/serviceOne" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "apache-camel.uri" "http://localhost:$portOne/serviceOne" + } + } + it.span(2) { + name "/serviceOne" + kind SERVER + parentSpanId(span(1).spanId) + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://localhost:$portOne/serviceOne" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "apache-camel.uri" "http://0.0.0.0:$portOne/serviceOne" + } + } + it.span(3) { + name "POST" + kind CLIENT + parentSpanId(span(2).spanId) + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://127.0.0.1:$portTwo/serviceTwo" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "apache-camel.uri" "http://127.0.0.1:$portTwo/serviceTwo" + } + } + it.span(4) { + name "/serviceTwo" + kind SERVER + parentSpanId(span(3).spanId) + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$SemanticAttributes.HTTP_URL.key" "http://127.0.0.1:$portTwo/serviceTwo" + "$SemanticAttributes.NET_PEER_PORT.key" Number + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.HTTP_USER_AGENT.key" "Jakarta Commons-HttpClient/3.1" + "$SemanticAttributes.HTTP_FLAVOR.key" "1.1" + "$SemanticAttributes.HTTP_CLIENT_IP.key" "127.0.0.1" + } + } + it.span(5) { + name "/serviceTwo" + kind INTERNAL + parentSpanId(span(4).spanId) + attributes { + "$SemanticAttributes.HTTP_METHOD.key" "POST" + "$SemanticAttributes.HTTP_URL.key" "http://127.0.0.1:$portTwo/serviceTwo" + "apache-camel.uri" "jetty:http://0.0.0.0:$portTwo/serviceTwo?arg=value" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.groovy new file mode 100644 index 000000000..e46199cf9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.groovy @@ -0,0 +1,173 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.client.builder.AwsClientBuilder +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.model.BucketNotificationConfiguration +import com.amazonaws.services.s3.model.ObjectListing +import com.amazonaws.services.s3.model.QueueConfiguration +import com.amazonaws.services.s3.model.S3Event +import com.amazonaws.services.s3.model.S3ObjectSummary +import com.amazonaws.services.s3.model.SetBucketNotificationConfigurationRequest +import com.amazonaws.services.sns.AmazonSNSAsyncClient +import com.amazonaws.services.sns.model.CreateTopicResult +import com.amazonaws.services.sns.model.SetTopicAttributesRequest +import com.amazonaws.services.sqs.AmazonSQSAsyncClient +import com.amazonaws.services.sqs.model.GetQueueAttributesRequest +import com.amazonaws.services.sqs.model.PurgeQueueRequest +import com.amazonaws.services.sqs.model.ReceiveMessageRequest +import com.amazonaws.services.sqs.model.SendMessageRequest +import io.opentelemetry.instrumentation.test.utils.PortUtils +import org.elasticmq.rest.sqs.SQSRestServer +import org.elasticmq.rest.sqs.SQSRestServerBuilder + +class AwsConnector { + + private SQSRestServer sqsRestServer + + private AmazonSQSAsyncClient sqsClient + private AmazonS3Client s3Client + private AmazonSNSAsyncClient snsClient + + static elasticMq() { + AwsConnector awsConnector = new AwsConnector() + def sqsPort = PortUtils.findOpenPort() + awsConnector.sqsRestServer = SQSRestServerBuilder.withPort(sqsPort).withInterface("localhost").start() + + def credentials = new AWSStaticCredentialsProvider(new BasicAWSCredentials("x", "x")) + def endpointConfiguration = new AwsClientBuilder.EndpointConfiguration("http://localhost:"+sqsPort, "elasticmq") + awsConnector.sqsClient = AmazonSQSAsyncClient.asyncBuilder().withCredentials(credentials).withEndpointConfiguration(endpointConfiguration).build() + + return awsConnector + } + + static liveAws() { + AwsConnector awsConnector = new AwsConnector() + + awsConnector.sqsClient = AmazonSQSAsyncClient.asyncBuilder() + .build() + awsConnector.s3Client = AmazonS3Client.builder() + .build() + awsConnector.snsClient = AmazonSNSAsyncClient.asyncBuilder() + .build() + + return awsConnector + } + + AmazonSQSAsyncClient getSqsClient() { + return sqsClient + } + + AmazonS3Client getS3Client() { + return s3Client + } + + AmazonSNSAsyncClient getSnsClient() { + return snsClient + } + + def createQueue(String queueName) { + println "Create queue ${queueName}" + return sqsClient.createQueue(queueName).getQueueUrl() + } + + def getQueueArn(String queueUrl) { + println "Get ARN for queue ${queueUrl}" + return sqsClient.getQueueAttributes( + new GetQueueAttributesRequest(queueUrl) + .withAttributeNames("QueueArn")).getAttributes() + .get("QueueArn") + } + + def setTopicPublishingPolicy(String topicArn) { + println "Set policy for topic ${topicArn}" + snsClient.setTopicAttributes(new SetTopicAttributesRequest(topicArn, "Policy", String.format(SNS_POLICY, topicArn))) + } + + private static final String SNS_POLICY = "{" + + " \"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Principal\": \"*\"," + + " \"Action\": \"sns:Publish\"," + + " \"Resource\": \"%s\"" + + " }]" + + "}" + + def setQueuePublishingPolicy(String queueUrl, String queueArn) { + println "Set policy for queue ${queueArn}" + sqsClient.setQueueAttributes(queueUrl, Collections.singletonMap("Policy", String.format(SQS_POLICY, queueArn))) + } + + private static final String SQS_POLICY = "{" + + " \"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Principal\": \"*\"," + + " \"Action\": \"sqs:SendMessage\"," + + " \"Resource\": \"%s\"" + + " }]" + + "}" + + def createBucket(String bucketName) { + println "Create bucket ${bucketName}" + s3Client.createBucket(bucketName) + } + + def deleteBucket(String bucketName) { + println "Delete bucket ${bucketName}" + ObjectListing objectListing = s3Client.listObjects(bucketName) + Iterator objIter = objectListing.getObjectSummaries().iterator() + while (objIter.hasNext()) { + s3Client.deleteObject(bucketName, objIter.next().getKey()) + } + s3Client.deleteBucket(bucketName) + } + + def enableS3ToSqsNotifications(String bucketName, String sqsQueueArn) { + println "Enable notification for bucket ${bucketName} to queue ${sqsQueueArn}" + BucketNotificationConfiguration notificationConfiguration = new BucketNotificationConfiguration() + notificationConfiguration.addConfiguration("sqsQueueConfig", + new QueueConfiguration(sqsQueueArn, EnumSet.of(S3Event.ObjectCreatedByPut))) + s3Client.setBucketNotificationConfiguration(new SetBucketNotificationConfigurationRequest( + bucketName, notificationConfiguration)) + } + + def createTopicAndSubscribeQueue(String topicName, String queueArn) { + println "Create topic ${topicName} and subscribe to queue ${queueArn}" + CreateTopicResult ctr = snsClient.createTopic(topicName) + snsClient.subscribe(ctr.getTopicArn(), "sqs", queueArn) + return ctr.getTopicArn() + } + + def receiveMessage(String queueUrl) { + println "Receive message from queue ${queueUrl}" + return sqsClient.receiveMessage(new ReceiveMessageRequest(queueUrl).withWaitTimeSeconds(20)) + } + + def purgeQueue(String queueUrl) { + println "Purge queue ${queueUrl}" + sqsClient.purgeQueue(new PurgeQueueRequest(queueUrl)) + } + + def publishSampleNotification(String topicArn) { + snsClient.publish(topicArn, "Hello There") + } + + def sendSampleMessage(String queueUrl) { + SendMessageRequest send = new SendMessageRequest(queueUrl, "{\"type\": \"hello\"}") + sqsClient.sendMessage(send) + } + + def disconnect() { + if (sqsRestServer != null) { + sqsRestServer.stopAndWait() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpan.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpan.groovy new file mode 100644 index 000000000..31cafaf71 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpan.groovy @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert + +class AwsSpan { + + static s3(TraceAssert traceAssert, int index, spanName, bucketName, method = "GET", parentSpan = null) { + return traceAssert.span(index) { + name spanName + kind CLIENT + if (index == 0) { + hasNoParent() + } else { + childOf parentSpan + } + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" spanName.substring(3) + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" method + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + + static sqs(TraceAssert traceAssert, int index, spanName, queueUrl = null, queueName = null, spanKind = CLIENT, parentSpan = null) { + return traceAssert.span(index) { + name spanName + kind spanKind + if (index == 0) { + hasNoParent() + } else { + childOf parentSpan + } + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" spanName.substring(4) + "aws.service" "AmazonSQS" + "aws.queue.name" { it == null || it == queueName } + "aws.queue.url" { it == null || it == queueUrl } + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "http.user_agent" { it == null || String } + "net.peer.name" String + "net.peer.port" { it == null || Number } + "net.transport" IP_TCP + } + } + } + + static sns(TraceAssert traceAssert, int index, spanName, parentSpan = null) { + return traceAssert.span(index) { + name spanName + kind CLIENT + if (index == 0) { + hasNoParent() + } else { + childOf parentSpan + } + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" spanName.substring(4) + "aws.service" "AmazonSNS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.peer.port" { it == null || Number } + "net.transport" IP_TCP + } + } + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpan.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpan.groovy new file mode 100644 index 000000000..e69f046a3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpan.groovy @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert + +class CamelSpan { + + static direct(TraceAssert traceAssert, int index, spanName) { + return traceAssert.span(index) { + name spanName + kind INTERNAL + hasNoParent() + attributes { + "apache-camel.uri" "direct://${spanName}" + } + } + } + + static sqsProduce(TraceAssert traceAssert, int index, queueName, parentSpan=null) { + return traceAssert.span(index) { + name queueName + kind INTERNAL + if (index == 0) { + hasNoParent() + } else { + childOf parentSpan + } + attributes { + "apache-camel.uri" "aws-sqs://${queueName}?amazonSQSClient=%23sqsClient&delay=1000" + "messaging.destination" queueName + } + } + } + + static sqsConsume(TraceAssert traceAssert, int index, queueName, parentSpan=null) { + return traceAssert.span(index) { + name queueName + kind INTERNAL + if (index == 0) { + hasNoParent() + } else { + childOf parentSpan + } + attributes { + "apache-camel.uri" "aws-sqs://${queueName}?amazonSQSClient=%23sqsClient&delay=1000" + "messaging.destination" queueName + "messaging.message_id" String + } + } + } + + static snsPublish(TraceAssert traceAssert, int index, topicName, parentSpan=null) { + return traceAssert.span(index) { + name topicName + kind INTERNAL + childOf parentSpan + attributes { + "apache-camel.uri" "aws-sns://${topicName}?amazonSNSClient=%23snsClient" + "messaging.destination" topicName + } + } + } + + static s3(TraceAssert traceAssert, int index, parentSpan=null) { + return traceAssert.span(index) { + name "aws-s3" + kind INTERNAL + childOf parentSpan + attributes { + "apache-camel.uri" "aws-s3://${bucketName}?amazonS3Client=%23s3Client" + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApp.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApp.groovy new file mode 100644 index 000000000..c58735be7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApp.groovy @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import org.apache.camel.CamelContext +import org.apache.camel.ProducerTemplate +import org.springframework.boot.SpringApplication +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.support.AbstractApplicationContext + +class CamelSpringApp { + + private SpringApplication springApplication + private ConfigurableApplicationContext context + + CamelSpringApp(AwsConnector awsConnector, Class config, Map properties) { + springApplication = new SpringApplication(config) + springApplication.setDefaultProperties(properties) + injectClients(awsConnector) + } + + private injectClients(AwsConnector awsConnector) { + springApplication.addInitializers(new ApplicationContextInitializer() { + @Override + void initialize(AbstractApplicationContext applicationContext) { + if (awsConnector.getSnsClient() != null) { + applicationContext.getBeanFactory().registerSingleton("snsClient", awsConnector.getSnsClient()) + } + if (awsConnector.getSqsClient() != null) { + applicationContext.getBeanFactory().registerSingleton("sqsClient", awsConnector.getSqsClient()) + } + if (awsConnector.getS3Client() != null) { + applicationContext.getBeanFactory().registerSingleton("s3Client", awsConnector.getS3Client()) + } + } + }) + } + + def start() { + context = springApplication.run() + } + + ProducerTemplate producerTemplate() { + def camelContext = context.getBean(CamelContext) + return camelContext.createProducerTemplate() + } + + def stop() { + if (context != null) { + SpringApplication.exit(context) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.groovy new file mode 100644 index 000000000..ac47a4ef5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.groovy @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap +import spock.lang.Ignore +import spock.lang.Shared + +@Ignore("Does not work with localstack - X-Ray features needed") +class S3CamelTest extends AgentInstrumentationSpecification { + + @Shared + AwsConnector awsConnector = AwsConnector.liveAws() + + def "camel S3 producer - camel SQS consumer"() { + setup: + String bucketName = "bucket-test-s3-sqs-camel" + String queueName = "s3SqsCamelTest" + def camelApp = new CamelSpringApp(awsConnector, S3Config, ImmutableMap.of("bucketName", bucketName, "queueName", queueName)) + + def queueUrl = setupTestInfrastructure(queueName, bucketName) + waitAndClearSetupTraces(queueUrl, queueName, bucketName) + + when: + camelApp.start() + camelApp.producerTemplate().sendBody("direct:input", "{\"type\": \"hello\"}") + + then: + assertTraces(6) { + trace(0, 1) { + AwsSpan.sqs(it, 0, "SQS.ListQueues") + } + trace(1, 1) { + AwsSpan.s3(it, 0, "S3.ListObjects", bucketName) + } + trace(2, 5) { + CamelSpan.direct(it, 0, "input") + CamelSpan.s3(it, 1, span(0)) + AwsSpan.s3(it, 2, "S3.PutObject", bucketName, "PUT", span(1)) + AwsSpan.sqs(it, 3, "SQS.ReceiveMessage", queueUrl, null, CONSUMER, span(2)) + CamelSpan.sqsConsume(it, 4, queueName, span(2)) + } + // HTTP "client" receiver span, one per each SQS request + trace(3, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + // camel polling + trace(4, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + + } + // camel cleaning received msg + trace(5, 1) { + AwsSpan.sqs(it, 0, "SQS.DeleteMessage", queueUrl) + } + } + + cleanup: + awsConnector.deleteBucket(bucketName) + awsConnector.purgeQueue(queueUrl) + camelApp.stop() + } + + def setupTestInfrastructure(queueName, bucketName) { + // setup infra + String queueUrl = awsConnector.createQueue(queueName) + awsConnector.createBucket(bucketName) + String queueArn = awsConnector.getQueueArn(queueUrl) + awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) + awsConnector.enableS3ToSqsNotifications(bucketName, queueArn) + + // consume test message from AWS + awsConnector.receiveMessage(queueUrl) + + return queueUrl + } + + def waitAndClearSetupTraces(queueUrl, queueName, bucketName) { + assertTraces(7) { + trace(0, 1) { + AwsSpan.sqs(it, 0, "SQS.CreateQueue", queueUrl, queueName) + } + trace(1, 1) { + AwsSpan.s3(it, 0, "S3.CreateBucket", bucketName, "PUT") + } + trace(2, 1) { + AwsSpan.sqs(it, 0, "SQS.GetQueueAttributes", queueUrl) + } + trace(3, 1) { + AwsSpan.sqs(it, 0, "SQS.SetQueueAttributes", queueUrl) + } + trace(4, 1) { + AwsSpan.s3(it, 0, "S3.SetBucketNotificationConfiguration", bucketName, "PUT") + } + trace(5, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + trace(6, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl, null, CONSUMER) + } + } + clearExportedData() + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.groovy new file mode 100644 index 000000000..8bf1c918b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.groovy @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.component.aws.s3.S3Constants +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class S3Config { + + @Bean + RouteBuilder sqsDirectlyFromS3ConsumerRoute(@Value("\${queueName}") String queueName) { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("aws-sqs://${queueName}?amazonSQSClient=#sqsClient&delay=1000") + .log(LoggingLevel.INFO, "test", "RECEIVER got body : \${body}") + .log(LoggingLevel.INFO, "test", "RECEIVER got headers : \${headers}") + } + } + } + + @Bean + RouteBuilder s3ToSqsProducerRoute(@Value("\${bucketName}") String bucketName) { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test", "SENDING body: \${body}") + .log(LoggingLevel.INFO, "test", "SENDING headers: \${headers}") + .convertBodyTo(byte[].class) + .setHeader(S3Constants.KEY,simple("test-data")) + .to("aws-s3://${bucketName}?amazonS3Client=#s3Client") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.groovy new file mode 100644 index 000000000..d11bb1bd2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.groovy @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap +import spock.lang.Ignore +import spock.lang.Shared + +@Ignore("Does not work with localstack - X-Ray features needed") +class SnsCamelTest extends AgentInstrumentationSpecification { + + @Shared + AwsConnector awsConnector = AwsConnector.liveAws() + + def "AWS SDK SNS producer - camel SQS consumer"() { + setup: + String topicName = "snsCamelTest" + String queueName = "snsCamelTest" + def camelApp = new CamelSpringApp(awsConnector, SnsConfig, ImmutableMap.of("topicName", topicName, "queueName", queueName)) + + def (queueUrl, topicArn) = setupTestInfrastructure(queueName, topicName) + waitAndClearSetupTraces(queueUrl, queueName) + + when: + camelApp.start() + awsConnector.publishSampleNotification(topicArn) + + then: + assertTraces(4) { + trace(0, 3) { + AwsSpan.sns(it, 0, "SNS.Publish") + AwsSpan.sqs(it, 1, "SQS.ReceiveMessage", queueUrl, null, CONSUMER, span(0)) + CamelSpan.sqsConsume(it, 2, queueName, span(0)) + } + // http client span + trace(1, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + trace(2, 1) { + AwsSpan.sqs(it, 0, "SQS.DeleteMessage", queueUrl) + } + // camel polling + trace(3, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + } + cleanup: + awsConnector.purgeQueue(queueUrl) + camelApp.stop() + } + + def "camel SNS producer - camel SQS consumer"() { + setup: + String topicName = "snsCamelTest" + String queueName = "snsCamelTest" + def camelApp = new CamelSpringApp(awsConnector, SnsConfig, ImmutableMap.of("topicName", topicName, "queueName", queueName)) + + def (queueUrl, topicArn) = setupTestInfrastructure(queueName, topicName) + waitAndClearSetupTraces(queueUrl, queueName) + + when: + camelApp.start() + camelApp.producerTemplate().sendBody("direct:input", "{\"type\": \"hello\"}") + + then: + assert topicArn != null + assertTraces(4) { + trace(0, 5) { + CamelSpan.direct(it, 0, "input") + CamelSpan.snsPublish(it, 1, topicName, span(0)) + AwsSpan.sns(it, 2, "SNS.Publish", span(1)) + AwsSpan.sqs(it, 3, "SQS.ReceiveMessage", queueUrl, null, CONSUMER, span(2)) + CamelSpan.sqsConsume(it, 4, queueName, span(2)) + } + trace(1, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + trace(2, 1) { + AwsSpan.sqs(it, 0, "SQS.DeleteMessage", queueUrl) + } + // camel polling + trace(3, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + } + cleanup: + awsConnector.purgeQueue(queueUrl) + camelApp.stop() + } + + def setupTestInfrastructure(queueName, topicName) { + // setup infra + String queueUrl = awsConnector.createQueue(queueName) + String queueArn = awsConnector.getQueueArn(queueName) + awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) + String topicArn = awsConnector.createTopicAndSubscribeQueue(topicName, queueArn) + + // consume test message from AWS + awsConnector.receiveMessage(queueUrl) + + return [queueUrl, topicArn] + } + + def waitAndClearSetupTraces(queueUrl, queueName) { + assertTraces(6) { + trace(0, 1) { + AwsSpan.sqs(it, 0, "SQS.CreateQueue", queueUrl, queueName) + } + trace(1, 1) { + AwsSpan.sqs(it, 0, "SQS.GetQueueAttributes", queueUrl) + } + trace(2, 1) { + AwsSpan.sqs(it, 0, "SQS.SetQueueAttributes", queueUrl) + } + trace(3, 1) { + AwsSpan.sns(it, 0, "SNS.CreateTopic") + } + trace(4, 1) { + AwsSpan.sns(it, 0, "SNS.Subscribe") + } + // test message + trace(5, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + } + clearExportedData() + } +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.groovy new file mode 100644 index 000000000..640da33bf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.groovy @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class SnsConfig { + + @Bean + RouteBuilder sqsConsumerRoute(@Value("\${queueName}") String queueName) { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("aws-sqs://${queueName}?amazonSQSClient=#sqsClient&delay=1000") + .log(LoggingLevel.INFO, "test", "RECEIVER got body : \${body}") + .log(LoggingLevel.INFO, "test", "RECEIVER got headers : \${headers}") + } + } + } + + @Bean + RouteBuilder snsProducerRoute(@Value("\${topicName}") String topicName) { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test", "SENDING body: \${body}") + .log(LoggingLevel.INFO, "test", "SENDING headers: \${headers}") + .to("aws-sns://${topicName}?amazonSNSClient=#snsClient") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.groovy new file mode 100644 index 000000000..5454c9ce4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.groovy @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap +import spock.lang.Shared + +class SqsCamelTest extends AgentInstrumentationSpecification { + + @Shared + AwsConnector awsConnector = AwsConnector.elasticMq() + + def cleanupSpec() { + awsConnector.disconnect() + } + + def "camel SQS producer - camel SQS consumer"() { + setup: + String queueName = "sqsCamelTest" + def camelApp = new CamelSpringApp(awsConnector, SqsConfig, ImmutableMap.of("queueName", queueName)) + def queueUrl = awsConnector.createQueue(queueName) + + waitAndClearSetupTraces(queueUrl, queueName) + + when: + camelApp.start() + camelApp.producerTemplate().sendBody("direct:input", "{\"type\": \"hello\"}") + + then: + assertTraces(4) { + trace(0, 1) { + AwsSpan.sqs(it, 0, "SQS.ListQueues") + } + trace(1, 5) { + CamelSpan.direct(it, 0, "input") + CamelSpan.sqsProduce(it, 1, queueName, span(0)) + AwsSpan.sqs(it, 2, "SQS.SendMessage", queueUrl, null, PRODUCER, span(1)) + AwsSpan.sqs(it, 3, "SQS.ReceiveMessage", queueUrl, null, CONSUMER, span(2)) + CamelSpan.sqsConsume(it, 4, queueName, span(2)) + } + trace(2, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + trace(3, 1) { + AwsSpan.sqs(it, 0, "SQS.DeleteMessage", queueUrl) + } + } + cleanup: + camelApp.stop() + } + + def "AWS SDK SQS producer - camel SQS consumer"() { + setup: + String queueName = "sqsCamelTest" + def camelApp = new CamelSpringApp(awsConnector, SqsConfig, ImmutableMap.of("queueName", queueName)) + def queueUrl = awsConnector.createQueue(queueName) + + waitAndClearSetupTraces(queueUrl, queueName) + + when: + camelApp.start() + awsConnector.sendSampleMessage(queueUrl) + + then: + assertTraces(5) { + trace(0, 1) { + AwsSpan.sqs(it, 0, "SQS.ListQueues") + } + trace(1, 3) { + AwsSpan.sqs(it, 0, "SQS.SendMessage", queueUrl, null, PRODUCER) + AwsSpan.sqs(it, 1, "SQS.ReceiveMessage", queueUrl, null, CONSUMER, span(0)) + CamelSpan.sqsConsume(it, 2, queueName, span(0)) + } + trace(2, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + trace(3, 1) { + AwsSpan.sqs(it, 0, "SQS.DeleteMessage", queueUrl) + } + trace(4, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + } + cleanup: + camelApp.stop() + } + + def "camel SQS producer - AWS SDK SQS consumer"() { + setup: + String queueName = "sqsCamelTestSdkConsumer" + def camelApp = new CamelSpringApp(awsConnector, SqsConfig, ImmutableMap.of("queueSdkConsumerName", queueName)) + def queueUrl = awsConnector.createQueue(queueName) + + waitAndClearSetupTraces(queueUrl, queueName) + + when: + camelApp.start() + camelApp.producerTemplate().sendBody("direct:inputSdkConsumer", "{\"type\": \"hello\"}") + awsConnector.receiveMessage(queueUrl) + + then: + assertTraces(3) { + trace(0, 1) { + AwsSpan.sqs(it, 0, "SQS.ListQueues") + } + trace(1, 4) { + CamelSpan.direct(it, 0, "inputSdkConsumer") + CamelSpan.sqsProduce(it, 1, queueName, span(0)) + AwsSpan.sqs(it, 2, "SQS.SendMessage", queueUrl, null, PRODUCER, span(1)) + AwsSpan.sqs(it, 3, "SQS.ReceiveMessage", queueUrl, null, CONSUMER, span(2)) + } + /** + * This span represents HTTP "sending of receive message" operation. It's always single, while there can be multiple CONSUMER spans (one per consumed message). + * This one could be suppressed (by IF in TracingRequestHandler#beforeRequest but then HTTP instrumentation span would appear + */ + trace(2, 1) { + AwsSpan.sqs(it, 0, "SQS.ReceiveMessage", queueUrl) + } + } + cleanup: + camelApp.stop() + } + + def waitAndClearSetupTraces(queueUrl, queueName) { + assertTraces(1) { + trace(0, 1) { + AwsSpan.sqs(it, 0, "SQS.CreateQueue", queueUrl, queueName) + } + } + clearExportedData() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.groovy new file mode 100644 index 000000000..370c98a9f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.groovy @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws + + +import org.apache.camel.LoggingLevel +import org.apache.camel.builder.RouteBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.context.annotation.Bean + +@SpringBootConfiguration +@EnableAutoConfiguration +class SqsConfig { + + @Bean + @ConditionalOnProperty("queueName") + RouteBuilder consumerRoute(@Value("\${queueName}") String queueName) { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("aws-sqs://${queueName}?amazonSQSClient=#sqsClient&delay=1000") + .log(LoggingLevel.INFO, "test", "RECEIVER got body : \${body}") + .log(LoggingLevel.INFO, "test", "RECEIVER got headers : \${headers}") + } + } + } + + @Bean + @ConditionalOnProperty("queueName") + RouteBuilder producerRoute(@Value("\${queueName}") String queueName) { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test", "SENDING body: \${body}") + .log(LoggingLevel.INFO, "test", "SENDING headers: \${headers}") + .to("aws-sqs://${queueName}?amazonSQSClient=#sqsClient&delay=1000") + } + } + } + + @Bean + @ConditionalOnProperty("queueSdkConsumerName") + RouteBuilder producerRouteForSdkConsumer(@Value("\${queueSdkConsumerName}") String queueSdkConsumerName) { + return new RouteBuilder() { + + @Override + void configure() throws Exception { + from("direct:inputSdkConsumer") + .log(LoggingLevel.INFO, "test", "SENDING body: \${body}") + .log(LoggingLevel.INFO, "test", "SENDING headers: \${headers}") + .to("aws-sqs://${queueSdkConsumerName}?amazonSQSClient=#sqsClient&delay=1000") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/resources/logback-test.xml b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/resources/logback-test.xml new file mode 100644 index 000000000..06d0ba8a8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-camel-2.20/javaagent/src/test/resources/logback-test.xml @@ -0,0 +1,23 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/apache-dubbo-2.7-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/apache-dubbo-2.7-javaagent.gradle new file mode 100644 index 000000000..e2e8dfd7e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/apache-dubbo-2.7-javaagent.gradle @@ -0,0 +1,23 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.dubbo" + module = "dubbo" + versions = "[2.7.0,3.0.0)" + } +} + +dependencies { + implementation project(':instrumentation:apache-dubbo-2.7:library') + + library("org.apache.dubbo:dubbo:2.7.0"){ + exclude group: 'com.alibaba.spring', module: 'spring-context-support' + } + + testImplementation project(':instrumentation:apache-dubbo-2.7:testing') + + testLibrary "org.apache.dubbo:dubbo-config-api:2.7.0" + latestDepTestLibrary "org.apache.dubbo:dubbo:2.+" + latestDepTestLibrary "org.apache.dubbo:dubbo-config-api:2.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboInstrumentationModule.java new file mode 100644 index 000000000..fcbe80854 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboInstrumentationModule.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachedubbo.v2_7; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class DubboInstrumentationModule extends InstrumentationModule { + public DubboInstrumentationModule() { + super("apache-dubbo", "apache-dubbo-2.7"); + } + + @Override + public List helperResourceNames() { + return singletonList("META-INF/services/org.apache.dubbo.rpc.Filter"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("org.apache.dubbo.rpc.Filter"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ResourceInjectingTypeInstrumentation()); + } + + // A type instrumentation is needed to trigger resource injection. + public static class ResourceInjectingTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.dubbo.common.extension.ExtensionLoader"); + } + + @Override + public void transform(TypeTransformer transformer) { + // Nothing to transform, this type instrumentation is only used for injecting resources. + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy new file mode 100644 index 000000000..6de451fb6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7 + + +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class DubboTest extends AbstractDubboTest implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/apache-dubbo-2.7-library.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/apache-dubbo-2.7-library.gradle new file mode 100644 index 000000000..0a54fca21 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/apache-dubbo-2.7-library.gradle @@ -0,0 +1,13 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library("org.apache.dubbo:dubbo:2.7.0"){ + exclude group: 'com.alibaba.spring', module: 'spring-context-support' + } + + testImplementation project(':instrumentation:apache-dubbo-2.7:testing') + + testLibrary "org.apache.dubbo:dubbo-config-api:2.7.0" + latestDepTestLibrary "org.apache.dubbo:dubbo:2.+" + latestDepTestLibrary "org.apache.dubbo:dubbo-config-api:2.+" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/CheckCodeResult.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/CheckCodeResult.java new file mode 100644 index 000000000..26754a34e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/CheckCodeResult.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +public class CheckCodeResult { + + + private String code; + + private boolean success; + + private String message; + + private Object data; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + @Override + public String toString() { + return "CheckCodeResult{" + + "code='" + code + '\'' + + ", success=" + success + + ", message='" + message + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/CodeHelper.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/CodeHelper.java new file mode 100644 index 000000000..6eb05e544 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/CodeHelper.java @@ -0,0 +1,113 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@SuppressWarnings({"SystemOut","CatchAndPrintStackTrace"}) +public class CodeHelper { + + private static CodeHelper ins; + + //Result结构有变更时,记得更新此处 + private static final Set MONE_RESULT_KEY = new HashSet<>(Arrays.asList(new String[]{"code", "message", "data", "traceId", "attachments", "class"})); + + private static final List RESULT_FILED_NAME = Arrays.asList("code", "message", "data"); + + private static class LazyHolder { + private static final CodeHelper ins = new CodeHelper(); + } + + public static CodeHelper ins() { + return LazyHolder.ins; + } + + + + public CheckCodeResult checkCode(Object result) { + CheckCodeResult ccr = new CheckCodeResult(); + ccr.setSuccess(true); + if (null == result) { + return ccr; + } + if ("run.mone.common.Result".equals(result.getClass().getName())) { + handleRpcResult(result, ccr); + } else if (result instanceof Map) { + Map m = (Map) result; + Set keyset = m.keySet(); + // mone result + if (MONE_RESULT_KEY.containsAll(keyset)) { + String code = String.valueOf(m.get("code")); + wrapperCcr(ccr, code, String.valueOf(m.get("message")), m.get("data")); + } + } + return ccr; + } + + private static void handleRpcResult(Object result, CheckCodeResult ccr) { + try { + // Use reflection instead of type casting to obtain the value of the Result attribute. + // This is to address the issue of the Muzzle check error in the business code where the Result dependency was not imported. + Class resultClass = result.getClass(); + if (checkFieldExist(resultClass.getDeclaredFields())) { + String code = getFieldStringValue(result, "code"); + String message = getFieldStringValue(result, "message"); + Object data = getFieldObjectValue(result, "data"); + wrapperCcr(ccr, code, message, data); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static String getFieldStringValue(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return String.valueOf(field.get(obj)); + } + + private static Object getFieldObjectValue(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } + + private static boolean checkFieldExist(Field[] fields) { + if (fields == null) { + return false; + } + Set fieldNamesSet = + Arrays.stream(fields).map(Field::getName).collect(Collectors.toSet()); + return fieldNamesSet.containsAll(RESULT_FILED_NAME); + } + + private static void wrapperCcr(CheckCodeResult ccr, String code, String message, Object data) { + if (code.startsWith("5")) { + ccr.setCode(code); + ccr.setSuccess(false); + ccr.setMessage(message); + ccr.setData(null == data ? "null" : data); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboExtractAdapter.java new file mode 100644 index 000000000..9b1038807 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboExtractAdapter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import io.opentelemetry.context.propagation.TextMapGetter; +import org.apache.dubbo.rpc.RpcInvocation; + +class DubboExtractAdapter implements TextMapGetter { + + static final DubboExtractAdapter GETTER = new DubboExtractAdapter(); + + @Override + public Iterable keys(RpcInvocation invocation) { + return invocation.getAttachments().keySet(); + } + + @Override + public String get(RpcInvocation carrier, String key) { + return carrier.getAttachment(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboHelper.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboHelper.java new file mode 100644 index 000000000..94f852e6c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboHelper.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.dubbo.rpc.Result; + +class DubboHelper { + + private DubboHelper() {} + + static void prepareSpan(SpanBuilder span, String interfaceName, String methodName) { + span.setAttribute(SemanticAttributes.RPC_SERVICE, interfaceName); + span.setAttribute(SemanticAttributes.RPC_METHOD, methodName); + } + + static String getSpanName(String interfaceName, String methodName) { + return interfaceName + "/" + methodName; + } + + static StatusCode statusFromResult(Result result) { + return !result.hasException() ? StatusCode.UNSET : StatusCode.ERROR; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboInjectAdapter.java new file mode 100644 index 000000000..46b4efcb2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.dubbo.rpc.RpcInvocation; + +class DubboInjectAdapter implements TextMapSetter { + + static final DubboInjectAdapter SETTER = new DubboInjectAdapter(); + + @Override + public void set(RpcInvocation rpcInvocation, String key, String value) { + rpcInvocation.setAttachment(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTracer.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTracer.java new file mode 100644 index 000000000..de695eb25 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTracer.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import com.google.gson.Gson; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.RpcServerTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcContext; +import org.apache.dubbo.rpc.RpcInvocation; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.SERVER; + +class DubboTracer extends RpcServerTracer { + + private static final String BUSSINESS_RESPONSE_DATA = "result.json"; + private static final String BUSSINESS_RESULT_CODE = "result.code"; + + protected DubboTracer() {} + + public Context startServerSpan( + String interfaceName, String methodName, RpcInvocation rpcInvocation) { + Context parentContext = extract(rpcInvocation, getGetter()); + SpanBuilder spanBuilder = + spanBuilder(parentContext, DubboHelper.getSpanName(interfaceName, methodName), SERVER) + .setAttribute(SemanticAttributes.RPC_SYSTEM, "dubbo"); + DubboHelper.prepareSpan(spanBuilder, interfaceName, methodName); + NetPeerAttributes.INSTANCE.setNetPeer(spanBuilder, RpcContext.getContext().getRemoteAddress()); + return withServerSpan(Context.current(), spanBuilder.startSpan()); + } + + public Context startClientSpan(String interfaceName, String methodName, URL url) { + Context parentContext = Context.current(); + SpanBuilder spanBuilder = + spanBuilder(parentContext, DubboHelper.getSpanName(interfaceName, methodName), CLIENT) + .setAttribute(SemanticAttributes.RPC_SYSTEM, "dubbo"); + DubboHelper.prepareSpan(spanBuilder, interfaceName, methodName); + NetPeerAttributes.INSTANCE.setNetPeer(spanBuilder, RpcContext.getContext().getRemoteAddress()); + return withClientSpan(parentContext, spanBuilder.startSpan()); + } + + public void end(Context context, Result result) { + StatusCode statusCode = DubboHelper.statusFromResult(result); + if (statusCode != StatusCode.UNSET) { + Span.fromContext(context).setStatus(statusCode); + } + end(context); + } + + public void end(Context context, Object bizResult) { + CheckCodeResult ccr = parseBussinessCode(context, bizResult); + if (!ccr.isSuccess()) { + Span.fromContext(context).setStatus(StatusCode.ERROR); + } + end(context); + } + + public void parseBussinessCode(Context context, Result result){ + this.parseBussinessCode(context, result.getValue()); + } + + public CheckCodeResult parseBussinessCode(Context context, Object bizResult){ + Span span = Span.fromContext(context); + CheckCodeResult ccr = CodeHelper.ins().checkCode(bizResult); + if (!ccr.isSuccess()) { + AttributesBuilder attributes = Attributes.builder(); + attributes.put(BUSSINESS_RESULT_CODE, ccr.getCode()); + attributes.put(BUSSINESS_RESPONSE_DATA, new Gson().toJson(ccr)); + span.addEvent("biz result code exception",attributes.build()); + span.setStatus(StatusCode.ERROR); + } + + return ccr; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.apache-dubbo-2.7:0.0.1:20210831"; + } + + @Override + protected TextMapGetter getGetter() { + return DubboExtractAdapter.GETTER; + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/OpenTelemetryFilter.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/OpenTelemetryFilter.java new file mode 100644 index 000000000..1f3a652a3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/OpenTelemetryFilter.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.SERVER; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +import java.util.concurrent.CompletableFuture; + +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.AsyncRpcResult; +import org.apache.dubbo.rpc.Filter; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcContext; +import org.apache.dubbo.rpc.RpcInvocation; +import org.apache.dubbo.rpc.support.RpcUtils; + +@Activate(group = {"consumer", "provider"}) +public class OpenTelemetryFilter implements Filter { + private final DubboTracer tracer; + + public OpenTelemetryFilter() { + this.tracer = new DubboTracer(); + } + + @Override + public Result invoke(Invoker invoker, Invocation invocation) { + if (!(invocation instanceof RpcInvocation)) { + return invoker.invoke(invocation); + } + String methodName = invocation.getMethodName(); + String interfaceName = invoker.getInterface().getName(); + RpcContext rpcContext = RpcContext.getContext(); + SpanKind kind = rpcContext.isProviderSide() ? SERVER : CLIENT; + final Context context; + if (kind.equals(CLIENT)) { + context = tracer.startClientSpan(interfaceName, methodName, invoker.getUrl()); + tracer.inject(context, (RpcInvocation) invocation, DubboInjectAdapter.SETTER); + RpcContext.getContext().getAttachments().put("traceparent", invocation.getAttachment("traceparent")); + } else { + context = tracer.startServerSpan(interfaceName, methodName, (RpcInvocation) invocation); + } + final Result result; + boolean isSynchronous = true; + try (Scope ignored = context.makeCurrent()) { + result = invoker.invoke(invocation); + if (kind.equals(CLIENT)) { + CompletableFuture future = rpcContext.getCompletableFuture(); + boolean async = RpcUtils.isAsync(invoker.getUrl(), invocation); + if (future != null && async) { + isSynchronous = false; + future.whenComplete((o, throwable) -> { + Object bizResult; + if (throwable != null) { + tracer.endExceptionally(context, throwable); + } else { + // 处理结果 + if (o instanceof AsyncRpcResult) { + AsyncRpcResult asyncResult = (AsyncRpcResult) o; + bizResult = asyncResult.getValue(); + } else { + bizResult = o; + } + tracer.parseBussinessCode(context, result); + tracer.end(context, bizResult); + } + }); + } + } + + } catch (Throwable e) { + tracer.endExceptionally(context, e); + throw e; + } + if (isSynchronous) { + if (result.hasException()) { + tracer.endExceptionally(context, result.getException()); + } else { + tracer.parseBussinessCode(context, result); + tracer.end(context, result); + } + } + return result; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/OpenTelemetryLeastFilter.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/OpenTelemetryLeastFilter.java new file mode 100644 index 000000000..2857bc16c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/OpenTelemetryLeastFilter.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.Filter; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcContext; +import org.apache.dubbo.rpc.RpcInvocation; + +@Activate(group = {"consumer", "provider"},order = Integer.MAX_VALUE) +public class OpenTelemetryLeastFilter implements Filter { + + public OpenTelemetryLeastFilter() { + } + + @Override + public Result invoke(Invoker invoker, Invocation invocation) { + if (!(invocation instanceof RpcInvocation)) { + return invoker.invoke(invocation); + } + Context context = Context.current(); + if(context != null) { + String heraContext = Span.fromContext(context).getSpanContext().getHeraContext().get(HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + if(heraContext != null) { + RpcContext.getContext().getAttachments().put("heracontext", heraContext); + } + } + return invoker.invoke(invocation); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/resources/META-INF/services/org.apache.dubbo.rpc.Filter b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/resources/META-INF/services/org.apache.dubbo.rpc.Filter new file mode 100644 index 000000000..1cd853833 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/main/resources/META-INF/services/org.apache.dubbo.rpc.Filter @@ -0,0 +1,2 @@ +io.opentelemetry.instrumentation.apachedubbo.v2_7.OpenTelemetryFilter +io.opentelemetry.instrumentation.apachedubbo.v2_7.OpenTelemetryLeastFilter \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy new file mode 100644 index 000000000..daa71bc33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/library/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7 + +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class DubboTest extends AbstractDubboTest implements LibraryTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/apache-dubbo-2.7-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/apache-dubbo-2.7-testing.gradle new file mode 100644 index 000000000..7afb5b7f0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/apache-dubbo-2.7-testing.gradle @@ -0,0 +1,23 @@ +plugins { + id "java-library" +} + +apply plugin: "otel.java-conventions" + +def apacheDubboVersion = '2.7.5' + +dependencies { + api project(':testing-common') + + api ("org.apache.dubbo:dubbo:${apacheDubboVersion}") { + exclude group: 'com.alibaba.spring', module: 'spring-context-support' + } + api "org.apache.dubbo:dubbo-config-api:${apacheDubboVersion}" + + implementation "javax.annotation:javax.annotation-api:1.3.2" + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.groovy new file mode 100644 index 000000000..7dfeca1d2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.groovy @@ -0,0 +1,191 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService +import io.opentelemetry.instrumentation.apachedubbo.v2_7.impl.HelloServiceImpl +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.apache.dubbo.common.utils.NetUtils +import org.apache.dubbo.config.ApplicationConfig +import org.apache.dubbo.config.ProtocolConfig +import org.apache.dubbo.config.ReferenceConfig +import org.apache.dubbo.config.RegistryConfig +import org.apache.dubbo.config.ServiceConfig +import org.apache.dubbo.config.bootstrap.DubboBootstrap +import org.apache.dubbo.config.utils.ReferenceConfigCache +import org.apache.dubbo.rpc.service.GenericService +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class AbstractDubboTest extends InstrumentationSpecification { + + @Shared + def protocolConfig = new ProtocolConfig() + + def setupSpec() { + NetUtils.LOCAL_ADDRESS = InetAddress.getLoopbackAddress() + } + + ReferenceConfig configureClient(int port) { + ReferenceConfig reference = new ReferenceConfig<>() + reference.setInterface(HelloService) + reference.setGeneric("true") + reference.setUrl("dubbo://localhost:" + port + "/?timeout=30000") + return reference + } + + ServiceConfig configureServer() { + def registerConfig = new RegistryConfig() + registerConfig.setAddress("N/A") + ServiceConfig service = new ServiceConfig<>() + service.setInterface(HelloService) + service.setRef(new HelloServiceImpl()) + service.setRegistry(registerConfig) + return service + } + + def "test apache dubbo base #dubbo"() { + setup: + def port = PortUtils.findOpenPort() + protocolConfig.setPort(port) + + DubboBootstrap bootstrap = DubboBootstrap.getInstance() + bootstrap.application(new ApplicationConfig("dubbo-test-provider")) + .service(configureServer()) + .protocol(protocolConfig) + .start() + + def consumerProtocolConfig = new ProtocolConfig() + consumerProtocolConfig.setRegister(false) + + def reference = configureClient(port) + DubboBootstrap consumerBootstrap = DubboBootstrap.getInstance() + consumerBootstrap.application(new ApplicationConfig("dubbo-demo-api-consumer")) + .reference(reference) + .protocol(consumerProtocolConfig) + .start() + + when: + GenericService genericService = ReferenceConfigCache.getCache().get(reference) as GenericService + def o = new Object[1] + o[0] = "hello" + def response = runUnderTrace("parent") { + genericService.$invoke("hello", [String.getName()] as String[], o) + } + + then: + response == "hello" + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + name "org.apache.dubbo.rpc.service.GenericService/\$invoke" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "dubbo" + "${SemanticAttributes.RPC_SERVICE.key}" "org.apache.dubbo.rpc.service.GenericService" + "${SemanticAttributes.RPC_METHOD.key}" "\$invoke" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + } + } + span(2) { + name "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService/hello" + kind SERVER + childOf span(1) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "dubbo" + "${SemanticAttributes.RPC_SERVICE.key}" "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService" + "${SemanticAttributes.RPC_METHOD.key}" "hello" + "${SemanticAttributes.NET_PEER_IP.key}" String + "${SemanticAttributes.NET_PEER_NAME.key}" { it == null || it instanceof String } + "${SemanticAttributes.NET_PEER_PORT.key}" Long + } + } + } + } + + cleanup: + bootstrap.destroy() + consumerBootstrap.destroy() + } + + def "test apache dubbo test #dubbo"() { + setup: + def port = PortUtils.findOpenPort() + protocolConfig.setPort(port) + + DubboBootstrap bootstrap = DubboBootstrap.getInstance() + bootstrap.application(new ApplicationConfig("dubbo-test-async-provider")) + .service(configureServer()) + .protocol(protocolConfig) + .start() + + def consumerProtocolConfig = new ProtocolConfig() + consumerProtocolConfig.setRegister(false) + + def reference = configureClient(port) + DubboBootstrap consumerBootstrap = DubboBootstrap.getInstance() + consumerBootstrap.application(new ApplicationConfig("dubbo-demo-async-api-consumer")) + .reference(reference) + .protocol(consumerProtocolConfig) + .start() + + when: + GenericService genericService = ReferenceConfigCache.getCache().get(reference) as GenericService + def o = new Object[1] + o[0] = "hello" + def responseAsync = runUnderTrace("parent") { + genericService.$invokeAsync("hello", [String.getName()] as String[], o) + } + + then: + responseAsync.get() == "hello" + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + name "org.apache.dubbo.rpc.service.GenericService/\$invokeAsync" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "dubbo" + "${SemanticAttributes.RPC_SERVICE.key}" "org.apache.dubbo.rpc.service.GenericService" + "${SemanticAttributes.RPC_METHOD.key}" "\$invokeAsync" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + } + } + span(2) { + name "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService/hello" + kind SERVER + childOf span(1) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "dubbo" + "${SemanticAttributes.RPC_SERVICE.key}" "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService" + "${SemanticAttributes.RPC_METHOD.key}" "hello" + "${SemanticAttributes.NET_PEER_IP.key}" String + "${SemanticAttributes.NET_PEER_NAME.key}" { it == null || it instanceof String } + "${SemanticAttributes.NET_PEER_PORT.key}" Long + } + } + } + } + + cleanup: + bootstrap.destroy() + consumerBootstrap.destroy() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/api/HelloService.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/api/HelloService.java new file mode 100644 index 000000000..a162307ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/api/HelloService.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7.api; + +public interface HelloService { + String hello(String hello); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/impl/HelloServiceImpl.java b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/impl/HelloServiceImpl.java new file mode 100644 index 000000000..59f6b6c16 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/impl/HelloServiceImpl.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7.impl; + +import io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService; + +public class HelloServiceImpl implements HelloService { + @Override + public String hello(String hello) { + return hello; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/apache-httpasyncclient-4.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/apache-httpasyncclient-4.1-javaagent.gradle new file mode 100644 index 000000000..ef37e58c4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/apache-httpasyncclient-4.1-javaagent.gradle @@ -0,0 +1,16 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.httpcomponents" + module = "httpasyncclient" + // 4.0 and 4.0.1 don't copy over the traceparent (etc) http headers on redirect + versions = "[4.1,)" + // TODO implement a muzzle check so that 4.0.x (at least 4.0 and 4.0.1) do not get applied + // and then bring back assertInverse + } +} + +dependencies { + library "org.apache.httpcomponents:httpasyncclient:4.1" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientInstrumentation.java new file mode 100644 index 000000000..ae51eadcb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientInstrumentation.java @@ -0,0 +1,247 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.ApacheHttpAsyncClientTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.io.IOException; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.nio.ContentEncoder; +import org.apache.http.nio.IOControl; +import org.apache.http.nio.protocol.HttpAsyncRequestProducer; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpCoreContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ApacheHttpAsyncClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.http.nio.client.HttpAsyncClient"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.apache.http.nio.client.HttpAsyncClient")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(takesArguments(4)) + .and(takesArgument(0, named("org.apache.http.nio.protocol.HttpAsyncRequestProducer"))) + .and(takesArgument(1, named("org.apache.http.nio.protocol.HttpAsyncResponseConsumer"))) + .and(takesArgument(2, named("org.apache.http.protocol.HttpContext"))) + .and(takesArgument(3, named("org.apache.http.concurrent.FutureCallback"))), + ApacheHttpAsyncClientInstrumentation.class.getName() + "$ClientAdvice"); + } + + @SuppressWarnings("unused") + public static class ClientAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(value = 0, readOnly = false) HttpAsyncRequestProducer requestProducer, + @Advice.Argument(2) HttpContext httpContext, + @Advice.Argument(value = 3, readOnly = false) FutureCallback futureCallback) { + + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + WrappedFutureCallback wrappedFutureCallback = + new WrappedFutureCallback<>(parentContext, httpContext, futureCallback); + requestProducer = + new DelegatingRequestProducer(parentContext, requestProducer, wrappedFutureCallback); + futureCallback = wrappedFutureCallback; + } + } + + public static class DelegatingRequestProducer implements HttpAsyncRequestProducer { + private final Context parentContext; + private final HttpAsyncRequestProducer delegate; + private final WrappedFutureCallback wrappedFutureCallback; + + public DelegatingRequestProducer( + Context parentContext, + HttpAsyncRequestProducer delegate, + WrappedFutureCallback wrappedFutureCallback) { + this.parentContext = parentContext; + this.delegate = delegate; + this.wrappedFutureCallback = wrappedFutureCallback; + } + + @Override + public HttpHost getTarget() { + return delegate.getTarget(); + } + + @Override + public HttpRequest generateRequest() throws IOException, HttpException { + HttpRequest request = delegate.generateRequest(); + wrappedFutureCallback.context = tracer().startSpan(parentContext, request, request); + return request; + } + + @Override + public void produceContent(ContentEncoder encoder, IOControl ioctrl) throws IOException { + delegate.produceContent(encoder, ioctrl); + } + + @Override + public void requestCompleted(HttpContext context) { + delegate.requestCompleted(context); + } + + @Override + public void failed(Exception ex) { + delegate.failed(ex); + } + + @Override + public boolean isRepeatable() { + return delegate.isRepeatable(); + } + + @Override + public void resetRequest() throws IOException { + delegate.resetRequest(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + } + + public static class WrappedFutureCallback implements FutureCallback { + + private static final Logger logger = LoggerFactory.getLogger(WrappedFutureCallback.class); + + private final Context parentContext; + private final HttpContext httpContext; + private final FutureCallback delegate; + + private volatile Context context; + + public WrappedFutureCallback( + Context parentContext, HttpContext httpContext, FutureCallback delegate) { + this.parentContext = parentContext; + this.httpContext = httpContext; + // Note: this can be null in real life, so we have to handle this carefully + this.delegate = delegate; + } + + @Override + public void completed(T result) { + if (context == null) { + // this is unexpected + logger.debug("context was never set"); + completeDelegate(result); + return; + } + + tracer().end(context, getResponse(httpContext)); + + if (parentContext == null) { + completeDelegate(result); + return; + } + + try (Scope ignored = parentContext.makeCurrent()) { + completeDelegate(result); + } + } + + @Override + public void failed(Exception ex) { + if (context == null) { + // this is unexpected + logger.debug("context was never set"); + failDelegate(ex); + return; + } + + // end span before calling delegate + tracer().endExceptionally(context, getResponse(httpContext), ex); + + if (parentContext == null) { + failDelegate(ex); + return; + } + + try (Scope ignored = parentContext.makeCurrent()) { + failDelegate(ex); + } + } + + @Override + public void cancelled() { + if (context == null) { + // this is unexpected + logger.debug("context was never set"); + cancelDelegate(); + return; + } + + // end span before calling delegate + tracer().end(context, getResponse(httpContext)); + + if (parentContext == null) { + cancelDelegate(); + return; + } + + try (Scope ignored = parentContext.makeCurrent()) { + cancelDelegate(); + } + } + + private void completeDelegate(T result) { + if (delegate != null) { + delegate.completed(result); + } + } + + private void failDelegate(Exception ex) { + if (delegate != null) { + delegate.failed(ex); + } + } + + private void cancelDelegate() { + if (delegate != null) { + delegate.cancelled(); + } + } + + private static HttpResponse getResponse(HttpContext context) { + return (HttpResponse) context.getAttribute(HttpCoreContext.HTTP_RESPONSE); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientInstrumentationModule.java new file mode 100644 index 000000000..40291335c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ApacheHttpAsyncClientInstrumentationModule extends InstrumentationModule { + public ApacheHttpAsyncClientInstrumentationModule() { + super("apache-httpasyncclient", "apache-httpasyncclient-4.1"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ApacheHttpAsyncClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientTracer.java new file mode 100644 index 000000000..649e753e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientTracer.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.HttpHeadersInjectAdapter.SETTER; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.http.Header; +import org.apache.http.HttpMessage; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.RequestLine; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.HttpUriRequest; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ApacheHttpAsyncClientTracer + extends HttpClientTracer { + + private static final ApacheHttpAsyncClientTracer TRACER = new ApacheHttpAsyncClientTracer(); + + private ApacheHttpAsyncClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static ApacheHttpAsyncClientTracer tracer() { + return TRACER; + } + + public Context startSpan(Context parentContext) { + Span span = spanBuilder(parentContext, DEFAULT_SPAN_NAME, CLIENT).startSpan(); + return withClientSpan(parentContext, span); + } + + @Override + protected String method(HttpRequest request) { + if (request instanceof HttpUriRequest) { + return ((HttpUriRequest) request).getMethod(); + } else { + RequestLine requestLine = request.getRequestLine(); + return requestLine == null ? null : requestLine.getMethod(); + } + } + + @Override + @Nullable + protected String flavor(HttpRequest httpRequest) { + return httpRequest.getProtocolVersion().toString(); + } + + @Override + protected URI url(HttpRequest request) throws URISyntaxException { + /* + * Note: this is essentially an optimization: HttpUriRequest allows quicker access to required information. + * The downside is that we need to load HttpUriRequest which essentially means we depend on httpasyncclient + * library depending on httpclient library. Currently this seems to be the case. + */ + if (request instanceof HttpUriRequest) { + return ((HttpUriRequest) request).getURI(); + } else { + RequestLine requestLine = request.getRequestLine(); + return requestLine == null ? null : new URI(requestLine.getUri()); + } + } + + @Override + protected Integer status(HttpResponse response) { + StatusLine statusLine = response.getStatusLine(); + return statusLine != null ? statusLine.getStatusCode() : null; + } + + @Override + protected String requestHeader(HttpRequest request, String name) { + return header(request, name); + } + + @Override + protected String responseHeader(HttpResponse response, String name) { + return header(response, name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + private static String header(HttpMessage message, String name) { + Header header = message.getFirstHeader(name); + return header != null ? header.getValue() : null; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.apache-httpasyncclient-4.1"; + } + + /** This method is overridden to allow other classes in this package to call it. */ + @Override + public String spanNameForRequest(HttpRequest httpRequest) { + return super.spanNameForRequest(httpRequest); + } + + /** This method is overridden to allow other classes in this package to call it. */ + @Override + protected void inject(Context context, HttpRequest httpRequest) { + super.inject(context, httpRequest); + } + + /** This method is overridden to allow other classes in this package to call it. */ + @Override + protected void onRequest(Span span, HttpRequest httpRequest) { + super.onRequest(span, httpRequest); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/HttpHeadersInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/HttpHeadersInjectAdapter.java new file mode 100644 index 000000000..839a24b91 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/HttpHeadersInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.http.HttpRequest; + +public class HttpHeadersInjectAdapter implements TextMapSetter { + + public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); + + @Override + public void set(HttpRequest carrier, String key, String value) { + carrier.setHeader(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/ApacheHttpAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/ApacheHttpAsyncClientTest.groovy new file mode 100644 index 000000000..005a3fae4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/ApacheHttpAsyncClientTest.groovy @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.util.concurrent.CancellationException +import org.apache.http.HttpResponse +import org.apache.http.client.config.RequestConfig +import org.apache.http.concurrent.FutureCallback +import org.apache.http.impl.nio.client.HttpAsyncClients +import org.apache.http.message.BasicHeader +import spock.lang.AutoCleanup +import spock.lang.Shared + +class ApacheHttpAsyncClientTest extends HttpClientTest implements AgentTestTrait { + + @Shared + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .build() + + @AutoCleanup + @Shared + def client = HttpAsyncClients.custom().setDefaultRequestConfig(requestConfig).build() + + def setupSpec() { + client.start() + } + + @Override + HttpUriRequest buildRequest(String method, URI uri, Map headers) { + def request = new HttpUriRequest(method, uri) + headers.entrySet().each { + request.addHeader(new BasicHeader(it.key, it.value)) + } + return request + } + + @Override + int sendRequest(HttpUriRequest request, String method, URI uri, Map headers) { + return client.execute(request, null).get().statusLine.statusCode + } + + @Override + void sendRequestWithCallback(HttpUriRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + client.execute(request, new FutureCallback() { + @Override + void completed(HttpResponse httpResponse) { + requestResult.complete(httpResponse.statusLine.statusCode) + } + + @Override + void failed(Exception e) { + requestResult.complete(e) + } + + @Override + void cancelled() { + throw new CancellationException() + } + }) + } + + @Override + Integer responseCodeOnRedirectError() { + return 302 + } + + @Override + boolean testRemoteConnection() { + false // otherwise SocketTimeoutException for https requests + } + + @Override + boolean testCausality() { + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/HttpUriRequest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/HttpUriRequest.groovy new file mode 100644 index 000000000..4a3c7bd57 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/HttpUriRequest.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.apache.http.client.methods.HttpRequestBase + +class HttpUriRequest extends HttpRequestBase { + + private final String methodName + + HttpUriRequest(final String methodName, final URI uri) { + this.methodName = methodName + setURI(uri) + } + + @Override + String getMethod() { + return methodName + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/apache-httpclient-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/apache-httpclient-2.0-javaagent.gradle new file mode 100644 index 000000000..609b5b7e1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/apache-httpclient-2.0-javaagent.gradle @@ -0,0 +1,16 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "commons-httpclient" + module = "commons-httpclient" + versions = "[2.0,4.0)" + assertInverse = true + } +} + +dependencies { + library "commons-httpclient:commons-httpclient:2.0" + + latestDepTestLibrary "commons-httpclient:commons-httpclient:3.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientHttpAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientHttpAttributesExtractor.java new file mode 100644 index 000000000..615863f00 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientHttpAttributesExtractor.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.StatusLine; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class ApacheHttpClientHttpAttributesExtractor + extends HttpAttributesExtractor { + + @Override + protected String method(HttpMethod request) { + return request.getName(); + } + + @Override + protected String url(HttpMethod request) { + return getUrl(request); + } + + @Override + protected String target(HttpMethod request) { + String queryString = request.getQueryString(); + return queryString != null ? request.getPath() + "?" + queryString : request.getPath(); + } + + @Override + @Nullable + protected String host(HttpMethod request) { + Header header = request.getRequestHeader("Host"); + if (header != null) { + return header.getValue(); + } + HostConfiguration hostConfiguration = request.getHostConfiguration(); + if (hostConfiguration != null) { + return hostConfiguration.getVirtualHost(); + } + return null; + } + + @Override + @Nullable + protected String scheme(HttpMethod request) { + HostConfiguration hostConfiguration = request.getHostConfiguration(); + return hostConfiguration != null ? hostConfiguration.getProtocol().getScheme() : null; + } + + @Override + @Nullable + protected String userAgent(HttpMethod request) { + Header header = request.getRequestHeader("User-Agent"); + return header != null ? header.getValue() : null; + } + + @Override + @Nullable + protected Long requestContentLength(HttpMethod request, @Nullable HttpMethod response) { + return null; + } + + @Override + @Nullable + protected Long requestContentLengthUncompressed( + HttpMethod request, @Nullable HttpMethod response) { + return null; + } + + @Override + @Nullable + protected Integer statusCode(HttpMethod request, HttpMethod response) { + StatusLine statusLine = response.getStatusLine(); + return statusLine == null ? null : statusLine.getStatusCode(); + } + + @Override + @Nullable + protected String flavor(HttpMethod request, @Nullable HttpMethod response) { + if (request instanceof HttpMethodBase) { + return ((HttpMethodBase) request).isHttp11() + ? SemanticAttributes.HttpFlavorValues.HTTP_1_1 + : SemanticAttributes.HttpFlavorValues.HTTP_1_0; + } + return null; + } + + @Override + @Nullable + protected Long responseContentLength(HttpMethod request, HttpMethod response) { + return null; + } + + @Override + @Nullable + protected Long responseContentLengthUncompressed(HttpMethod request, HttpMethod response) { + return null; + } + + @Override + @Nullable + protected String serverName(HttpMethod request, @Nullable HttpMethod response) { + return null; + } + + @Override + @Nullable + protected String route(HttpMethod request) { + return null; + } + + @Override + @Nullable + protected String clientIp(HttpMethod request, @Nullable HttpMethod response) { + return null; + } + + // mirroring implementation HttpMethodBase.getURI(), to avoid converting to URI and back to String + private static String getUrl(HttpMethod request) { + HostConfiguration hostConfiguration = request.getHostConfiguration(); + if (hostConfiguration == null) { + String queryString = request.getQueryString(); + if (queryString == null) { + return request.getPath(); + } else { + return request.getPath() + "?" + request.getQueryString(); + } + } else { + StringBuilder url = new StringBuilder(); + url.append(hostConfiguration.getProtocol().getScheme()); + url.append("://"); + url.append(hostConfiguration.getHost()); + int port = hostConfiguration.getPort(); + if (port != hostConfiguration.getProtocol().getDefaultPort()) { + url.append(":"); + url.append(port); + } + url.append(request.getPath()); + String queryString = request.getQueryString(); + if (queryString != null) { + url.append("?"); + url.append(request.getQueryString()); + } + return url.toString(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientInstrumentation.java new file mode 100644 index 000000000..d71c9e4ad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientInstrumentation.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0.ApacheHttpClientSingletons.instrumenter; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.commons.httpclient.HttpMethod; + +public class ApacheHttpClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.commons.httpclient.HttpClient"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("org.apache.commons.httpclient.HttpClient")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("executeMethod")) + .and(takesArguments(3)) + .and(takesArgument(1, named("org.apache.commons.httpclient.HttpMethod"))), + this.getClass().getName() + "$ExecuteMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(1) HttpMethod httpMethod, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, httpMethod)) { + return; + } + + context = instrumenter().start(parentContext, httpMethod); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(1) HttpMethod httpMethod, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + instrumenter().end(context, httpMethod, httpMethod, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientInstrumentationModule.java new file mode 100644 index 000000000..6b6bd1897 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ApacheHttpClientInstrumentationModule extends InstrumentationModule { + + public ApacheHttpClientInstrumentationModule() { + super("apache-httpclient", "apache-httpclient-2.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ApacheHttpClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientNetAttributesExtractor.java new file mode 100644 index 000000000..d8c1ce772 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientNetAttributesExtractor.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpMethod; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class ApacheHttpClientNetAttributesExtractor + extends NetAttributesExtractor { + + @Override + public String transport(HttpMethod request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + public @Nullable String peerName(HttpMethod request, @Nullable HttpMethod response) { + HostConfiguration hostConfiguration = request.getHostConfiguration(); + return hostConfiguration != null ? hostConfiguration.getHost() : null; + } + + @Override + public @Nullable Integer peerPort(HttpMethod request, @Nullable HttpMethod response) { + HostConfiguration hostConfiguration = request.getHostConfiguration(); + return hostConfiguration != null ? hostConfiguration.getPort() : null; + } + + @Override + public @Nullable String peerIp(HttpMethod request, @Nullable HttpMethod response) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java new file mode 100644 index 000000000..7594393cd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; +import org.apache.commons.httpclient.HttpMethod; + +public final class ApacheHttpClientSingletons { + private static final String INSTRUMENTATION_NAME = + "io.opentelemetry.javaagent.apache-httpclient-2.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + HttpAttributesExtractor httpAttributesExtractor = + new ApacheHttpClientHttpAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor spanStatusExtractor = + HttpSpanStatusExtractor.create(httpAttributesExtractor); + ApacheHttpClientNetAttributesExtractor netAttributesExtractor = + new ApacheHttpClientNetAttributesExtractor(); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor) + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor)) + .newClientInstrumenter(new HttpHeaderSetter()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private ApacheHttpClientSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/HttpHeaderSetter.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/HttpHeaderSetter.java new file mode 100644 index 000000000..7f50abfe0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/HttpHeaderSetter.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpMethod; + +final class HttpHeaderSetter implements TextMapSetter { + + @Override + public void set(HttpMethod carrier, String key, String value) { + carrier.setRequestHeader(new Header(key, value)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientTest.groovy new file mode 100644 index 000000000..199ff5eb5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientTest.groovy @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.apache.commons.httpclient.HttpClient +import org.apache.commons.httpclient.HttpMethod +import org.apache.commons.httpclient.methods.DeleteMethod +import org.apache.commons.httpclient.methods.GetMethod +import org.apache.commons.httpclient.methods.HeadMethod +import org.apache.commons.httpclient.methods.OptionsMethod +import org.apache.commons.httpclient.methods.PostMethod +import org.apache.commons.httpclient.methods.PutMethod +import org.apache.commons.httpclient.methods.TraceMethod +import spock.lang.Shared + +class CommonsHttpClientTest extends HttpClientTest implements AgentTestTrait { + @Shared + HttpClient client = new HttpClient() + + def setupSpec() { + client.setConnectionTimeout(CONNECT_TIMEOUT_MS) + } + + @Override + boolean testCausality() { + return false + } + + @Override + HttpMethod buildRequest(String method, URI uri, Map headers) { + def request + switch (method) { + case "GET": + request = new GetMethod(uri.toString()) + break + case "PUT": + request = new PutMethod(uri.toString()) + break + case "POST": + request = new PostMethod(uri.toString()) + break + case "HEAD": + request = new HeadMethod(uri.toString()) + break + case "DELETE": + request = new DeleteMethod(uri.toString()) + break + case "OPTIONS": + request = new OptionsMethod(uri.toString()) + break + case "TRACE": + request = new TraceMethod(uri.toString()) + break + default: + throw new IllegalStateException("Unsupported method: " + method) + } + headers.each { request.setRequestHeader(it.key, it.value) } + return request + } + + @Override + int sendRequest(HttpMethod request, String method, URI uri, Map headers) { + try { + client.executeMethod(request) + return request.getStatusCode() + } finally { + request.releaseConnection() + } + } + + @Override + boolean testRedirects() { + // Generates 4 spans + false + } + + @Override + boolean testReusedRequest() { + // apache commons throws an exception if the request is reused without being recycled first + // at which point this test is not useful (and requires re-populating uri) + false + } + + @Override + boolean testCallback() { + false + } + + @Override + Set> httpAttributes(URI uri) { + Set> extra = [ + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_TARGET + ] + super.httpAttributes(uri) + extra + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/apache-httpclient-4.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/apache-httpclient-4.0-javaagent.gradle new file mode 100644 index 000000000..2e39b629d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/apache-httpclient-4.0-javaagent.gradle @@ -0,0 +1,26 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + fail { + group = "commons-httpclient" + module = "commons-httpclient" + versions = "[,4.0)" + } + pass { + group = "org.apache.httpcomponents" + module = "httpclient" + versions = "[4.0,)" + assertInverse = true + } + pass { + // We want to support the dropwizard clients too. + group = 'io.dropwizard' + module = 'dropwizard-client' + versions = "(,)" + assertInverse = true + } +} + +dependencies { + library "org.apache.httpcomponents:httpclient:4.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHelper.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHelper.java new file mode 100644 index 000000000..84e14c105 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHelper.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0.ApacheHttpClientSingletons.instrumenter; + +import io.opentelemetry.context.Context; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; + +public final class ApacheHttpClientHelper { + + public static void doMethodExit( + Context context, HttpUriRequest request, Object result, Throwable throwable) { + if (throwable != null) { + instrumenter().end(context, request, null, throwable); + } else if (result instanceof HttpResponse) { + instrumenter().end(context, request, (HttpResponse) result, null); + } else { + // ended in WrappingStatusSettingResponseHandler + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHttpAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHttpAttributesExtractor.java new file mode 100644 index 000000000..d5106842d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHttpAttributesExtractor.java @@ -0,0 +1,142 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.HttpUriRequest; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ApacheHttpClientHttpAttributesExtractor + extends HttpAttributesExtractor { + + private static final Logger logger = + LoggerFactory.getLogger(ApacheHttpClientHttpAttributesExtractor.class); + + @Override + protected String method(HttpUriRequest request) { + return request.getMethod(); + } + + @Override + protected String url(HttpUriRequest request) { + return request.getURI().toString(); + } + + @Override + protected String target(HttpUriRequest request) { + URI uri = request.getURI(); + String pathString = uri.getPath(); + String queryString = uri.getQuery(); + if (pathString != null && queryString != null) { + return pathString + "?" + queryString; + } else if (queryString != null) { + return "?" + queryString; + } else { + return pathString; + } + } + + @Override + @Nullable + protected String host(HttpUriRequest request) { + Header header = request.getFirstHeader("Host"); + if (header != null) { + return header.getValue(); + } + return null; + } + + @Override + @Nullable + protected String scheme(HttpUriRequest request) { + return request.getURI().getScheme(); + } + + @Override + @Nullable + protected String userAgent(HttpUriRequest request) { + Header header = request.getFirstHeader("User-Agent"); + return header != null ? header.getValue() : null; + } + + @Override + @Nullable + protected Long requestContentLength(HttpUriRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + @Nullable + protected Long requestContentLengthUncompressed( + HttpUriRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + protected Integer statusCode(HttpUriRequest request, HttpResponse response) { + return response.getStatusLine().getStatusCode(); + } + + @Override + @Nullable + protected String flavor(HttpUriRequest request, @Nullable HttpResponse response) { + ProtocolVersion protocolVersion = request.getRequestLine().getProtocolVersion(); + String protocol = protocolVersion.getProtocol(); + if (!protocol.equals("HTTP")) { + return null; + } + int major = protocolVersion.getMajor(); + int minor = protocolVersion.getMinor(); + if (major == 1 && minor == 0) { + return SemanticAttributes.HttpFlavorValues.HTTP_1_0; + } + if (major == 1 && minor == 1) { + return SemanticAttributes.HttpFlavorValues.HTTP_1_1; + } + if (major == 2 && minor == 0) { + return SemanticAttributes.HttpFlavorValues.HTTP_2_0; + } + logger.debug("unexpected http protocol version: " + protocolVersion); + return null; + } + + @Override + @Nullable + protected Long responseContentLength(HttpUriRequest request, HttpResponse response) { + return null; + } + + @Override + @Nullable + protected Long responseContentLengthUncompressed(HttpUriRequest request, HttpResponse response) { + return null; + } + + @Override + @Nullable + protected String serverName(HttpUriRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + @Nullable + protected String route(HttpUriRequest request) { + return null; + } + + @Override + @Nullable + protected String clientIp(HttpUriRequest request, @Nullable HttpResponse response) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientInstrumentation.java new file mode 100644 index 000000000..f727fd087 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientInstrumentation.java @@ -0,0 +1,279 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0.ApacheHttpClientSingletons.instrumenter; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpUriRequest; + +public class ApacheHttpClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.http.client.HttpClient"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.apache.http.client.HttpClient")); + } + + @Override + public void transform(TypeTransformer transformer) { + // There are 8 execute(...) methods. Depending on the version, they may or may not delegate + // to each other. Thus, all methods need to be instrumented. Because of argument position and + // type, some methods can share the same advice class. The call depth tracking ensures only 1 + // span is created + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(1)) + .and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest"))), + this.getClass().getName() + "$UriRequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest"))) + .and(takesArgument(1, named("org.apache.http.protocol.HttpContext"))), + this.getClass().getName() + "$UriRequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest"))) + .and(takesArgument(1, named("org.apache.http.client.ResponseHandler"))), + this.getClass().getName() + "$UriRequestWithHandlerAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest"))) + .and(takesArgument(1, named("org.apache.http.client.ResponseHandler"))) + .and(takesArgument(2, named("org.apache.http.protocol.HttpContext"))), + this.getClass().getName() + "$UriRequestWithHandlerAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.http.HttpRequest"))), + this.getClass().getName() + "$RequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.http.HttpRequest"))) + .and(takesArgument(2, named("org.apache.http.protocol.HttpContext"))), + this.getClass().getName() + "$RequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.http.HttpRequest"))) + .and(takesArgument(2, named("org.apache.http.client.ResponseHandler"))), + this.getClass().getName() + "$RequestWithHandlerAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(4)) + .and(takesArgument(0, named("org.apache.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.http.HttpRequest"))) + .and(takesArgument(2, named("org.apache.http.client.ResponseHandler"))) + .and(takesArgument(3, named("org.apache.http.protocol.HttpContext"))), + this.getClass().getName() + "$RequestWithHandlerAdvice"); + } + + @SuppressWarnings("unused") + public static class UriRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpUriRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) HttpUriRequest request, + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, request, result, throwable); + } + } + + @SuppressWarnings("unused") + public static class UriRequestWithHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpUriRequest request, + @Advice.Argument(value = 1, readOnly = false) ResponseHandler handler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = + new WrappingStatusSettingResponseHandler<>(context, parentContext, request, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) HttpUriRequest request, + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, request, result, throwable); + } + } + + @SuppressWarnings("unused") + public static class RequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpHost host, + @Advice.Argument(1) HttpRequest request, + @Advice.Local("fullRequest") HttpUriRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + + fullRequest = new HostAndRequestAsHttpUriRequest(host, request); + + if (!instrumenter().shouldStart(parentContext, fullRequest)) { + return; + } + + context = instrumenter().start(parentContext, fullRequest); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("fullRequest") HttpUriRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, fullRequest, result, throwable); + } + } + + @SuppressWarnings("unused") + public static class RequestWithHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpHost host, + @Advice.Argument(1) HttpRequest request, + @Advice.Argument(value = 2, readOnly = false) ResponseHandler handler, + @Advice.Local("fullRequest") HttpUriRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, fullRequest)) { + return; + } + + context = instrumenter().start(parentContext, fullRequest); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = + new WrappingStatusSettingResponseHandler<>( + context, parentContext, fullRequest, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("fullRequest") HttpUriRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, fullRequest, result, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientInstrumentationModule.java new file mode 100644 index 000000000..b0821b7dc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ApacheHttpClientInstrumentationModule extends InstrumentationModule { + + public ApacheHttpClientInstrumentationModule() { + super("apache-httpclient", "apache-httpclient-4.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ApacheHttpClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientNetAttributesExtractor.java new file mode 100644 index 000000000..3a1f152ec --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientNetAttributesExtractor.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ApacheHttpClientNetAttributesExtractor + extends NetAttributesExtractor { + + private static final Logger logger = + LoggerFactory.getLogger(ApacheHttpClientNetAttributesExtractor.class); + + @Override + public String transport(HttpUriRequest request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + public @Nullable String peerName(HttpUriRequest request, @Nullable HttpResponse response) { + return request.getURI().getHost(); + } + + @Override + public Integer peerPort(HttpUriRequest request, @Nullable HttpResponse response) { + URI uri = request.getURI(); + int port = uri.getPort(); + if (port != -1) { + return port; + } + switch (uri.getScheme()) { + case "http": + return 80; + case "https": + return 443; + default: + logger.debug("no default port mapping for scheme: {}", uri.getScheme()); + return null; + } + } + + @Override + public @Nullable String peerIp(HttpUriRequest request, @Nullable HttpResponse response) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java new file mode 100644 index 000000000..b4a014e70 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; + +public final class ApacheHttpClientSingletons { + private static final String INSTRUMENTATION_NAME = + "io.opentelemetry.javaagent.apache-httpclient-4.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + HttpAttributesExtractor httpAttributesExtractor = + new ApacheHttpClientHttpAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor spanStatusExtractor = + HttpSpanStatusExtractor.create(httpAttributesExtractor); + ApacheHttpClientNetAttributesExtractor netAttributesExtractor = + new ApacheHttpClientNetAttributesExtractor(); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor) + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor)) + .newClientInstrumenter(new HttpHeaderSetter()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private ApacheHttpClientSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/HostAndRequestAsHttpUriRequest.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/HostAndRequestAsHttpUriRequest.java new file mode 100644 index 000000000..b5a2daa48 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/HostAndRequestAsHttpUriRequest.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.ProtocolVersion; +import org.apache.http.RequestLine; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.AbstractHttpMessage; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Wraps HttpHost and HttpRequest into a HttpUriRequest for tracers and injectors. */ +public final class HostAndRequestAsHttpUriRequest extends AbstractHttpMessage + implements HttpUriRequest { + + private final String method; + private final RequestLine requestLine; + private final ProtocolVersion protocolVersion; + @Nullable private final URI uri; + + private final HttpRequest actualRequest; + + public HostAndRequestAsHttpUriRequest(HttpHost httpHost, HttpRequest httpRequest) { + method = httpRequest.getRequestLine().getMethod(); + requestLine = httpRequest.getRequestLine(); + protocolVersion = requestLine.getProtocolVersion(); + + URI calculatedUri; + try { + calculatedUri = new URI(httpHost.toURI() + httpRequest.getRequestLine().getUri()); + } catch (URISyntaxException e) { + calculatedUri = null; + } + uri = calculatedUri; + actualRequest = httpRequest; + } + + @Override + public void abort() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isAborted() { + return false; + } + + @Override + public void setHeader(String name, String value) { + actualRequest.setHeader(name, value); + } + + @Override + public String getMethod() { + return method; + } + + @Override + public RequestLine getRequestLine() { + return requestLine; + } + + @Override + public ProtocolVersion getProtocolVersion() { + return protocolVersion; + } + + @Override + public URI getURI() { + return uri; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/HttpHeaderSetter.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/HttpHeaderSetter.java new file mode 100644 index 000000000..d4172ca28 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/HttpHeaderSetter.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.http.client.methods.HttpUriRequest; + +final class HttpHeaderSetter implements TextMapSetter { + + @Override + public void set(HttpUriRequest carrier, String key, String value) { + carrier.setHeader(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/WrappingStatusSettingResponseHandler.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/WrappingStatusSettingResponseHandler.java new file mode 100644 index 000000000..8297a2200 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/WrappingStatusSettingResponseHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0.ApacheHttpClientSingletons.instrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import org.apache.http.HttpResponse; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpUriRequest; + +public final class WrappingStatusSettingResponseHandler implements ResponseHandler { + private final Context context; + private final Context parentContext; + private final HttpUriRequest request; + private final ResponseHandler handler; + + public WrappingStatusSettingResponseHandler( + Context context, Context parentContext, HttpUriRequest request, ResponseHandler handler) { + this.context = context; + this.parentContext = parentContext; + this.request = request; + this.handler = handler; + } + + @Override + public T handleResponse(HttpResponse response) throws IOException { + instrumenter().end(context, request, response, null); + // ending the span before executing the callback handler (and scoping the callback handler to + // the parent context), even though we are inside of a synchronous http client callback + // underneath HttpClient.execute(..), in order to not attribute other CLIENT span timings that + // may be performed in the callback handler to the http client span (and so we don't end up with + // nested CLIENT spans, which we currently suppress) + try (Scope ignored = parentContext.makeCurrent()) { + return handler.handleResponse(response); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy new file mode 100644 index 000000000..334d5a239 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy @@ -0,0 +1,172 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.function.Consumer +import org.apache.http.HttpHost +import org.apache.http.HttpRequest +import org.apache.http.HttpResponse +import org.apache.http.impl.client.DefaultHttpClient +import org.apache.http.message.BasicHeader +import org.apache.http.message.BasicHttpRequest +import org.apache.http.params.HttpConnectionParams +import org.apache.http.params.HttpParams +import org.apache.http.protocol.BasicHttpContext +import spock.lang.Shared + +abstract class ApacheHttpClientTest extends HttpClientTest implements AgentTestTrait { + @Shared + def client = new DefaultHttpClient() + + def setupSpec() { + HttpParams httpParams = client.getParams() + HttpConnectionParams.setConnectionTimeout(httpParams, CONNECT_TIMEOUT_MS) + } + + @Override + boolean testCausality() { + false + } + + @Override + T buildRequest(String method, URI uri, Map headers) { + def request = createRequest(method, uri) + headers.entrySet().each { + request.setHeader(new BasicHeader(it.key, it.value)) + } + return request + } + + @Override + Set> httpAttributes(URI uri) { + Set> extra = [ + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_TARGET + ] + super.httpAttributes(uri) + extra + } + + // compilation fails with @Override annotation on this method (groovy quirk?) + int sendRequest(T request, String method, URI uri, Map headers) { + def response = executeRequest(request, uri) + response.entity?.content?.close() // Make sure the connection is closed. + return response.statusLine.statusCode + } + + // compilation fails with @Override annotation on this method (groovy quirk?) + void sendRequestWithCallback(T request, String method, URI uri, Map headers, RequestResult requestResult) { + try { + executeRequestWithCallback(request, uri) { + it.entity?.content?.close() // Make sure the connection is closed. + requestResult.complete(it.statusLine.statusCode) + } + } catch (Throwable throwable) { + requestResult.complete(throwable) + } + } + + abstract T createRequest(String method, URI uri) + + abstract HttpResponse executeRequest(T request, URI uri) + + abstract void executeRequestWithCallback(T request, URI uri, Consumer callback) + + static String fullPathFromURI(URI uri) { + StringBuilder builder = new StringBuilder() + if (uri.getPath() != null) { + builder.append(uri.getPath()) + } + + if (uri.getQuery() != null) { + builder.append('?') + builder.append(uri.getQuery()) + } + + if (uri.getFragment() != null) { + builder.append('#') + builder.append(uri.getFragment()) + } + return builder.toString() + } +} + +class ApacheClientHostRequest extends ApacheHttpClientTest { + @Override + BasicHttpRequest createRequest(String method, URI uri) { + return new BasicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + HttpResponse executeRequest(BasicHttpRequest request, URI uri) { + return client.execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request) + } + + @Override + void executeRequestWithCallback(BasicHttpRequest request, URI uri, Consumer callback) { + client.execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request) { + callback.accept(it) + } + } +} + +class ApacheClientHostRequestContext extends ApacheHttpClientTest { + @Override + BasicHttpRequest createRequest(String method, URI uri) { + return new BasicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + HttpResponse executeRequest(BasicHttpRequest request, URI uri) { + return client.execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request, new BasicHttpContext()) + } + + @Override + void executeRequestWithCallback(BasicHttpRequest request, URI uri, Consumer callback) { + client.execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request, { + callback.accept(it) + }, new BasicHttpContext()) + } +} + +class ApacheClientUriRequest extends ApacheHttpClientTest { + @Override + HttpUriRequest createRequest(String method, URI uri) { + return new HttpUriRequest(method, uri) + } + + @Override + HttpResponse executeRequest(HttpUriRequest request, URI uri) { + return client.execute(request) + } + + @Override + void executeRequestWithCallback(HttpUriRequest request, URI uri, Consumer callback) { + client.execute(request) { + callback.accept(it) + } + } +} + +class ApacheClientUriRequestContext extends ApacheHttpClientTest { + @Override + HttpUriRequest createRequest(String method, URI uri) { + return new HttpUriRequest(method, uri) + } + + @Override + HttpResponse executeRequest(HttpUriRequest request, URI uri) { + return client.execute(request, new BasicHttpContext()) + } + + @Override + void executeRequestWithCallback(HttpUriRequest request, URI uri, Consumer callback) { + client.execute(request, { + callback.accept(it) + }, new BasicHttpContext()) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/HttpUriRequest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/HttpUriRequest.groovy new file mode 100644 index 000000000..4a3c7bd57 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/HttpUriRequest.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.apache.http.client.methods.HttpRequestBase + +class HttpUriRequest extends HttpRequestBase { + + private final String methodName + + HttpUriRequest(final String methodName, final URI uri) { + this.methodName = methodName + setURI(uri) + } + + @Override + String getMethod() { + return methodName + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/apache-httpclient-5.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/apache-httpclient-5.0-javaagent.gradle new file mode 100644 index 000000000..1e0b6ab9e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/apache-httpclient-5.0-javaagent.gradle @@ -0,0 +1,13 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.httpcomponents.client5" + module = "httpclient5" + versions = "[5.0,)" + } +} + +dependencies { + library "org.apache.httpcomponents.client5:httpclient5:5.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java new file mode 100644 index 000000000..f8adce35b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.instrumenter; + +import io.opentelemetry.context.Context; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpResponse; + +public class ApacheHttpClientHelper { + + public static void doMethodExit( + Context context, ClassicHttpRequest request, Object result, Throwable throwable) { + if (throwable != null) { + instrumenter().end(context, request, null, throwable); + } else if (result instanceof HttpResponse) { + instrumenter().end(context, request, (HttpResponse) result, null); + } else { + // ended in WrappingStatusSettingResponseHandler + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesExtractor.java new file mode 100644 index 000000000..2530e1bc9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesExtractor.java @@ -0,0 +1,165 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.net.URIAuthority; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ApacheHttpClientHttpAttributesExtractor + extends HttpAttributesExtractor { + + private static final Logger logger = + LoggerFactory.getLogger(ApacheHttpClientHttpAttributesExtractor.class); + + @Override + protected String method(ClassicHttpRequest request) { + return request.getMethod(); + } + + @Override + protected String url(ClassicHttpRequest request) { + // similar to org.apache.hc.core5.http.message.BasicHttpRequest.getUri() + // not calling getUri() to avoid unnecessary conversion + StringBuilder url = new StringBuilder(); + URIAuthority authority = request.getAuthority(); + if (authority != null) { + String scheme = request.getScheme(); + if (scheme != null) { + url.append(scheme); + url.append("://"); + } else { + url.append("http://"); + } + url.append(authority.getHostName()); + int port = authority.getPort(); + if (port >= 0) { + url.append(":"); + url.append(port); + } + } + String path = request.getPath(); + if (path != null) { + if (url.length() > 0 && !path.startsWith("/")) { + url.append("/"); + } + url.append(path); + } else { + url.append("/"); + } + return url.toString(); + } + + @Override + protected String target(ClassicHttpRequest request) { + return request.getRequestUri(); + } + + @Override + @Nullable + protected String host(ClassicHttpRequest request) { + Header header = request.getFirstHeader("Host"); + if (header != null) { + return header.getValue(); + } + return null; + } + + @Override + protected String scheme(ClassicHttpRequest request) { + String scheme = request.getScheme(); + return scheme != null ? scheme : "http"; + } + + @Override + @Nullable + protected String userAgent(ClassicHttpRequest request) { + Header header = request.getFirstHeader("User-Agent"); + return header != null ? header.getValue() : null; + } + + @Override + @Nullable + protected Long requestContentLength(ClassicHttpRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + @Nullable + protected Long requestContentLengthUncompressed( + ClassicHttpRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + protected Integer statusCode(ClassicHttpRequest request, HttpResponse response) { + return response.getCode(); + } + + @Override + @Nullable + protected String flavor(ClassicHttpRequest request, @Nullable HttpResponse response) { + ProtocolVersion protocolVersion = request.getVersion(); + if (protocolVersion == null) { + return SemanticAttributes.HttpFlavorValues.HTTP_1_1; + } + String protocol = protocolVersion.getProtocol(); + if (!protocol.equals("HTTP")) { + return null; + } + int major = protocolVersion.getMajor(); + int minor = protocolVersion.getMinor(); + if (major == 1 && minor == 0) { + return SemanticAttributes.HttpFlavorValues.HTTP_1_0; + } + if (major == 1 && minor == 1) { + return SemanticAttributes.HttpFlavorValues.HTTP_1_1; + } + if (major == 2 && minor == 0) { + return SemanticAttributes.HttpFlavorValues.HTTP_2_0; + } + logger.debug("unexpected http protocol version: " + protocolVersion); + return null; + } + + @Override + @Nullable + protected Long responseContentLength(ClassicHttpRequest request, HttpResponse response) { + return null; + } + + @Override + @Nullable + protected Long responseContentLengthUncompressed( + ClassicHttpRequest request, HttpResponse response) { + return null; + } + + @Override + @Nullable + protected String serverName(ClassicHttpRequest request, @Nullable HttpResponse response) { + return null; + } + + @Override + @Nullable + protected String route(ClassicHttpRequest request) { + return null; + } + + @Override + @Nullable + protected String clientIp(ClassicHttpRequest request, @Nullable HttpResponse response) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentation.java new file mode 100644 index 000000000..2ea337ca2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentation.java @@ -0,0 +1,360 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.instrumenter; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; + +public class ApacheHttpClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.hc.client5.http.classic.HttpClient"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.apache.hc.client5.http.classic.HttpClient")); + } + + @Override + public void transform(TypeTransformer transformer) { + // There are 8 execute(...) methods. Depending on the version, they may or may not delegate + // to each other. Thus, all methods need to be instrumented. Because of argument position and + // type, some methods can share the same advice class. The call depth tracking ensures only 1 + // span is created + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(1)) + .and(takesArgument(0, named("org.apache.hc.core5.http.ClassicHttpRequest"))), + this.getClass().getName() + "$RequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.protocol.HttpContext"))), + this.getClass().getName() + "$RequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.hc.core5.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.ClassicHttpRequest"))), + this.getClass().getName() + "$RequestWithHostAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.hc.core5.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(2, named("org.apache.hc.core5.http.protocol.HttpContext"))), + this.getClass().getName() + "$RequestWithHostAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.io.HttpClientResponseHandler"))), + this.getClass().getName() + "$RequestWithHandlerAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.protocol.HttpContext"))) + .and(takesArgument(2, named("org.apache.hc.core5.http.io.HttpClientResponseHandler"))), + this.getClass().getName() + "$RequestWithContextAndHandlerAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.hc.core5.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(2, named("org.apache.hc.core5.http.io.HttpClientResponseHandler"))), + this.getClass().getName() + "$RequestWithHostAndHandlerAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(4)) + .and(takesArgument(0, named("org.apache.hc.core5.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(2, named("org.apache.hc.core5.http.protocol.HttpContext"))) + .and(takesArgument(3, named("org.apache.hc.core5.http.io.HttpClientResponseHandler"))), + this.getClass().getName() + "$RequestWithHostAndContextAndHandlerAdvice"); + } + + @SuppressWarnings("unused") + public static class RequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, request, result, throwable); + } + } + + @SuppressWarnings("unused") + public static class RequestWithHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Argument(value = 1, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = + new WrappingStatusSettingResponseHandler(context, parentContext, request, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, request, result, throwable); + } + } + + @SuppressWarnings("unused") + public static class RequestWithContextAndHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Argument(value = 2, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = + new WrappingStatusSettingResponseHandler(context, parentContext, request, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, request, result, throwable); + } + } + + @SuppressWarnings("unused") + public static class RequestWithHostAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpHost host, + @Advice.Argument(1) ClassicHttpRequest request, + @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + fullRequest = new RequestWithHost(host, request); + if (!instrumenter().shouldStart(parentContext, fullRequest)) { + return; + } + + context = instrumenter().start(parentContext, fullRequest); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, fullRequest, result, throwable); + } + } + + @SuppressWarnings("unused") + public static class RequestWithHostAndHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpHost host, + @Advice.Argument(1) ClassicHttpRequest request, + @Advice.Argument(value = 2, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = currentContext(); + fullRequest = new RequestWithHost(host, request); + if (!instrumenter().shouldStart(parentContext, fullRequest)) { + return; + } + + context = instrumenter().start(parentContext, fullRequest); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = + new WrappingStatusSettingResponseHandler(context, parentContext, fullRequest, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, fullRequest, result, throwable); + } + } + + @SuppressWarnings("unused") + public static class RequestWithHostAndContextAndHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpHost host, + @Advice.Argument(1) ClassicHttpRequest request, + @Advice.Argument(value = 3, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = currentContext(); + fullRequest = new RequestWithHost(host, request); + if (!instrumenter().shouldStart(parentContext, fullRequest)) { + return; + } + + context = instrumenter().start(parentContext, fullRequest); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = + new WrappingStatusSettingResponseHandler(context, parentContext, fullRequest, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelFullRequest") ClassicHttpRequest fullRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, fullRequest, result, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentationModule.java new file mode 100644 index 000000000..a454220bb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ApacheHttpClientInstrumentationModule extends InstrumentationModule { + + public ApacheHttpClientInstrumentationModule() { + super("apache-httpclient", "apache-httpclient-5.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ApacheHttpClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesExtractor.java new file mode 100644 index 000000000..951330a4a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesExtractor.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ApacheHttpClientNetAttributesExtractor + extends NetAttributesExtractor { + + private static final Logger logger = + LoggerFactory.getLogger(ApacheHttpClientNetAttributesExtractor.class); + + @Override + public String transport(ClassicHttpRequest request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + public @Nullable String peerName(ClassicHttpRequest request, @Nullable HttpResponse response) { + return request.getAuthority().getHostName(); + } + + @Override + public Integer peerPort(ClassicHttpRequest request, @Nullable HttpResponse response) { + int port = request.getAuthority().getPort(); + if (port != -1) { + return port; + } + String scheme = request.getScheme(); + if (scheme == null) { + return 80; + } + switch (scheme) { + case "http": + return 80; + case "https": + return 443; + default: + logger.debug("no default port mapping for scheme: {}", scheme); + return null; + } + } + + @Override + public @Nullable String peerIp(ClassicHttpRequest request, @Nullable HttpResponse response) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java new file mode 100644 index 000000000..53fe88bfa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpResponse; + +public final class ApacheHttpClientSingletons { + private static final String INSTRUMENTATION_NAME = + "io.opentelemetry.javaagent.apache-httpclient-5.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + HttpAttributesExtractor httpAttributesExtractor = + new ApacheHttpClientHttpAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor spanStatusExtractor = + HttpSpanStatusExtractor.create(httpAttributesExtractor); + ApacheHttpClientNetAttributesExtractor netAttributesExtractor = + new ApacheHttpClientNetAttributesExtractor(); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor) + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor)) + .newClientInstrumenter(new HttpHeaderSetter()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private ApacheHttpClientSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeaderSetter.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeaderSetter.java new file mode 100644 index 000000000..505e52e1b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeaderSetter.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.hc.core5.http.ClassicHttpRequest; + +final class HttpHeaderSetter implements TextMapSetter { + + @Override + public void set(ClassicHttpRequest carrier, String key, String value) { + carrier.setHeader(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java new file mode 100644 index 000000000..6fd5767e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import java.net.URI; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.HttpRequestWrapper; +import org.apache.hc.core5.net.URIAuthority; + +public class RequestWithHost extends HttpRequestWrapper implements ClassicHttpRequest { + + private final String scheme; + private final URIAuthority authority; + + public RequestWithHost(HttpHost httpHost, ClassicHttpRequest httpRequest) { + super(httpRequest); + + this.scheme = httpHost.getSchemeName(); + this.authority = new URIAuthority(httpHost.getHostName(), httpHost.getPort()); + } + + @Override + public String getScheme() { + return scheme; + } + + @Override + public URIAuthority getAuthority() { + return authority; + } + + @Override + public URI getUri() { + // overriding super because it's not correct (doesn't incorporate authority) + // and isn't needed anyways + throw new UnsupportedOperationException(); + } + + @Override + public HttpEntity getEntity() { + throw new UnsupportedOperationException(); + } + + @Override + public void setEntity(HttpEntity entity) { + throw new UnsupportedOperationException(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java new file mode 100644 index 000000000..4735955e5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientSingletons.instrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; + +public class WrappingStatusSettingResponseHandler implements HttpClientResponseHandler { + final Context context; + final Context parentContext; + final ClassicHttpRequest request; + final HttpClientResponseHandler handler; + + public WrappingStatusSettingResponseHandler( + Context context, + Context parentContext, + ClassicHttpRequest request, + HttpClientResponseHandler handler) { + this.context = context; + this.parentContext = parentContext; + this.request = request; + this.handler = handler; + } + + @Override + public T handleResponse(ClassicHttpResponse response) throws IOException, HttpException { + instrumenter().end(context, request, response, null); + // ending the span before executing the callback handler (and scoping the callback handler to + // the parent context), even though we are inside of a synchronous http client callback + // underneath HttpClient.execute(..), in order to not attribute other CLIENT span timings that + // may be performed in the callback handler to the http client span (and so we don't end up with + // nested CLIENT spans, which we currently suppress) + try (Scope ignored = parentContext.makeCurrent()) { + return handler.handleResponse(response); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy new file mode 100644 index 000000000..487f12558 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy @@ -0,0 +1,177 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.TimeUnit +import java.util.function.Consumer +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase +import org.apache.hc.client5.http.config.RequestConfig +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder +import org.apache.hc.client5.http.impl.classic.HttpClients +import org.apache.hc.core5.http.ClassicHttpRequest +import org.apache.hc.core5.http.ClassicHttpResponse +import org.apache.hc.core5.http.HttpHost +import org.apache.hc.core5.http.HttpRequest +import org.apache.hc.core5.http.message.BasicClassicHttpRequest +import org.apache.hc.core5.http.message.BasicHeader +import org.apache.hc.core5.http.protocol.BasicHttpContext +import spock.lang.AutoCleanup +import spock.lang.Shared + +abstract class ApacheHttpClientTest extends HttpClientTest implements AgentTestTrait { + @Shared + @AutoCleanup + CloseableHttpClient client + + def setupSpec() { + HttpClientBuilder builder = HttpClients.custom() + builder.setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build()) + + client = builder.build() + } + + @Override + T buildRequest(String method, URI uri, Map headers) { + def request = createRequest(method, uri) + headers.entrySet().each { + request.setHeader(new BasicHeader(it.key, it.value)) + } + return request + } + + @Override + Set> httpAttributes(URI uri) { + Set> extra = [ + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_TARGET + ] + super.httpAttributes(uri) + extra + } + + // compilation fails with @Override annotation on this method (groovy quirk?) + int sendRequest(T request, String method, URI uri, Map headers) { + def response = executeRequest(request, uri) + response.close() // Make sure the connection is closed. + return response.code + } + + // compilation fails with @Override annotation on this method (groovy quirk?) + void sendRequestWithCallback(T request, String method, URI uri, Map headers, RequestResult requestResult) { + try { + executeRequestWithCallback(request, uri) { + it.close() // Make sure the connection is closed. + requestResult.complete(it.code) + } + } catch (Throwable throwable) { + requestResult.complete(throwable) + } + } + + abstract T createRequest(String method, URI uri) + + abstract ClassicHttpResponse executeRequest(T request, URI uri) + + abstract void executeRequestWithCallback(T request, URI uri, Consumer callback) + + static String fullPathFromURI(URI uri) { + StringBuilder builder = new StringBuilder() + if (uri.getPath() != null) { + builder.append(uri.getPath()) + } + + if (uri.getQuery() != null) { + builder.append('?') + builder.append(uri.getQuery()) + } + + if (uri.getFragment() != null) { + builder.append('#') + builder.append(uri.getFragment()) + } + return builder.toString() + } +} + +class ApacheClientHostRequest extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new BasicClassicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri) { + return client.execute(new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), request) + } + + @Override + void executeRequestWithCallback(ClassicHttpRequest request, URI uri, Consumer callback) { + client.execute(new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), request) { + callback.accept(it) + } + } +} + +class ApacheClientHostRequestContext extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new BasicClassicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri) { + return client.execute(new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), request, new BasicHttpContext()) + } + + @Override + void executeRequestWithCallback(ClassicHttpRequest request, URI uri, Consumer callback) { + client.execute(new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), request, new BasicHttpContext()) { + callback.accept(it) + } + } +} + +class ApacheClientUriRequest extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new HttpUriRequestBase(method, uri) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri) { + return client.execute(request) + } + + @Override + void executeRequestWithCallback(ClassicHttpRequest request, URI uri, Consumer callback) { + client.execute(request) { + callback.accept(it) + } + } +} + +class ApacheClientUriRequestContext extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new HttpUriRequestBase(method, uri) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri) { + return client.execute(request, new BasicHttpContext()) + } + + @Override + void executeRequestWithCallback(ClassicHttpRequest request, URI uri, Consumer callback) { + client.execute(request, new BasicHttpContext()) { + callback.accept(it) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/armeria-1.3-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/armeria-1.3-javaagent.gradle new file mode 100644 index 000000000..f33ff6ac9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/armeria-1.3-javaagent.gradle @@ -0,0 +1,18 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.linecorp.armeria" + module = "armeria" + versions = "[1.3.0,)" + assertInverse = true + } +} + +dependencies { + implementation project(':instrumentation:armeria-1.3:library') + + library "com.linecorp.armeria:armeria:1.3.0" + + testImplementation project(':instrumentation:armeria-1.3:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/AbstractStreamMessageSubscriptionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/AbstractStreamMessageSubscriptionInstrumentation.java new file mode 100644 index 000000000..05b432352 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/AbstractStreamMessageSubscriptionInstrumentation.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.reactivestreams.Subscriber; + +public class AbstractStreamMessageSubscriptionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.linecorp.armeria.common.stream.AbstractStreamMessage$SubscriptionImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor() + .and( + takesArgument(0, named("com.linecorp.armeria.common.stream.AbstractStreamMessage"))) + .and(takesArgument(1, named("org.reactivestreams.Subscriber"))), + AbstractStreamMessageSubscriptionInstrumentation.class.getName() + "$WrapSubscriberAdvice"); + } + + @SuppressWarnings("unused") + public static class WrapSubscriberAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void attachContext( + @Advice.Argument(value = 1, readOnly = false) Subscriber subscriber) { + subscriber = SubscriberWrapper.wrap(subscriber); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaInstrumentationModule.java new file mode 100644 index 000000000..b85398965 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaInstrumentationModule.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class ArmeriaInstrumentationModule extends InstrumentationModule { + public ArmeriaInstrumentationModule() { + super("armeria", "armeria-1.3"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // Unrelated class which was added in Armeria 1.3.0, the minimum version we support. + return hasClassesNamed("com.linecorp.armeria.server.metric.PrometheusExpositionServiceBuilder"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ArmeriaWebClientBuilderInstrumentation(), + new ArmeriaServerBuilderInstrumentation(), + new AbstractStreamMessageSubscriptionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaServerBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaServerBuilderInstrumentation.java new file mode 100644 index 000000000..3cf870fa6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaServerBuilderInstrumentation.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.linecorp.armeria.server.ServerBuilder; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ArmeriaServerBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.linecorp.armeria.server.ServerBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")), + ArmeriaServerBuilderInstrumentation.class.getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter + public static void onEnter(@Advice.This ServerBuilder builder) { + builder.decorator(ArmeriaSingletons.SERVER_DECORATOR); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java new file mode 100644 index 000000000..227ad16d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.client.HttpClient; +import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.server.HttpService; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.armeria.v1_3.ArmeriaTracing; +import io.opentelemetry.instrumentation.armeria.v1_3.ArmeriaTracingBuilder; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; +import java.util.function.Function; + +// Holds singleton references to decorators to match against during suppression. +// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/903 +public final class ArmeriaSingletons { + public static final Function CLIENT_DECORATOR; + + public static final Function SERVER_DECORATOR; + + static { + ArmeriaTracingBuilder builder = ArmeriaTracing.newBuilder(GlobalOpenTelemetry.get()); + + AttributesExtractor peerServiceAttributesExtractor = + PeerServiceAttributesExtractor.createUsingReflection( + "io.opentelemetry.instrumentation.armeria.v1_3.ArmeriaNetAttributesExtractor"); + if (peerServiceAttributesExtractor != null) { + builder.addAttributeExtractor(peerServiceAttributesExtractor); + } + + ArmeriaTracing tracing = builder.build(); + CLIENT_DECORATOR = tracing.newClientDecorator(); + SERVER_DECORATOR = tracing.newServiceDecorator(); + } + + private ArmeriaSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaWebClientBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaWebClientBuilderInstrumentation.java new file mode 100644 index 000000000..522a26d81 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaWebClientBuilderInstrumentation.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.linecorp.armeria.client.WebClientBuilder; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.function.Function; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ArmeriaWebClientBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.linecorp.armeria.client.WebClientBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("decorator").and(takesArgument(0, Function.class))), + ArmeriaWebClientBuilderInstrumentation.class.getName() + "$SuppressDecoratorAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")), + ArmeriaWebClientBuilderInstrumentation.class.getName() + "$BuildAdvice"); + } + + // Intercept calls from app to register decorator and suppress them to avoid registering + // multiple decorators, one from user app and one from our auto instrumentation. Otherwise, we + // will end up with double telemetry. + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/903 + @SuppressWarnings("unused") + public static class SuppressDecoratorAdvice { + + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + public static boolean suppressDecorator(@Advice.Argument(0) Function decorator) { + return decorator != ArmeriaSingletons.CLIENT_DECORATOR; + } + + @Advice.OnMethodExit + public static void handleSuppression( + @Advice.This WebClientBuilder builder, + @Advice.Enter boolean suppressed, + @Advice.Return(readOnly = false) WebClientBuilder returned) { + if (suppressed) { + returned = builder; + } + } + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter + public static void build(@Advice.This WebClientBuilder builder) { + builder.decorator(ArmeriaSingletons.CLIENT_DECORATOR); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/SubscriberWrapper.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/SubscriberWrapper.java new file mode 100644 index 000000000..ef006aec6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/SubscriberWrapper.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public class SubscriberWrapper implements Subscriber { + private final Subscriber delegate; + private final Context context; + + private SubscriberWrapper(Subscriber delegate, Context context) { + this.delegate = delegate; + this.context = context; + } + + public static Subscriber wrap(Subscriber delegate) { + Context context = Context.current(); + if (context != Context.root()) { + return new SubscriberWrapper(delegate, context); + } + return delegate; + } + + @Override + public void onSubscribe(Subscription subscription) { + try (Scope ignored = context.makeCurrent()) { + delegate.onSubscribe(subscription); + } + } + + @Override + public void onNext(Object o) { + try (Scope ignored = context.makeCurrent()) { + delegate.onNext(o); + } + } + + @Override + public void onError(Throwable throwable) { + try (Scope ignored = context.makeCurrent()) { + delegate.onError(throwable); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + delegate.onComplete(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy new file mode 100644 index 000000000..16bc9afa2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3 + +import com.linecorp.armeria.client.WebClientBuilder +import io.opentelemetry.instrumentation.armeria.v1_3.AbstractArmeriaHttpClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class ArmeriaHttpClientTest extends AbstractArmeriaHttpClientTest implements AgentTestTrait { + @Override + WebClientBuilder configureClient(WebClientBuilder clientBuilder) { + return clientBuilder + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.groovy new file mode 100644 index 000000000..bbdef4911 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3 + +import com.linecorp.armeria.server.ServerBuilder +import io.opentelemetry.instrumentation.armeria.v1_3.AbstractArmeriaHttpServerTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class ArmeriaHttpServerTest extends AbstractArmeriaHttpServerTest implements AgentTestTrait { + @Override + ServerBuilder configureServer(ServerBuilder sb) { + return sb + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/armeria-1.3-library.gradle b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/armeria-1.3-library.gradle new file mode 100644 index 000000000..245497a09 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/armeria-1.3-library.gradle @@ -0,0 +1,8 @@ +apply plugin: "otel.library-instrumentation" +apply plugin: "net.ltgt.nullaway" + +dependencies { + library "com.linecorp.armeria:armeria:1.3.0" + + testImplementation project(':instrumentation:armeria-1.3:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpAttributesExtractor.java new file mode 100644 index 000000000..6e932e8ba --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpAttributesExtractor.java @@ -0,0 +1,134 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.common.HttpHeaderNames; +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.server.ServiceRequestContext; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class ArmeriaHttpAttributesExtractor + extends HttpAttributesExtractor { + + @Override + protected String method(RequestContext ctx) { + return ctx.method().name(); + } + + @Override + protected String url(RequestContext ctx) { + return request(ctx).uri().toString(); + } + + @Override + protected String target(RequestContext ctx) { + return request(ctx).path(); + } + + @Override + @Nullable + protected String host(RequestContext ctx) { + return request(ctx).authority(); + } + + @Override + @Nullable + protected String scheme(RequestContext ctx) { + return request(ctx).scheme(); + } + + @Override + @Nullable + protected String userAgent(RequestContext ctx) { + return request(ctx).headers().get(HttpHeaderNames.USER_AGENT); + } + + @Override + @Nullable + protected Long requestContentLength(RequestContext ctx, @Nullable RequestLog requestLog) { + if (requestLog == null) { + return null; + } + return requestLog.requestLength(); + } + + @Override + @Nullable + protected Long requestContentLengthUncompressed( + RequestContext ctx, @Nullable RequestLog requestLog) { + return null; + } + + @Override + @Nullable + protected Integer statusCode(RequestContext ctx, RequestLog requestLog) { + HttpStatus status = requestLog.responseHeaders().status(); + if (!status.equals(HttpStatus.UNKNOWN)) { + return status.code(); + } + return null; + } + + @Override + protected String flavor(RequestContext ctx, @Nullable RequestLog requestLog) { + SessionProtocol protocol = ctx.sessionProtocol(); + if (protocol.isMultiplex()) { + return SemanticAttributes.HttpFlavorValues.HTTP_2_0; + } else { + return SemanticAttributes.HttpFlavorValues.HTTP_1_1; + } + } + + @Override + protected Long responseContentLength(RequestContext ctx, RequestLog requestLog) { + return requestLog.responseLength(); + } + + @Override + @Nullable + protected Long responseContentLengthUncompressed(RequestContext ctx, RequestLog requestLog) { + return null; + } + + @Override + @Nullable + protected String serverName(RequestContext ctx, @Nullable RequestLog requestLog) { + if (ctx instanceof ServiceRequestContext) { + return ((ServiceRequestContext) ctx).config().virtualHost().hostnamePattern(); + } + return null; + } + + @Override + @Nullable + protected String route(RequestContext ctx) { + if (ctx instanceof ServiceRequestContext) { + return ((ServiceRequestContext) ctx).config().route().patternString(); + } + return null; + } + + @Override + @Nullable + protected String clientIp(RequestContext ctx, @Nullable RequestLog requestLog) { + return null; + } + + private static HttpRequest request(RequestContext ctx) { + HttpRequest request = ctx.request(); + if (request == null) { + throw new IllegalStateException( + "Context always has a request in decorators, this exception indicates a programming bug."); + } + return request; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaNetAttributesExtractor.java new file mode 100644 index 000000000..3a589f196 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaNetAttributesExtractor.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.logging.RequestLog; +import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class ArmeriaNetAttributesExtractor + extends InetSocketAddressNetAttributesExtractor { + + @Override + public String transport(RequestContext ctx) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + @Nullable + public InetSocketAddress getAddress(RequestContext ctx, @Nullable RequestLog requestLog) { + SocketAddress address = ctx.remoteAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracing.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracing.java new file mode 100644 index 000000000..be751aa94 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracing.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.client.HttpClient; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.server.HttpService; +import com.linecorp.armeria.server.ServiceRequestContext; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.function.Function; + +/** Entrypoint for tracing Armeria services or clients. */ +public final class ArmeriaTracing { + + /** Returns a new {@link ArmeriaTracing} configured with the given {@link OpenTelemetry}. */ + public static ArmeriaTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + public static ArmeriaTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new ArmeriaTracingBuilder(openTelemetry); + } + + private final Instrumenter clientInstrumenter; + private final Instrumenter serverInstrumenter; + + ArmeriaTracing( + Instrumenter clientInstrumenter, + Instrumenter serverInstrumenter) { + this.clientInstrumenter = clientInstrumenter; + this.serverInstrumenter = serverInstrumenter; + } + + /** + * Returns a new {@link HttpClient} decorator for use with methods like {@link + * com.linecorp.armeria.client.ClientBuilder#decorator(Function)}. + */ + public Function newClientDecorator() { + return client -> new OpenTelemetryClient(client, clientInstrumenter); + } + + /** + * Returns a new {@link HttpService} decorator for use with methods like {@link + * HttpService#decorate(Function)}. + */ + public Function newServiceDecorator() { + return service -> new OpenTelemetryService(service, serverInstrumenter); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracingBuilder.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracingBuilder.java new file mode 100644 index 000000000..5b0a87775 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTracingBuilder.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.server.ServiceRequestContext; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; + +public final class ArmeriaTracingBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.armeria-1.3"; + + private final OpenTelemetry openTelemetry; + + private final List> + additionalExtractors = new ArrayList<>(); + + private Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractorTransformer = Function.identity(); + + ArmeriaTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + public ArmeriaTracingBuilder setStatusExtractor( + Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractor) { + this.statusExtractorTransformer = statusExtractor; + return this; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. The {@link AttributesExtractor} will be executed after all default extractors. + */ + public ArmeriaTracingBuilder addAttributeExtractor( + AttributesExtractor attributesExtractor) { + additionalExtractors.add(attributesExtractor); + return this; + } + + public ArmeriaTracing build() { + ArmeriaHttpAttributesExtractor httpAttributesExtractor = new ArmeriaHttpAttributesExtractor(); + ArmeriaNetAttributesExtractor netAttributesExtractor = new ArmeriaNetAttributesExtractor(); + + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor spanStatusExtractor = + statusExtractorTransformer.apply(HttpSpanStatusExtractor.create(httpAttributesExtractor)); + + InstrumenterBuilder clientInstrumenterBuilder = + Instrumenter.newBuilder(openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor); + InstrumenterBuilder serverInstrumenterBuilder = + Instrumenter.newBuilder(openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor); + + Stream.of(clientInstrumenterBuilder, serverInstrumenterBuilder) + .forEach( + instrumenter -> + instrumenter + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractors(additionalExtractors)); + + serverInstrumenterBuilder.addRequestMetrics(HttpServerMetrics.get()); + + return new ArmeriaTracing( + clientInstrumenterBuilder.newClientInstrumenter(new ClientRequestContextSetter()), + serverInstrumenterBuilder.newServerInstrumenter(new RequestContextGetter())); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ClientRequestContextSetter.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ClientRequestContextSetter.java new file mode 100644 index 000000000..19f6d9496 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ClientRequestContextSetter.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.client.ClientRequestContext; +import io.opentelemetry.context.propagation.TextMapSetter; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class ClientRequestContextSetter implements TextMapSetter { + + @Override + public void set(@Nullable ClientRequestContext carrier, String key, String value) { + if (carrier != null) { + carrier.setAdditionalRequestHeader(key, value); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryClient.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryClient.java new file mode 100644 index 000000000..d05b3aa67 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryClient.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.client.HttpClient; +import com.linecorp.armeria.client.SimpleDecoratingHttpClient; +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.logging.RequestLog; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +/** Decorates an {@link HttpClient} to trace outbound {@link HttpResponse}s. */ +final class OpenTelemetryClient extends SimpleDecoratingHttpClient { + + private final Instrumenter instrumenter; + + OpenTelemetryClient( + HttpClient delegate, Instrumenter instrumenter) { + super(delegate); + this.instrumenter = instrumenter; + } + + @Override + public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Exception { + Context parentContext = Context.current(); + if (!instrumenter.shouldStart(parentContext, ctx)) { + return unwrap().execute(ctx, req); + } + + Context context = instrumenter.start(Context.current(), ctx); + + Span span = Span.fromContext(context); + if (span.isRecording()) { + ctx.log() + .whenComplete() + .thenAccept(log -> instrumenter.end(context, ctx, log, log.responseCause())); + } + + try (Scope ignored = context.makeCurrent()) { + return unwrap().execute(ctx, req); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryService.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryService.java new file mode 100644 index 000000000..88e1b3e09 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryService.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.server.HttpService; +import com.linecorp.armeria.server.ServiceRequestContext; +import com.linecorp.armeria.server.SimpleDecoratingHttpService; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +/** Decorates an {@link HttpService} to trace inbound {@link HttpRequest}s. */ +final class OpenTelemetryService extends SimpleDecoratingHttpService { + + private final Instrumenter instrumenter; + + OpenTelemetryService( + HttpService delegate, Instrumenter instrumenter) { + super(delegate); + this.instrumenter = instrumenter; + } + + @Override + public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { + Context context = instrumenter.start(Context.current(), ctx); + + Span span = Span.fromContext(context); + if (span.isRecording()) { + ctx.log() + .whenComplete() + .thenAccept( + log -> { + if (log.responseHeaders().status().equals(HttpStatus.NOT_FOUND)) { + // Assume a not-found request was not served. The route we use by default will be + // some fallback like `/*` which is not as useful as the requested path. + span.updateName(ctx.path()); + } + instrumenter.end(context, ctx, log, log.responseCause()); + }); + } + + try (Scope ignored = context.makeCurrent()) { + return unwrap().serve(ctx, req); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/RequestContextGetter.java b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/RequestContextGetter.java new file mode 100644 index 000000000..0ed1d558e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/RequestContextGetter.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.server.ServiceRequestContext; +import io.netty.util.AsciiString; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Collections; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class RequestContextGetter implements TextMapGetter { + + @Override + public Iterable keys(@Nullable ServiceRequestContext carrier) { + if (carrier == null) { + return Collections.emptyList(); + } + return carrier.request().headers().names().stream() + .map(AsciiString::toString) + .collect(Collectors.toList()); + } + + @Override + @Nullable + public String get(@Nullable ServiceRequestContext carrier, String key) { + if (carrier == null) { + return null; + } + return carrier.request().headers().get(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy new file mode 100644 index 000000000..6bee41bdd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3 + +import com.linecorp.armeria.client.WebClientBuilder +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class ArmeriaHttpClientTest extends AbstractArmeriaHttpClientTest implements LibraryTestTrait { + @Override + WebClientBuilder configureClient(WebClientBuilder clientBuilder) { + return clientBuilder.decorator(ArmeriaTracing.create(getOpenTelemetry()).newClientDecorator()) + } + + // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet + @Override + boolean testWithClientParent() { + false + } + + // Agent users have automatic propagation through executor instrumentation, but library users + // should do manually using Armeria patterns. + @Override + boolean testCallbackWithParent() { + false + } + + @Override + boolean testErrorWithCallback() { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.groovy new file mode 100644 index 000000000..77f7d2b4d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.groovy @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3 + +import com.linecorp.armeria.server.ServerBuilder +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class ArmeriaHttpServerTest extends AbstractArmeriaHttpServerTest implements LibraryTestTrait{ + @Override + ServerBuilder configureServer(ServerBuilder sb) { + return sb.decorator(ArmeriaTracing.create(getOpenTelemetry()).newServiceDecorator()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/armeria-1.3-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/armeria-1.3-testing.gradle new file mode 100644 index 000000000..398f7b28b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/armeria-1.3-testing.gradle @@ -0,0 +1,14 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "com.linecorp.armeria:armeria:1.3.0" + api "com.linecorp.armeria:armeria-junit4:1.3.0" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.groovy new file mode 100644 index 000000000..b9fe64594 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.groovy @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3 + +import com.linecorp.armeria.client.WebClient +import com.linecorp.armeria.client.WebClientBuilder +import com.linecorp.armeria.common.HttpMethod +import com.linecorp.armeria.common.HttpRequest +import com.linecorp.armeria.common.RequestHeaders +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CompletionException +import spock.lang.Shared + +abstract class AbstractArmeriaHttpClientTest extends HttpClientTest { + + abstract WebClientBuilder configureClient(WebClientBuilder clientBuilder) + + @Shared + def client = configureClient(WebClient.builder()).build() + + @Override + HttpRequest buildRequest(String method, URI uri, Map headers) { + return HttpRequest.of( + RequestHeaders.builder(HttpMethod.valueOf(method), uri.toString()) + .set(headers.entrySet()) + .build()) + } + + @Override + int sendRequest(HttpRequest request, String method, URI uri, Map headers) { + try { + return client.execute(request) + .aggregate() + .join() + .status() + .code() + } catch (CompletionException e) { + throw e.cause + } + } + + @Override + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + client.execute(request).aggregate().whenComplete {response, throwable -> + requestResult.complete({ response.status().code() }, throwable) + } + } + + // Not supported yet: https://github.com/line/armeria/issues/2489 + @Override + boolean testRedirects() { + false + } + + @Override + boolean testReusedRequest() { + // armeria requests can't be reused + false + } + + @Override + Set> httpAttributes(URI uri) { + Set> extra = [ + SemanticAttributes.HTTP_HOST, + SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_TARGET + ] + super.httpAttributes(uri) + extra + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.groovy new file mode 100644 index 000000000..75ba7ccdf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.groovy @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3 + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import com.linecorp.armeria.common.HttpHeaderNames +import com.linecorp.armeria.common.HttpRequest +import com.linecorp.armeria.common.HttpResponse +import com.linecorp.armeria.common.HttpStatus +import com.linecorp.armeria.common.MediaType +import com.linecorp.armeria.common.QueryParams +import com.linecorp.armeria.common.ResponseHeaders +import com.linecorp.armeria.server.DecoratingHttpServiceFunction +import com.linecorp.armeria.server.HttpService +import com.linecorp.armeria.server.Server +import com.linecorp.armeria.server.ServerBuilder +import com.linecorp.armeria.server.ServiceRequestContext +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.function.Function + +abstract class AbstractArmeriaHttpServerTest extends HttpServerTest { + + abstract ServerBuilder configureServer(ServerBuilder serverBuilder) + + @Override + List> extraAttributes() { + [ + SemanticAttributes.HTTP_HOST, + SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + SemanticAttributes.HTTP_ROUTE, + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_SERVER_NAME, + SemanticAttributes.HTTP_TARGET, + SemanticAttributes.NET_PEER_NAME, + SemanticAttributes.NET_TRANSPORT + ] + } + + @Override + boolean testNotFound() { + // currently span name is /notFound which indicates it won't be low-cardinality + false + } + + @Override + Server startServer(int port) { + ServerBuilder sb = Server.builder() + + sb.http(port) + + sb.service(SUCCESS.path) { ctx, req -> + controller(SUCCESS) { + HttpResponse.of(HttpStatus.valueOf(SUCCESS.status), MediaType.PLAIN_TEXT_UTF_8, SUCCESS.body) + } + } + + sb.service(REDIRECT.path) { ctx, req -> + controller(REDIRECT) { + HttpResponse.of(ResponseHeaders.of(HttpStatus.valueOf(REDIRECT.status), HttpHeaderNames.LOCATION, REDIRECT.body)) + } + } + + sb.service(ERROR.path) { ctx, req -> + controller(ERROR) { + HttpResponse.of(HttpStatus.valueOf(ERROR.status), MediaType.PLAIN_TEXT_UTF_8, ERROR.body) + } + } + + sb.service(EXCEPTION.path) { ctx, req -> + controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + + sb.service("/query") { ctx, req -> + controller(QUERY_PARAM) { + HttpResponse.of(HttpStatus.valueOf(QUERY_PARAM.status), MediaType.PLAIN_TEXT_UTF_8, "some=${QueryParams.fromQueryString(ctx.query()).get("some")}") + } + } + + sb.service("/path/:id/param") { ctx, req -> + controller(PATH_PARAM) { + HttpResponse.of(HttpStatus.valueOf(PATH_PARAM.status), MediaType.PLAIN_TEXT_UTF_8, ctx.pathParam("id")) + } + } + + // Make sure user decorators see spans. + sb.decorator(new DecoratingHttpServiceFunction() { + @Override + HttpResponse serve(HttpService delegate, ServiceRequestContext ctx, HttpRequest req) throws Exception { + if (!Span.current().spanContext.isValid()) { + // Return an invalid code to fail any assertion + return HttpResponse.of(600) + } + ctx.addAdditionalResponseHeader("decoratinghttpservicefunction", "ok") + return delegate.serve(ctx, req) + } + }) + + sb.decorator(new Function() { + @Override + HttpService apply(HttpService delegate) { + return new HttpService() { + @Override + HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { + if (!Span.current().spanContext.isValid()) { + // Return an invalid code to fail any assertion + return HttpResponse.of(601) + } + ctx.addAdditionalResponseHeader("decoratingfunction", "ok") + return delegate.serve(ctx, req) + } + } + } + }) + + configureServer(sb) + + def server = sb.build() + server.start().join() + return server + } + + @Override + void stopServer(Server server) { + server.stop() + } + + @Override + boolean testPathParam() { + true + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/async-http-client-1.9-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/async-http-client-1.9-javaagent.gradle new file mode 100644 index 000000000..1e27b63ad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/async-http-client-1.9-javaagent.gradle @@ -0,0 +1,14 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.ning" + module = "async-http-client" + versions = "[1.9.0,)" + assertInverse = true + } +} + +dependencies { + library "com.ning:async-http-client:1.9.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientInjectAdapter.java new file mode 100644 index 000000000..30ef202f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v1_9; + +import com.ning.http.client.Request; +import io.opentelemetry.context.propagation.TextMapSetter; + +public class AsyncHttpClientInjectAdapter implements TextMapSetter { + + public static final AsyncHttpClientInjectAdapter SETTER = new AsyncHttpClientInjectAdapter(); + + @Override + public void set(Request carrier, String key, String value) { + carrier.getHeaders().replaceWith(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientInstrumentationModule.java new file mode 100644 index 000000000..ea5fb41e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v1_9; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class AsyncHttpClientInstrumentationModule extends InstrumentationModule { + public AsyncHttpClientInstrumentationModule() { + super("async-http-client", "async-http-client-1.9"); + } + + @Override + public List typeInstrumentations() { + return asList(new RequestInstrumentation(), new ResponseInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientTracer.java new file mode 100644 index 000000000..5c7afcb04 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientTracer.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v1_9; + +import com.ning.http.client.Request; +import com.ning.http.client.Response; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import java.net.URISyntaxException; + +public class AsyncHttpClientTracer extends HttpClientTracer { + + private static final AsyncHttpClientTracer TRACER = new AsyncHttpClientTracer(); + + private AsyncHttpClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static AsyncHttpClientTracer tracer() { + return TRACER; + } + + @Override + protected String method(Request request) { + return request.getMethod(); + } + + @Override + protected URI url(Request request) throws URISyntaxException { + return request.getUri().toJavaNetURI(); + } + + @Override + protected Integer status(Response response) { + return response.getStatusCode(); + } + + @Override + protected String requestHeader(Request request, String name) { + return request.getHeaders().getFirstValue(name); + } + + @Override + protected String responseHeader(Response response, String name) { + return response.getHeaders().getFirstValue(name); + } + + @Override + protected TextMapSetter getSetter() { + return AsyncHttpClientInjectAdapter.SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.async-http-client-1.9"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/RequestInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/RequestInstrumentation.java new file mode 100644 index 000000000..15c830e1f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/RequestInstrumentation.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v1_9; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.asynchttpclient.v1_9.AsyncHttpClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.ning.http.client.AsyncHandler; +import com.ning.http.client.Request; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Pair; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RequestInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.ning.http.client.AsyncHttpClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("executeRequest") + .and(takesArgument(0, named("com.ning.http.client.Request"))) + .and(takesArgument(1, named("com.ning.http.client.AsyncHandler"))) + .and(isPublic()), + this.getClass().getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(1) AsyncHandler handler, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + Context context = tracer().startSpan(parentContext, request, request); + InstrumentationContext.get(AsyncHandler.class, Pair.class) + .put(handler, Pair.of(parentContext, context)); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + // span ended in ResponseAdvice or ResponseFailureAdvice + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/ResponseInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/ResponseInstrumentation.java new file mode 100644 index 000000000..28da75929 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/ResponseInstrumentation.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v1_9; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.hasSuperClass; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.ning.http.client.AsyncCompletionHandler; +import com.ning.http.client.AsyncHandler; +import com.ning.http.client.Response; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Pair; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ResponseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.ning.http.client.AsyncCompletionHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return hasSuperClass(named("com.ning.http.client.AsyncCompletionHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("onCompleted") + .and(takesArgument(0, named("com.ning.http.client.Response"))) + .and(isPublic()), + this.getClass().getName() + "$OnCompletedAdvice"); + transformer.applyAdviceToMethod( + named("onThrowable").and(takesArgument(0, Throwable.class)).and(isPublic()), + this.getClass().getName() + "$OnThrowableAdvice"); + } + + @SuppressWarnings("unused") + public static class OnCompletedAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter( + @Advice.This AsyncCompletionHandler handler, @Advice.Argument(0) Response response) { + + ContextStore contextStore = + InstrumentationContext.get(AsyncHandler.class, Pair.class); + Pair parentAndChildContext = contextStore.get(handler); + if (parentAndChildContext == null) { + return null; + } + contextStore.put(handler, null); + AsyncHttpClientTracer.tracer().end(parentAndChildContext.getRight(), response); + return parentAndChildContext.getLeft().makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (null != scope) { + scope.close(); + } + } + } + + @SuppressWarnings("unused") + public static class OnThrowableAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter( + @Advice.This AsyncCompletionHandler handler, @Advice.Argument(0) Throwable throwable) { + + ContextStore, Pair> contextStore = + InstrumentationContext.get(AsyncHandler.class, Pair.class); + Pair parentAndChildContext = contextStore.get(handler); + if (parentAndChildContext == null) { + return null; + } + contextStore.put(handler, null); + AsyncHttpClientTracer.tracer().endExceptionally(parentAndChildContext.getRight(), throwable); + return parentAndChildContext.getLeft().makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (null != scope) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy new file mode 100644 index 000000000..236494016 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.ning.http.client.AsyncCompletionHandler +import com.ning.http.client.AsyncHttpClient +import com.ning.http.client.AsyncHttpClientConfig +import com.ning.http.client.Request +import com.ning.http.client.RequestBuilder +import com.ning.http.client.Response +import com.ning.http.client.uri.Uri +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import spock.lang.AutoCleanup +import spock.lang.Shared + +class AsyncHttpClientTest extends HttpClientTest implements AgentTestTrait { + + @AutoCleanup + @Shared + def client = new AsyncHttpClient(new AsyncHttpClientConfig.Builder() + .setConnectTimeout(CONNECT_TIMEOUT_MS).build()) + + @Override + Request buildRequest(String method, URI uri, Map headers) { + def requestBuilder = new RequestBuilder(method) + .setUri(Uri.create(uri.toString())) + headers.entrySet().each { + requestBuilder.setHeader(it.key, it.value) + } + return requestBuilder.build() + } + + @Override + int sendRequest(Request request, String method, URI uri, Map headers) { + return client.executeRequest(request).get().statusCode + } + + @Override + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + // TODO(anuraaga): Do we also need to test ListenableFuture callback? + client.executeRequest(request, new AsyncCompletionHandler() { + @Override + Void onCompleted(Response response) throws Exception { + requestResult.complete(response.statusCode) + return null + } + + @Override + void onThrowable(Throwable throwable) { + requestResult.complete(throwable) + } + }) + } + + @Override + boolean testRedirects() { + false + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + // AsyncHttpClient does not support HTTP 1.1 pipelining nor waiting for connection pool slots to + // free up (it immediately throws "Too many connections" IOException). Therefore making a single + // connection test would require manually sequencing the connections, which is not meaningful + // for a high concurrency test. + return null + } +} + + diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/async-http-client-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/async-http-client-2.0-javaagent.gradle new file mode 100644 index 000000000..c9bea7671 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/async-http-client-2.0-javaagent.gradle @@ -0,0 +1,39 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.asynchttpclient" + module = "async-http-client" + versions = "[2.0.0,)" + assertInverse = true + } +} + +dependencies { + library "org.asynchttpclient:async-http-client:2.0.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + testInstrumentation project(':instrumentation:netty:netty-4.0:javaagent') +} + +otelJava { + //AHC uses Unsafe and so does not run on later java version + maxJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +// async-http-client 2.0.0 does not work with Netty versions newer than this due to referencing an +// internal file. +if (!testLatestDeps) { + configurations.each { + it.resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + //specifying a fixed version for all libraries with io.netty' group + if (details.requested.group == 'io.netty' && details.requested.name != "netty-bom") { + details.useVersion "4.0.34.Final" + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncCompletionHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncCompletionHandlerInstrumentation.java new file mode 100644 index 000000000..c9d8753bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncCompletionHandlerInstrumentation.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0.AsyncHttpClientSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.hasSuperClass; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.asynchttpclient.AsyncCompletionHandler; +import org.asynchttpclient.AsyncHandler; +import org.asynchttpclient.Response; + +public class AsyncCompletionHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.asynchttpclient.AsyncCompletionHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return hasSuperClass(named("org.asynchttpclient.AsyncCompletionHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("onCompleted") + .and(takesArgument(0, named("org.asynchttpclient.Response"))) + .and(isPublic()), + this.getClass().getName() + "$OnCompletedAdvice"); + transformer.applyAdviceToMethod( + named("onThrowable").and(takesArgument(0, Throwable.class)).and(isPublic()), + this.getClass().getName() + "$OnThrowableAdvice"); + } + + @SuppressWarnings("unused") + public static class OnCompletedAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter( + @Advice.This AsyncCompletionHandler handler, @Advice.Argument(0) Response response) { + + ContextStore, AsyncHandlerData> contextStore = + InstrumentationContext.get(AsyncHandler.class, AsyncHandlerData.class); + AsyncHandlerData data = contextStore.get(handler); + if (data == null) { + return null; + } + contextStore.put(handler, null); + instrumenter().end(data.getContext(), data.getRequest(), response, null); + return data.getParentContext().makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (null != scope) { + scope.close(); + } + } + } + + @SuppressWarnings("unused") + public static class OnThrowableAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter( + @Advice.This AsyncCompletionHandler handler, @Advice.Argument(0) Throwable throwable) { + + ContextStore, AsyncHandlerData> contextStore = + InstrumentationContext.get(AsyncHandler.class, AsyncHandlerData.class); + AsyncHandlerData data = contextStore.get(handler); + if (data == null) { + return null; + } + contextStore.put(handler, null); + instrumenter().end(data.getContext(), data.getRequest(), null, throwable); + return data.getParentContext().makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (null != scope) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHandlerData.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHandlerData.java new file mode 100644 index 000000000..6c6b915e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHandlerData.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.context.Context; +import org.asynchttpclient.Request; + +@AutoValue +public abstract class AsyncHandlerData { + + public static AsyncHandlerData create(Context parentContext, Context context, Request request) { + return new AutoValue_AsyncHandlerData(parentContext, context, request); + } + + public abstract Context getParentContext(); + + public abstract Context getContext(); + + public abstract Request getRequest(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientHttpAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientHttpAttributesExtractor.java new file mode 100644 index 000000000..e7868157b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientHttpAttributesExtractor.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.asynchttpclient.Request; +import org.asynchttpclient.Response; +import org.asynchttpclient.uri.Uri; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class AsyncHttpClientHttpAttributesExtractor + extends HttpAttributesExtractor { + + @Override + protected String method(Request request) { + return request.getMethod(); + } + + @Override + protected String url(Request request) { + return request.getUri().toUrl(); + } + + @Override + protected String target(Request request) { + Uri uri = request.getUri(); + String query = uri.getQuery(); + return query != null ? uri.getPath() + "?" + query : uri.getPath(); + } + + @Override + @Nullable + protected String host(Request request) { + String host = request.getHeaders().get("Host"); + if (host != null) { + return host; + } + return request.getVirtualHost(); + } + + @Override + @Nullable + protected String scheme(Request request) { + return request.getUri().getScheme(); + } + + @Override + @Nullable + protected String userAgent(Request request) { + return null; + } + + @Override + @Nullable + protected Long requestContentLength(Request request, @Nullable Response response) { + return null; + } + + @Override + @Nullable + protected Long requestContentLengthUncompressed(Request request, @Nullable Response response) { + return null; + } + + @Override + protected Integer statusCode(Request request, Response response) { + return response.getStatusCode(); + } + + @Override + protected String flavor(Request request, @Nullable Response response) { + return SemanticAttributes.HttpFlavorValues.HTTP_1_1; + } + + @Override + @Nullable + protected Long responseContentLength(Request request, Response response) { + return null; + } + + @Override + @Nullable + protected Long responseContentLengthUncompressed(Request request, Response response) { + return null; + } + + @Override + @Nullable + protected String serverName(Request request, @Nullable Response response) { + return null; + } + + @Override + @Nullable + protected String route(Request request) { + return null; + } + + @Override + @Nullable + protected String clientIp(Request request, @Nullable Response response) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientInstrumentation.java new file mode 100644 index 000000000..da0d81fdf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientInstrumentation.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0.AsyncHttpClientSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.asynchttpclient.AsyncHandler; +import org.asynchttpclient.Request; + +public class AsyncHttpClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.asynchttpclient.AsyncHttpClient")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("executeRequest") + .and(takesArgument(0, named("org.asynchttpclient.Request"))) + .and(takesArgument(1, named("org.asynchttpclient.AsyncHandler"))) + .and(isPublic()), + this.getClass().getName() + "$ExecuteRequestAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(1) AsyncHandler handler, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + Context context = instrumenter().start(parentContext, request); + + // TODO (trask) instead of using InstrumentationContext, wrap the AsyncHandler in an + // instrumented AsyncHandler which delegates to the original AsyncHandler + // (similar to other http client instrumentations, and needed for library instrumentation) + // + // when doing this, note that AsyncHttpClient has different behavior if the AsyncHandler also + // implements ProgressAsyncHandler or StreamedAsyncHandler (or both) + // so four wrappers are needed to match the different combinations so that the wrapper won't + // affect the behavior + // + // when doing this, also note that there was a breaking change in AsyncHandler between 2.0 and + // 2.1, so the instrumentation module will need to be essentially duplicated (or a common + // module introduced) + + InstrumentationContext.get(AsyncHandler.class, AsyncHandlerData.class) + .put(handler, AsyncHandlerData.create(parentContext, context, request)); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + // span ended in ResponseAdvice or ResponseFailureAdvice + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientInstrumentationModule.java new file mode 100644 index 000000000..7e9c89774 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class AsyncHttpClientInstrumentationModule extends InstrumentationModule { + public AsyncHttpClientInstrumentationModule() { + super("async-http-client", "async-http-client-2.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new AsyncHttpClientInstrumentation(), + new AsyncCompletionHandlerInstrumentation(), + new NettyRequestSenderInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientNetAttributesExtractor.java new file mode 100644 index 000000000..45cbc4255 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientNetAttributesExtractor.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.asynchttpclient.Request; +import org.asynchttpclient.Response; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class AsyncHttpClientNetAttributesExtractor + extends NetAttributesExtractor { + + @Override + public String transport(Request request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } + + @Override + public String peerName(Request request, @Nullable Response response) { + return request.getUri().getHost(); + } + + @Override + public Integer peerPort(Request request, @Nullable Response response) { + return request.getUri().getPort(); + } + + @Override + @Nullable + public String peerIp(Request request, @Nullable Response response) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java new file mode 100644 index 000000000..29c62ecac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; +import org.asynchttpclient.Request; +import org.asynchttpclient.Response; + +public final class AsyncHttpClientSingletons { + private static final String INSTRUMENTATION_NAME = + "io.opentelemetry.javaagent.async-http-client-2.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + HttpAttributesExtractor httpAttributesExtractor = + new AsyncHttpClientHttpAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + HttpSpanNameExtractor.create(httpAttributesExtractor); + SpanStatusExtractor spanStatusExtractor = + HttpSpanStatusExtractor.create(httpAttributesExtractor); + AsyncHttpClientNetAttributesExtractor netAttributesExtractor = + new AsyncHttpClientNetAttributesExtractor(); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor) + .setSpanStatusExtractor(spanStatusExtractor) + .addAttributesExtractor(httpAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor)) + .newClientInstrumenter(new HttpHeaderSetter()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private AsyncHttpClientSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/HttpHeaderSetter.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/HttpHeaderSetter.java new file mode 100644 index 000000000..166c346f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/HttpHeaderSetter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.asynchttpclient.Request; + +public class HttpHeaderSetter implements TextMapSetter { + + public static final HttpHeaderSetter SETTER = new HttpHeaderSetter(); + + @Override + public void set(Request carrier, String key, String value) { + carrier.getHeaders().set(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/NettyRequestSenderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/NettyRequestSenderInstrumentation.java new file mode 100644 index 000000000..39c2fdbcc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/NettyRequestSenderInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.asynchttpclient.Request; +import org.asynchttpclient.netty.NettyResponseFuture; + +public class NettyRequestSenderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.asynchttpclient.netty.request.NettyRequestSender"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("sendRequest") + .and(takesArgument(0, named("org.asynchttpclient.Request"))) + .and(takesArgument(1, named("org.asynchttpclient.AsyncHandler"))) + .and(isPublic()), + NettyRequestSenderInstrumentation.class.getName() + "$AttachContextAdvice"); + + transformer.applyAdviceToMethod( + named("writeRequest") + .and(takesArgument(0, named("org.asynchttpclient.netty.NettyResponseFuture"))) + .and(takesArgument(1, named("io.netty.channel.Channel"))) + .and(isPublic()), + NettyRequestSenderInstrumentation.class.getName() + "$MountContextAdvice"); + } + + @SuppressWarnings("unused") + public static class AttachContextAdvice { + + @Advice.OnMethodEnter + public static void attachContext(@Advice.Argument(0) Request request) { + InstrumentationContext.get(Request.class, Context.class) + .put(request, Java8BytecodeBridge.currentContext()); + } + } + + @SuppressWarnings("unused") + public static class MountContextAdvice { + + @Advice.OnMethodEnter + public static Scope mountContext(@Advice.Argument(0) NettyResponseFuture responseFuture) { + Request request = responseFuture.getCurrentRequest(); + Context context = InstrumentationContext.get(Request.class, Context.class).get(request); + return context == null ? null : context.makeCurrent(); + } + + @Advice.OnMethodExit + public static void unmountContext(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy new file mode 100644 index 000000000..d1fb9d1d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.asynchttpclient.AsyncCompletionHandler +import org.asynchttpclient.Dsl +import org.asynchttpclient.Request +import org.asynchttpclient.RequestBuilder +import org.asynchttpclient.Response +import org.asynchttpclient.uri.Uri +import spock.lang.AutoCleanup +import spock.lang.Shared + +class AsyncHttpClientTest extends HttpClientTest implements AgentTestTrait { + + // request timeout is needed in addition to connect timeout on async-http-client versions 2.1.0+ + @AutoCleanup + @Shared + def client = Dsl.asyncHttpClient(Dsl.config().setConnectTimeout(CONNECT_TIMEOUT_MS) + .setRequestTimeout(CONNECT_TIMEOUT_MS)) + + @Override + Request buildRequest(String method, URI uri, Map headers) { + def requestBuilder = new RequestBuilder(method) + .setUri(Uri.create(uri.toString())) + headers.entrySet().each { + requestBuilder.setHeader(it.key, it.value) + } + return requestBuilder.build() + } + + @Override + int sendRequest(Request request, String method, URI uri, Map headers) { + return client.executeRequest(request).get().statusCode + } + + @Override + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + client.executeRequest(request, new AsyncCompletionHandler() { + @Override + Void onCompleted(Response response) throws Exception { + requestResult.complete(response.statusCode) + return null + } + + @Override + void onThrowable(Throwable throwable) { + requestResult.complete(throwable) + } + }) + } + + //TODO see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2347 +// @Override +// String userAgent() { +// return "AHC" +// } + + @Override + boolean testRedirects() { + false + } + + @Override + Set> httpAttributes(URI uri) { + Set> extra = [ + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_TARGET + ] + super.httpAttributes(uri) + extra + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/aws-lambda-1.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/aws-lambda-1.0-javaagent.gradle new file mode 100644 index 000000000..f1785e397 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/aws-lambda-1.0-javaagent.gradle @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.amazonaws" + module = "aws-lambda-java-core" + versions = "[1.0.0,)" + extraDependency('com.amazonaws:aws-lambda-java-events:2.2.1') + extraDependency('com.amazonaws.serverless:aws-serverless-java-container-core:1.5.2') + } +} + +dependencies { + implementation project(':instrumentation:aws-lambda-1.0:library') + + library "com.amazonaws:aws-lambda-java-core:1.0.0" + // First version to includes support for SQSEvent, currently the most popular message queue used + // with lambda. + // NB: 2.2.0 includes a class called SQSEvent but isn't usable due to it returning private classes + // in public API. + library "com.amazonaws:aws-lambda-java-events:2.2.1" + + testImplementation project(':instrumentation:aws-lambda-1.0:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaInstrumentationHelper.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaInstrumentationHelper.java new file mode 100644 index 000000000..ce70be1fc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaInstrumentationHelper.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awslambda.v1_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.awslambda.v1_0.AwsLambdaMessageTracer; +import io.opentelemetry.instrumentation.awslambda.v1_0.AwsLambdaTracer; + +public final class AwsLambdaInstrumentationHelper { + + private static final AwsLambdaTracer FUNCTION_TRACER = + new AwsLambdaTracer(GlobalOpenTelemetry.get()); + + public static AwsLambdaTracer functionTracer() { + return FUNCTION_TRACER; + } + + private static final AwsLambdaMessageTracer MESSAGE_TRACER = + new AwsLambdaMessageTracer(GlobalOpenTelemetry.get()); + + public static AwsLambdaMessageTracer messageTracer() { + return MESSAGE_TRACER; + } + + private AwsLambdaInstrumentationHelper() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaInstrumentationModule.java new file mode 100644 index 000000000..71fe85821 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awslambda.v1_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class AwsLambdaInstrumentationModule extends InstrumentationModule { + public AwsLambdaInstrumentationModule() { + super("aws-lambda", "aws-lambda-1.0"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.extension.aws."); + } + + @Override + public List typeInstrumentations() { + return singletonList(new AwsLambdaRequestHandlerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaRequestHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaRequestHandlerInstrumentation.java new file mode 100644 index 000000000..4501ab337 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambda/v1_0/AwsLambdaRequestHandlerInstrumentation.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awslambda.v1_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.awslambda.v1_0.AwsLambdaInstrumentationHelper.functionTracer; +import static io.opentelemetry.javaagent.instrumentation.awslambda.v1_0.AwsLambdaInstrumentationHelper.messageTracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.OpenTelemetrySdkAccess; +import java.util.concurrent.TimeUnit; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner.Typing; +import net.bytebuddy.matcher.ElementMatcher; + +public class AwsLambdaRequestHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.amazonaws.services.lambda.runtime.RequestHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("com.amazonaws.services.lambda.runtime.RequestHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("handleRequest")) + .and(takesArgument(1, named("com.amazonaws.services.lambda.runtime.Context"))), + AwsLambdaRequestHandlerInstrumentation.class.getName() + "$HandleRequestAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, typing = Typing.DYNAMIC) Object arg, + @Advice.Argument(1) Context context, + @Advice.Local("otelFunctionContext") io.opentelemetry.context.Context functionContext, + @Advice.Local("otelFunctionScope") Scope functionScope, + @Advice.Local("otelMessageContext") io.opentelemetry.context.Context messageContext, + @Advice.Local("otelMessageScope") Scope messageScope) { + functionContext = functionTracer().startSpan(context, SpanKind.SERVER, arg); + functionScope = functionContext.makeCurrent(); + if (arg instanceof SQSEvent) { + messageContext = messageTracer().startSpan(functionContext, (SQSEvent) arg); + messageScope = messageContext.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelFunctionContext") io.opentelemetry.context.Context functionContext, + @Advice.Local("otelFunctionScope") Scope functionScope, + @Advice.Local("otelMessageContext") io.opentelemetry.context.Context messageContext, + @Advice.Local("otelMessageScope") Scope messageScope) { + + if (messageScope != null) { + messageScope.close(); + if (throwable != null) { + messageTracer().endExceptionally(messageContext, throwable); + } else { + messageTracer().end(messageContext); + } + } + + functionScope.close(); + if (throwable != null) { + functionTracer().endExceptionally(functionContext, throwable); + } else { + functionTracer().end(functionContext); + } + OpenTelemetrySdkAccess.forceFlush(1, TimeUnit.SECONDS); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/test/groovy/io/opentelemetry/test/AwsLambdaSqsHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/test/groovy/io/opentelemetry/test/AwsLambdaSqsHandlerTest.groovy new file mode 100644 index 000000000..74b316827 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/test/groovy/io/opentelemetry/test/AwsLambdaSqsHandlerTest.groovy @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import com.amazonaws.services.lambda.runtime.events.SQSEvent +import io.opentelemetry.instrumentation.awslambda.v1_0.AbstractAwsLambdaSqsHandlerTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +/** + * Note: this has to stay outside of 'io.opentelemetry.javaagent' package to be considered for + * instrumentation + */ +class AwsLambdaSqsHandlerTest extends AbstractAwsLambdaSqsHandlerTest implements AgentTestTrait { + + static class TestRequestHandler implements RequestHandler { + @Override + Void handleRequest(SQSEvent input, Context context) { + return null + } + } + + @Override + RequestHandler handler() { + return new TestRequestHandler() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/test/groovy/io/opentelemetry/test/AwsLambdaTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/test/groovy/io/opentelemetry/test/AwsLambdaTest.groovy new file mode 100644 index 000000000..a799a274f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/javaagent/src/test/groovy/io/opentelemetry/test/AwsLambdaTest.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import io.opentelemetry.instrumentation.awslambda.v1_0.AbstractAwsLambdaRequestHandlerTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.javaagent.testing.common.AgentTestingExporterAccess + +/** + * Note: this has to stay outside of 'io.opentelemetry.javaagent' package to be considered for + * instrumentation + */ +class AwsLambdaTest extends AbstractAwsLambdaRequestHandlerTest implements AgentTestTrait { + + def cleanup() { + assert AgentTestingExporterAccess.forceFlushCalled() + } + + static class TestRequestHandler implements RequestHandler { + @Override + String handleRequest(String input, Context context) { + return doHandleRequest(input, context) + } + } + + @Override + RequestHandler handler() { + return new TestRequestHandler() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/README.md b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/README.md new file mode 100644 index 000000000..42d33b3ba --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/README.md @@ -0,0 +1,124 @@ +# AWS Lambda Instrumentation + +This package contains libraries to help instrument AWS lambda functions in your code. + +## Using wrappers +To use the instrumentation, configure `OTEL_INSTRUMENTATION_AWS_LAMBDA_HANDLER` env property to your lambda handler method in following format `package.ClassName::methodName` +and use one of wrappers as your lambda `Handler`. + +In order to configure a span flush timeout (default is set to 1 second), please configure `OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT` env property. The value is in seconds. + +Available wrappers: +- `io.opentelemetry.instrumentation.awslambda.v1_0.TracingRequestWrapper` - for wrapping regular handlers (implementing `RequestHandler`) +- `io.opentelemetry.instrumentation.awslambda.v1_0.TracingRequestApiGatewayWrapper` - for wrapping regular handlers (implementing `RequestHandler`) proxied through API Gateway, enabling HTTP context propagation +- `io.opentelemetry.instrumentation.awslambda.v1_0.TracingRequestStreamWrapper` - for wrapping streaming handlers (implementing `RequestStreamHandler`), enabling HTTP context propagation for HTTP requests + +## Using handlers +To use the instrumentation, replace your function classes that implement `RequestHandler` (or `RequestStreamHandler`) with those +that extend `TracingRequestHandler` (or `TracingRequestStreamHandler`). You will need to change the method name to `doHandleRequest` +and pass an initialized `OpenTelemetrySdk` to the base class. + +```java +public class MyRequestHandler extends TracingRequestHandler { + + private static final OpenTelemetrySdk SDK = OpenTelemetrySdk.builder() + .addSpanProcessor(spanProcessor) + .buildAndRegisterGlobal(); + + public MyRequestHandler() { + super(SDK); + } + + // Note the method is named doHandleRequest instead of handleRequest. + @Override + protected String doHandleRequest(String input, Context context) { + if (input.equals("hello")) { + return "world"; + } + return "goodbye"; + } +} +``` + +A `SERVER` span will be created with the name you specify for the function when deploying it. + +In addition, it is recommended to set up X-Ray trace propagation to be able to +link to tracing information provided by Lambda itself. To do so, add a dependency on +`opentelemetry-extension-tracepropagators`. Make sure the version matches the version of the SDK +you use. + +Gradle: +```kotlin +dependencies { + implementation("io.opentelemetry:opentelemetry-extension-trace-propagators:0.8.0") +} +``` + +Maven: +```xml + + + io.opentelemetry + opentelemetry-extension-trace-propagators + 0.8.0 + + +``` + +## SQS Handler + +This package provides a special handler for SQS-triggered functions to include messaging data. +If using SQS, it is recommended to use them instead of `TracingRequestHandler`. + +If your application processes one message at a time, each independently, it is recommended to extend +`TracingSQSMessageHandler`. This will create a single span corresponding to a received batch of +messages along with one span for each of the messages as you process them. + +```java +public class MyMessageHandler extends TracingSQSMessageHandler { + @Override + protected void handleMessage(SQSMessage message, Context context) { + System.out.println(message.getBody()); + } +} +``` + +If you handle a batch of messages together, for example by aggregating them into a single unit, +extend `TracingSQSEventHandler` to process a batch at a time. + +```java +public class MyBatchHandler extends TracingSQSEventHandler { + @Override + protected void handleEvent(SQSEvent event, Context context) { + System.out.println(event.getRecords().size()); + } +} +``` + +## Trace propagation + +Context propagation for this instrumentation can be done either with X-Ray propagation or regular HTTP propagation. If X-Ray is enabled for instrumented lambda, it will be preferred. If X-Ray is disabled, HTTP propagation will be tried (that is HTTP headers will be read to check for a valid trace context). + + +### X-Ray propagation +This instrumentation supports propagating traces using the `X-Amzn-Trace-Id` format for both normal +requests and SQS requests. X-Ray propagation is always enabled, there is no need to configure it explicitely. + +### HTTP headers based propagation +For API Gateway (HTTP) requests instrumented by using one of following methods: +- extending `TracingRequestStreamHandler` or `TracingRequestHandler` +- wrapping with `TracingRequestStreamWrapper` or `TracingRequestApiGatewayWrapper` +traces can be propagated with supported HTTP headers (see https://github.com/open-telemetry/opentelemetry-java/tree/master/extensions/trace_propagators). + +In order to enable requested propagation for a handler, configure it on the SDK you build. + +```java + static { + OpenTelemetrySdk.builder() + ... + .setPropagators(ContextPropagators.create(B3Propagator.injectingSingleHeader())) + .buildAndRegisterGlobal(); + } +``` + +If using the wrappers, set the `OTEL_PROPAGATORS` environment variable as descibed [here](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#propagator). diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/aws-lambda-1.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/aws-lambda-1.0-library.gradle new file mode 100644 index 000000000..80a150a8d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/aws-lambda-1.0-library.gradle @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "run.mone:opentelemetry-sdk" + compileOnly "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure" + + library "com.amazonaws:aws-lambda-java-core:1.0.0" + // First version to includes support for SQSEvent, currently the most popular message queue used + // with lambda. + // NB: 2.2.0 includes a class called SQSEvent but isn't usable due to it returning private classes + // in public API. + library "com.amazonaws:aws-lambda-java-events:2.2.1" + + compileOnly( + 'com.fasterxml.jackson.core:jackson-databind', + 'commons-io:commons-io:2.2') + compileOnly "org.slf4j:slf4j-api" + + implementation "run.mone:opentelemetry-extension-aws" + + // 1.2.0 allows to get the function ARN + testLibrary "com.amazonaws:aws-lambda-java-core:1.2.0" + + testImplementation( + 'com.fasterxml.jackson.core:jackson-databind', + 'commons-io:commons-io:2.2') + + testImplementation "run.mone:opentelemetry-sdk-extension-autoconfigure" + testImplementation "run.mone:opentelemetry-extension-trace-propagators" + testImplementation "com.google.guava:guava" + + testImplementation project(':instrumentation:aws-lambda-1.0:testing') + testImplementation "org.mockito:mockito-core" + testImplementation "org.assertj:assertj-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/ApiGatewayProxyRequest.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/ApiGatewayProxyRequest.java new file mode 100644 index 000000000..cb54a3810 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/ApiGatewayProxyRequest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import static io.opentelemetry.instrumentation.awslambda.v1_0.HeadersFactory.ofStream; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import org.apache.commons.io.IOUtils; +import org.checkerframework.checker.nullness.qual.Nullable; + +abstract class ApiGatewayProxyRequest { + + // TODO(anuraaga): We should create a RequestFactory type of class instead of evaluating this + // for every request. + private static boolean noHttpPropagationNeeded() { + Collection fields = + GlobalOpenTelemetry.getPropagators().getTextMapPropagator().fields(); + return fields.isEmpty() || xrayPropagationFieldsOnly(fields); + } + + private static boolean xrayPropagationFieldsOnly(Collection fields) { + // ugly but faster than typical convert-to-set-and-check-contains-only + return (fields.size() == 1) + && ParentContextExtractor.AWS_TRACE_HEADER_PROPAGATOR_KEY.equalsIgnoreCase( + fields.iterator().next()); + } + + static ApiGatewayProxyRequest forStream(InputStream source) throws IOException { + + if (noHttpPropagationNeeded()) { + return new NoopRequest(source); + } + + if (source.markSupported()) { + return new MarkableApiGatewayProxyRequest(source); + } + // fallback + return new CopiedApiGatewayProxyRequest(source); + } + + @Nullable + Map getHeaders() throws IOException { + Map headers = ofStream(freshStream()); + return (headers == null ? Collections.emptyMap() : headers); + } + + abstract InputStream freshStream() throws IOException; + + private static class NoopRequest extends ApiGatewayProxyRequest { + + private final InputStream stream; + + private NoopRequest(InputStream stream) { + this.stream = stream; + } + + @Override + InputStream freshStream() { + return stream; + } + + @Override + Map getHeaders() { + return Collections.emptyMap(); + } + } + + private static class MarkableApiGatewayProxyRequest extends ApiGatewayProxyRequest { + + private final InputStream inputStream; + + private MarkableApiGatewayProxyRequest(InputStream inputStream) { + this.inputStream = inputStream; + inputStream.mark(Integer.MAX_VALUE); + } + + @Override + InputStream freshStream() throws IOException { + + inputStream.reset(); + inputStream.mark(Integer.MAX_VALUE); + return inputStream; + } + } + + private static class CopiedApiGatewayProxyRequest extends ApiGatewayProxyRequest { + + private final byte[] data; + + private CopiedApiGatewayProxyRequest(InputStream inputStream) throws IOException { + data = IOUtils.toByteArray(inputStream); + } + + @Override + InputStream freshStream() { + return new ByteArrayInputStream(data); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaMessageTracer.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaMessageTracer.java new file mode 100644 index 000000000..75ec49e5f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaMessageTracer.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; + +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +public class AwsLambdaMessageTracer extends BaseTracer { + + private static final String AWS_TRACE_HEADER_SQS_ATTRIBUTE_KEY = "AWSTraceHeader"; + + public AwsLambdaMessageTracer(OpenTelemetry openTelemetry) { + super(openTelemetry); + } + + public Context startSpan(Context parentContext, SQSEvent event) { + // Use event source in name if all messages have the same source, otherwise use placeholder. + String source = "multiple_sources"; + if (!event.getRecords().isEmpty()) { + String messageSource = event.getRecords().get(0).getEventSource(); + for (int i = 1; i < event.getRecords().size(); i++) { + SQSMessage message = event.getRecords().get(i); + if (!message.getEventSource().equals(messageSource)) { + messageSource = null; + break; + } + } + if (messageSource != null) { + source = messageSource; + } + } + + SpanBuilder span = spanBuilder(parentContext, source + " process", CONSUMER); + + span.setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"); + span.setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process"); + + for (SQSMessage message : event.getRecords()) { + addLinkToMessageParent(message, span); + } + + return parentContext.with(span.startSpan()); + } + + public Context startSpan(Context parentContext, SQSMessage message) { + SpanBuilder span = spanBuilder(parentContext, message.getEventSource() + " process", CONSUMER); + + span.setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"); + span.setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process"); + span.setAttribute(SemanticAttributes.MESSAGING_MESSAGE_ID, message.getMessageId()); + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION, message.getEventSource()); + + addLinkToMessageParent(message, span); + + return parentContext.with(span.startSpan()); + } + + private static void addLinkToMessageParent(SQSMessage message, SpanBuilder span) { + String parentHeader = message.getAttributes().get(AWS_TRACE_HEADER_SQS_ATTRIBUTE_KEY); + if (parentHeader != null) { + SpanContext parentCtx = + Span.fromContext(ParentContextExtractor.fromXRayHeader(parentHeader)).getSpanContext(); + if (parentCtx.isValid()) { + span.addLink(parentCtx); + } + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.aws-lambda-1.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaTracer.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaTracer.java new file mode 100644 index 000000000..e85015a5f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaTracer.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.CLOUD_ACCOUNT_ID; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.FAAS_ID; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FAAS_EXECUTION; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FAAS_TRIGGER; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FaasTriggerValues; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Collections; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; + +// Context is defined in both OTel and Lambda +@SuppressWarnings("ParameterPackage") +public class AwsLambdaTracer extends BaseTracer { + + @Nullable private static final MethodHandle GET_FUNCTION_ARN; + + static { + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + MethodHandle getFunctionArn; + try { + getFunctionArn = + lookup.findVirtual( + Context.class, "getInvokedFunctionArn", MethodType.methodType(String.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + getFunctionArn = null; + } + GET_FUNCTION_ARN = getFunctionArn; + } + + // cached accountId value + private volatile String accountId; + + public AwsLambdaTracer(OpenTelemetry openTelemetry) { + super(openTelemetry); + } + + private void setAttributes(SpanBuilder span, Context context, Object input) { + setCommonAttributes(span, context); + if (input instanceof APIGatewayProxyRequestEvent) { + span.setAttribute(FAAS_TRIGGER, FaasTriggerValues.HTTP); + HttpSpanAttributes.onRequest(span, (APIGatewayProxyRequestEvent) input); + } + } + + private void setCommonAttributes(SpanBuilder span, Context context) { + span.setAttribute(FAAS_EXECUTION, context.getAwsRequestId()); + String arn = getFunctionArn(context); + if (arn != null) { + span.setAttribute(FAAS_ID, arn); + } + String accountId = getAccountId(arn); + if (accountId != null) { + span.setAttribute(CLOUD_ACCOUNT_ID, accountId); + } + } + + @Nullable + private static String getFunctionArn(Context context) { + if (GET_FUNCTION_ARN == null) { + return null; + } + try { + return (String) GET_FUNCTION_ARN.invoke(context); + } catch (Throwable throwable) { + return null; + } + } + + @Nullable + private String getAccountId(@Nullable String arn) { + if (arn == null) { + return null; + } + if (accountId == null) { + synchronized (this) { + if (accountId == null) { + String[] arnParts = arn.split(":"); + if (arnParts.length >= 5) { + accountId = arnParts[4]; + } + } + } + } + return accountId; + } + + private static String spanName(Context context, Object input) { + String name = null; + if (input instanceof APIGatewayProxyRequestEvent) { + name = ((APIGatewayProxyRequestEvent) input).getResource(); + } + return name == null ? context.getFunctionName() : name; + } + + public io.opentelemetry.context.Context startSpan( + Context awsContext, SpanKind kind, Object input) { + return startSpan(awsContext, kind, input, Collections.emptyMap()); + } + + public io.opentelemetry.context.Context startSpan( + Context awsContext, SpanKind kind, Object input, Map headers) { + io.opentelemetry.context.Context parentContext = ParentContextExtractor.extract(headers, this); + + SpanBuilder spanBuilder = spanBuilder(parentContext, spanName(awsContext, input), kind); + setAttributes(spanBuilder, awsContext, input); + + return withServerSpan(parentContext, spanBuilder.startSpan()); + } + + public void onOutput(io.opentelemetry.context.Context context, Object output) { + if (output instanceof APIGatewayProxyResponseEvent) { + HttpSpanAttributes.onResponse( + Span.fromContext(context), (APIGatewayProxyResponseEvent) output); + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.aws-lambda-1.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/HeadersFactory.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/HeadersFactory.java new file mode 100644 index 000000000..94c7320ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/HeadersFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class HeadersFactory { + + private static final Logger log = LoggerFactory.getLogger(HeadersFactory.class); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Nullable + static Map ofStream(InputStream inputStream) { + try (JsonParser jParser = new JsonFactory().createParser(inputStream)) { + while (jParser.nextToken() != null) { + String name = jParser.getCurrentName(); + if ("headers".equalsIgnoreCase(name)) { + jParser.nextToken(); + return OBJECT_MAPPER.readValue(jParser, Map.class); + } + } + } catch (Exception e) { + log.debug("Could not get headers from request, ", e); + } + return null; + } + + private HeadersFactory() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/HttpSpanAttributes.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/HttpSpanAttributes.java new file mode 100644 index 000000000..165ab5daa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/HttpSpanAttributes.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import static io.opentelemetry.instrumentation.awslambda.v1_0.MapUtils.emptyIfNull; +import static io.opentelemetry.instrumentation.awslambda.v1_0.MapUtils.lowercaseMap; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_URL; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_USER_AGENT; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +final class HttpSpanAttributes { + static void onRequest(SpanBuilder span, APIGatewayProxyRequestEvent request) { + String httpMethod = request.getHttpMethod(); + if (httpMethod != null) { + span.setAttribute(HTTP_METHOD, httpMethod); + } + + Map headers = lowercaseMap(request.getHeaders()); + String userAgent = headers.get("user-agent"); + if (userAgent != null) { + span.setAttribute(HTTP_USER_AGENT, userAgent); + } + String url = getHttpUrl(request, headers); + if (!url.isEmpty()) { + span.setAttribute(HTTP_URL, url); + } + } + + private static String getHttpUrl( + APIGatewayProxyRequestEvent request, Map headers) { + StringBuilder str = new StringBuilder(); + + String scheme = headers.get("x-forwarded-proto"); + if (scheme != null) { + str.append(scheme).append("://"); + } + String host = headers.get("host"); + if (host != null) { + str.append(host); + } + String path = request.getPath(); + if (path != null) { + str.append(path); + } + + try { + boolean first = true; + for (Map.Entry entry : + emptyIfNull(request.getQueryStringParameters()).entrySet()) { + String key = URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()); + String value = URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()); + str.append(first ? '?' : '&').append(key).append('=').append(value); + first = false; + } + } catch (UnsupportedEncodingException ignored) { + // Ignore + } + return str.toString(); + } + + static void onResponse(Span span, APIGatewayProxyResponseEvent response) { + Integer statusCode = response.getStatusCode(); + if (statusCode != null) { + span.setAttribute(HTTP_STATUS_CODE, statusCode); + } + } + + private HttpSpanAttributes() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/LambdaUtils.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/LambdaUtils.java new file mode 100644 index 000000000..f747f7ffc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/LambdaUtils.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.util.concurrent.TimeUnit; + +final class LambdaUtils { + + static void forceFlush() { + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + if (openTelemetry instanceof OpenTelemetrySdk) { + ((OpenTelemetrySdk) openTelemetry) + .getSdkTracerProvider() + .forceFlush() + .join(1, TimeUnit.SECONDS); + } + } + + private LambdaUtils() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/MapUtils.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/MapUtils.java new file mode 100644 index 000000000..190746d5e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/MapUtils.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +final class MapUtils { + static Map lowercaseMap(Map source) { + return emptyIfNull(source).entrySet().stream() + .filter(e -> e.getKey() != null) + .collect(Collectors.toMap(e -> e.getKey().toLowerCase(Locale.ROOT), Map.Entry::getValue)); + } + + static Map emptyIfNull(Map map) { + return map == null ? Collections.emptyMap() : map; + } + + private MapUtils() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/ParentContextExtractor.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/ParentContextExtractor.java new file mode 100644 index 000000000..63f7e76b4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/ParentContextExtractor.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import static io.opentelemetry.instrumentation.awslambda.v1_0.MapUtils.lowercaseMap; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +public final class ParentContextExtractor { + + private static final String AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID"; + + static Context extract(Map headers, BaseTracer tracer) { + Context parentContext = null; + String parentTraceHeader = System.getenv(AWS_TRACE_HEADER_ENV_KEY); + if (parentTraceHeader != null) { + parentContext = fromXRayHeader(parentTraceHeader); + } + if (!isValidAndSampled(parentContext)) { + // try http + parentContext = fromHttpHeaders(headers, tracer); + } + return parentContext; + } + + private static boolean isValidAndSampled(Context context) { + if (context == null) { + return false; + } + Span parentSpan = Span.fromContext(context); + SpanContext parentSpanContext = parentSpan.getSpanContext(); + return (parentSpanContext.isValid() && parentSpanContext.isSampled()); + } + + private static Context fromHttpHeaders(Map headers, BaseTracer tracer) { + return tracer.extract(lowercaseMap(headers), MapGetter.INSTANCE); + } + + // lower-case map getter used for extraction + static final String AWS_TRACE_HEADER_PROPAGATOR_KEY = "x-amzn-trace-id"; + + static Context fromXRayHeader(String parentHeader) { + return AwsXrayPropagator.getInstance() + .extract( + // see BaseTracer#extract() on why we're using root() here + Context.root(), + Collections.singletonMap(AWS_TRACE_HEADER_PROPAGATOR_KEY, parentHeader), + MapGetter.INSTANCE); + } + + private static class MapGetter implements TextMapGetter> { + + private static final MapGetter INSTANCE = new MapGetter(); + + @Override + public Iterable keys(Map map) { + return map.keySet(); + } + + @Override + public String get(Map map, String s) { + return map.get(s.toLowerCase(Locale.ROOT)); + } + } + + private ParentContextExtractor() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestApiGatewayWrapper.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestApiGatewayWrapper.java new file mode 100644 index 000000000..cd82f6b0f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestApiGatewayWrapper.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import io.opentelemetry.sdk.OpenTelemetrySdk; + +/** + * Wrapper for {@link TracingRequestHandler}. Allows for wrapping a lambda proxied through API + * Gateway, enabling single span tracing and HTTP context propagation. + */ +public class TracingRequestApiGatewayWrapper + extends TracingRequestWrapperBase { + + public TracingRequestApiGatewayWrapper() { + super(); + } + + // Visible for testing + TracingRequestApiGatewayWrapper(OpenTelemetrySdk openTelemetrySdk, WrappedLambda wrappedLambda) { + super(openTelemetrySdk, wrappedLambda); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestHandler.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestHandler.java new file mode 100644 index 000000000..d31333c47 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestHandler.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * A base class similar to {@link RequestHandler} but will automatically trace invocations of {@link + * #doHandleRequest(Object, Context)}. For API Gateway requests (ie of APIGatewayProxyRequestEvent + * type parameter) also HTTP propagation can be enabled. + */ +public abstract class TracingRequestHandler implements RequestHandler { + + protected static final Duration DEFAULT_FLUSH_TIMEOUT = Duration.ofSeconds(1); + + private final AwsLambdaTracer tracer; + private final OpenTelemetrySdk openTelemetrySdk; + private final long flushTimeoutNanos; + + /** + * Creates a new {@link TracingRequestHandler} which traces using the provided {@link + * OpenTelemetrySdk} and has a timeout of 1s when flushing at the end of an invocation. + */ + protected TracingRequestHandler(OpenTelemetrySdk openTelemetrySdk) { + this(openTelemetrySdk, DEFAULT_FLUSH_TIMEOUT); + } + + /** + * Creates a new {@link TracingRequestHandler} which traces using the provided {@link + * OpenTelemetrySdk} and has a timeout of {@code flushTimeout} when flushing at the end of an + * invocation. + */ + protected TracingRequestHandler(OpenTelemetrySdk openTelemetrySdk, Duration flushTimeout) { + this(openTelemetrySdk, flushTimeout, new AwsLambdaTracer(openTelemetrySdk)); + } + + /** + * Creates a new {@link TracingRequestHandler} which flushes the provided {@link + * OpenTelemetrySdk}, has a timeout of {@code flushTimeout} when flushing at the end of an + * invocation, and traces using the provided {@link AwsLambdaTracer}. + */ + protected TracingRequestHandler( + OpenTelemetrySdk openTelemetrySdk, Duration flushTimeout, AwsLambdaTracer tracer) { + this.openTelemetrySdk = openTelemetrySdk; + this.flushTimeoutNanos = flushTimeout.toNanos(); + this.tracer = tracer; + } + + private Map getHeaders(I input) { + if (input instanceof APIGatewayProxyRequestEvent) { + APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) input; + return event.getHeaders(); + } + return Collections.emptyMap(); + } + + @Override + public final O handleRequest(I input, Context context) { + io.opentelemetry.context.Context otelContext = + tracer.startSpan(context, SpanKind.SERVER, input, getHeaders(input)); + Throwable error = null; + try (Scope ignored = otelContext.makeCurrent()) { + O output = doHandleRequest(input, context); + tracer.onOutput(otelContext, output); + return output; + } catch (Throwable t) { + error = t; + throw t; + } finally { + if (error != null) { + tracer.endExceptionally(otelContext, error); + } else { + tracer.end(otelContext); + } + openTelemetrySdk + .getSdkTracerProvider() + .forceFlush() + .join(flushTimeoutNanos, TimeUnit.NANOSECONDS); + } + } + + protected abstract O doHandleRequest(I input, Context context); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamHandler.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamHandler.java new file mode 100644 index 000000000..930e14a36 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamHandler.java @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * A base class similar to {@link RequestStreamHandler} but will automatically trace invocations of + * {@link #doHandleRequest(InputStream input, OutputStream output, Context)}. + */ +public abstract class TracingRequestStreamHandler implements RequestStreamHandler { + + private static final Duration DEFAULT_FLUSH_TIMEOUT = Duration.ofSeconds(1); + + private final OpenTelemetrySdk openTelemetrySdk; + private final long flushTimeoutNanos; + private final AwsLambdaTracer tracer; + + /** + * Creates a new {@link TracingRequestStreamHandler} which traces using the provided {@link + * OpenTelemetrySdk} and has a timeout of 1s when flushing at the end of an invocation. + */ + protected TracingRequestStreamHandler(OpenTelemetrySdk openTelemetrySdk) { + this(openTelemetrySdk, DEFAULT_FLUSH_TIMEOUT); + } + + /** + * Creates a new {@link TracingRequestStreamHandler} which traces using the provided {@link + * OpenTelemetrySdk} and has a timeout of {@code flushTimeout} when flushing at the end of an + * invocation. + */ + protected TracingRequestStreamHandler(OpenTelemetrySdk openTelemetrySdk, Duration flushTimeout) { + this(openTelemetrySdk, flushTimeout, new AwsLambdaTracer(openTelemetrySdk)); + } + + /** + * Creates a new {@link TracingRequestStreamHandler} which flushes the provided {@link + * OpenTelemetrySdk}, has a timeout of {@code flushTimeout} when flushing at the end of an + * invocation, and traces using the provided {@link AwsLambdaTracer}. + */ + protected TracingRequestStreamHandler( + OpenTelemetrySdk openTelemetrySdk, Duration flushTimeout, AwsLambdaTracer tracer) { + this.openTelemetrySdk = openTelemetrySdk; + this.flushTimeoutNanos = flushTimeout.toNanos(); + this.tracer = tracer; + } + + @Override + public void handleRequest(InputStream input, OutputStream output, Context context) + throws IOException { + + ApiGatewayProxyRequest proxyRequest = ApiGatewayProxyRequest.forStream(input); + io.opentelemetry.context.Context otelContext = + tracer.startSpan(context, SpanKind.SERVER, input, proxyRequest.getHeaders()); + + try (Scope ignored = otelContext.makeCurrent()) { + doHandleRequest( + proxyRequest.freshStream(), + new OutputStreamWrapper(output, otelContext, openTelemetrySdk), + context); + } catch (Throwable t) { + tracer.endExceptionally(otelContext, t); + openTelemetrySdk + .getSdkTracerProvider() + .forceFlush() + .join(flushTimeoutNanos, TimeUnit.NANOSECONDS); + throw t; + } + } + + protected abstract void doHandleRequest(InputStream input, OutputStream output, Context context) + throws IOException; + + private class OutputStreamWrapper extends OutputStream { + + private final OutputStream delegate; + private final io.opentelemetry.context.Context otelContext; + private final OpenTelemetrySdk openTelemetrySdk; + + private OutputStreamWrapper( + OutputStream delegate, + io.opentelemetry.context.Context otelContext, + OpenTelemetrySdk openTelemetrySdk) { + this.delegate = delegate; + this.otelContext = otelContext; + this.openTelemetrySdk = openTelemetrySdk; + } + + @Override + public void write(byte[] b) throws IOException { + delegate.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + delegate.write(b, off, len); + } + + @Override + public void write(int b) throws IOException { + delegate.write(b); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + delegate.close(); + tracer.end(otelContext); + openTelemetrySdk + .getSdkTracerProvider() + .forceFlush() + .join(flushTimeoutNanos, TimeUnit.NANOSECONDS); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapper.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapper.java new file mode 100644 index 000000000..6a3e1ce3f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapper.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Wrapper for {@link TracingRequestStreamHandler}. Allows for wrapping a regular lambda, enabling + * single span tracing. Main lambda class should be configured as env property + * OTEL_INSTRUMENTATION_AWS_LAMBDA_HANDLER in package.ClassName::methodName format. Lambda class + * must implement {@link RequestStreamHandler}. + */ +public class TracingRequestStreamWrapper extends TracingRequestStreamHandler { + + private final WrappedLambda wrappedLambda; + + public TracingRequestStreamWrapper() { + this(OpenTelemetrySdkAutoConfiguration.initialize(), WrappedLambda.fromConfiguration()); + } + + // Visible for testing + TracingRequestStreamWrapper(OpenTelemetrySdk openTelemetrySdk, WrappedLambda wrappedLambda) { + super(openTelemetrySdk, WrapperConfiguration.flushTimeout()); + this.wrappedLambda = wrappedLambda; + } + + @Override + protected void doHandleRequest(InputStream inputStream, OutputStream output, Context context) + throws IOException { + + if (!(wrappedLambda.getTargetObject() instanceof RequestStreamHandler)) { + throw new IllegalStateException( + wrappedLambda.getTargetClass().getName() + " is not an instance of RequestStreamHandler"); + } + + ((RequestStreamHandler) wrappedLambda.getTargetObject()) + .handleRequest(inputStream, output, context); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapper.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapper.java new file mode 100644 index 000000000..5498cb044 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapper.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import io.opentelemetry.sdk.OpenTelemetrySdk; + +/** + * Wrapper for {@link TracingRequestHandler}. Allows for wrapping a regular lambda, not proxied + * through API Gateway. Therefore, HTTP headers propagation is not supported. + */ +public class TracingRequestWrapper extends TracingRequestWrapperBase { + public TracingRequestWrapper() { + super(); + } + + // Visible for testing + TracingRequestWrapper(OpenTelemetrySdk openTelemetrySdk, WrappedLambda wrappedLambda) { + super(openTelemetrySdk, wrappedLambda); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperBase.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperBase.java new file mode 100644 index 000000000..583babdab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperBase.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.amazonaws.services.lambda.runtime.Context; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Base abstract wrapper for {@link TracingRequestHandler}. Provides: - delegation to a lambda via + * env property OTEL_INSTRUMENTATION_AWS_LAMBDA_HANDLER in package.ClassName::methodName format + */ +abstract class TracingRequestWrapperBase extends TracingRequestHandler { + + private final WrappedLambda wrappedLambda; + + protected TracingRequestWrapperBase() { + this(OpenTelemetrySdkAutoConfiguration.initialize(), WrappedLambda.fromConfiguration()); + } + + // Visible for testing + TracingRequestWrapperBase(OpenTelemetrySdk openTelemetrySdk, WrappedLambda wrappedLambda) { + super(openTelemetrySdk, WrapperConfiguration.flushTimeout()); + this.wrappedLambda = wrappedLambda; + } + + private Object[] createParametersArray(Method targetMethod, I input, Context context) { + Class[] parameterTypes = targetMethod.getParameterTypes(); + Object[] parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + // loop through to populate each index of parameter + Object parameter = null; + Class clazz = parameterTypes[i]; + boolean isContext = clazz.equals(Context.class); + if (i == 0 && !isContext) { + // first position if it's not context + parameter = input; + } else if (isContext) { + // populate context + parameter = context; + } + parameters[i] = parameter; + } + return parameters; + } + + @Override + protected O doHandleRequest(I input, Context context) { + Method targetMethod = wrappedLambda.getRequestTargetMethod(); + Object[] parameters = createParametersArray(targetMethod, input, context); + + O result; + try { + result = (O) targetMethod.invoke(wrappedLambda.getTargetObject(), parameters); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Method is inaccessible", e); + } catch (InvocationTargetException e) { + throw (e.getCause() instanceof RuntimeException + ? (RuntimeException) e.getCause() + : new IllegalStateException(e.getTargetException())); + } + return result; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingSqsEventHandler.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingSqsEventHandler.java new file mode 100644 index 000000000..7d85ff56e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingSqsEventHandler.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.time.Duration; + +public abstract class TracingSqsEventHandler extends TracingRequestHandler { + + private final AwsLambdaMessageTracer tracer; + + /** + * Creates a new {@link TracingSqsEventHandler} which traces using the provided {@link + * OpenTelemetrySdk} and has a timeout of 1s when flushing at the end of an invocation. + */ + protected TracingSqsEventHandler(OpenTelemetrySdk openTelemetrySdk) { + this(openTelemetrySdk, DEFAULT_FLUSH_TIMEOUT); + } + + /** + * Creates a new {@link TracingSqsEventHandler} which traces using the provided {@link + * OpenTelemetrySdk} and has a timeout of {@code flushTimeout} when flushing at the end of an + * invocation. + */ + protected TracingSqsEventHandler(OpenTelemetrySdk openTelemetrySdk, Duration flushTimeout) { + this(openTelemetrySdk, flushTimeout, new AwsLambdaMessageTracer(openTelemetrySdk)); + } + + /** + * Creates a new {@link TracingSqsEventHandler} which flushes the provided {@link + * OpenTelemetrySdk}, has a timeout of {@code flushTimeout} when flushing at the end of an + * invocation, and traces using the provided {@link AwsLambdaTracer}. + */ + protected TracingSqsEventHandler( + OpenTelemetrySdk openTelemetrySdk, Duration flushTimeout, AwsLambdaMessageTracer tracer) { + super(openTelemetrySdk, flushTimeout); + this.tracer = tracer; + } + + @Override + public Void doHandleRequest(SQSEvent event, Context context) { + io.opentelemetry.context.Context parentContext = io.opentelemetry.context.Context.current(); + io.opentelemetry.context.Context otelContext = tracer.startSpan(parentContext, event); + Throwable error = null; + try (Scope ignored = otelContext.makeCurrent()) { + handleEvent(event, context); + } catch (Throwable t) { + error = t; + throw t; + } finally { + if (error != null) { + tracer.endExceptionally(otelContext, error); + } else { + tracer.end(otelContext); + } + } + return null; + } + + /** + * Handles a {@linkplain SQSEvent batch of messages}. Implement this class to do the actual + * processing of incoming SQS messages. + */ + protected abstract void handleEvent(SQSEvent event, Context context); + + // We use in SQS message handler too. + AwsLambdaMessageTracer getTracer() { + return tracer; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingSqsMessageHandler.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingSqsMessageHandler.java new file mode 100644 index 000000000..62341abbc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/TracingSqsMessageHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.time.Duration; + +public abstract class TracingSqsMessageHandler extends TracingSqsEventHandler { + + /** + * Creates a new {@link TracingSqsMessageHandler} which traces using the provided {@link + * OpenTelemetrySdk} and has a timeout of 1s when flushing at the end of an invocation. + */ + protected TracingSqsMessageHandler(OpenTelemetrySdk openTelemetrySdk) { + super(openTelemetrySdk); + } + + /** + * Creates a new {@link TracingSqsMessageHandler} which traces using the provided {@link + * OpenTelemetrySdk} and has a timeout of {@code flushTimeout} when flushing at the end of an + * invocation. + */ + protected TracingSqsMessageHandler(OpenTelemetrySdk openTelemetrySdk, Duration flushTimeout) { + super(openTelemetrySdk, flushTimeout); + } + + /** + * Creates a new {@link TracingSqsMessageHandler} which flushes the provided {@link + * OpenTelemetrySdk}, has a timeout of {@code flushTimeout} when flushing at the end of an + * invocation, and traces using the provided {@link AwsLambdaTracer}. + */ + protected TracingSqsMessageHandler( + OpenTelemetrySdk openTelemetrySdk, Duration flushTimeout, AwsLambdaMessageTracer tracer) { + super(openTelemetrySdk, flushTimeout, tracer); + } + + @Override + protected final void handleEvent(SQSEvent event, Context context) { + io.opentelemetry.context.Context parentContext = io.opentelemetry.context.Context.current(); + for (SQSMessage message : event.getRecords()) { + io.opentelemetry.context.Context otelContext = getTracer().startSpan(parentContext, message); + Throwable error = null; + try (Scope ignored = otelContext.makeCurrent()) { + handleMessage(message, context); + } catch (Throwable t) { + error = t; + throw t; + } finally { + if (error != null) { + getTracer().endExceptionally(otelContext, error); + } else { + getTracer().end(otelContext); + } + } + } + } + + /** + * Handles a {@linkplain SQSMessage message}. Implement this class to do the actual processing of + * incoming SQS messages. + */ + protected abstract void handleMessage(SQSMessage message, Context context); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/WrappedLambda.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/WrappedLambda.java new file mode 100644 index 000000000..6be352e5b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/WrappedLambda.java @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import com.amazonaws.services.lambda.runtime.Context; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** Model for wrapped lambda function (object, class, method). */ +class WrappedLambda { + + public static final String OTEL_LAMBDA_HANDLER_ENV_KEY = + "OTEL_INSTRUMENTATION_AWS_LAMBDA_HANDLER"; + + private final Object targetObject; + private final Class targetClass; + private final String targetMethodName; + + /** + * Creates new lambda wrapper out of configuration. Supported env properties: - {@value + * OTEL_LAMBDA_HANDLER_ENV_KEY} - lambda handler in format: package.ClassName::methodName + */ + static WrappedLambda fromConfiguration() { + + String lambdaHandler = System.getenv(OTEL_LAMBDA_HANDLER_ENV_KEY); + if (lambdaHandler == null || lambdaHandler.isEmpty()) { + throw new IllegalStateException(OTEL_LAMBDA_HANDLER_ENV_KEY + " was not specified."); + } + // expect format to be package.ClassName::methodName + String[] split = lambdaHandler.split("::"); + if (split.length != 2) { + throw new IllegalStateException( + lambdaHandler + + " is not a valid handler name. Expected format: package.ClassName::methodName"); + } + String handlerClassName = split[0]; + String targetMethodName = split[1]; + Class targetClass; + try { + targetClass = Class.forName(handlerClassName); + } catch (ClassNotFoundException e) { + // no class found + throw new IllegalStateException(handlerClassName + " not found in classpath", e); + } + return new WrappedLambda(targetClass, targetMethodName); + } + + WrappedLambda(Class targetClass, String targetMethodName) { + this.targetClass = targetClass; + this.targetMethodName = targetMethodName; + this.targetObject = instantiateTargetClass(); + } + + private Object instantiateTargetClass() { + Object targetObject; + try { + Constructor ctor = targetClass.getConstructor(); + targetObject = ctor.newInstance(); + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + targetClass.getName() + " does not have an appropriate constructor", e); + } catch (InstantiationException e) { + throw new IllegalStateException(targetClass.getName() + " cannot be an abstract class", e); + } catch (IllegalAccessException e) { + throw new IllegalStateException( + targetClass.getName() + "'s constructor is not accessible", e); + } catch (InvocationTargetException e) { + throw new IllegalStateException( + targetClass.getName() + " threw an exception from the constructor", e); + } + return targetObject; + } + + private static boolean isLastParameterContext(Parameter[] parameters) { + if (parameters.length == 0) { + return false; + } + return parameters[parameters.length - 1].getType().equals(Context.class); + } + + Method getRequestTargetMethod() { + + List methods = Arrays.asList(targetClass.getMethods()); + Optional firstOptional = + methods.stream() + .filter((Method m) -> m.getName().equals(targetMethodName)) + .min(WrappedLambda::methodComparator); + if (!firstOptional.isPresent()) { + throw new IllegalStateException("Method " + targetMethodName + " not found"); + } + return firstOptional.get(); + } + + /* + Per method selection specifications + http://docs.aws.amazon.com/lambda/latest/dg/java-programming-model-handler-types.html + - Context can be omitted + - Select the method with the largest number of parameters. + - If two or more methods have the same number of parameters, AWS Lambda selects the method that has the Context as the last parameter. + - Non-Bridge methods are preferred + - If none or all of these methods have the Context parameter, then the behavior is undefined. + + Examples: + - handleA(String, String, Integer), handleB(String, Context) - handleA is selected (number of parameters) + - handleA(String, String, Integer), handleB(String, String, Context) - handleB is selected (has Context as the last parameter) + - generic method handleG(T, U, Context), implementation (T, U - String) handleA(String, String, Context), bridge method handleB(Object, Object, Context) - handleA is selected (non-bridge) + */ + private static int methodComparator(Method a, Method b) { + // greater number of params wins + if (a.getParameterCount() != b.getParameterCount()) { + return b.getParameterCount() - a.getParameterCount(); + } + // only one of the methods has last param context ? + int onlyOneHasCtx = onlyOneHasContextAsLastParam(a, b); + if (onlyOneHasCtx != 0) { + return onlyOneHasCtx; + } + // one of the methods is a bridge, otherwise - undefined + return onlyOneIsBridgeMethod(a, b); + } + + private static int onlyOneIsBridgeMethod(Method first, Method second) { + boolean firstBridge = first.isBridge(); + boolean secondBridge = second.isBridge(); + if (firstBridge && !secondBridge) { + return 1; + } else if (!firstBridge && secondBridge) { + return -1; + } + return 0; + } + + private static int onlyOneHasContextAsLastParam(Method first, Method second) { + boolean firstCtx = isLastParameterContext(first.getParameters()); + boolean secondCtx = isLastParameterContext(second.getParameters()); + // only one of the methods has last param context ? + if (firstCtx && !secondCtx) { + return -1; + } else if (!firstCtx && secondCtx) { + return 1; + } + return 0; + } + + Object getTargetObject() { + return targetObject; + } + + Class getTargetClass() { + return targetClass; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/WrapperConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/WrapperConfiguration.java new file mode 100644 index 000000000..c176ad30c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambda/v1_0/WrapperConfiguration.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import java.time.Duration; + +public final class WrapperConfiguration { + + private WrapperConfiguration() {} + + public static final String OTEL_LAMBDA_FLUSH_TIMEOUT_ENV_KEY = + "OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT"; + public static final Duration OTEL_LAMBDA_FLUSH_TIMEOUT_DEFAULT = Duration.ofSeconds(10); + + public static Duration flushTimeout() { + String lambdaFlushTimeout = System.getenv(OTEL_LAMBDA_FLUSH_TIMEOUT_ENV_KEY); + if (lambdaFlushTimeout != null && !lambdaFlushTimeout.isEmpty()) { + try { + return Duration.ofMillis(Long.parseLong(lambdaFlushTimeout)); + } catch (NumberFormatException nfe) { + // ignored - default used + } + } + return OTEL_LAMBDA_FLUSH_TIMEOUT_DEFAULT; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaSqsHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaSqsHandlerTest.groovy new file mode 100644 index 000000000..8caa2991e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaSqsHandlerTest.groovy @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import com.amazonaws.services.lambda.runtime.events.SQSEvent +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import io.opentelemetry.sdk.OpenTelemetrySdk + +class AwsLambdaSqsHandlerTest extends AbstractAwsLambdaSqsHandlerTest implements LibraryTestTrait { + + static class TestHandler extends TracingSqsEventHandler { + + TestHandler(OpenTelemetrySdk openTelemetrySdk) { + super(openTelemetrySdk) + } + + @Override + protected void handleEvent(SQSEvent event, Context context) { + } + } + + @Override + RequestHandler handler() { + return new TestHandler(testRunner().openTelemetrySdk) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaSqsMessageHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaSqsMessageHandlerTest.groovy new file mode 100644 index 000000000..ca29951aa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaSqsMessageHandlerTest.groovy @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.events.SQSEvent +import io.opentelemetry.instrumentation.test.LibraryInstrumentationSpecification +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class AwsLambdaSqsMessageHandlerTest extends LibraryInstrumentationSpecification { + + private static final String AWS_TRACE_HEADER1 = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" + private static final String AWS_TRACE_HEADER2 = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad9;Sampled=1" + + static class TestHandler extends TracingSqsMessageHandler { + + TestHandler(OpenTelemetrySdk openTelemetrySdk) { + super(openTelemetrySdk) + } + + @Override + protected void handleMessage(SQSEvent.SQSMessage event, Context context) { + } + } + + def "messages with process spans"() { + when: + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + + def message1 = new SQSEvent.SQSMessage() + message1.setAttributes(["AWSTraceHeader": AWS_TRACE_HEADER1]) + message1.setMessageId("message1") + message1.setEventSource("queue1") + + def message2 = new SQSEvent.SQSMessage() + message2.setAttributes(["AWSTraceHeader": AWS_TRACE_HEADER2]) + message2.setMessageId("message2") + message2.setEventSource("queue1") + + def event = new SQSEvent() + event.setRecords([message1, message2]) + + new TestHandler(testRunner().openTelemetrySdk).handleRequest(event, context) + + then: + assertTraces(1) { + trace(0, 4) { + span(0) { + name("my_function") + kind SERVER + attributes { + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + span(1) { + name("queue1 process") + kind CONSUMER + parentSpanId(span(0).spanId) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "AmazonSQS" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + } + hasLink("5759e988bd862e3fe1be46a994272793", "53995c3f42cd8ad8") + hasLink("5759e988bd862e3fe1be46a994272793", "53995c3f42cd8ad9") + } + span(2) { + name("queue1 process") + kind CONSUMER + parentSpanId(span(1).spanId) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "AmazonSQS" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" "message1" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" "queue1" + } + hasLink("5759e988bd862e3fe1be46a994272793", "53995c3f42cd8ad8") + } + span(3) { + name("queue1 process") + kind CONSUMER + parentSpanId(span(1).spanId) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "AmazonSQS" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" "message2" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" "queue1" + } + hasLink("5759e988bd862e3fe1be46a994272793", "53995c3f42cd8ad9") + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaTest.groovy new file mode 100644 index 000000000..6ff575595 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AwsLambdaTest.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import io.opentelemetry.sdk.OpenTelemetrySdk + +class AwsLambdaTest extends AbstractAwsLambdaRequestHandlerTest implements LibraryTestTrait { + + def cleanup() { + assert forceFlushCalled() + } + + static class TestRequestHandler extends TracingRequestHandler { + + TestRequestHandler(OpenTelemetrySdk openTelemetrySdk) { + super(openTelemetrySdk) + } + + @Override + protected String doHandleRequest(String input, Context context) { + return AbstractAwsLambdaRequestHandlerTest.doHandleRequest(input, context) + } + } + + @Override + RequestHandler handler() { + return new TestRequestHandler(testRunner().openTelemetrySdk) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestApiGatewayWrapperTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestApiGatewayWrapperTest.groovy new file mode 100644 index 000000000..4d9def1f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestApiGatewayWrapperTest.groovy @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent +import com.google.common.collect.ImmutableMap +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class TracingRequestApiGatewayWrapperTest extends TracingRequestWrapperTestBase { + + static class TestApiGatewayHandler implements RequestHandler { + + @Override + APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + if (input.getBody() == "hello") { + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody("world") + } else if (input.getBody() == "empty") { + return new APIGatewayProxyResponseEvent() + } + throw new IllegalStateException("bad request") + } + } + + def propagationHeaders() { + return Collections.singletonMap("traceparent", "00-4fd0b6131f19f39af59518d127b0cafe-0000000000000456-01") + } + + def "handler traced with trace propagation"() { + given: + setLambda(TestApiGatewayHandler.getName() + "::handleRequest", TracingRequestApiGatewayWrapper.metaClass.&invokeConstructor) + + def headers = ImmutableMap.builder() + .putAll(propagationHeaders()) + .put("User-Agent", "Test Client") + .put("host", "localhost:123") + .put("X-FORWARDED-PROTO", "http") + .build() + def input = new APIGatewayProxyRequestEvent() + .withHttpMethod("GET") + .withResource("/hello/{param}") + .withPath("/hello/world") + .withBody("hello") + .withQueryStringParamters(["a": "b", "c": "d"]) + .withHeaders(headers) + + when: + APIGatewayProxyResponseEvent result = wrapper.handleRequest(input, context) + + then: + result.getBody() == "world" + assertTraces(1) { + trace(0, 1) { + span(0) { + parentSpanId("0000000000000456") + traceId("4fd0b6131f19f39af59518d127b0cafe") + name("/hello/{param}") + kind SERVER + attributes { + "$ResourceAttributes.FAAS_ID.key" "arn:aws:lambda:us-east-1:123456789:function:test" + "$ResourceAttributes.CLOUD_ACCOUNT_ID.key" "123456789" + "$SemanticAttributes.FAAS_EXECUTION.key" "1-22-333" + "$SemanticAttributes.FAAS_TRIGGER.key" "http" + "$SemanticAttributes.HTTP_METHOD.key" "GET" + "$SemanticAttributes.HTTP_USER_AGENT.key" "Test Client" + "$SemanticAttributes.HTTP_URL.key" "http://localhost:123/hello/world?a=b&c=d" + "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + } + } + } + } + } + + def "test empty request & response"() { + given: + setLambda(TestApiGatewayHandler.getName() + "::handleRequest", TracingRequestApiGatewayWrapper.metaClass.&invokeConstructor) + + def input = new APIGatewayProxyRequestEvent() + .withBody("empty") + + when: + APIGatewayProxyResponseEvent result = wrapper.handleRequest(input, context) + + then: + result.body == null + assertTraces(1) { + trace(0, 1) { + span(0) { + name("my_function") + kind SERVER + attributes { + "$ResourceAttributes.FAAS_ID.key" "arn:aws:lambda:us-east-1:123456789:function:test" + "$ResourceAttributes.CLOUD_ACCOUNT_ID.key" "123456789" + "$SemanticAttributes.FAAS_EXECUTION.key" "1-22-333" + "$SemanticAttributes.FAAS_TRIGGER.key" "http" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapperPropagationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapperPropagationTest.groovy new file mode 100644 index 000000000..af2766d22 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapperPropagationTest.groovy @@ -0,0 +1,134 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestStreamHandler +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import io.opentelemetry.instrumentation.test.LibraryInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.nio.charset.Charset +import org.junit.Rule +import org.junit.contrib.java.lang.system.EnvironmentVariables +import spock.lang.Shared + +class TracingRequestStreamWrapperPropagationTest extends LibraryInstrumentationSpecification { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + + @Rule + public final EnvironmentVariables environmentVariables = new EnvironmentVariables() + + static class TestRequestHandler implements RequestStreamHandler { + + @Override + void handleRequest(InputStream input, OutputStream output, Context context) { + + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output)) + + JsonNode root = OBJECT_MAPPER.readTree(input) + String body = root.get("body").asText() + if (body == "hello") { + writer.write("world") + writer.flush() + writer.close() + } else { + throw new IllegalArgumentException("bad argument") + } + } + } + + @Shared + TracingRequestStreamWrapper wrapper + + def setup() { + environmentVariables.set(WrappedLambda.OTEL_LAMBDA_HANDLER_ENV_KEY, "io.opentelemetry.instrumentation.awslambda.v1_0.TracingRequestStreamWrapperPropagationTest\$TestRequestHandler::handleRequest") + wrapper = new TracingRequestStreamWrapper(testRunner().openTelemetrySdk, WrappedLambda.fromConfiguration()) + } + + def cleanup() { + environmentVariables.clear(WrappedLambda.OTEL_LAMBDA_HANDLER_ENV_KEY) + } + + def "handler traced with trace propagation"() { + when: + String content = + "{" + + "\"headers\" : {" + + "\"traceparent\": \"00-4fd0b6131f19f39af59518d127b0cafe-0000000000000456-01\"" + + "}," + + "\"body\" : \"hello\"" + + "}" + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + def input = new ByteArrayInputStream(content.getBytes(Charset.defaultCharset())) + def output = new ByteArrayOutputStream() + + wrapper.handleRequest(input, output, context) + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + parentSpanId("0000000000000456") + traceId("4fd0b6131f19f39af59518d127b0cafe") + name("my_function") + kind SERVER + attributes { + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } + + def "handler traced with exception and trace propagation"() { + when: + String content = + "{" + + "\"headers\" : {" + + "\"traceparent\": \"00-4fd0b6131f19f39af59518d127b0cafe-0000000000000456-01\"" + + "}," + + "\"body\" : \"bye\"" + + "}" + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + def input = new ByteArrayInputStream(content.getBytes(Charset.defaultCharset())) + def output = new ByteArrayOutputStream() + + def thrown + try { + wrapper.handleRequest(input, output, context) + } catch (Throwable t) { + thrown = t + } + + then: + thrown != null + assertTraces(1) { + trace(0, 1) { + span(0) { + parentSpanId("0000000000000456") + traceId("4fd0b6131f19f39af59518d127b0cafe") + name("my_function") + kind SERVER + status ERROR + errorEvent(IllegalArgumentException, "bad argument") + attributes { + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapperTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapperTest.groovy new file mode 100644 index 000000000..bcb41f7fc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestStreamWrapperTest.groovy @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestStreamHandler +import io.opentelemetry.instrumentation.test.LibraryInstrumentationSpecification +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.nio.charset.Charset +import org.junit.Rule +import org.junit.contrib.java.lang.system.EnvironmentVariables +import spock.lang.Shared + +class TracingRequestStreamWrapperTest extends LibraryInstrumentationSpecification { + + @Rule + public final EnvironmentVariables environmentVariables = new EnvironmentVariables() + + static class TestRequestHandler implements RequestStreamHandler { + + @Override + void handleRequest(InputStream input, OutputStream output, Context context) { + + BufferedReader reader = new BufferedReader(new InputStreamReader(input)) + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output)) + String line = reader.readLine() + if (line == "hello") { + writer.write("world") + writer.flush() + writer.close() + } else { + throw new IllegalArgumentException("bad argument") + } + } + } + + @Shared + TracingRequestStreamWrapper wrapper + + def setup() { + environmentVariables.set(WrappedLambda.OTEL_LAMBDA_HANDLER_ENV_KEY, "io.opentelemetry.instrumentation.awslambda.v1_0.TracingRequestStreamWrapperTest\$TestRequestHandler::handleRequest") + wrapper = new TracingRequestStreamWrapper(testRunner().openTelemetrySdk, WrappedLambda.fromConfiguration()) + } + + def cleanup() { + environmentVariables.clear(WrappedLambda.OTEL_LAMBDA_HANDLER_ENV_KEY) + } + + def "handler traced"() { + when: + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + context.getInvokedFunctionArn() >> "arn:aws:lambda:us-east-1:123456789:function:test" + def input = new ByteArrayInputStream("hello\n".getBytes(Charset.defaultCharset())) + def output = new ByteArrayOutputStream() + + wrapper.handleRequest(input, output, context) + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name("my_function") + kind SERVER + attributes { + "$ResourceAttributes.FAAS_ID.key" "arn:aws:lambda:us-east-1:123456789:function:test" + "$ResourceAttributes.CLOUD_ACCOUNT_ID.key" "123456789" + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } + + def "handler traced with exception"() { + when: + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + context.getInvokedFunctionArn() >> "arn:aws:lambda:us-east-1:123456789:function:test" + def input = new ByteArrayInputStream("bye".getBytes(Charset.defaultCharset())) + def output = new ByteArrayOutputStream() + + def thrown + try { + wrapper.handleRequest(input, output, context) + } catch (Throwable t) { + thrown = t + } + + then: + thrown != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name("my_function") + kind SERVER + status ERROR + errorEvent(IllegalArgumentException, "bad argument") + attributes { + "$ResourceAttributes.FAAS_ID.key" "arn:aws:lambda:us-east-1:123456789:function:test" + "$ResourceAttributes.CLOUD_ACCOUNT_ID.key" "123456789" + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperTest.groovy new file mode 100644 index 000000000..e7ab02259 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperTest.groovy @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class TracingRequestWrapperTest extends TracingRequestWrapperTestBase { + + static class TestRequestHandlerString implements RequestHandler { + + @Override + String handleRequest(String input, Context context) { + if (input == "hello") { + return "world" + } + throw new IllegalArgumentException("bad argument") + } + } + + def "handler string traced"() { + given: + setLambda(TestRequestHandlerString.getName() + "::handleRequest", TracingRequestWrapper.metaClass.&invokeConstructor) + + when: + def result = wrapper.handleRequest("hello", context) + + then: + result == "world" + assertTraces(1) { + trace(0, 1) { + span(0) { + name("my_function") + kind SERVER + attributes { + "$ResourceAttributes.FAAS_ID.key" "arn:aws:lambda:us-east-1:123456789:function:test" + "$ResourceAttributes.CLOUD_ACCOUNT_ID.key" "123456789" + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } + + def "handler with exception"() { + given: + setLambda(TestRequestHandlerString.getName() + "::handleRequest", TracingRequestWrapper.metaClass.&invokeConstructor) + + when: + def thrown + try { + wrapper.handleRequest("goodbye", context) + } catch (Throwable t) { + thrown = t + } + + then: + thrown != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name("my_function") + kind SERVER + status ERROR + errorEvent(IllegalArgumentException, "bad argument") + attributes { + "$ResourceAttributes.FAAS_ID.key" "arn:aws:lambda:us-east-1:123456789:function:test" + "$ResourceAttributes.CLOUD_ACCOUNT_ID.key" "123456789" + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperTestBase.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperTestBase.groovy new file mode 100644 index 000000000..8bdfc5954 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/TracingRequestWrapperTestBase.groovy @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import com.amazonaws.services.lambda.runtime.Context +import io.opentelemetry.instrumentation.test.LibraryInstrumentationSpecification +import org.junit.Rule +import org.junit.contrib.java.lang.system.EnvironmentVariables +import spock.lang.Shared + +class TracingRequestWrapperTestBase extends LibraryInstrumentationSpecification { + + @Rule + public final EnvironmentVariables environmentVariables = new EnvironmentVariables() + + @Shared + TracingRequestWrapperBase wrapper + + @Shared + Context context + + def setup() { + context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + context.getInvokedFunctionArn() >> "arn:aws:lambda:us-east-1:123456789:function:test" + } + + def setLambda(handler, Closure wrapperConstructor) { + environmentVariables.set(WrappedLambda.OTEL_LAMBDA_HANDLER_ENV_KEY, handler) + wrapper = wrapperConstructor.call(testRunner().openTelemetrySdk, WrappedLambda.fromConfiguration()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/ApiGatewayProxyRequestTest.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/ApiGatewayProxyRequestTest.java new file mode 100644 index 000000000..04832becf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/ApiGatewayProxyRequestTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ApiGatewayProxyRequestTest { + + @BeforeEach + void resetOpenTelemetry() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + public void shouldCreateNoopRequestIfNoPropagatorsSet() throws IOException { + // given + InputStream mock = mock(InputStream.class); + GlobalOpenTelemetry.set(OpenTelemetry.noop()); + // when + ApiGatewayProxyRequest created = ApiGatewayProxyRequest.forStream(mock); + // then + assertThat(created.freshStream()).isEqualTo(mock); + assertThat(created.getHeaders()).isEmpty(); + } + + @Test + public void shouldCreateNoopRequestIfXRayPropagatorsSet() throws IOException { + // given + InputStream mock = mock(InputStream.class); + GlobalOpenTelemetry.set( + OpenTelemetry.propagating(ContextPropagators.create(AwsXrayPropagator.getInstance()))); + // when + ApiGatewayProxyRequest created = ApiGatewayProxyRequest.forStream(mock); + // then + assertThat(created.freshStream()).isEqualTo(mock); + assertThat(created.getHeaders()).isEmpty(); + } + + @Test + public void shouldUseStreamMarkingIfHttpPropagatorsSet() throws IOException { + // given + InputStream mock = mock(InputStream.class); + given(mock.markSupported()).willReturn(true); + GlobalOpenTelemetry.set( + OpenTelemetry.propagating(ContextPropagators.create(B3Propagator.injectingSingleHeader()))); + // when + ApiGatewayProxyRequest created = ApiGatewayProxyRequest.forStream(mock); + // then + assertThat(created.freshStream()).isEqualTo(mock); + then(mock).should(atLeastOnce()).mark(Integer.MAX_VALUE); + then(mock).should().reset(); + } + + @Test + public void shouldUseCopyIfMarkingNotAvailableAndHttpPropagatorsSet() throws IOException { + // given + InputStream mock = mock(InputStream.class); + given(mock.markSupported()).willReturn(false); + given(mock.read(any(byte[].class))).willReturn(-1); + GlobalOpenTelemetry.set( + OpenTelemetry.propagating(ContextPropagators.create(B3Propagator.injectingSingleHeader()))); + // when + ApiGatewayProxyRequest created = ApiGatewayProxyRequest.forStream(mock); + // then + assertThat(created.freshStream()).isInstanceOf(ByteArrayInputStream.class); + then(mock).should(never()).mark(any(Integer.class)); + then(mock).should(never()).reset(); + then(mock).should().read(any()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/HeadersFactoryTest.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/HeadersFactoryTest.java new file mode 100644 index 000000000..a00317763 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/HeadersFactoryTest.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class HeadersFactoryTest { + + @Test + public void shouldReadHeadersFromStream() { + // given + String json = + "{" + + "\"headers\" : {" + + "\"X-B3-TraceId\": \"4fd0b6131f19f39af59518d127b0cafe\", \"X-B3-SpanId\": \"0000000000000456\", \"X-B3-Sampled\": \"true\"" + + "}," + + "\"body\" : \"hello\"" + + "}"; + InputStream inputStream = new ByteArrayInputStream(json.getBytes(Charset.defaultCharset())); + // when + Map headers = HeadersFactory.ofStream(inputStream); + // then + assertThat(headers).isNotNull(); + assertThat(headers.size()).isEqualTo(3); + assertThat(headers) + .containsOnly( + entry("X-B3-TraceId", "4fd0b6131f19f39af59518d127b0cafe"), + entry("X-B3-SpanId", "0000000000000456"), + entry("X-B3-Sampled", "true")); + } + + @Test + public void shouldReturnNullIfNoHeadersInStream() { + // given + String json = "{\"something\" : \"else\"}"; + InputStream inputStream = new ByteArrayInputStream(json.getBytes(Charset.defaultCharset())); + // when + Map headers = HeadersFactory.ofStream(inputStream); // then + assertThat(headers).isNull(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/ParentContextExtractorTest.java b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/ParentContextExtractorTest.java new file mode 100644 index 000000000..32663fecc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambda/v1_0/ParentContextExtractorTest.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.EnvironmentVariables; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; + +public class ParentContextExtractorTest { + + @Rule + public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties(); + + @Rule public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + private static final OpenTelemetry OTEL = + OpenTelemetry.propagating(ContextPropagators.create(B3Propagator.injectingSingleHeader())); + + private static final AwsLambdaTracer TRACER = new AwsLambdaTracer(OTEL); + + @Test + public void shouldUseHttpIfAwsParentNotSampled() { + // given + Map headers = + ImmutableMap.of( + "X-b3-traceId", + "4fd0b6131f19f39af59518d127b0cafe", + "x-b3-spanid", + "0000000000000123", + "X-B3-Sampled", + "true"); + environmentVariables.set( + "_X_AMZN_TRACE_ID", + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0"); + + // when + Context context = ParentContextExtractor.extract(headers, TRACER); + // then + Span span = Span.fromContext(context); + SpanContext spanContext = span.getSpanContext(); + assertThat(spanContext.isValid()).isTrue(); + assertThat(spanContext.isValid()).isTrue(); + assertThat(spanContext.getSpanId()).isEqualTo("0000000000000123"); + assertThat(spanContext.getTraceId()).isEqualTo("4fd0b6131f19f39af59518d127b0cafe"); + } + + @Test + public void shouldPreferAwsParentHeaderIfValidAndSampled() { + // given + Map headers = + ImmutableMap.of( + "X-b3-traceId", + "4fd0b6131f19f39af59518d127b0cafe", + "x-b3-spanid", + "0000000000000456", + "X-B3-Sampled", + "true"); + environmentVariables.set( + "_X_AMZN_TRACE_ID", + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1"); + + // when + Context context = ParentContextExtractor.extract(headers, TRACER); + // then + Span span = Span.fromContext(context); + SpanContext spanContext = span.getSpanContext(); + assertThat(spanContext.isValid()).isTrue(); + assertThat(spanContext.isValid()).isTrue(); + assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); + assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); + } + + @Test + public void shouldExtractCaseInsensitiveHeaders() { + // given + Map headers = + ImmutableMap.of( + "X-b3-traceId", + "4fd0b6131f19f39af59518d127b0cafe", + "x-b3-spanid", + "0000000000000456", + "X-B3-Sampled", + "true"); + + // when + Context context = ParentContextExtractor.extract(headers, TRACER); + // then + Span span = Span.fromContext(context); + SpanContext spanContext = span.getSpanContext(); + assertThat(spanContext.isValid()).isTrue(); + assertThat(spanContext.isValid()).isTrue(); + assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); + assertThat(spanContext.getTraceId()).isEqualTo("4fd0b6131f19f39af59518d127b0cafe"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/aws-lambda-1.0-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/aws-lambda-1.0-testing.gradle new file mode 100644 index 000000000..536987034 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/aws-lambda-1.0-testing.gradle @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "com.amazonaws:aws-lambda-java-core:1.0.0" + api "com.amazonaws:aws-lambda-java-events:2.2.1" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" + implementation "com.github.stefanbirkner:system-lambda" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AbstractAwsLambdaRequestHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AbstractAwsLambdaRequestHandlerTest.groovy new file mode 100644 index 000000000..b2ec9cd71 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AbstractAwsLambdaRequestHandlerTest.groovy @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import com.github.stefanbirkner.systemlambda.SystemLambda +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +abstract class AbstractAwsLambdaRequestHandlerTest extends InstrumentationSpecification { + + protected static String doHandleRequest(String input, Context context) { + if (input == "hello") { + return "world" + } + throw new IllegalArgumentException("bad argument") + } + + abstract RequestHandler handler() + + def "handler traced"() { + when: + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + + def result = handler().handleRequest("hello", context) + + then: + result == "world" + assertTraces(1) { + trace(0, 1) { + span(0) { + name("my_function") + kind SERVER + attributes { + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } + + def "handler traced with exception"() { + when: + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + + def thrown + try { + handler().handleRequest("goodbye", context) + } catch (Throwable t) { + thrown = t + } + + then: + thrown != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name("my_function") + kind SERVER + status ERROR + errorEvent(IllegalArgumentException, "bad argument") + attributes { + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } + + def "handler links to lambda trace"() { + when: + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + + def result + SystemLambda.withEnvironmentVariable("_X_AMZN_TRACE_ID", "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1") + .execute({ + result = handler().handleRequest("hello", context) + }) + + then: + result == "world" + assertTraces(1) { + trace(0, 1) { + span(0) { + name("my_function") + kind SERVER + parentSpanId("0000000000000456") + traceId("8a3c60f7d188f8fa79d48a391a778fa6") + attributes { + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AbstractAwsLambdaSqsHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AbstractAwsLambdaSqsHandlerTest.groovy new file mode 100644 index 000000000..96ebc5d10 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-lambda-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/awslambda/v1_0/AbstractAwsLambdaSqsHandlerTest.groovy @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambda.v1_0 + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestHandler +import com.amazonaws.services.lambda.runtime.events.SQSEvent +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.instrumentation.test.InstrumentationSpecification + +abstract class AbstractAwsLambdaSqsHandlerTest extends InstrumentationSpecification { + + private static final String AWS_TRACE_HEADER = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" + + abstract RequestHandler handler() + + def "messages from same source"() { + when: + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + + def message1 = new SQSEvent.SQSMessage() + message1.setAttributes(["AWSTraceHeader": AWS_TRACE_HEADER]) + message1.setMessageId("message1") + message1.setEventSource("queue1") + + def message2 = new SQSEvent.SQSMessage() + message2.setAttributes([:]) + message2.setMessageId("message2") + message2.setEventSource("queue1") + + def event = new SQSEvent() + event.setRecords([message1, message2]) + + handler().handleRequest(event, context) + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name("my_function") + kind SERVER + attributes { + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + span(1) { + name("queue1 process") + kind CONSUMER + parentSpanId(span(0).spanId) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "AmazonSQS" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + } + hasLink("5759e988bd862e3fe1be46a994272793", "53995c3f42cd8ad8") + } + } + } + } + + def "messages from different source"() { + when: + def context = Mock(Context) + context.getFunctionName() >> "my_function" + context.getAwsRequestId() >> "1-22-333" + + def message1 = new SQSEvent.SQSMessage() + message1.setAttributes(["AWSTraceHeader": AWS_TRACE_HEADER]) + message1.setMessageId("message1") + message1.setEventSource("queue1") + + def message2 = new SQSEvent.SQSMessage() + message2.setAttributes([:]) + message2.setMessageId("message2") + message2.setEventSource("queue2") + + def event = new SQSEvent() + event.setRecords([message1, message2]) + + handler().handleRequest(event, context) + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name("my_function") + kind SERVER + attributes { + "${SemanticAttributes.FAAS_EXECUTION.key}" "1-22-333" + } + } + span(1) { + name("multiple_sources process") + kind CONSUMER + parentSpanId(span(0).spanId) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "AmazonSQS" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + } + hasLink("5759e988bd862e3fe1be46a994272793", "53995c3f42cd8ad8") + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent-unit-tests/aws-sdk-1.11-javaagent-unit-tests.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent-unit-tests/aws-sdk-1.11-javaagent-unit-tests.gradle new file mode 100644 index 000000000..208b78bfc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent-unit-tests/aws-sdk-1.11-javaagent-unit-tests.gradle @@ -0,0 +1,11 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + testImplementation project(':instrumentation:aws-sdk:aws-sdk-1.11:javaagent') + + testImplementation "com.amazonaws:aws-java-sdk-core:1.11.0" + testImplementation "com.amazonaws:aws-java-sdk-sqs:1.11.106" + + testImplementation "org.assertj:assertj-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent-unit-tests/src/test/java/TracingRequestHandlerTest.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent-unit-tests/src/test/java/TracingRequestHandlerTest.java new file mode 100644 index 000000000..e9a5da634 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent-unit-tests/src/test/java/TracingRequestHandlerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.testing.util.TraceUtils.withClientSpan; +import static org.assertj.core.api.Assertions.assertThat; + +import com.amazonaws.DefaultRequest; +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.http.HttpResponse; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.amazonaws.services.sqs.model.SendMessageResult; +import io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.TracingRequestHandler; +import java.net.URI; +import org.apache.http.client.methods.HttpGet; +import org.junit.Test; + +public class TracingRequestHandlerTest { + + private static Response response(Request request) { + return new Response<>(new SendMessageResult(), new HttpResponse(request, new HttpGet())); + } + + private static Request request() { + Request request = new DefaultRequest<>(new SendMessageRequest(), "test"); + request.setEndpoint(URI.create("http://test.uri")); + return request; + } + + @Test + public void shouldNotSetScopeAndNotFailIfClientSpanAlreadyPresent() { + // given + TracingRequestHandler underTest = new TracingRequestHandler(); + Request request = request(); + + withClientSpan( + "test", + () -> { + // when + underTest.beforeRequest(request); + // then - no exception and scope not set + assertThat(request.getHandlerContext(TracingRequestHandler.SCOPE)).isNull(); + underTest.afterResponse(request, response(request)); + }); + } + + @Test + public void shouldSetScopeIfClientSpanNotPresent() { + // given + TracingRequestHandler underTest = new TracingRequestHandler(); + Request request = request(); + + // when + underTest.beforeRequest(request); + // then - no exception and scope not set + assertThat(request.getHandlerContext(TracingRequestHandler.SCOPE)).isNotNull(); + underTest.afterResponse(request, response(request)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/README.md b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/README.md new file mode 100644 index 000000000..3481c4d57 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/README.md @@ -0,0 +1,12 @@ +# AWS Java SDK v1 Instrumentation + +Instrumentation for [AWS Java SDK v1](https://github.com/aws/aws-sdk-java). + +## Trace propagation + +The AWS SDK instrumentation currently only supports injecting the trace header into the request +using the [AWS Trace Header](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader) format. +This format is the only format recognized by AWS managed services, and populating will allow +propagating the trace through them. If this does not fulfill your use case, perhaps because you are +using the same SDK with a different non-AWS managed service, let us know so we can provide +configuration for this behavior. diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/aws-sdk-1.11-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/aws-sdk-1.11-javaagent.gradle new file mode 100644 index 000000000..5d4f6f931 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/aws-sdk-1.11-javaagent.gradle @@ -0,0 +1,102 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +// compiling against 1.11.0, but instrumentation should work against 1.10.33 with varying effects, +// depending on the version's implementation. (i.e. DeleteOptionGroup may have less handlerCounts than +// expected in 1.11.84. Testing against 1.11.0 instead of 1.10.33 because the RequestHandler class +// used in testing is abstract in 1.10.33 +// keeping base test version on 1.11.0 because RequestHandler2 is abstract in 1.10.33, +// therefore keeping base version as 1.11.0 even though the instrumentation probably +// is able to support up to 1.10.33 +muzzle { + pass { + group = "com.amazonaws" + module = "aws-java-sdk-core" + versions = "[1.10.33,)" + assertInverse = true + } +} + +testSets { + // Features used in test_1_11_106 (builder) is available since 1.11.84, but + // using 1.11.106 because of previous concerns with byte code differences + // in 1.11.106, also, the DeleteOptionGroup request generates more spans + // in 1.11.106 than 1.11.84. + // We test older version in separate test set to test newer version and latest deps in the 'default' + // test dir. Otherwise we get strange warnings in Idea. + test_before_1_11_106 { + filter { + // this is needed because "test.dependsOn test_before_1_11_106", and so without this, + // running a single test in the default test set will fail + setFailOnNoMatchingTests(false) + } + } + + // We test SQS separately since we have special logic for it and want to make sure the presence of + // SQS on the classpath doesn't conflict with tests for usage of the core SDK. This only affects + // the agent. + testSqs +} + +configurations { + test_before_1_11_106RuntimeClasspath { + resolutionStrategy.force 'com.amazonaws:aws-java-sdk-s3:1.11.0' + resolutionStrategy.force 'com.amazonaws:aws-java-sdk-rds:1.11.0' + resolutionStrategy.force 'com.amazonaws:aws-java-sdk-ec2:1.11.0' + resolutionStrategy.force 'com.amazonaws:aws-java-sdk-kinesis:1.11.0' + resolutionStrategy.force 'com.amazonaws:aws-java-sdk-sqs:1.11.0' + resolutionStrategy.force 'com.amazonaws:aws-java-sdk-sns:1.11.0' + resolutionStrategy.force 'com.amazonaws:aws-java-sdk-dynamodb:1.11.0' + } +} + +dependencies { + compileOnly "io.opentelemetry:opentelemetry-extension-aws" + + implementation project(':instrumentation:aws-sdk:aws-sdk-1.11:library') + + library "com.amazonaws:aws-java-sdk-core:1.11.0" + + testLibrary "com.amazonaws:aws-java-sdk-s3:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-rds:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-ec2:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-kinesis:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-dynamodb:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-sns:1.11.106" + + testImplementation project(':instrumentation:aws-sdk:aws-sdk-1.11:testing') + + testSqsImplementation "com.amazonaws:aws-java-sdk-sqs:1.11.106" + + // Include httpclient instrumentation for testing because it is a dependency for aws-sdk. + testInstrumentation project(':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent') + + // needed for kinesis: + testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor" + + // needed for SNS + testImplementation "org.testcontainers:localstack:${versions["org.testcontainers"]}" + + // needed by S3 + testImplementation 'javax.xml.bind:jaxb-api:2.3.1' + + test_before_1_11_106Implementation("com.amazonaws:aws-java-sdk-s3:1.11.0") + test_before_1_11_106Implementation("com.amazonaws:aws-java-sdk-rds:1.11.0") + test_before_1_11_106Implementation("com.amazonaws:aws-java-sdk-ec2:1.11.0") + test_before_1_11_106Implementation("com.amazonaws:aws-java-sdk-kinesis:1.11.0") + test_before_1_11_106Implementation("com.amazonaws:aws-java-sdk-dynamodb:1.11.0") + test_before_1_11_106Implementation("com.amazonaws:aws-java-sdk-sns:1.11.0") +} + +test { + systemProperty "testLatestDeps", testLatestDeps +} + +if (!testLatestDeps) { + check.dependsOn test_before_1_11_106, testSqs +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.aws-sdk.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsClientInstrumentation.java new file mode 100644 index 000000000..0c613139f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsClientInstrumentation.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11; + +import static net.bytebuddy.matcher.ElementMatchers.declaresField; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.amazonaws.handlers.RequestHandler2; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This instrumentation might work with versions before 1.11.0, but this was the first version that + * is tested. It could possibly be extended earlier. + */ +public class AwsClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.amazonaws.AmazonWebServiceClient") + .and(declaresField(named("requestHandler2s"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), AwsClientInstrumentation.class.getName() + "$AwsClientAdvice"); + } + + @SuppressWarnings("unused") + public static class AwsClientAdvice { + + // Since we're instrumenting the constructor, we can't add onThrowable. + @Advice.OnMethodExit(suppress = Throwable.class) + public static void addHandler( + @Advice.FieldValue("requestHandler2s") List handlers) { + boolean hasAgentHandler = false; + for (RequestHandler2 handler : handlers) { + if (handler instanceof TracingRequestHandler) { + hasAgentHandler = true; + break; + } + } + if (!hasAgentHandler) { + handlers.add(new TracingRequestHandler()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsHttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsHttpClientInstrumentation.java new file mode 100644 index 000000000..f76d02d35 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsHttpClientInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11; + +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.handlers.RequestHandler2; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This is additional 'helper' to catch cases when HTTP request throws exception different from + * {@link AmazonClientException} (for example an error thrown by another handler). In these cases + * {@link RequestHandler2#afterError} is not called. + */ +public class AwsHttpClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.amazonaws.http.AmazonHttpClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(not(isAbstract())) + .and(named("doExecute")) + .and(takesArgument(0, named("com.amazonaws.Request"))) + .and(returns(named("com.amazonaws.Response"))), + AwsHttpClientInstrumentation.class.getName() + "$HttpClientAdvice"); + } + + @SuppressWarnings("unused") + public static class HttpClientAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(value = 0) Request request, + @Advice.Return Response response, + @Advice.Thrown Throwable throwable) { + if (throwable instanceof Exception) { + TracingRequestHandler.tracingHandler.afterError(request, response, (Exception) throwable); + } + Scope scope = request.getHandlerContext(TracingRequestHandler.SCOPE); + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSdkInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSdkInstrumentationModule.java new file mode 100644 index 000000000..947730ad8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSdkInstrumentationModule.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class AwsSdkInstrumentationModule extends InstrumentationModule { + public AwsSdkInstrumentationModule() { + super("aws-sdk", "aws-sdk-1.11"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.extension.aws."); + } + + @Override + public List typeInstrumentations() { + return asList( + new AwsClientInstrumentation(), + new AwsHttpClientInstrumentation(), + new RequestExecutorInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/RequestExecutorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/RequestExecutorInstrumentation.java new file mode 100644 index 000000000..9913d8aaf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/RequestExecutorInstrumentation.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11; + +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.amazonaws.Request; +import com.amazonaws.Response; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Due to a change in the AmazonHttpClient class, this instrumentation is needed to support newer + * versions. The {@link AwsHttpClientInstrumentation} class should cover older versions. + */ +public class RequestExecutorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.amazonaws.http.AmazonHttpClient$RequestExecutor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(not(isAbstract())) + .and(named("doExecute")) + .and(returns(named("com.amazonaws.Response"))), + RequestExecutorInstrumentation.class.getName() + "$RequestExecutorAdvice"); + } + + @SuppressWarnings("unused") + public static class RequestExecutorAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.FieldValue("request") Request request, + @Advice.Return Response response, + @Advice.Thrown Throwable throwable) { + if (throwable instanceof Exception) { + TracingRequestHandler.tracingHandler.afterError(request, response, (Exception) throwable); + } + Scope scope = request.getHandlerContext(TracingRequestHandler.SCOPE); + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/TracingRequestHandler.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/TracingRequestHandler.java new file mode 100644 index 000000000..58dc61fad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/TracingRequestHandler.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11; + +import com.amazonaws.AmazonWebServiceRequest; +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.handlers.HandlerContextKey; +import com.amazonaws.handlers.RequestHandler2; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.awssdk.v1_11.AwsSdkTracing; + +/** + * A {@link RequestHandler2} for use in the agent. Unlike library instrumentation, the agent will + * also instrument the underlying HTTP client, and we must set the context as current to be able to + * suppress it. Also unlike library instrumentation, we are able to instrument the SDK's internal + * classes to handle buggy behavior related to exceptions that can cause scopes to never be closed + * otherwise which would be disastrous. We hope there won't be anymore significant changes to this + * legacy SDK that would cause these workarounds to break in the future. + */ +// NB: If the error-handling workarounds stop working, we should consider introducing the same +// x-amzn-request-id header check in Apache instrumentation for suppressing spans that we have in +// Netty instrumentation. +public class TracingRequestHandler extends RequestHandler2 { + + public static final HandlerContextKey SCOPE = + new HandlerContextKey<>(Scope.class.getName()); + + public static final RequestHandler2 tracingHandler = + AwsSdkTracing.newBuilder(GlobalOpenTelemetry.get()) + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBooleanProperty( + "otel.instrumentation.aws-sdk.experimental-span-attributes", false)) + .build() + .newRequestHandler(); + + @Override + public void beforeRequest(Request request) { + tracingHandler.beforeRequest(request); + Context context = AwsSdkTracing.getOpenTelemetryContext(request); + // it is possible that context is not set by lib's handler + if (context != null) { + Scope scope = context.makeCurrent(); + request.addHandlerContext(SCOPE, scope); + } + } + + @Override + public AmazonWebServiceRequest beforeMarshalling(AmazonWebServiceRequest request) { + return tracingHandler.beforeMarshalling(request); + } + + @Override + public void afterResponse(Request request, Response response) { + tracingHandler.afterResponse(request, response); + } + + @Override + public void afterError(Request request, Response response, Exception e) { + tracingHandler.afterError(request, response, e); + finish(request); + } + + private static void finish(Request request) { + Scope scope = request.getHandlerContext(SCOPE); + if (scope == null) { + return; + } + scope.close(); + request.addHandlerContext(SCOPE, null); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/AwsConnector.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/AwsConnector.groovy new file mode 100644 index 000000000..b04b85b98 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/AwsConnector.groovy @@ -0,0 +1,188 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.amazonaws.regions.Regions +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.model.BucketNotificationConfiguration +import com.amazonaws.services.s3.model.ObjectListing +import com.amazonaws.services.s3.model.QueueConfiguration +import com.amazonaws.services.s3.model.S3Event +import com.amazonaws.services.s3.model.S3ObjectSummary +import com.amazonaws.services.s3.model.SetBucketNotificationConfigurationRequest +import com.amazonaws.services.s3.model.TopicConfiguration +import com.amazonaws.services.sns.AmazonSNSAsyncClient +import com.amazonaws.services.sns.model.CreateTopicResult +import com.amazonaws.services.sns.model.SetTopicAttributesRequest +import com.amazonaws.services.sqs.AmazonSQSAsyncClient +import com.amazonaws.services.sqs.model.GetQueueAttributesRequest +import com.amazonaws.services.sqs.model.PurgeQueueRequest +import com.amazonaws.services.sqs.model.ReceiveMessageRequest +import org.slf4j.LoggerFactory +import org.testcontainers.containers.localstack.LocalStackContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import org.testcontainers.utility.DockerImageName + +class AwsConnector { + + private LocalStackContainer localstack + + private AmazonSQSAsyncClient sqsClient + private AmazonS3Client s3Client + private AmazonSNSAsyncClient snsClient + + static localstack() { + AwsConnector awsConnector = new AwsConnector() + + awsConnector.localstack = new LocalStackContainer(DockerImageName.parse("localstack/localstack:latest")) + .withServices(LocalStackContainer.Service.SQS, LocalStackContainer.Service.SNS) + .withEnv("DEBUG", "1") + .withEnv("SQS_PROVIDER", "elasticmq") + awsConnector.localstack.start() + awsConnector.localstack.followOutput(new Slf4jLogConsumer(LoggerFactory.getLogger("test"))) + + awsConnector.sqsClient = AmazonSQSAsyncClient.asyncBuilder() + .withEndpointConfiguration(awsConnector.localstack.getEndpointConfiguration(LocalStackContainer.Service.SQS)) + .withCredentials(awsConnector.localstack.getDefaultCredentialsProvider()) + .build() + + awsConnector.s3Client = AmazonS3Client.builder() + .withEndpointConfiguration(awsConnector.localstack.getEndpointConfiguration(LocalStackContainer.Service.S3)) + .withCredentials(awsConnector.localstack.getDefaultCredentialsProvider()) + .build() + + awsConnector.snsClient = AmazonSNSAsyncClient.asyncBuilder() + .withEndpointConfiguration(awsConnector.localstack.getEndpointConfiguration(LocalStackContainer.Service.SNS)) + .withCredentials(awsConnector.localstack.getDefaultCredentialsProvider()) + .build() + + return awsConnector + } + + static liveAws() { + AwsConnector awsConnector = new AwsConnector() + + awsConnector.sqsClient = AmazonSQSAsyncClient.asyncBuilder() + .withRegion(Regions.US_EAST_1) + .build() + + awsConnector.s3Client = AmazonS3Client.builder() + .withRegion(Regions.US_EAST_1) + .build() + + awsConnector.snsClient = AmazonSNSAsyncClient.asyncBuilder() + .withRegion(Regions.US_EAST_1) + .build() + + return awsConnector + } + + def createQueue(String queueName) { + println "Create queue ${queueName}" + return sqsClient.createQueue(queueName).getQueueUrl() + } + + def getQueueArn(String queueUrl) { + println "Get ARN for queue ${queueUrl}" + return sqsClient.getQueueAttributes( + new GetQueueAttributesRequest(queueUrl) + .withAttributeNames("QueueArn")).getAttributes() + .get("QueueArn") + } + + def setTopicPublishingPolicy(String topicArn) { + println "Set policy for topic ${topicArn}" + snsClient.setTopicAttributes(new SetTopicAttributesRequest(topicArn, "Policy", String.format(SNS_POLICY, topicArn))) + } + + private static final String SNS_POLICY = "{" + + " \"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Principal\": \"*\"," + + " \"Action\": \"sns:Publish\"," + + " \"Resource\": \"%s\"" + + " }]" + + "}" + + def setQueuePublishingPolicy(String queueUrl, String queueArn) { + println "Set policy for queue ${queueArn}" + sqsClient.setQueueAttributes(queueUrl, Collections.singletonMap("Policy", String.format(SQS_POLICY, queueArn))) + } + + private static final String SQS_POLICY = "{" + + " \"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Principal\": \"*\"," + + " \"Action\": \"sqs:SendMessage\"," + + " \"Resource\": \"%s\"" + + " }]" + + "}" + + def createBucket(String bucketName) { + println "Create bucket ${bucketName}" + s3Client.createBucket(bucketName) + } + + def deleteBucket(String bucketName) { + println "Delete bucket ${bucketName}" + ObjectListing objectListing = s3Client.listObjects(bucketName) + Iterator objIter = objectListing.getObjectSummaries().iterator() + while (objIter.hasNext()) { + s3Client.deleteObject(bucketName, objIter.next().getKey()) + } + s3Client.deleteBucket(bucketName) + } + + def enableS3ToSqsNotifications(String bucketName, String sqsQueueArn) { + println "Enable notification for bucket ${bucketName} to queue ${sqsQueueArn}" + BucketNotificationConfiguration notificationConfiguration = new BucketNotificationConfiguration() + notificationConfiguration.addConfiguration("sqsQueueConfig", + new QueueConfiguration(sqsQueueArn, EnumSet.of(S3Event.ObjectCreatedByPut))) + s3Client.setBucketNotificationConfiguration(new SetBucketNotificationConfigurationRequest( + bucketName, notificationConfiguration)) + } + + def enableS3ToSnsNotifications(String bucketName, String snsTopicArn) { + println "Enable notification for bucket ${bucketName} to topic ${snsTopicArn}" + BucketNotificationConfiguration notificationConfiguration = new BucketNotificationConfiguration() + notificationConfiguration.addConfiguration("snsTopicConfig", + new TopicConfiguration(snsTopicArn, EnumSet.of(S3Event.ObjectCreatedByPut))) + s3Client.setBucketNotificationConfiguration(new SetBucketNotificationConfigurationRequest( + bucketName, notificationConfiguration)) + } + + def createTopicAndSubscribeQueue(String topicName, String queueArn) { + println "Create topic ${topicName} and subscribe to queue ${queueArn}" + CreateTopicResult ctr = snsClient.createTopic(topicName) + snsClient.subscribe(ctr.getTopicArn(), "sqs", queueArn) + return ctr.getTopicArn() + } + + def receiveMessage(String queueUrl) { + println "Receive message from queue ${queueUrl}" + sqsClient.receiveMessage(new ReceiveMessageRequest(queueUrl).withWaitTimeSeconds(20)) + } + + def purgeQueue(String queueUrl) { + println "Purge queue ${queueUrl}" + sqsClient.purgeQueue(new PurgeQueueRequest(queueUrl)) + } + + def putSampleData(String bucketName) { + println "Put sample data to bucket ${bucketName}" + s3Client.putObject(bucketName, "otelTestKey", "otelTestData") + } + + def publishSampleNotification(String topicArn) { + snsClient.publish(topicArn, "Hello There") + } + + def disconnect() { + if (localstack != null) { + localstack.stop() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy new file mode 100644 index 000000000..4504a5865 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy @@ -0,0 +1,751 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import spock.lang.Ignore +import spock.lang.Shared + +@Ignore("Requires https://github.com/localstack/localstack/issues/3686 and #3669") + +class S3TracingTest extends AgentInstrumentationSpecification { + + @Shared + AwsConnector awsConnector = AwsConnector.localstack() + + def cleanupSpec() { + awsConnector.disconnect() + } + + def "S3 upload triggers SQS message"() { + setup: + String queueName = "s3ToSqsTestQueue" + String bucketName = "otel-s3-to-sqs-test-bucket" + + String queueUrl = awsConnector.createQueue(queueName) + awsConnector.createBucket(bucketName) + + String queueArn = awsConnector.getQueueArn(queueUrl) + awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) + awsConnector.enableS3ToSqsNotifications(bucketName, queueArn) + + when: + // test message, auto created by AWS + awsConnector.receiveMessage(queueUrl) + awsConnector.putSampleData(bucketName) + // traced message + awsConnector.receiveMessage(queueUrl) + // cleanup + awsConnector.deleteBucket(bucketName) + awsConnector.purgeQueue(queueUrl) + + then: + assertTraces(13) { + trace(0, 1) { + + span(0) { + name "SQS.CreateQueue" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "CreateQueue" + "aws.queue.name" queueName + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(1, 1) { + + span(0) { + name "S3.CreateBucket" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "CreateBucket" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "PUT" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(2, 1) { + + span(0) { + name "SQS.GetQueueAttributes" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "GetQueueAttributes" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(3, 1) { + + span(0) { + name "SQS.SetQueueAttributes" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "SetQueueAttributes" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(4, 1) { + + span(0) { + name "S3.SetBucketNotificationConfiguration" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "SetBucketNotificationConfiguration" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "PUT" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(5, 1) { + span(0) { + name "SQS.ReceiveMessage" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + /** + * This span represents HTTP "sending of receive message" operation. It's always single, while there can be multiple CONSUMER spans (one per consumed message). + * This one could be suppressed (by IF in TracingRequestHandler#beforeRequest but then HTTP instrumentation span would appear + */ + trace(6, 1) { + span(0) { + name "SQS.ReceiveMessage" + kind CONSUMER + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "http.user_agent" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(7, 2) { + span(0) { + name "S3.PutObject" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "PutObject" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "PUT" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + span(1) { + name "SQS.ReceiveMessage" + kind CONSUMER + childOf span(0) + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "http.user_agent" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + /** + * This span represents HTTP "sending of receive message" operation. It's always single, while there can be multiple CONSUMER spans (one per consumed message). + * This one could be suppressed (by IF in TracingRequestHandler#beforeRequest but then HTTP instrumentation span would appear + */ + trace(8, 1) { + span(0) { + name "SQS.ReceiveMessage" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(9, 1) { + span(0) { + name "S3.ListObjects" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ListObjects" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "GET" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(10, 1) { + span(0) { + name "S3.DeleteObject" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "DeleteObject" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "DELETE" + "http.status_code" 204 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(11, 1) { + span(0) { + name "S3.DeleteBucket" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "DeleteBucket" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "DELETE" + "http.status_code" 204 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(12, 1) { + span(0) { + name "SQS.PurgeQueue" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "PurgeQueue" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + } + } + + def "S3 upload triggers SNS topic notification, then creates SQS message"() { + setup: + String queueName = "s3ToSnsToSqsTestQueue" + String bucketName = "otel-s3-sns-sqs-test-bucket" + String topicName = "s3ToSnsToSqsTestTopic" + + String queueUrl = awsConnector.createQueue(queueName) + String queueArn = awsConnector.getQueueArn(queueUrl) + awsConnector.createBucket(bucketName) + String topicArn = awsConnector.createTopicAndSubscribeQueue(topicName, queueArn) + + awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) + awsConnector.setTopicPublishingPolicy(topicArn) + awsConnector.enableS3ToSnsNotifications(bucketName, topicArn) + + when: + // test message, auto created by AWS + awsConnector.receiveMessage(queueUrl) + awsConnector.putSampleData(bucketName) + // traced message + awsConnector.receiveMessage(queueUrl) + // cleanup + awsConnector.deleteBucket(bucketName) + awsConnector.purgeQueue(queueUrl) + + then: + assertTraces(16) { + trace(0, 1) { + span(0) { + name "SQS.CreateQueue" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "CreateQueue" + "aws.queue.name" queueName + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(1, 1) { + span(0) { + name "SQS.GetQueueAttributes" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "GetQueueAttributes" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(2, 1) { + span(0) { + name "S3.CreateBucket" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "CreateBucket" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "PUT" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(3, 1) { + span(0) { + name "SNS.CreateTopic" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "CreateTopic" + "aws.service" "AmazonSNS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(4, 1) { + span(0) { + name "SNS.Subscribe" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "Subscribe" + "aws.service" "AmazonSNS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(5, 1) { + span(0) { + name "SQS.SetQueueAttributes" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "SetQueueAttributes" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(6, 1) { + span(0) { + name "SNS.SetTopicAttributes" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "SetTopicAttributes" + "aws.service" "AmazonSNS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(7, 1) { + span(0) { + name "S3.SetBucketNotificationConfiguration" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "SetBucketNotificationConfiguration" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "PUT" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + // test even receive + trace(8, 1) { + span(0) { + name "SQS.ReceiveMessage" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + /** + * This span represents HTTP "sending of receive message" operation. It's always single, while there can be multiple CONSUMER spans (one per consumed message). + * This one could be suppressed (by IF in TracingRequestHandler#beforeRequest but then HTTP instrumentation span would appear + */ + trace(9, 1) { + span(0) { + name "SQS.ReceiveMessage" + kind CONSUMER + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "http.user_agent" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(10, 2) { + span(0) { + name "S3.PutObject" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "PutObject" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "PUT" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + span(1) { + name "SQS.ReceiveMessage" + kind CONSUMER + childOf span(0) + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "http.user_agent" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + /** + * This span represents HTTP "sending of receive message" operation. It's always single, while there can be multiple CONSUMER spans (one per consumed message). + * This one could be suppressed (by IF in TracingRequestHandler#beforeRequest but then HTTP instrumentation span would appear + */ + trace(11, 1) { + span(0) { + name "SQS.ReceiveMessage" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(12, 1) { + span(0) { + name "S3.ListObjects" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ListObjects" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "GET" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(13, 1) { + span(0) { + name "S3.DeleteObject" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "DeleteObject" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "DELETE" + "http.status_code" 204 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(14, 1) { + span(0) { + name "S3.DeleteBucket" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "DeleteBucket" + "aws.service" "Amazon S3" + "aws.bucket.name" bucketName + "http.flavor" "1.1" + "http.method" "DELETE" + "http.status_code" 204 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(15, 1) { + span(0) { + name "SQS.PurgeQueue" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "PurgeQueue" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy new file mode 100644 index 000000000..86bfbd8b4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy @@ -0,0 +1,216 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import spock.lang.Ignore +import spock.lang.Shared + +@Ignore("Requires https://github.com/localstack/localstack/issues/3669 to work with localstack") +class SnsTracingTest extends AgentInstrumentationSpecification { + + @Shared + AwsConnector awsConnector = AwsConnector.liveAws() + + + def cleanupSpec() { + awsConnector.disconnect() + } + + def "SNS notification triggers SQS message consumed with AWS SDK"() { + setup: + String queueName = "snsToSqsTestQueue" + String topicName = "snsToSqsTestTopic" + + String queueUrl = awsConnector.createQueue(queueName) + String queueArn = awsConnector.getQueueArn(queueUrl) + awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) + String topicArn = awsConnector.createTopicAndSubscribeQueue(topicName, queueArn) + + when: + awsConnector.publishSampleNotification(topicArn) + awsConnector.receiveMessage(queueUrl) + + then: + assertTraces(7) { + trace(0, 1) { + + span(0) { + name "SQS.CreateQueue" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "CreateQueue" + "aws.queue.name" queueName + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(1, 1) { + + span(0) { + name "SQS.GetQueueAttributes" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "GetQueueAttributes" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(2, 1) { + + span(0) { + name "SQS.SetQueueAttributes" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "SetQueueAttributes" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(3, 1) { + + span(0) { + name "SNS.CreateTopic" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "CreateTopic" + "aws.service" "AmazonSNS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(4, 1) { + + span(0) { + name "SNS.Subscribe" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "Subscribe" + "aws.service" "AmazonSNS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + trace(5, 2) { + span(0) { + name "SNS.Publish" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "Publish" + "aws.service" "AmazonSNS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + span(1) { + name "SQS.ReceiveMessage" + kind CONSUMER + childOf span(0) + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "http.user_agent" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + /** + * This span represents HTTP "sending of receive message" operation. It's always single, while there can be multiple CONSUMER spans (one per consumed message). + * This one could be suppressed (by IF in TracingRequestHandler#beforeRequest but then HTTP instrumentation span would appear + */ + trace(6, 1) { + span(0) { + name "SQS.ReceiveMessage" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" String + "aws.operation" "ReceiveMessage" + "aws.queue.url" queueUrl + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" String + "net.peer.name" String + "net.transport" IP_TCP + "net.peer.port" { it == null || Number } + } + } + } + } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy new file mode 100644 index 000000000..c6e982f18 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11 + +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import com.amazonaws.AmazonWebServiceClient +import com.amazonaws.Request +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.handlers.RequestHandler2 +import com.amazonaws.regions.Regions +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.AmazonS3ClientBuilder +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.awssdk.v1_11.AbstractAws1ClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class Aws1ClientTest extends AbstractAws1ClientTest implements AgentTestTrait { + @Override + def configureClient(def client) { + return client + } + + // Verify agent instruments old and new construction patterns. + + def "request handler is hooked up with builder"() { + setup: + def builder = AmazonS3ClientBuilder.standard() + .withRegion(Regions.US_EAST_1) + if (addHandler) { + builder.withRequestHandlers(new RequestHandler2() {}) + } + AmazonWebServiceClient client = builder.build() + + expect: + client.requestHandler2s != null + client.requestHandler2s.size() == size + client.requestHandler2s.get(position).getClass().getSimpleName() == "TracingRequestHandler" + + where: + addHandler | size | position + true | 2 | 1 + false | 1 | 0 + } + + def "request handler is hooked up with constructor"() { + setup: + String accessKey = "asdf" + String secretKey = "qwerty" + def credentials = new BasicAWSCredentials(accessKey, secretKey) + def client = new AmazonS3Client(credentials) + if (addHandler) { + client.addRequestHandler(new RequestHandler2() {}) + } + + expect: + client.requestHandler2s != null + client.requestHandler2s.size() == size + client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler" + + where: + addHandler | size + true | 2 + false | 1 + } + + // Test cases that require workarounds using bytecode instrumentation + + def "naughty request handler doesn't break the trace"() { + setup: + def client = new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN) + client.addRequestHandler(new RequestHandler2() { + void beforeRequest(Request request) { + throw new IllegalStateException("bad handler") + } + }) + + when: + client.getObject("someBucket", "someKey") + + then: + !Span.current().getSpanContext().isValid() + thrown IllegalStateException + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "S3.HeadBucket" + kind SpanKind.CLIENT + status ERROR + errorEvent IllegalStateException, "bad handler" + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "https://s3.amazonaws.com" + "${SemanticAttributes.HTTP_METHOD.key}" "HEAD" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "s3.amazonaws.com" + "aws.service" "Amazon S3" + "aws.endpoint" "https://s3.amazonaws.com" + "aws.operation" "HeadBucket" + "aws.agent" "java-aws-sdk" + "aws.bucket.name" "someBucket" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/resources/logback-test.xml b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/resources/logback-test.xml new file mode 100644 index 000000000..2082c6f37 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/resources/logback-test.xml @@ -0,0 +1,20 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.groovy new file mode 100644 index 000000000..80b19b8f0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11 + +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder +import io.opentelemetry.instrumentation.awssdk.v1_11.AbstractSqsTracingTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class SqsTracingTest extends AbstractSqsTracingTest implements AgentTestTrait { + @Override + AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { + return client + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy new file mode 100644 index 000000000..315ed99af --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy @@ -0,0 +1,277 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import com.amazonaws.AmazonClientException +import com.amazonaws.ClientConfiguration +import com.amazonaws.Request +import com.amazonaws.SDKGlobalConfiguration +import com.amazonaws.auth.AWSCredentialsProviderChain +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.auth.EnvironmentVariableCredentialsProvider +import com.amazonaws.auth.InstanceProfileCredentialsProvider +import com.amazonaws.auth.SystemPropertiesCredentialsProvider +import com.amazonaws.auth.profile.ProfileCredentialsProvider +import com.amazonaws.handlers.RequestHandler2 +import com.amazonaws.retry.PredefinedRetryPolicies +import com.amazonaws.services.ec2.AmazonEC2Client +import com.amazonaws.services.rds.AmazonRDSClient +import com.amazonaws.services.rds.model.DeleteOptionGroupRequest +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.S3ClientOptions +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.common.HttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpStatus +import io.opentelemetry.testing.internal.armeria.common.MediaType +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension +import java.time.Duration +import spock.lang.Shared + +class Aws0ClientTest extends AgentInstrumentationSpecification { + + private static final CREDENTIALS_PROVIDER_CHAIN = new AWSCredentialsProviderChain( + new EnvironmentVariableCredentialsProvider(), + new SystemPropertiesCredentialsProvider(), + new ProfileCredentialsProvider(), + new InstanceProfileCredentialsProvider()) + + @Shared + def server = new MockWebServerExtension() + + def setupSpec() { + System.setProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY, "my-access-key") + System.setProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY, "my-secret-key") + server.start() + } + + def cleanupSpec() { + System.clearProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY) + System.clearProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY) + server.stop() + } + + def setup() { + server.beforeTestExecution(null) + } + + def "request handler is hooked up with constructor"() { + setup: + String accessKey = "asdf" + String secretKey = "qwerty" + def credentials = new BasicAWSCredentials(accessKey, secretKey) + def client = new AmazonS3Client(credentials) + if (addHandler) { + client.addRequestHandler(new RequestHandler2() {}) + } + + expect: + client.requestHandler2s != null + client.requestHandler2s.size() == size + client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler" + + where: + addHandler | size + true | 2 + false | 1 + } + + def "send #operation request with mocked response"() { + setup: + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) + + when: + def response = call.call(client) + + then: + response != null + + client.requestHandler2s != null + client.requestHandler2s.size() == handlerCount + client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "$service.$operation" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "${server.httpUri()}" + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "aws.service" { it.contains(service) } + "aws.endpoint" "${server.httpUri()}" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + for (def addedTag : additionalAttributes) { + "$addedTag.key" "$addedTag.value" + } + } + } + } + } + + def request = server.takeRequest() + request.request().headers().get("X-Amzn-Trace-Id") != null + request.request().headers().get("traceparent") == null + + where: + service | operation | method | path | handlerCount | client | additionalAttributes | call | body + "S3" | "CreateBucket" | "PUT" | "/testbucket/" | 1 | new AmazonS3Client().withEndpoint("${server.httpUri()}") | ["aws.bucket.name": "testbucket"] | { client -> client.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build()); client.createBucket("testbucket") } | "" + "S3" | "GetObject" | "GET" | "/someBucket/someKey" | 1 | new AmazonS3Client().withEndpoint("${server.httpUri()}") | ["aws.bucket.name": "someBucket"] | { client -> client.getObject("someBucket", "someKey") } | "" + "EC2" | "AllocateAddress" | "POST" | "/" | 4 | new AmazonEC2Client().withEndpoint("${server.httpUri()}") | [:] | { client -> client.allocateAddress() } | """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + 192.0.2.1 + standard + + """ + "RDS" | "DeleteOptionGroup" | "POST" | "/" | 1 | new AmazonRDSClient().withEndpoint("${server.httpUri()}") | [:] | { client -> client.deleteOptionGroup(new DeleteOptionGroupRequest()) } | """ + + + 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 + + + """ + } + + def "send #operation request to closed port"() { + setup: + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) + + when: + call.call(client) + + then: + thrown AmazonClientException + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "$service.$operation" + kind CLIENT + status ERROR + errorEvent AmazonClientException, ~/Unable to execute HTTP request/ + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:${UNUSABLE_PORT}" + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.NET_PEER_PORT.key}" 61 + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "aws.service" { it.contains(service) } + "aws.endpoint" "http://localhost:${UNUSABLE_PORT}" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + for (def addedTag : additionalAttributes) { + "$addedTag.key" "$addedTag.value" + } + } + } + } + } + + where: + service | operation | method | url | call | additionalAttributes | body | client + "S3" | "GetObject" | "GET" | "someBucket/someKey" | { client -> client.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" | new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN, new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))).withEndpoint("http://localhost:${UNUSABLE_PORT}") + } + + def "naughty request handler doesn't break the trace"() { + setup: + def client = new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN) + client.addRequestHandler(new RequestHandler2() { + void beforeRequest(Request request) { + throw new IllegalStateException("bad handler") + } + }) + + when: + client.getObject("someBucket", "someKey") + + then: + !Span.current().getSpanContext().isValid() + thrown IllegalStateException + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "S3.GetObject" + kind CLIENT + status ERROR + errorEvent IllegalStateException, "bad handler" + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "https://s3.amazonaws.com" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "s3.amazonaws.com" + "aws.service" "Amazon S3" + "aws.endpoint" "https://s3.amazonaws.com" + "aws.operation" "GetObject" + "aws.agent" "java-aws-sdk" + "aws.bucket.name" "someBucket" + } + } + } + } + } + + // TODO(anuraaga): Add events for retries. + def "timeout and retry errors not captured"() { + setup: + def response = HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(500)) + // One retry so two requests. + server.enqueue(response) + server.enqueue(response) + AmazonS3Client client = new AmazonS3Client(new ClientConfiguration() + .withRequestTimeout(50 /* ms */) + .withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(1))) + .withEndpoint("${server.httpUri()}") + + when: + client.getObject("someBucket", "someKey") + + then: + !Span.current().getSpanContext().isValid() + thrown AmazonClientException + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "S3.GetObject" + kind CLIENT + status ERROR + errorEvent AmazonClientException, ~/Unable to execute HTTP request/ + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "${server.httpUri()}" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "aws.service" "Amazon S3" + "aws.endpoint" "${server.httpUri()}" + "aws.operation" "GetObject" + "aws.agent" "java-aws-sdk" + "aws.bucket.name" "someBucket" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/aws-sdk-1.11-library.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/aws-sdk-1.11-library.gradle new file mode 100644 index 000000000..16d4564dc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/aws-sdk-1.11-library.gradle @@ -0,0 +1,17 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + implementation "io.opentelemetry:opentelemetry-extension-aws" + + library "com.amazonaws:aws-java-sdk-core:1.11.0" + + testImplementation project(':instrumentation:aws-sdk:aws-sdk-1.11:testing') + + testLibrary "com.amazonaws:aws-java-sdk-s3:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-rds:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-ec2:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-kinesis:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-dynamodb:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-sns:1.11.106" + testLibrary "com.amazonaws:aws-java-sdk-sqs:1.11.106" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkClientTracer.java new file mode 100644 index 000000000..105ad4c2b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkClientTracer.java @@ -0,0 +1,164 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.AmazonWebServiceResponse; +import com.amazonaws.Request; +import com.amazonaws.Response; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import java.util.concurrent.ConcurrentHashMap; + +final class AwsSdkClientTracer extends HttpClientTracer, Request, Response> { + + private static final ClassValue OPERATION_NAME = + new ClassValue() { + @Override + protected String computeValue(Class type) { + String ret = type.getSimpleName(); + ret = ret.substring(0, ret.length() - 7); // remove 'Request' + return ret; + } + }; + + static final String COMPONENT_NAME = "java-aws-sdk"; + + private final NamesCache namesCache = new NamesCache(); + + private final boolean captureExperimentalSpanAttributes; + + AwsSdkClientTracer(OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { + super(openTelemetry, new NetPeerAttributes()); + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + @Override + protected void inject(Context context, Request request) { + AwsXrayPropagator.getInstance().inject(context, request, AwsSdkInjectAdapter.INSTANCE); + } + + @Override + protected String spanNameForRequest(Request request) { + if (request == null) { + return DEFAULT_SPAN_NAME; + } + String awsServiceName = request.getServiceName(); + Class awsOperation = request.getOriginalRequest().getClass(); + return qualifiedOperation(awsServiceName, awsOperation); + } + + public Context startSpan(SpanKind kind, Context parentContext, Request request) { + Context context = super.startSpan(kind, parentContext, request, request, -1); + Span span = Span.fromContext(context); + + String awsServiceName = request.getServiceName(); + + if (captureExperimentalSpanAttributes) { + span.setAttribute("aws.agent", COMPONENT_NAME); + span.setAttribute("aws.service", awsServiceName); + span.setAttribute("aws.operation", extractOperationName(request)); + span.setAttribute("aws.endpoint", request.getEndpoint().toString()); + + Object originalRequest = request.getOriginalRequest(); + String bucketName = RequestAccess.getBucketName(originalRequest); + if (bucketName != null) { + span.setAttribute("aws.bucket.name", bucketName); + } + String queueUrl = RequestAccess.getQueueUrl(originalRequest); + if (queueUrl != null) { + span.setAttribute("aws.queue.url", queueUrl); + } + String queueName = RequestAccess.getQueueName(originalRequest); + if (queueName != null) { + span.setAttribute("aws.queue.name", queueName); + } + String streamName = RequestAccess.getStreamName(originalRequest); + if (streamName != null) { + span.setAttribute("aws.stream.name", streamName); + } + String tableName = RequestAccess.getTableName(originalRequest); + if (tableName != null) { + span.setAttribute("aws.table.name", tableName); + } + } + return context; + } + + @Override + public void onResponse(Span span, Response response) { + if (captureExperimentalSpanAttributes + && response != null + && response.getAwsResponse() instanceof AmazonWebServiceResponse) { + AmazonWebServiceResponse awsResp = (AmazonWebServiceResponse) response.getAwsResponse(); + span.setAttribute("aws.requestId", awsResp.getRequestId()); + } + super.onResponse(span, response); + } + + private String qualifiedOperation(String service, Class operation) { + ConcurrentHashMap cache = namesCache.get(operation); + return cache.computeIfAbsent( + service, + s -> + s.replace("Amazon", "").trim() + + '.' + + operation.getSimpleName().replace("Request", "")); + } + + @Override + protected String method(Request request) { + return request.getHttpMethod().name(); + } + + @Override + protected URI url(Request request) { + return request.getEndpoint(); + } + + @Override + protected Integer status(Response response) { + return response.getHttpResponse().getStatusCode(); + } + + @Override + protected String requestHeader(Request request, String name) { + return request.getHeaders().get(name); + } + + @Override + protected String responseHeader(Response response, String name) { + return response.getHttpResponse().getHeaders().get(name); + } + + @Override + protected TextMapSetter> getSetter() { + // We override injection and don't want to have the base class do it accidentally. + return null; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.aws-sdk-1.11"; + } + + static final class NamesCache extends ClassValue> { + @Override + protected ConcurrentHashMap computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + } + + private static String extractOperationName(Request request) { + return OPERATION_NAME.get(request.getOriginalRequest().getClass()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkInjectAdapter.java new file mode 100644 index 000000000..08b8c034e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Request; +import io.opentelemetry.context.propagation.TextMapSetter; + +final class AwsSdkInjectAdapter implements TextMapSetter> { + + static final AwsSdkInjectAdapter INSTANCE = new AwsSdkInjectAdapter(); + + @Override + public void set(Request request, String name, String value) { + request.addHeader(name, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTracing.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTracing.java new file mode 100644 index 000000000..fd8fcb771 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTracing.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Request; +import com.amazonaws.handlers.RequestHandler2; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; + +/** + * Entrypoint for tracing AWS SDK v1 clients. + * + *

AWS SDK v1 is quite old and has some known bugs that are not fixed due to possible backwards + * compatibility issues. Notably, if a {@link RequestHandler2} throws an exception in a callback, + * this exception will leak up the chain and prevent other handlers from being executed. You must + * ensure you do not register any problematic {@link RequestHandler2}s on your clients or you will + * witness broken traces. + */ +public class AwsSdkTracing { + + /** + * Returns the OpenTelemetry {@link Context} stored in the {@link Request}, or {@code null} if + * there is no {@link Context}. This is generally not needed unless you are implementing your own + * instrumentation that delegates to this one. + */ + public static Context getOpenTelemetryContext(Request request) { + return request.getHandlerContext(TracingRequestHandler.CONTEXT); + } + + /** Returns a new {@link AwsSdkTracing} configured with the given {@link OpenTelemetry}. */ + public static AwsSdkTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + /** Returns a new {@link AwsSdkTracingBuilder} configured with the given {@link OpenTelemetry}. */ + public static AwsSdkTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new AwsSdkTracingBuilder(openTelemetry); + } + + private final AwsSdkClientTracer tracer; + + AwsSdkTracing(OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { + tracer = new AwsSdkClientTracer(openTelemetry, captureExperimentalSpanAttributes); + } + + /** + * Returns a {@link RequestHandler2} for registration to AWS SDK client builders using {@code + * withRequestHandlers}. + */ + public RequestHandler2 newRequestHandler() { + return new TracingRequestHandler(tracer); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTracingBuilder.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTracingBuilder.java new file mode 100644 index 000000000..e03192802 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTracingBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import io.opentelemetry.api.OpenTelemetry; + +/** A builder of {@link AwsSdkTracing}. */ +public class AwsSdkTracingBuilder { + + private final OpenTelemetry openTelemetry; + + private boolean captureExperimentalSpanAttributes; + + AwsSdkTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Sets whether experimental attributes should be set to spans. These attributes may be changed or + * removed in the future, so only enable this if you know you do not require attributes filled by + * this instrumentation to be stable across versions + */ + public AwsSdkTracingBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + /** Returns a new {@link AwsSdkTracing} with the settings of this {@link AwsSdkTracingBuilder}. */ + public AwsSdkTracing build() { + return new AwsSdkTracing(openTelemetry, captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java new file mode 100644 index 000000000..d7732341a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class RequestAccess { + + private static final ClassValue REQUEST_ACCESSORS = + new ClassValue() { + @Override + protected RequestAccess computeValue(Class type) { + return new RequestAccess(type); + } + }; + + @Nullable + static String getBucketName(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getBucketName, request); + } + + @Nullable + static String getQueueUrl(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getQueueUrl, request); + } + + @Nullable + static String getQueueName(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getQueueName, request); + } + + @Nullable + static String getStreamName(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getStreamName, request); + } + + @Nullable + static String getTableName(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getTableName, request); + } + + @Nullable + private static String invokeOrNull(@Nullable MethodHandle method, Object obj) { + if (method == null) { + return null; + } + try { + return (String) method.invoke(obj); + } catch (Throwable t) { + return null; + } + } + + @Nullable private final MethodHandle getBucketName; + @Nullable private final MethodHandle getQueueUrl; + @Nullable private final MethodHandle getQueueName; + @Nullable private final MethodHandle getStreamName; + @Nullable private final MethodHandle getTableName; + + private RequestAccess(Class clz) { + getBucketName = findAccessorOrNull(clz, "getBucketName"); + getQueueUrl = findAccessorOrNull(clz, "getQueueUrl"); + getQueueName = findAccessorOrNull(clz, "getQueueName"); + getStreamName = findAccessorOrNull(clz, "getStreamName"); + getTableName = findAccessorOrNull(clz, "getTableName"); + } + + @Nullable + private static MethodHandle findAccessorOrNull(Class clz, String methodName) { + try { + return MethodHandles.publicLookup() + .findVirtual(clz, methodName, MethodType.methodType(String.class)); + } catch (Throwable t) { + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageAccess.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageAccess.java new file mode 100644 index 000000000..e090178db --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageAccess.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import static java.lang.invoke.MethodType.methodType; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Reflective access to aws-sdk-java-sqs class Message. + * + *

We currently don't have a good pattern of instrumenting a core library with various plugins + * that need plugin-specific instrumentation - if we accessed the class directly, Muzzle would + * prevent the entire instrumentation from loading when the plugin isn't available. We need to + * carefully check this class has all reflection errors result in no-op, and in the future we will + * hopefully come up with a better pattern. + */ +final class SqsMessageAccess { + + @Nullable private static final MethodHandle GET_ATTRIBUTES; + + static { + Class messageClass = null; + try { + messageClass = Class.forName("com.amazonaws.services.sqs.model.Message"); + } catch (Throwable t) { + // Ignore. + } + if (messageClass != null) { + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + MethodHandle getAttributes = null; + try { + getAttributes = lookup.findVirtual(messageClass, "getAttributes", methodType(Map.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + // Ignore + } + GET_ATTRIBUTES = getAttributes; + } else { + GET_ATTRIBUTES = null; + } + } + + @SuppressWarnings("unchecked") + static Map getAttributes(Object message) { + if (GET_ATTRIBUTES == null) { + return Collections.emptyMap(); + } + try { + return (Map) GET_ATTRIBUTES.invoke(message); + } catch (Throwable t) { + return Collections.emptyMap(); + } + } + + private SqsMessageAccess() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsParentContext.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsParentContext.java new file mode 100644 index 000000000..928933109 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsParentContext.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import java.util.Collections; +import java.util.Map; + +final class SqsParentContext { + + private static class MapGetter implements TextMapGetter> { + + private static final MapGetter INSTANCE = new MapGetter(); + + @Override + public Iterable keys(Map map) { + return map.keySet(); + } + + @Override + public String get(Map map, String s) { + return map.get(s); + } + } + + static final String AWS_TRACE_SYSTEM_ATTRIBUTE = "AWSTraceHeader"; + + static Context ofSystemAttributes(Map systemAttributes) { + String traceHeader = systemAttributes.get(AWS_TRACE_SYSTEM_ATTRIBUTE); + return AwsXrayPropagator.getInstance() + .extract( + Context.root(), + Collections.singletonMap("X-Amzn-Trace-Id", traceHeader), + MapGetter.INSTANCE); + } + + private SqsParentContext() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageRequestAccess.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageRequestAccess.java new file mode 100644 index 000000000..7122134a8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageRequestAccess.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import static java.lang.invoke.MethodType.methodType; + +import com.amazonaws.AmazonWebServiceRequest; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Reflective access to aws-sdk-java-sqs class ReceiveMessageRequest. + * + *

We currently don't have a good pattern of instrumenting a core library with various plugins + * that need plugin-specific instrumentation - if we accessed the class directly, Muzzle would + * prevent the entire instrumentation from loading when the plugin isn't available. We need to + * carefully check this class has all reflection errors result in no-op, and in the future we will + * hopefully come up with a better pattern. + */ +final class SqsReceiveMessageRequestAccess { + + @Nullable private static final MethodHandle WITH_ATTRIBUTE_NAMES; + @Nullable private static final MethodHandle GET_ATTRIBUTE_NAMES; + + static { + Class receiveMessageRequestClass = null; + try { + receiveMessageRequestClass = + Class.forName("com.amazonaws.services.sqs.model.ReceiveMessageRequest"); + } catch (Throwable t) { + // Ignore. + } + if (receiveMessageRequestClass != null) { + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + MethodHandle withAttributeNames = null; + try { + withAttributeNames = + lookup.findVirtual( + receiveMessageRequestClass, + "withAttributeNames", + methodType(receiveMessageRequestClass, String[].class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + // Ignore + } + WITH_ATTRIBUTE_NAMES = withAttributeNames; + + MethodHandle getAttributeNames = null; + try { + getAttributeNames = + lookup.findVirtual( + receiveMessageRequestClass, "getAttributeNames", methodType(List.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + // Ignore + } + GET_ATTRIBUTE_NAMES = getAttributeNames; + } else { + WITH_ATTRIBUTE_NAMES = null; + GET_ATTRIBUTE_NAMES = null; + } + } + + static boolean isInstance(AmazonWebServiceRequest request) { + return request + .getClass() + .getName() + .equals("com.amazonaws.services.sqs.model.ReceiveMessageRequest"); + } + + static void withAttributeNames(AmazonWebServiceRequest request, String name) { + if (WITH_ATTRIBUTE_NAMES == null) { + return; + } + try { + WITH_ATTRIBUTE_NAMES.invoke(request, name); + } catch (Throwable throwable) { + // Ignore + } + } + + static List getAttributeNames(AmazonWebServiceRequest request) { + if (GET_ATTRIBUTE_NAMES == null) { + return Collections.emptyList(); + } + try { + return (List) GET_ATTRIBUTE_NAMES.invoke(request); + } catch (Throwable t) { + return Collections.emptyList(); + } + } + + private SqsReceiveMessageRequestAccess() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageResultAccess.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageResultAccess.java new file mode 100644 index 000000000..5a0084adf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageResultAccess.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import static java.lang.invoke.MethodType.methodType; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Reflective access to aws-sdk-java-sqs class ReceiveMessageResult. + * + *

We currently don't have a good pattern of instrumenting a core library with various plugins + * that need plugin-specific instrumentation - if we accessed the class directly, Muzzle would + * prevent the entire instrumentation from loading when the plugin isn't available. We need to + * carefully check this class has all reflection errors result in no-op, and in the future we will + * hopefully come up with a better pattern. + */ +final class SqsReceiveMessageResultAccess { + + @Nullable private static final MethodHandle GET_MESSAGES; + + static { + Class receiveMessageResultClass = null; + try { + receiveMessageResultClass = + Class.forName("com.amazonaws.services.sqs.model.ReceiveMessageResult"); + } catch (Throwable t) { + // Ignore. + } + if (receiveMessageResultClass != null) { + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + MethodHandle getMessages = null; + try { + getMessages = + lookup.findVirtual(receiveMessageResultClass, "getMessages", methodType(List.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + // Ignore + } + GET_MESSAGES = getMessages; + } else { + GET_MESSAGES = null; + } + } + + static List getMessages(Object result) { + if (GET_MESSAGES == null) { + return Collections.emptyList(); + } + try { + return (List) GET_MESSAGES.invoke(result); + } catch (Throwable t) { + return Collections.emptyList(); + } + } + + private SqsReceiveMessageResultAccess() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java new file mode 100644 index 000000000..575c7fa14 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.AmazonWebServiceRequest; +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.handlers.HandlerContextKey; +import com.amazonaws.handlers.RequestHandler2; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Tracing Request Handler. */ +final class TracingRequestHandler extends RequestHandler2 { + + static final HandlerContextKey CONTEXT = + new HandlerContextKey<>(Context.class.getName()); + + private final AwsSdkClientTracer tracer; + + TracingRequestHandler(AwsSdkClientTracer tracer) { + this.tracer = tracer; + } + + @Override + public void beforeRequest(Request request) { + AmazonWebServiceRequest originalRequest = request.getOriginalRequest(); + SpanKind kind = (isSqsProducer(originalRequest) ? SpanKind.PRODUCER : SpanKind.CLIENT); + + Context parentContext = Context.current(); + if (!tracer.shouldStartSpan(parentContext)) { + return; + } + Context context = tracer.startSpan(kind, parentContext, request); + request.addHandlerContext(CONTEXT, context); + } + + private static boolean isSqsProducer(AmazonWebServiceRequest request) { + return request + .getClass() + .getName() + .equals("com.amazonaws.services.sqs.model.SendMessageRequest"); + } + + @Override + public AmazonWebServiceRequest beforeMarshalling(AmazonWebServiceRequest request) { + if (SqsReceiveMessageRequestAccess.isInstance(request)) { + if (!SqsReceiveMessageRequestAccess.getAttributeNames(request) + .contains(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE)) { + SqsReceiveMessageRequestAccess.withAttributeNames( + request, SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE); + } + } + return request; + } + + @Override + public void afterResponse(Request request, Response response) { + if (SqsReceiveMessageRequestAccess.isInstance(request.getOriginalRequest())) { + afterConsumerResponse(request, response); + } + finish(request, response, null); + } + + /** Create and close CONSUMER span for each message consumed. */ + private void afterConsumerResponse(Request request, Response response) { + Object receiveMessageResult = response.getAwsResponse(); + List messages = SqsReceiveMessageResultAccess.getMessages(receiveMessageResult); + for (Object message : messages) { + createConsumerSpan(message, request, response); + } + } + + private void createConsumerSpan(Object message, Request request, Response response) { + Context parentContext = + SqsParentContext.ofSystemAttributes(SqsMessageAccess.getAttributes(message)); + Context context = tracer.startSpan(SpanKind.CONSUMER, parentContext, request); + tracer.end(context, response); + } + + @Override + public void afterError(Request request, Response response, Exception e) { + finish(request, response, e); + } + + private void finish(Request request, Response response, @Nullable Throwable error) { + // close outstanding "client" span + Context context = request.getHandlerContext(CONTEXT); + if (context == null) { + return; + } + request.addHandlerContext(CONTEXT, null); + if (error == null) { + tracer.end(context, response); + } else { + tracer.endExceptionally(context, response, error); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy new file mode 100644 index 000000000..d865eb64b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11 + +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class Aws1ClientTest extends AbstractAws1ClientTest implements LibraryTestTrait { + @Override + def configureClient(def client) { + client.withRequestHandlers( + AwsSdkTracing.newBuilder(getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .build() + .newRequestHandler()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.groovy new file mode 100644 index 000000000..d9538f148 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.groovy @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11 + +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class SqsTracingTest extends AbstractSqsTracingTest implements LibraryTestTrait { + @Override + AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { + return client.withRequestHandlers( + AwsSdkTracing.newBuilder(getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .build() + .newRequestHandler()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/aws-sdk-1.11-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/aws-sdk-1.11-testing.gradle new file mode 100644 index 000000000..5896133cb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/aws-sdk-1.11-testing.gradle @@ -0,0 +1,24 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "com.amazonaws:aws-java-sdk-core:1.11.0" + + compileOnly "com.amazonaws:aws-java-sdk-s3:1.11.106" + compileOnly "com.amazonaws:aws-java-sdk-rds:1.11.106" + compileOnly "com.amazonaws:aws-java-sdk-ec2:1.11.106" + compileOnly "com.amazonaws:aws-java-sdk-kinesis:1.11.106" + compileOnly "com.amazonaws:aws-java-sdk-dynamodb:1.11.106" + compileOnly "com.amazonaws:aws-java-sdk-sns:1.11.106" + compileOnly "com.amazonaws:aws-java-sdk-sqs:1.11.106" + + // needed for SQS - using emq directly as localstack references emq v0.15.7 ie WITHOUT AWS trace header propagation + implementation "org.elasticmq:elasticmq-rest-sqs_2.12:1.0.0" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy new file mode 100644 index 000000000..96f69003f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy @@ -0,0 +1,247 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.PRODUCER +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import com.amazonaws.AmazonClientException +import com.amazonaws.ClientConfiguration +import com.amazonaws.SDKGlobalConfiguration +import com.amazonaws.SdkClientException +import com.amazonaws.auth.AWSCredentialsProviderChain +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.AnonymousAWSCredentials +import com.amazonaws.auth.EnvironmentVariableCredentialsProvider +import com.amazonaws.auth.InstanceProfileCredentialsProvider +import com.amazonaws.auth.SystemPropertiesCredentialsProvider +import com.amazonaws.auth.profile.ProfileCredentialsProvider +import com.amazonaws.client.builder.AwsClientBuilder +import com.amazonaws.retry.PredefinedRetryPolicies +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest +import com.amazonaws.services.ec2.AmazonEC2ClientBuilder +import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder +import com.amazonaws.services.kinesis.model.DeleteStreamRequest +import com.amazonaws.services.rds.AmazonRDSClientBuilder +import com.amazonaws.services.rds.model.DeleteOptionGroupRequest +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.AmazonS3ClientBuilder +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.common.HttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpStatus +import io.opentelemetry.testing.internal.armeria.common.MediaType +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension +import java.time.Duration +import spock.lang.Shared +import spock.lang.Unroll + +abstract class AbstractAws1ClientTest extends InstrumentationSpecification { + + abstract T configureClient(T client) + + static final CREDENTIALS_PROVIDER_CHAIN = new AWSCredentialsProviderChain( + new EnvironmentVariableCredentialsProvider(), + new SystemPropertiesCredentialsProvider(), + new ProfileCredentialsProvider(), + new InstanceProfileCredentialsProvider()) + + @Shared + def credentialsProvider = new AWSStaticCredentialsProvider(new AnonymousAWSCredentials()) + + @Shared + def server = new MockWebServerExtension() + + @Shared + def endpoint + + def setupSpec() { + System.setProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY, "my-access-key") + System.setProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY, "my-secret-key") + server.start() + endpoint = new AwsClientBuilder.EndpointConfiguration("${server.httpUri()}", "us-west-2") + } + + def cleanupSpec() { + System.clearProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY) + System.clearProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY) + server.stop() + } + + def setup() { + server.beforeTestExecution(null) + } + + @Unroll + def "send #operation request with mocked response"() { + setup: + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) + + when: + def client = configureClient(clientBuilder).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() + def response = call.call(client) + + then: + response != null + + client.requestHandler2s != null + client.requestHandler2s.get(0).getClass().getSimpleName() == "TracingRequestHandler" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "$service.$operation" + kind operation == "SendMessage" ? PRODUCER : CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "${server.httpUri()}" + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "aws.service" { it.contains(service) } + "aws.endpoint" "${server.httpUri()}" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + for (def addedTag : additionalAttributes) { + "$addedTag.key" "$addedTag.value" + } + } + } + } + } + + def request = server.takeRequest() + request.request().headers().get("X-Amzn-Trace-Id") != null + request.request().headers().get("traceparent") == null + + where: + service | operation | method | path | clientBuilder | call | additionalAttributes | body + "S3" | "CreateBucket" | "PUT" | "/testbucket/" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true) | { c -> c.createBucket("testbucket") } | ["aws.bucket.name": "testbucket"] | "" + "S3" | "GetObject" | "GET" | "/someBucket/someKey" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true) | { c -> c.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" + "DynamoDBv2" | "CreateTable" | "POST" | "/" | AmazonDynamoDBClientBuilder.standard() | { c -> c.createTable(new CreateTableRequest("sometable", null)) } | ["aws.table.name": "sometable"] | "" + "Kinesis" | "DeleteStream" | "POST" | "/" | AmazonKinesisClientBuilder.standard() | { c -> c.deleteStream(new DeleteStreamRequest().withStreamName("somestream")) } | ["aws.stream.name": "somestream"] | "" + "EC2" | "AllocateAddress" | "POST" | "/" | AmazonEC2ClientBuilder.standard() | { c -> c.allocateAddress() } | [:] | """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + 192.0.2.1 + standard + + """ + "RDS" | "DeleteOptionGroup" | "POST" | "/" | AmazonRDSClientBuilder.standard() | { c -> c.deleteOptionGroup(new DeleteOptionGroupRequest()) } | [:] | """ + + + 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 + + + """ + } + + def "send #operation request to closed port"() { + setup: + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) + + when: + def client = configureClient(clientBuilder) + .withCredentials(CREDENTIALS_PROVIDER_CHAIN) + .withClientConfiguration(new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))) + .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://127.0.0.1:${UNUSABLE_PORT}", "us-east-1")) + .build() + call.call(client) + + then: + thrown SdkClientException + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "$service.$operation" + kind CLIENT + status ERROR + errorEvent SdkClientException, ~/Unable to execute HTTP request/ + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "http://127.0.0.1:${UNUSABLE_PORT}" + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" 61 + "aws.service" { it.contains(service) } + "aws.endpoint" "http://127.0.0.1:${UNUSABLE_PORT}" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + for (def addedTag : additionalAttributes) { + "$addedTag.key" "$addedTag.value" + } + } + } + } + } + + where: + service | operation | method | url | call | additionalAttributes | body | clientBuilder + "S3" | "GetObject" | "GET" | "someBucket/someKey" | { c -> c.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" | AmazonS3ClientBuilder.standard() + } + + // TODO(anuraaga): Add events for retries. + def "timeout and retry errors not captured"() { + setup: + def response = HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(500)) + // One retry so two requests. + server.enqueue(response) + server.enqueue(response) + AmazonS3Client client = configureClient(AmazonS3ClientBuilder.standard()) + .withClientConfiguration(new ClientConfiguration() + .withRequestTimeout(50 /* ms */) + .withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(1))) + .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("${server.httpUri()}", "us-east-1")) + .build() + + when: + client.getObject("someBucket", "someKey") + + then: + !Span.current().getSpanContext().isValid() + thrown AmazonClientException + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "S3.GetObject" + kind CLIENT + status ERROR + try { + errorEvent AmazonClientException, ~/Unable to execute HTTP request/ + } catch (AssertionError e) { + errorEvent SdkClientException, "Unable to execute HTTP request: Request did not complete before the request timeout configuration." + } + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "${server.httpUri()}" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "aws.service" "Amazon S3" + "aws.endpoint" "${server.httpUri()}" + "aws.operation" "GetObject" + "aws.agent" "java-aws-sdk" + "aws.bucket.name" "someBucket" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.groovy new file mode 100644 index 000000000..672cab4e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.groovy @@ -0,0 +1,170 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.client.builder.AwsClientBuilder +import com.amazonaws.services.sqs.AmazonSQSAsyncClient +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder +import com.amazonaws.services.sqs.model.ReceiveMessageRequest +import com.amazonaws.services.sqs.model.SendMessageRequest +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import org.elasticmq.rest.sqs.SQSRestServerBuilder +import spock.lang.Shared + +abstract class AbstractSqsTracingTest extends InstrumentationSpecification { + + abstract AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) + + @Shared + def sqs + @Shared + AmazonSQSAsyncClient client + @Shared + int sqsPort + + def setupSpec() { + + sqsPort = PortUtils.findOpenPort() + sqs = SQSRestServerBuilder.withPort(sqsPort).withInterface("localhost").start() + println getClass().name + " SQS server started at: localhost:$sqsPort/" + + def credentials = new AWSStaticCredentialsProvider(new BasicAWSCredentials("x", "x")) + def endpointConfiguration = new AwsClientBuilder.EndpointConfiguration("http://localhost:" + sqsPort, "elasticmq") + client = configureClient(AmazonSQSAsyncClient.asyncBuilder()).withCredentials(credentials).withEndpointConfiguration(endpointConfiguration).build() + } + + def cleanupSpec() { + if (sqs != null) { + sqs.stopAndWait() + } + } + + def "simple sqs producer-consumer services"() { + setup: + client.createQueue("testSdkSqs") + + when: + SendMessageRequest send = new SendMessageRequest("http://localhost:$sqsPort/000000000000/testSdkSqs", "{\"type\": \"hello\"}") + client.sendMessage(send) + client.receiveMessage("http://localhost:$sqsPort/000000000000/testSdkSqs") + + then: + assertTraces(3) { + trace(0, 1) { + + span(0) { + name "SQS.CreateQueue" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" "http://localhost:$sqsPort" + "aws.operation" "CreateQueue" + "aws.queue.name" "testSdkSqs" + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" "http://localhost:$sqsPort" + "net.peer.name" "localhost" + "net.peer.port" sqsPort + "net.transport" IP_TCP + } + } + } + trace(1, 2) { + span(0) { + name "SQS.SendMessage" + kind PRODUCER + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" "http://localhost:$sqsPort" + "aws.operation" "SendMessage" + "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" "http://localhost:$sqsPort" + "net.peer.name" "localhost" + "net.peer.port" sqsPort + "net.transport" IP_TCP + } + } + span(1) { + name "SQS.ReceiveMessage" + kind CONSUMER + childOf span(0) + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" "http://localhost:$sqsPort" + "aws.operation" "ReceiveMessage" + "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" "http://localhost:$sqsPort" + "http.user_agent" String + "net.peer.name" "localhost" + "net.peer.port" sqsPort + "net.transport" IP_TCP + } + } + } + /** + * This span represents HTTP "sending of receive message" operation. It's always single, while there can be multiple CONSUMER spans (one per consumed message). + * This one could be suppressed (by IF in TracingRequestHandler#beforeRequest but then HTTP instrumentation span would appear + */ + trace(2, 1) { + span(0) { + name "SQS.ReceiveMessage" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.endpoint" "http://localhost:$sqsPort" + "aws.operation" "ReceiveMessage" + "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" + "aws.service" "AmazonSQS" + "http.flavor" "1.1" + "http.method" "POST" + "http.status_code" 200 + "http.url" "http://localhost:$sqsPort" + "net.peer.name" "localhost" + "net.peer.port" sqsPort + "net.transport" IP_TCP + } + } + } + } + } + + def "only adds attribute name once when request reused"() { + setup: + client.createQueue("testSdkSqs2") + + when: + SendMessageRequest send = new SendMessageRequest("http://localhost:$sqsPort/000000000000/testSdkSqs2", "{\"type\": \"hello\"}") + client.sendMessage(send) + ReceiveMessageRequest receive = new ReceiveMessageRequest("http://localhost:$sqsPort/000000000000/testSdkSqs2") + client.receiveMessage(receive) + client.sendMessage(send) + client.receiveMessage(receive) + + then: + receive.getAttributeNames() == ["AWSTraceHeader"] + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/aws-sdk-2.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/aws-sdk-2.2-javaagent.gradle new file mode 100644 index 000000000..e9b1cb8b2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/aws-sdk-2.2-javaagent.gradle @@ -0,0 +1,28 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "software.amazon.awssdk" + module = "aws-core" + versions = "[2.2.0,)" + // Used by all SDK services, the only case it isn't is an SDK extension such as a custom HTTP + // client, which is not target of instrumentation anyways. + extraDependency "software.amazon.awssdk:protocol-core" + } +} + +dependencies { + implementation project(':instrumentation:aws-sdk:aws-sdk-2.2:library') + + library "software.amazon.awssdk:aws-core:2.2.0" + + testImplementation project(':instrumentation:aws-sdk:aws-sdk-2.2:testing') + // Make sure these don't add HTTP headers + testImplementation project(':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent') + testImplementation project(':instrumentation:netty:netty-4.1:javaagent') +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.aws-sdk.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsSdkInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsSdkInstrumentationModule.java new file mode 100644 index 000000000..694ff38a2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsSdkInstrumentationModule.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class AwsSdkInstrumentationModule extends InstrumentationModule { + public AwsSdkInstrumentationModule() { + super("aws-sdk", "aws-sdk-2.2"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.extension.aws."); + } + + /** + * Injects resource file with reference to our {@link TracingExecutionInterceptor} to allow SDK's + * service loading mechanism to pick it up. + */ + @Override + public List helperResourceNames() { + return singletonList("software/amazon/awssdk/global/handlers/execution.interceptors"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // We don't actually transform it but want to make sure we only apply the instrumentation when + // our key dependency is present. + return hasClassesNamed("software.amazon.awssdk.core.interceptor.ExecutionInterceptor"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ResourceInjectingTypeInstrumentation()); + } + + // A type instrumentation is needed to trigger resource injection. + public static class ResourceInjectingTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // This is essentially the entry point of the AWS SDK, all clients implement it. We can ensure + // our interceptor service definition is injected as early as possible if we typematch against + // it. + return named("software.amazon.awssdk.core.SdkClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + // Nothing to transform, this type instrumentation is only used for injecting resources. + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java new file mode 100644 index 000000000..c3859f629 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkTracing; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Optional; +import org.reactivestreams.Publisher; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.interceptor.Context.AfterExecution; +import software.amazon.awssdk.core.interceptor.Context.AfterMarshalling; +import software.amazon.awssdk.core.interceptor.Context.AfterTransmission; +import software.amazon.awssdk.core.interceptor.Context.AfterUnmarshalling; +import software.amazon.awssdk.core.interceptor.Context.BeforeExecution; +import software.amazon.awssdk.core.interceptor.Context.BeforeMarshalling; +import software.amazon.awssdk.core.interceptor.Context.BeforeTransmission; +import software.amazon.awssdk.core.interceptor.Context.BeforeUnmarshalling; +import software.amazon.awssdk.core.interceptor.Context.FailedExecution; +import software.amazon.awssdk.core.interceptor.Context.ModifyHttpRequest; +import software.amazon.awssdk.core.interceptor.Context.ModifyHttpResponse; +import software.amazon.awssdk.core.interceptor.Context.ModifyRequest; +import software.amazon.awssdk.core.interceptor.Context.ModifyResponse; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; + +/** + * {@link ExecutionInterceptor} that delegates to {@link AwsSdkTracing}, augmenting {@link + * #beforeTransmission(BeforeTransmission, ExecutionAttributes)} to make sure the span is set to the + * current context to allow downstream instrumentation like Netty to pick it up. + */ +public class TracingExecutionInterceptor implements ExecutionInterceptor { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.aws-sdk.experimental-span-attributes", false); + + private final ExecutionInterceptor delegate; + + public TracingExecutionInterceptor() { + delegate = + AwsSdkTracing.newBuilder(GlobalOpenTelemetry.get()) + .setCaptureExperimentalSpanAttributes(CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) + .build() + .newExecutionInterceptor(); + } + + @Override + public void beforeExecution(BeforeExecution context, ExecutionAttributes executionAttributes) { + delegate.beforeExecution(context, executionAttributes); + } + + @Override + public SdkRequest modifyRequest(ModifyRequest context, ExecutionAttributes executionAttributes) { + return delegate.modifyRequest(context, executionAttributes); + } + + @Override + public void beforeMarshalling( + BeforeMarshalling context, ExecutionAttributes executionAttributes) { + delegate.beforeMarshalling(context, executionAttributes); + } + + @Override + public void afterMarshalling(AfterMarshalling context, ExecutionAttributes executionAttributes) { + delegate.afterMarshalling(context, executionAttributes); + } + + @Override + public SdkHttpRequest modifyHttpRequest( + ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + return delegate.modifyHttpRequest(context, executionAttributes); + } + + @Override + public Optional modifyHttpContent( + ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + return delegate.modifyHttpContent(context, executionAttributes); + } + + @Override + public Optional modifyAsyncHttpContent( + ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + return delegate.modifyAsyncHttpContent(context, executionAttributes); + } + + @Override + public void beforeTransmission( + BeforeTransmission context, ExecutionAttributes executionAttributes) { + delegate.beforeTransmission(context, executionAttributes); + } + + @Override + public void afterTransmission( + AfterTransmission context, ExecutionAttributes executionAttributes) { + delegate.afterTransmission(context, executionAttributes); + } + + @Override + public SdkHttpResponse modifyHttpResponse( + ModifyHttpResponse context, ExecutionAttributes executionAttributes) { + return delegate.modifyHttpResponse(context, executionAttributes); + } + + @Override + public Optional> modifyAsyncHttpResponseContent( + ModifyHttpResponse context, ExecutionAttributes executionAttributes) { + return delegate.modifyAsyncHttpResponseContent(context, executionAttributes); + } + + @Override + public Optional modifyHttpResponseContent( + ModifyHttpResponse context, ExecutionAttributes executionAttributes) { + return delegate.modifyHttpResponseContent(context, executionAttributes); + } + + @Override + public void beforeUnmarshalling( + BeforeUnmarshalling context, ExecutionAttributes executionAttributes) { + delegate.beforeUnmarshalling(context, executionAttributes); + } + + @Override + public void afterUnmarshalling( + AfterUnmarshalling context, ExecutionAttributes executionAttributes) { + delegate.afterUnmarshalling(context, executionAttributes); + } + + @Override + public SdkResponse modifyResponse( + ModifyResponse context, ExecutionAttributes executionAttributes) { + return delegate.modifyResponse(context, executionAttributes); + } + + @Override + public void afterExecution(AfterExecution context, ExecutionAttributes executionAttributes) { + delegate.afterExecution(context, executionAttributes); + } + + @Override + public Throwable modifyException( + FailedExecution context, ExecutionAttributes executionAttributes) { + return delegate.modifyException(context, executionAttributes); + } + + @Override + public void onExecutionFailure(FailedExecution context, ExecutionAttributes executionAttributes) { + delegate.onExecutionFailure(context, executionAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors new file mode 100644 index 000000000..f71c84351 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors @@ -0,0 +1 @@ +io.opentelemetry.javaagent.instrumentation.awssdk.v2_2.TracingExecutionInterceptor diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2ClientTest.groovy new file mode 100644 index 000000000..6edf5c7ea --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2ClientTest.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2ClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import software.amazon.awssdk.core.client.builder.SdkClientBuilder + +class Aws2ClientTest extends AbstractAws2ClientTest implements AgentTestTrait { + @Override + void configureSdkClient(SdkClientBuilder builder) { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/README.md b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/README.md new file mode 100644 index 000000000..80f8eb739 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/README.md @@ -0,0 +1,24 @@ +# AWS Java SDK v2 Instrumentation + +Instrumentation for [AWS Java SDK v2](https://github.com/aws/aws-sdk-java-v2). + +## Usage + +To register instrumentation on an SDK client, register the interceptor when creating it. + +```java +DynamoDbClient client = DynamoDbClient.builder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .addExecutionInterceptor(AwsSdk.newInterceptor())) + .build()) + .build(); +``` + +## Trace propagation + +The AWS SDK instrumentation currently only supports injecting the trace header into the request +using the [AWS Trace Header](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader) format. +This format is the only format recognized by AWS managed services, and populating will allow +propagating the trace through them. If this does not fulfill your use case, perhaps because you are +using the same SDK with a different non-AWS managed service, let us know so we can provide +configuration for this behavior. diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/aws-sdk-2.2-library.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/aws-sdk-2.2-library.gradle new file mode 100644 index 000000000..eb93a1748 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/aws-sdk-2.2-library.gradle @@ -0,0 +1,17 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + implementation "io.opentelemetry:opentelemetry-extension-aws" + + library "software.amazon.awssdk:aws-core:2.2.0" + library "software.amazon.awssdk:aws-json-protocol:2.2.0" + + testImplementation project(':instrumentation:aws-sdk:aws-sdk-2.2:testing') + + testImplementation "org.assertj:assertj-core" + testImplementation "org.mockito:mockito-core" +} + +test { + systemProperty "otel.instrumentation.aws-sdk.experimental-span-attributes", true +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsJsonProtocolFactoryAccess.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsJsonProtocolFactoryAccess.java new file mode 100644 index 000000000..af4b3d17f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsJsonProtocolFactoryAccess.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.net.URI; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.protocols.core.OperationInfo; +import software.amazon.awssdk.protocols.core.ProtocolMarshaller; + +final class AwsJsonProtocolFactoryAccess { + + private static final OperationInfo OPERATION_INFO = + OperationInfo.builder().hasPayloadMembers(true).httpMethod(SdkHttpMethod.POST).build(); + + @Nullable private static final MethodHandle INVOKE_CREATE_PROTOCOL_MARSHALLER; + + static { + MethodHandle invokeCreateProtocolMarshaller = null; + try { + Class awsJsonProtocolFactoryClass = + Class.forName("software.amazon.awssdk.protocols.json.AwsJsonProtocolFactory"); + Object awsJsonProtocolFactoryBuilder = + awsJsonProtocolFactoryClass.getMethod("builder").invoke(null); + awsJsonProtocolFactoryBuilder + .getClass() + .getMethod("clientConfiguration", SdkClientConfiguration.class) + .invoke( + awsJsonProtocolFactoryBuilder, + SdkClientConfiguration.builder() + // AwsJsonProtocolFactory requires any URI to be present + .option(SdkClientOption.ENDPOINT, URI.create("http://empty")) + .build()); + Object awsJsonProtocolFactory = + awsJsonProtocolFactoryBuilder + .getClass() + .getMethod("build") + .invoke(awsJsonProtocolFactoryBuilder); + + MethodHandle createProtocolMarshaller = + MethodHandles.publicLookup() + .findVirtual( + awsJsonProtocolFactoryClass, + "createProtocolMarshaller", + MethodType.methodType(ProtocolMarshaller.class, OperationInfo.class)); + invokeCreateProtocolMarshaller = + createProtocolMarshaller.bindTo(awsJsonProtocolFactory).bindTo(OPERATION_INFO); + } catch (Throwable t) { + // Ignore; + } + INVOKE_CREATE_PROTOCOL_MARSHALLER = invokeCreateProtocolMarshaller; + } + + @SuppressWarnings("unchecked") + @Nullable + static ProtocolMarshaller createMarshaller() { + if (INVOKE_CREATE_PROTOCOL_MARSHALLER == null) { + return null; + } + + try { + return (ProtocolMarshaller) INVOKE_CREATE_PROTOCOL_MARSHALLER.invoke(); + } catch (Throwable t) { + return null; + } + } + + private AwsJsonProtocolFactoryAccess() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpClientTracer.java new file mode 100644 index 000000000..be8f064df --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpClientTracer.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; +import software.amazon.awssdk.http.SdkHttpHeaders; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; + +final class AwsSdkHttpClientTracer + extends HttpClientTracer { + + AwsSdkHttpClientTracer(OpenTelemetry openTelemetry) { + super(openTelemetry, new NetPeerAttributes()); + } + + public Context startSpan(Context parentContext, ExecutionAttributes attributes) { + String spanName = spanName(attributes); + Span span = spanBuilder(parentContext, spanName, CLIENT).startSpan(); + return withClientSpan(parentContext, span); + } + + @Override + public void inject(Context context, SdkHttpRequest.Builder builder) { + AwsXrayPropagator.getInstance().inject(context, builder, getSetter()); + } + + @Override + protected String method(SdkHttpRequest request) { + return request.method().name(); + } + + @Override + protected URI url(SdkHttpRequest request) { + return request.getUri(); + } + + @Override + protected Integer status(SdkHttpResponse response) { + return response.statusCode(); + } + + @Override + protected String requestHeader(SdkHttpRequest sdkHttpRequest, String name) { + return header(sdkHttpRequest, name); + } + + @Override + protected String responseHeader(SdkHttpResponse sdkHttpResponse, String name) { + return header(sdkHttpResponse, name); + } + + @Override + protected TextMapSetter getSetter() { + return AwsSdkInjectAdapter.INSTANCE; + } + + private static String header(SdkHttpHeaders headers, String name) { + return headers.firstMatchingHeader(name).orElse(null); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.aws-sdk-2.2"; + } + + /** This method is overridden to allow other classes in this package to call it. */ + @Override + protected void onRequest(Span span, SdkHttpRequest sdkHttpRequest) { + super.onRequest(span, sdkHttpRequest); + } + + private static String spanName(ExecutionAttributes attributes) { + String awsServiceName = attributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME); + String awsOperation = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); + return awsServiceName + "." + awsOperation; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkInjectAdapter.java new file mode 100644 index 000000000..46d2f6410 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.context.propagation.TextMapSetter; +import software.amazon.awssdk.http.SdkHttpRequest; + +final class AwsSdkInjectAdapter implements TextMapSetter { + + static final AwsSdkInjectAdapter INSTANCE = new AwsSdkInjectAdapter(); + + @Override + public void set(SdkHttpRequest.Builder builder, String name, String value) { + builder.appendHeader(name, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java new file mode 100644 index 000000000..7e6a8f4ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java @@ -0,0 +1,158 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequestType.DynamoDB; +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequestType.Kinesis; +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequestType.S3; +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequestType.SQS; +import static io.opentelemetry.instrumentation.awssdk.v2_2.FieldMapping.request; +import static io.opentelemetry.instrumentation.awssdk.v2_2.FieldMapping.response; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.awssdk.core.SdkRequest; + +/** + * Temporary solution - maps only DynamoDB attributes. Final solution should be generated from AWS + * SDK automatically + * (https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2291). + */ +enum AwsSdkRequest { + // generic requests + DynamoDbRequest(DynamoDB, "DynamoDbRequest"), + S3Request(S3, "S3Request"), + SqsRequest(SQS, "SqsRequest"), + KinesisRequest(Kinesis, "KinesisRequest"), + // specific requests + BatchGetItem( + DynamoDB, + "BatchGetItemRequest", + request("aws.dynamodb.table_names", "RequestItems"), + response("aws.dynamodb.consumed_capacity", "ConsumedCapacity")), + BatchWriteItem( + DynamoDB, + "BatchWriteItemRequest", + request("aws.dynamodb.table_names", "RequestItems"), + response("aws.dynamodb.consumed_capacity", "ConsumedCapacity"), + response("aws.dynamodb.item_collection_metrics", "ItemCollectionMetrics")), + CreateTable( + DynamoDB, + "CreateTableRequest", + request("aws.dynamodb.global_secondary_indexes", "GlobalSecondaryIndexes"), + request("aws.dynamodb.local_secondary_indexes", "LocalSecondaryIndexes"), + request( + "aws.dynamodb.provisioned_throughput.read_capacity_units", + "ProvisionedThroughput.ReadCapacityUnits"), + request( + "aws.dynamodb.provisioned_throughput.write_capacity_units", + "ProvisionedThroughput.WriteCapacityUnits")), + DeleteItem( + DynamoDB, + "DeleteItemRequest", + response("aws.dynamodb.consumed_capacity", "ConsumedCapacity"), + response("aws.dynamodb.item_collection_metrics", "ItemCollectionMetrics")), + GetItem( + DynamoDB, + "GetItemRequest", + request("aws.dynamodb.projection_expression", "ProjectionExpression"), + response("aws.dynamodb.consumed_capacity", "ConsumedCapacity"), + request("aws.dynamodb.consistent_read", "ConsistentRead")), + ListTables( + DynamoDB, + "ListTablesRequest", + request("aws.dynamodb.exclusive_start_table_name", "ExclusiveStartTableName"), + response("aws.dynamodb.table_count", "TableNames"), + request("aws.dynamodb.limit", "Limit")), + PutItem( + DynamoDB, + "PutItemRequest", + response("aws.dynamodb.consumed_capacity", "ConsumedCapacity"), + response("aws.dynamodb.item_collection_metrics", "ItemCollectionMetrics")), + Query( + DynamoDB, + "QueryRequest", + request("aws.dynamodb.attributes_to_get", "AttributesToGet"), + request("aws.dynamodb.consistent_read", "ConsistentRead"), + request("aws.dynamodb.index_name", "IndexName"), + request("aws.dynamodb.limit", "Limit"), + request("aws.dynamodb.projection_expression", "ProjectionExpression"), + request("aws.dynamodb.scan_index_forward", "ScanIndexForward"), + request("aws.dynamodb.select", "Select"), + response("aws.dynamodb.consumed_capacity", "ConsumedCapacity")), + Scan( + DynamoDB, + "ScanRequest", + request("aws.dynamodb.attributes_to_get", "AttributesToGet"), + request("aws.dynamodb.consistent_read", "ConsistentRead"), + request("aws.dynamodb.index_name", "IndexName"), + request("aws.dynamodb.limit", "Limit"), + request("aws.dynamodb.projection_expression", "ProjectionExpression"), + request("aws.dynamodb.segment", "Segment"), + request("aws.dynamodb.select", "Select"), + request("aws.dynamodb.total_segments", "TotalSegments"), + response("aws.dynamodb.consumed_capacity", "ConsumedCapacity"), + response("aws.dynamodb.count", "Count"), + response("aws.dynamodb.scanned_count", "ScannedCount")), + UpdateItem( + DynamoDB, + "UpdateItemRequest", + response("aws.dynamodb.consumed_capacity", "ConsumedCapacity"), + response("aws.dynamodb.item_collection_metrics", "ItemCollectionMetrics")), + UpdateTable( + DynamoDB, + "UpdateTableRequest", + request("aws.dynamodb.attribute_definitions", "AttributeDefinitions"), + request("aws.dynamodb.global_secondary_index_updates", "GlobalSecondaryIndexUpdates"), + request( + "aws.dynamodb.provisioned_throughput.read_capacity_units", + "ProvisionedThroughput.ReadCapacityUnits"), + request( + "aws.dynamodb.provisioned_throughput.write_capacity_units", + "ProvisionedThroughput.WriteCapacityUnits")); + + private final AwsSdkRequestType type; + private final String requestClass; + // Wrap in unmodifiableMap + @SuppressWarnings("ImmutableEnumChecker") + private final Map> fields; + + AwsSdkRequest(AwsSdkRequestType type, String requestClass, FieldMapping... fields) { + this.type = type; + this.requestClass = requestClass; + this.fields = Collections.unmodifiableMap(FieldMapping.groupByType(fields)); + } + + @Nullable + static AwsSdkRequest ofSdkRequest(SdkRequest request) { + // try request type + AwsSdkRequest result = ofType(request.getClass().getSimpleName()); + // try parent - generic + if (result == null) { + result = ofType(request.getClass().getSuperclass().getSimpleName()); + } + return result; + } + + private static AwsSdkRequest ofType(String typeName) { + for (AwsSdkRequest type : values()) { + if (type.requestClass.equals(typeName)) { + return type; + } + } + return null; + } + + List fields(FieldMapping.Type type) { + return fields.get(type); + } + + AwsSdkRequestType type() { + return type; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java new file mode 100644 index 000000000..24f3d4270 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.instrumentation.awssdk.v2_2.FieldMapping.request; + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +enum AwsSdkRequestType { + S3(request("aws.bucket.name", "Bucket")), + SQS(request("aws.queue.url", "QueueUrl"), request("aws.queue.name", "QueueName")), + Kinesis(request("aws.stream.name", "StreamName")), + DynamoDB( + request("aws.table.name", "TableName"), + request(SemanticAttributes.DB_NAME.getKey(), "TableName")); + + // Wrapping in unmodifiableMap + @SuppressWarnings("ImmutableEnumChecker") + private final Map> fields; + + AwsSdkRequestType(FieldMapping... fieldMappings) { + this.fields = Collections.unmodifiableMap(FieldMapping.groupByType(fieldMappings)); + } + + List fields(FieldMapping.Type type) { + return fields.get(type); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTracing.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTracing.java new file mode 100644 index 000000000..e067f6b8a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTracing.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.api.OpenTelemetry; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; + +/** + * Entrypoint to OpenTelemetry instrumentation of the AWS SDK. Register the {@link + * ExecutionInterceptor} returned by {@link #newExecutionInterceptor()} with an SDK client to have + * all requests traced. + * + *

{@code
+ * DynamoDbClient dynamoDb = DynamoDbClient.builder()
+ *     .overrideConfiguration(ClientOverrideConfiguration.builder()
+ *         .addExecutionInterceptor(AwsSdkTracing.create(openTelemetry).newExecutionInterceptor())
+ *         .build())
+ *     .build();
+ * }
+ */ +public class AwsSdkTracing { + + /** Returns a new {@link AwsSdkTracing} configured with the given {@link OpenTelemetry}. */ + public static AwsSdkTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + /** Returns a new {@link AwsSdkTracingBuilder} configured with the given {@link OpenTelemetry}. */ + public static AwsSdkTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new AwsSdkTracingBuilder(openTelemetry); + } + + private final AwsSdkHttpClientTracer tracer; + private final boolean captureExperimentalSpanAttributes; + + AwsSdkTracing(OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { + this.tracer = new AwsSdkHttpClientTracer(openTelemetry); + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + /** + * Returns a new {@link ExecutionInterceptor} that can be used with methods like {@link + * ClientOverrideConfiguration.Builder#addExecutionInterceptor(ExecutionInterceptor)}. + */ + public ExecutionInterceptor newExecutionInterceptor() { + return new TracingExecutionInterceptor(tracer, captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTracingBuilder.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTracingBuilder.java new file mode 100644 index 000000000..f008d7805 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTracingBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.api.OpenTelemetry; + +/** A builder of {@link AwsSdkTracing}. */ +public final class AwsSdkTracingBuilder { + + private final OpenTelemetry openTelemetry; + + private boolean captureExperimentalSpanAttributes; + + AwsSdkTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Sets whether experimental attributes should be set to spans. These attributes may be changed or + * removed in the future, so only enable this if you know you do not require attributes filled by + * this instrumentation to be stable across versions + */ + public AwsSdkTracingBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + /** Returns a new {@link AwsSdkTracing} with the settings of this {@link AwsSdkTracingBuilder}. */ + public AwsSdkTracing build() { + return new AwsSdkTracing(openTelemetry, captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapper.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapper.java new file mode 100644 index 000000000..ec65689ca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapper.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.api.trace.Span; +import java.util.List; +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.utils.StringUtils; + +class FieldMapper { + + private final Serializer serializer; + private final MethodHandleFactory methodHandleFactory; + + FieldMapper() { + serializer = new Serializer(); + methodHandleFactory = new MethodHandleFactory(); + } + + FieldMapper(Serializer serializer, MethodHandleFactory methodHandleFactory) { + this.methodHandleFactory = methodHandleFactory; + this.serializer = serializer; + } + + void mapToAttributes(SdkRequest sdkRequest, AwsSdkRequest request, Span span) { + mapToAttributes( + field -> sdkRequest.getValueForField(field, Object.class).orElse(null), + FieldMapping.Type.REQUEST, + request, + span); + } + + void mapToAttributes(SdkResponse sdkResponse, AwsSdkRequest request, Span span) { + mapToAttributes( + field -> sdkResponse.getValueForField(field, Object.class).orElse(null), + FieldMapping.Type.RESPONSE, + request, + span); + } + + private void mapToAttributes( + Function fieldValueProvider, + FieldMapping.Type type, + AwsSdkRequest request, + Span span) { + for (FieldMapping fieldMapping : request.fields(type)) { + mapToAttributes(fieldValueProvider, fieldMapping, span); + } + for (FieldMapping fieldMapping : request.type().fields(type)) { + mapToAttributes(fieldValueProvider, fieldMapping, span); + } + } + + private void mapToAttributes( + Function fieldValueProvider, FieldMapping fieldMapping, Span span) { + // traverse path + List path = fieldMapping.getFields(); + Object target = fieldValueProvider.apply(path.get(0)); + for (int i = 1; i < path.size() && target != null; i++) { + target = next(target, path.get(i)); + } + if (target != null) { + String value = serializer.serialize(target); + if (!StringUtils.isEmpty(value)) { + span.setAttribute(fieldMapping.getAttribute(), value); + } + } + } + + @Nullable + private Object next(Object current, String fieldName) { + try { + return methodHandleFactory.forField(current.getClass(), fieldName).invoke(current); + } catch (Throwable t) { + // ignore + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapping.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapping.java new file mode 100644 index 000000000..6858113f4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapping.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +class FieldMapping { + + enum Type { + REQUEST, + RESPONSE + } + + private final Type type; + private final String attribute; + private final List fields; + + static FieldMapping request(String attribute, String fieldPath) { + return new FieldMapping(Type.REQUEST, attribute, fieldPath); + } + + static FieldMapping response(String attribute, String fieldPath) { + return new FieldMapping(Type.RESPONSE, attribute, fieldPath); + } + + FieldMapping(Type type, String attribute, String fieldPath) { + this.type = type; + this.attribute = attribute; + this.fields = Collections.unmodifiableList(Arrays.asList(fieldPath.split("\\."))); + } + + String getAttribute() { + return attribute; + } + + List getFields() { + return fields; + } + + Type getType() { + return type; + } + + static Map> groupByType(FieldMapping[] fieldMappings) { + + EnumMap> fields = new EnumMap<>(Type.class); + for (FieldMapping.Type type : FieldMapping.Type.values()) { + fields.put(type, new ArrayList<>()); + } + for (FieldMapping fieldMapping : fieldMappings) { + fields.get(fieldMapping.getType()).add(fieldMapping); + } + return fields; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/MethodHandleFactory.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/MethodHandleFactory.java new file mode 100644 index 000000000..211e31aa2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/MethodHandleFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +class MethodHandleFactory { + + private static String unCapitalize(String string) { + return string.substring(0, 1).toLowerCase(Locale.ROOT) + string.substring(1); + } + + private final ClassValue> getterCache = + new ClassValue>() { + @Override + protected ConcurrentHashMap computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + }; + + MethodHandle forField(Class clazz, String fieldName) + throws NoSuchMethodException, IllegalAccessException { + MethodHandle methodHandle = getterCache.get(clazz).get(fieldName); + if (methodHandle == null) { + // getter in AWS SDK is lowercased field name + methodHandle = + MethodHandles.publicLookup().unreflect(clazz.getMethod(unCapitalize(fieldName))); + getterCache.get(clazz).put(fieldName, methodHandle); + } + return methodHandle; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Serializer.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Serializer.java new file mode 100644 index 000000000..d3460bff4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Serializer.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.protocols.core.ProtocolMarshaller; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.awssdk.utils.StringUtils; + +class Serializer { + + @Nullable + String serialize(Object target) { + + if (target == null) { + return null; + } + + if (target instanceof SdkPojo) { + return serialize((SdkPojo) target); + } + if (target instanceof Collection) { + return serialize((Collection) target); + } + if (target instanceof Map) { + return serialize(((Map) target).keySet()); + } + // simple type + return target.toString(); + } + + @Nullable + private static String serialize(SdkPojo sdkPojo) { + ProtocolMarshaller marshaller = + AwsJsonProtocolFactoryAccess.createMarshaller(); + if (marshaller == null) { + return null; + } + Optional optional = marshaller.marshall(sdkPojo).contentStreamProvider(); + return optional + .map( + csp -> { + try (InputStream cspIs = csp.newStream()) { + return IoUtils.toUtf8String(cspIs); + } catch (IOException e) { + return null; + } + }) + .orElse(null); + } + + private String serialize(Collection collection) { + String serialized = collection.stream().map(this::serialize).collect(Collectors.joining(",")); + return (StringUtils.isEmpty(serialized) ? null : "[" + serialized + "]"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java new file mode 100644 index 000000000..fda5b0abf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java @@ -0,0 +1,182 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequestType.DynamoDB; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttribute; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; +import software.amazon.awssdk.http.SdkHttpRequest; + +/** AWS request execution interceptor. */ +final class TracingExecutionInterceptor implements ExecutionInterceptor { + + // the class name is part of the attribute name, so that it will be shaded when used in javaagent + // instrumentation, and won't conflict with usage outside javaagent instrumentation + static final ExecutionAttribute CONTEXT_ATTRIBUTE = + new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".Context"); + static final ExecutionAttribute SCOPE_ATTRIBUTE = + new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".Scope"); + static final ExecutionAttribute AWS_SDK_REQUEST_ATTRIBUTE = + new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".AwsSdkRequest"); + + static final String COMPONENT_NAME = "java-aws-sdk"; + + private final AwsSdkHttpClientTracer tracer; + private final boolean captureExperimentalSpanAttributes; + private final FieldMapper fieldMapper; + + TracingExecutionInterceptor( + AwsSdkHttpClientTracer tracer, boolean captureExperimentalSpanAttributes) { + this.tracer = tracer; + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + fieldMapper = new FieldMapper(); + } + + @Override + public void beforeExecution( + Context.BeforeExecution context, ExecutionAttributes executionAttributes) { + io.opentelemetry.context.Context parentOtelContext = io.opentelemetry.context.Context.current(); + if (!tracer.shouldStartSpan(parentOtelContext)) { + return; + } + io.opentelemetry.context.Context otelContext = + tracer.startSpan(parentOtelContext, executionAttributes); + executionAttributes.putAttribute(CONTEXT_ATTRIBUTE, otelContext); + if (executionAttributes + .getAttribute(SdkExecutionAttribute.CLIENT_TYPE) + .equals(ClientType.SYNC)) { + // We can only activate context for synchronous clients, which allows downstream + // instrumentation like Apache to know about the SDK span. + executionAttributes.putAttribute(SCOPE_ATTRIBUTE, otelContext.makeCurrent()); + } + } + + @Override + public SdkHttpRequest modifyHttpRequest( + Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + io.opentelemetry.context.Context otelContext = getContext(executionAttributes); + if (otelContext == null) { + return context.httpRequest(); + } + + SdkHttpRequest.Builder builder = context.httpRequest().toBuilder(); + tracer.inject(otelContext, builder); + return builder.build(); + } + + @Override + public void afterMarshalling( + Context.AfterMarshalling context, ExecutionAttributes executionAttributes) { + io.opentelemetry.context.Context otelContext = getContext(executionAttributes); + if (otelContext == null) { + return; + } + + Span span = Span.fromContext(otelContext); + tracer.onRequest(span, context.httpRequest()); + + AwsSdkRequest awsSdkRequest = AwsSdkRequest.ofSdkRequest(context.request()); + if (awsSdkRequest != null) { + executionAttributes.putAttribute(AWS_SDK_REQUEST_ATTRIBUTE, awsSdkRequest); + populateRequestAttributes(span, awsSdkRequest, context.request(), executionAttributes); + } + populateGenericAttributes(span, executionAttributes); + } + + private void populateRequestAttributes( + Span span, + AwsSdkRequest awsSdkRequest, + SdkRequest sdkRequest, + ExecutionAttributes attributes) { + + fieldMapper.mapToAttributes(sdkRequest, awsSdkRequest, span); + + if (awsSdkRequest.type() == DynamoDB) { + span.setAttribute(SemanticAttributes.DB_SYSTEM, "dynamodb"); + String operation = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); + if (operation != null) { + span.setAttribute(SemanticAttributes.DB_OPERATION, operation); + } + } + } + + private void populateGenericAttributes(Span span, ExecutionAttributes attributes) { + if (captureExperimentalSpanAttributes) { + String awsServiceName = attributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME); + String awsOperation = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); + + span.setAttribute("aws.agent", COMPONENT_NAME); + span.setAttribute("aws.service", awsServiceName); + span.setAttribute("aws.operation", awsOperation); + } + } + + @Override + public void afterExecution( + Context.AfterExecution context, ExecutionAttributes executionAttributes) { + io.opentelemetry.context.Context otelContext = getContext(executionAttributes); + clearAttributes(executionAttributes); + Span span = Span.fromContext(otelContext); + onUserAgentHeaderAvailable(span, context.httpRequest()); + onSdkResponse(span, context.response(), executionAttributes); + tracer.end(otelContext, context.httpResponse()); + } + + // Certain headers in the request like User-Agent are only available after execution. + private void onUserAgentHeaderAvailable(Span span, SdkHttpRequest request) { + span.setAttribute( + SemanticAttributes.HTTP_USER_AGENT, tracer.requestHeader(request, "User-Agent")); + } + + private void onSdkResponse( + Span span, SdkResponse response, ExecutionAttributes executionAttributes) { + if (captureExperimentalSpanAttributes) { + if (response instanceof AwsResponse) { + span.setAttribute("aws.requestId", ((AwsResponse) response).responseMetadata().requestId()); + } + AwsSdkRequest sdkRequest = executionAttributes.getAttribute(AWS_SDK_REQUEST_ATTRIBUTE); + if (sdkRequest != null) { + fieldMapper.mapToAttributes(response, sdkRequest, span); + } + } + } + + @Override + public void onExecutionFailure( + Context.FailedExecution context, ExecutionAttributes executionAttributes) { + io.opentelemetry.context.Context otelContext = getContext(executionAttributes); + clearAttributes(executionAttributes); + tracer.endExceptionally(otelContext, context.exception()); + } + + private static void clearAttributes(ExecutionAttributes executionAttributes) { + Scope scope = executionAttributes.getAttribute(SCOPE_ATTRIBUTE); + if (scope != null) { + scope.close(); + } + executionAttributes.putAttribute(CONTEXT_ATTRIBUTE, null); + executionAttributes.putAttribute(AWS_SDK_REQUEST_ATTRIBUTE, null); + } + + /** + * Returns the {@link Context} stored in the {@link ExecutionAttributes}, or {@code null} if there + * is no operation set. + */ + private static io.opentelemetry.context.Context getContext(ExecutionAttributes attributes) { + return attributes.getAttribute(CONTEXT_ATTRIBUTE); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy new file mode 100644 index 000000000..cd3fbe66e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2 + +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import software.amazon.awssdk.core.client.builder.SdkClientBuilder +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration + +class Aws2ClientTest extends AbstractAws2ClientTest implements LibraryTestTrait { + @Override + void configureSdkClient(SdkClientBuilder builder) { + builder.overrideConfiguration(ClientOverrideConfiguration.builder() + .addExecutionInterceptor( + AwsSdkTracing.newBuilder(getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .build() + .newExecutionInterceptor()) + .build()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapperTest.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapperTest.java new file mode 100644 index 000000000..c72ede715 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapperTest.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequest.BatchWriteItem; +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequest.UpdateTable; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import io.opentelemetry.api.trace.Span; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemResponse; +import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; +import software.amazon.awssdk.services.dynamodb.model.ItemCollectionMetrics; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.UpdateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.WriteRequest; + +public class FieldMapperTest { + + @Test + public void shouldMapNestedField() { + + // given + AwsSdkRequest awsSdkRequest = UpdateTable; + MethodHandleFactory methodHandleFactory = new MethodHandleFactory(); + Serializer serializer = mock(Serializer.class); + FieldMapper underTest = new FieldMapper(serializer, methodHandleFactory); + UpdateTableRequest sdkRequest = + UpdateTableRequest.builder() + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(55L) + .writeCapacityUnits(77L) + .build()) + .build(); + given(serializer.serialize(55L)).willReturn("55"); + given(serializer.serialize(77L)).willReturn("77"); + + Span span = mock(Span.class); + // when + underTest.mapToAttributes(sdkRequest, awsSdkRequest, span); + // then + verify(span).setAttribute("aws.dynamodb.provisioned_throughput.read_capacity_units", "55"); + verify(span).setAttribute("aws.dynamodb.provisioned_throughput.write_capacity_units", "77"); + verifyNoMoreInteractions(span); + } + + @Test + public void shouldMapRequestFieldsOnly() { + + // given + AwsSdkRequest awsSdkRequest = BatchWriteItem; + MethodHandleFactory methodHandleFactory = new MethodHandleFactory(); + Serializer serializer = mock(Serializer.class); + FieldMapper underTest = new FieldMapper(serializer, methodHandleFactory); + Map> items = new HashMap(); + BatchWriteItemRequest sdkRequest = BatchWriteItemRequest.builder().requestItems(items).build(); + given(serializer.serialize(items)).willReturn("firstTable,secondTable"); + + Span span = mock(Span.class); + // when + underTest.mapToAttributes(sdkRequest, awsSdkRequest, span); + // then + verify(span).setAttribute("aws.dynamodb.table_names", "firstTable,secondTable"); + verifyNoMoreInteractions(span); + } + + @Test + public void shouldMapResponseFieldsOnly() { + + // given + AwsSdkRequest awsSdkRequest = BatchWriteItem; + MethodHandleFactory methodHandleFactory = new MethodHandleFactory(); + Serializer serializer = mock(Serializer.class); + FieldMapper underTest = new FieldMapper(serializer, methodHandleFactory); + Map> items = new HashMap(); + BatchWriteItemResponse sdkResponse = + BatchWriteItemResponse.builder() + .consumedCapacity(ConsumedCapacity.builder().build()) + .itemCollectionMetrics(items) + .build(); + given(serializer.serialize(sdkResponse.consumedCapacity())).willReturn("consumedCapacity"); + given(serializer.serialize(items)).willReturn("itemCollectionMetrics"); + + Span span = mock(Span.class); + // when + underTest.mapToAttributes(sdkResponse, awsSdkRequest, span); + // then + verify(span).setAttribute("aws.dynamodb.consumed_capacity", "consumedCapacity"); + verify(span).setAttribute("aws.dynamodb.item_collection_metrics", "itemCollectionMetrics"); + verifyNoMoreInteractions(span); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/SerializerTest.java b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/SerializerTest.java new file mode 100644 index 000000000..aa267e524 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/SerializerTest.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; + +public class SerializerTest { + + @Test + public void shouldSerializeSimpleString() { + // given + // when + String serialized = new Serializer().serialize("simpleString"); + // then + assertThat(serialized).isEqualTo("simpleString"); + } + + @Test + public void shouldSerializeSdkPojo() { + // given + SdkPojo sdkPojo = + ProvisionedThroughput.builder().readCapacityUnits(1L).writeCapacityUnits(2L).build(); + // when + String serialized = new Serializer().serialize(sdkPojo); + // then + assertThat(serialized).isEqualTo("{\"ReadCapacityUnits\":1,\"WriteCapacityUnits\":2}"); + } + + @Test + public void shouldSerializeCollection() { + // given + List collection = Arrays.asList("one", "two", "three"); + // when + String serialized = new Serializer().serialize(collection); + // then + assertThat(serialized).isEqualTo("[one,two,three]"); + } + + @Test + public void shouldSerializeEmptyCollectionAsNull() { + // given + List collection = Collections.emptyList(); + // when + String serialized = new Serializer().serialize(collection); + // then + assertThat(serialized).isNull(); + } + + @Test + public void shouldSerializeMapAsKeyCollection() { + // given + Map map = new HashMap<>(); + map.put("uno", 1L); + map.put("dos", new LinkedHashMap<>()); + map.put("tres", "cuatro"); + // when + String serialized = new Serializer().serialize(map); + // then + assertThat(serialized).isEqualTo("[uno,dos,tres]"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/testing/aws-sdk-2.2-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/testing/aws-sdk-2.2-testing.gradle new file mode 100644 index 000000000..bc866149a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/testing/aws-sdk-2.2-testing.gradle @@ -0,0 +1,19 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "software.amazon.awssdk:apache-client:2.2.0" + api "software.amazon.awssdk:s3:2.2.0" + api "software.amazon.awssdk:rds:2.2.0" + api "software.amazon.awssdk:ec2:2.2.0" + api "software.amazon.awssdk:sqs:2.2.0" + api "software.amazon.awssdk:dynamodb:2.2.0" + api "software.amazon.awssdk:kinesis:2.2.0" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy new file mode 100644 index 000000000..2d1248caf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy @@ -0,0 +1,547 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2 + +import static com.google.common.collect.ImmutableMap.of +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.common.HttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpStatus +import io.opentelemetry.testing.internal.armeria.common.MediaType +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension +import java.time.Duration +import java.util.concurrent.Future +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.ResponseInputStream +import software.amazon.awssdk.core.async.AsyncResponseTransformer +import software.amazon.awssdk.core.client.builder.SdkClientBuilder +import software.amazon.awssdk.core.client.config.SdkClientOption +import software.amazon.awssdk.core.exception.SdkClientException +import software.amazon.awssdk.core.retry.RetryPolicy +import software.amazon.awssdk.http.apache.ApacheHttpClient +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import software.amazon.awssdk.services.dynamodb.DynamoDbClient +import software.amazon.awssdk.services.dynamodb.model.AttributeValue +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest +import software.amazon.awssdk.services.dynamodb.model.QueryRequest +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest +import software.amazon.awssdk.services.ec2.Ec2AsyncClient +import software.amazon.awssdk.services.ec2.Ec2Client +import software.amazon.awssdk.services.kinesis.KinesisClient +import software.amazon.awssdk.services.kinesis.model.DeleteStreamRequest +import software.amazon.awssdk.services.rds.RdsAsyncClient +import software.amazon.awssdk.services.rds.RdsClient +import software.amazon.awssdk.services.rds.model.DeleteOptionGroupRequest +import software.amazon.awssdk.services.s3.S3AsyncClient +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.CreateBucketRequest +import software.amazon.awssdk.services.s3.model.GetObjectRequest +import software.amazon.awssdk.services.sqs.SqsAsyncClient +import software.amazon.awssdk.services.sqs.SqsClient +import software.amazon.awssdk.services.sqs.model.CreateQueueRequest +import software.amazon.awssdk.services.sqs.model.SendMessageRequest +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class AbstractAws2ClientTest extends InstrumentationSpecification { + + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider + .create(AwsBasicCredentials.create("my-access-key", "my-secret-key")) + + @Shared + def server = new MockWebServerExtension() + + def setupSpec() { + server.start() + } + + def cleanupSpec() { + server.stop() + } + + def setup() { + server.beforeTestExecution(null) + } + + abstract void configureSdkClient(SdkClientBuilder builder) + + def "send DynamoDB #operation request with builder #builder.class.getName() mocked response"() { + setup: + configureSdkClient(builder) + def client = builder + .endpointOverride(server.httpUri()) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")) + def response = call.call(client) + + if (response instanceof Future) { + response = response.get() + } + + expect: + response != null + response.class.simpleName.startsWith(operation) + switch (operation) { + case "CreateTable": + assertCreateTableRequest(path, method, requestId) + break + case "Query": + assertQueryRequest(path, method, requestId) + break + default: + assertDynamoDbRequest(service, operation, path, method, requestId) + } + + where: + [service, operation, method, path, requestId, builder, call] << dynamoDbRequestDataTable(DynamoDbClient.builder()) + } + + def "send DynamoDB #operation async request with builder #builder.class.getName() mocked response"() { + setup: + configureSdkClient(builder) + def client = builder + .endpointOverride(server.httpUri()) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")) + def response = call.call(client) + + if (response instanceof Future) { + response = response.get() + } + + expect: + response != null + switch (operation) { + case "CreateTable": + assertCreateTableRequest(path, method, requestId) + break + case "Query": + assertQueryRequest(path, method, requestId) + break + default: + assertDynamoDbRequest(service, operation, path, method, requestId) + } + + where: + [service, operation, method, path, requestId, builder, call] << dynamoDbRequestDataTable(DynamoDbAsyncClient.builder()) + } + + def assertCreateTableRequest(path, method, requestId) { + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DynamoDb.CreateTable" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" { it.startsWith("${server.httpUri()}${path}") } + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_USER_AGENT.key}" { it.startsWith("aws-sdk-java/") } + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "aws.service" "DynamoDb" + "aws.operation" "CreateTable" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + "aws.table.name" "sometable" + "${SemanticAttributes.DB_SYSTEM.key}" "dynamodb" + "${SemanticAttributes.DB_NAME.key}" "sometable" + "${SemanticAttributes.DB_OPERATION.key}" "CreateTable" + "aws.dynamodb.global_secondary_indexes" "[{\"IndexName\":\"globalIndex\",\"KeySchema\":[{\"AttributeName\":\"attribute\"}],\"ProvisionedThroughput\":{\"ReadCapacityUnits\":10,\"WriteCapacityUnits\":12}},{\"IndexName\":\"globalIndexSecondary\",\"KeySchema\":[{\"AttributeName\":\"attributeSecondary\"}],\"ProvisionedThroughput\":{\"ReadCapacityUnits\":7,\"WriteCapacityUnits\":8}}]" + "aws.dynamodb.provisioned_throughput.read_capacity_units" "1" + "aws.dynamodb.provisioned_throughput.write_capacity_units" "1" + } + } + } + } + def request = server.takeRequest() + request.request().headers().get("X-Amzn-Trace-Id") != null + request.request().headers().get("traceparent") == null + } + + def assertQueryRequest(path, method, requestId) { + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DynamoDb.Query" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" { it.startsWith("${server.httpUri()}${path}") } + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_USER_AGENT.key}" { it.startsWith("aws-sdk-java/") } + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "aws.service" "DynamoDb" + "aws.operation" "Query" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + "aws.table.name" "sometable" + "${SemanticAttributes.DB_SYSTEM.key}" "dynamodb" + "${SemanticAttributes.DB_NAME.key}" "sometable" + "${SemanticAttributes.DB_OPERATION.key}" "Query" + "aws.dynamodb.limit" "10" + "aws.dynamodb.select" "ALL_ATTRIBUTES" + } + } + } + } + def request = server.takeRequest() + request.request().headers().get("X-Amzn-Trace-Id") != null + request.request().headers().get("traceparent") == null + } + + def assertDynamoDbRequest(service, operation, path, method, requestId) { + assertTraces(1) { + trace(0, 1) { + span(0) { + name "$service.$operation" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" { it.startsWith("${server.httpUri()}${path}") } + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_USER_AGENT.key}" { it.startsWith("aws-sdk-java/") } + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "aws.service" "$service" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + "aws.table.name" "sometable" + "${SemanticAttributes.DB_SYSTEM.key}" "dynamodb" + "${SemanticAttributes.DB_NAME.key}" "sometable" + "${SemanticAttributes.DB_OPERATION.key}" "${operation}" + } + } + } + } + def request = server.takeRequest() + request.request().headers().get("X-Amzn-Trace-Id") != null + request.request().headers().get("traceparent") == null + } + + static dynamoDbRequestDataTable(client) { + [ + ["DynamoDb", "CreateTable", "POST", "/", "UNKNOWN", client, + { c -> c.createTable(createTableRequest()) }], + ["DynamoDb", "DeleteItem", "POST", "/", "UNKNOWN", client, + { c -> c.deleteItem(DeleteItemRequest.builder().tableName("sometable").key(of("anotherKey", val("value"), "key", val("value"))).conditionExpression("property in (:one :two)").build()) }], + ["DynamoDb", "DeleteTable", "POST", "/", "UNKNOWN", client, + { c -> c.deleteTable(DeleteTableRequest.builder().tableName("sometable").build()) }], + ["DynamoDb", "GetItem", "POST", "/", "UNKNOWN", client, + { c -> c.getItem(GetItemRequest.builder().tableName("sometable").key(of("keyOne", val("value"), "keyTwo", val("differentValue"))).attributesToGet("propertyOne", "propertyTwo").build()) }], + ["DynamoDb", "PutItem", "POST", "/", "UNKNOWN", client, + { c -> c.putItem(PutItemRequest.builder().tableName("sometable").item(of("key", val("value"), "attributeOne", val("one"), "attributeTwo", val("two"))).conditionExpression("attributeOne <> :someVal").build()) }], + ["DynamoDb", "Query", "POST", "/", "UNKNOWN", client, + { c -> c.query(QueryRequest.builder().tableName("sometable").select("ALL_ATTRIBUTES").keyConditionExpression("attribute = :aValue").filterExpression("anotherAttribute = :someVal").limit(10).build()) }], + ["DynamoDb", "UpdateItem", "POST", "/", "UNKNOWN", client, + { c -> c.updateItem(UpdateItemRequest.builder().tableName("sometable").key(of("keyOne", val("value"), "keyTwo", val("differentValue"))).conditionExpression("attributeOne <> :someVal").updateExpression("set attributeOne = :updateValue").build()) }] + ] + } + + static CreateTableRequest createTableRequest() { + return CreateTableRequest.builder() + .tableName("sometable") + .globalSecondaryIndexes(Arrays.asList( + GlobalSecondaryIndex.builder() + .indexName("globalIndex") + .keySchema( + KeySchemaElement.builder() + .attributeName("attribute") + .build()) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(10) + .writeCapacityUnits(12) + .build() + ) + .build(), + GlobalSecondaryIndex.builder() + .indexName("globalIndexSecondary") + .keySchema( + KeySchemaElement.builder() + .attributeName("attributeSecondary") + .build()) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(7) + .writeCapacityUnits(8) + .build() + ) + .build())) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(1) + .writeCapacityUnits(1) + .build() + ) + .build() + } + + static val(String value) { + return AttributeValue.builder().s(value).build() + } + + def "send #operation request with builder #builder.class.getName() mocked response"() { + setup: + configureSdkClient(builder) + def client = builder + .endpointOverride(server.httpUri()) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) + def response = call.call(client) + + if (response instanceof Future) { + response = response.get() + } + + expect: + response != null + response.class.simpleName.startsWith(operation) || response instanceof ResponseInputStream + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "$service.$operation" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" { it.startsWith("${server.httpUri()}${path}") } + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_USER_AGENT.key}" { it.startsWith("aws-sdk-java/") } + "aws.service" "$service" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + if (service == "S3") { + "aws.bucket.name" "somebucket" + } else if (service == "Sqs" && operation == "CreateQueue") { + "aws.queue.name" "somequeue" + } else if (service == "Sqs" && operation == "SendMessage") { + "aws.queue.url" "someurl" + } else if (service == "Kinesis") { + "aws.stream.name" "somestream" + } + } + } + } + } + def request = server.takeRequest() + request.request().headers().get("X-Amzn-Trace-Id") != null + request.request().headers().get("traceparent") == null + + where: + service | operation | method | path | requestId | builder | call | body + "S3" | "CreateBucket" | "PUT" | "/somebucket" | "UNKNOWN" | S3Client.builder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" + "S3" | "GetObject" | "GET" | "/somebucket/somekey" | "UNKNOWN" | S3Client.builder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) } | "" + "Kinesis" | "DeleteStream" | "POST" | "" | "UNKNOWN" | KinesisClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" + "Sqs" | "CreateQueue" | "POST" | "" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | """ + + https://queue.amazonaws.com/123456789012/MyQueue + 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 + + """ + "Sqs" | "SendMessage" | "POST" | "" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl("someurl").messageBody("").build()) } | """ + + + d41d8cd98f00b204e9800998ecf8427e + 3ae8f24a165a8cedc005670c81a27295 + 5fea7756-0ea4-451a-a703-a558b933e274 + + 27daac76-34dd-47df-bd01-1f6e873584a0 + + """ + "Ec2" | "AllocateAddress" | "POST" | "" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2Client.builder() | { c -> c.allocateAddress() } | """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + 192.0.2.1 + standard + + """ + "Rds" | "DeleteOptionGroup" | "POST" | "" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """ + + 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 + + """ + } + + def "send #operation async request with builder #builder.class.getName() mocked response"() { + setup: + configureSdkClient(builder) + def client = builder + .endpointOverride(server.httpUri()) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) + def response = call.call(client) + + if (response instanceof Future) { + response = response.get() + } + + expect: + response != null + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "$service.$operation" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" { it.startsWith("${server.httpUri()}${path}") } + "${SemanticAttributes.HTTP_METHOD.key}" "$method" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_USER_AGENT.key}" { it.startsWith("aws-sdk-java/") } + "aws.service" "$service" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + if (service == "S3") { + "aws.bucket.name" "somebucket" + } else if (service == "Sqs" && operation == "CreateQueue") { + "aws.queue.name" "somequeue" + } else if (service == "Sqs" && operation == "SendMessage") { + "aws.queue.url" "someurl" + } else if (service == "Kinesis") { + "aws.stream.name" "somestream" + } + } + } + } + } + def request = server.takeRequest() + request.request().headers().get("X-Amzn-Trace-Id") != null + request.request().headers().get("traceparent") == null + + where: + service | operation | method | path | requestId | builder | call | body + "S3" | "CreateBucket" | "PUT" | "/somebucket" | "UNKNOWN" | S3AsyncClient.builder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" + "S3" | "GetObject" | "GET" | "/somebucket/somekey" | "UNKNOWN" | S3AsyncClient.builder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build(), AsyncResponseTransformer.toBytes()) } | "1234567890" + // Kinesis seems to expect an http2 response which is incompatible with our test server. + // "Kinesis" | "DeleteStream" | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" + "Sqs" | "CreateQueue" | "POST" | "" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsAsyncClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | """ + + https://queue.amazonaws.com/123456789012/MyQueue + 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 + + """ + "Sqs" | "SendMessage" | "POST" | "" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsAsyncClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl("someurl").messageBody("").build()) } | """ + + + d41d8cd98f00b204e9800998ecf8427e + 3ae8f24a165a8cedc005670c81a27295 + 5fea7756-0ea4-451a-a703-a558b933e274 + + 27daac76-34dd-47df-bd01-1f6e873584a0 + + """ + "Ec2" | "AllocateAddress" | "POST" | "" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2AsyncClient.builder() | { c -> c.allocateAddress() } | """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + 192.0.2.1 + standard + + """ + "Rds" | "DeleteOptionGroup" | "POST" | "" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsAsyncClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """ + + 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 + + """ + } + + // TODO(anuraaga): Without AOP instrumentation of the HTTP client, we cannot model retries as + // spans because of https://github.com/aws/aws-sdk-java-v2/issues/1741. We should at least tweak + // the instrumentation to add Events for retries instead. + def "timeout and retry errors not captured"() { + setup: + def response = HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(500)) + // One retry so two requests. + server.enqueue(response) + server.enqueue(response) + def builder = S3Client.builder() + configureSdkClient(builder) + // Because the client builder does not merge overrides, the simplest way to set retry policy + // is to access the private field for now. + builder.clientConfiguration.option(SdkClientOption.RETRY_POLICY, RetryPolicy.builder().numRetries(1).build()) + def client = builder + .endpointOverride(server.httpUri()) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClientBuilder(ApacheHttpClient.builder().socketTimeout(Duration.ofMillis(50))) + .build() + + when: + client.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) + + then: + thrown SdkClientException + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "S3.GetObject" + kind CLIENT + status ERROR + errorEvent SdkClientException, "Unable to execute HTTP request: Read timed out" + hasNoParent() + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" "${server.httpUri()}/somebucket/somekey" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "aws.service" "S3" + "aws.operation" "GetObject" + "aws.agent" "java-aws-sdk" + "aws.bucket.name" "somebucket" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/cassandra-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/cassandra-3.0-javaagent.gradle new file mode 100644 index 000000000..5bf02d15d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/cassandra-3.0-javaagent.gradle @@ -0,0 +1,42 @@ +// Set properties before any plugins get loaded +ext { + cassandraDriverTestVersions = "[3.0,4.0)" +} + +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + + pass { + group = "com.datastax.cassandra" + module = "cassandra-driver-core" + versions = cassandraDriverTestVersions + assertInverse = true + } + + // Making sure that instrumentation works with recent versions of Guava which removed method + // Futures::transform(input, function) in favor of Futures::transform(input, function, executor) + pass { + name = "Newest versions of Guava" + group = "com.datastax.cassandra" + module = "cassandra-driver-core" + versions = cassandraDriverTestVersions + // While com.datastax.cassandra uses old versions of Guava, users may depends themselves on newer versions of Guava + extraDependency "com.google.guava:guava:27.0-jre" + } +} + +dependencies { + library "com.datastax.cassandra:cassandra-driver-core:3.0.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + testLibrary "com.datastax.cassandra:cassandra-driver-core:3.2.0" + testInstrumentation project(':instrumentation:guava-10.0:javaagent') + + latestDepTestLibrary "com.datastax.cassandra:cassandra-driver-core:3.+" +} + +// Requires old Guava. Can't use enforcedPlatform since predates BOM +configurations.testRuntimeClasspath.resolutionStrategy.force "com.google.guava:guava:19.0" diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraInstrumentationModule.java new file mode 100644 index 000000000..e99b3cc09 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class CassandraInstrumentationModule extends InstrumentationModule { + public CassandraInstrumentationModule() { + super("cassandra", "cassandra-3.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new CassandraManagerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraKeyspaceExtractor.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraKeyspaceExtractor.java new file mode 100644 index 000000000..2f9c0ee6a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraKeyspaceExtractor.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; + +import com.datastax.driver.core.ExecutionInfo; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +final class CassandraKeyspaceExtractor + extends AttributesExtractor { + + @Override + protected void onStart(AttributesBuilder attributes, CassandraRequest request) { + attributes.put( + SemanticAttributes.DB_CASSANDRA_KEYSPACE, request.getSession().getLoggedKeyspace()); + } + + @Override + protected void onEnd( + AttributesBuilder attributes, CassandraRequest request, ExecutionInfo executionInfo) {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraManagerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraManagerInstrumentation.java new file mode 100644 index 000000000..a126a1703 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraManagerInstrumentation.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPrivate; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.datastax.driver.core.Session; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class CassandraManagerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // Note: Cassandra has a large driver and we instrument single class in it. + // The rest is ignored in AdditionalLibraryIgnoresMatcher + return named("com.datastax.driver.core.Cluster$Manager"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPrivate()).and(named("newSession")).and(takesArguments(0)), + this.getClass().getName() + "$NewSessionAdvice"); + } + + @SuppressWarnings("unused") + public static class NewSessionAdvice { + + /** + * Strategy: each time we build a connection to a Cassandra cluster, the + * com.datastax.driver.core.Cluster$Manager.newSession() method is called. The opentracing + * contribution is a simple wrapper, so we just have to wrap the new session. + * + * @param session The fresh session to patch. This session is replaced with new session + */ + @Advice.OnMethodExit(suppress = Throwable.class) + public static void injectTracingSession(@Advice.Return(readOnly = false) Session session) { + // This should cover ours and OT's TracingSession + if (session.getClass().getName().endsWith("cassandra.TracingSession")) { + return; + } + session = new TracingSession(session); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetAttributesExtractor.java new file mode 100644 index 000000000..191fbe1a6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetAttributesExtractor.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; + +import com.datastax.driver.core.ExecutionInfo; +import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor; +import java.net.InetSocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class CassandraNetAttributesExtractor + extends InetSocketAddressNetAttributesExtractor { + + @Override + @Nullable + public String transport(CassandraRequest request) { + return null; + } + + @Override + public @Nullable InetSocketAddress getAddress( + CassandraRequest request, @Nullable ExecutionInfo executionInfo) { + return executionInfo == null ? null : executionInfo.getQueriedHost().getSocketAddress(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraRequest.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraRequest.java new file mode 100644 index 000000000..5407bb6d2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraRequest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; + +import com.datastax.driver.core.Session; +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class CassandraRequest { + + public static CassandraRequest create(Session session, String statement) { + return new AutoValue_CassandraRequest(session, statement); + } + + public abstract Session getSession(); + + public abstract String getStatement(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSingletons.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSingletons.java new file mode 100644 index 000000000..8444f3dc9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSingletons.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; + +import com.datastax.driver.core.ExecutionInfo; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; + +public final class CassandraSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.cassandra-3.0"; + + // could use RESPONSE "ResultSet" here, but using RESPONSE "ExecutionInfo" in cassandra-4.0 + // instrumentation (see comment over there for why), so also using here for consistency + private static final Instrumenter INSTRUMENTER; + + static { + DbAttributesExtractor attributesExtractor = + new CassandraSqlAttributesExtractor(); + SpanNameExtractor spanName = DbSpanNameExtractor.create(attributesExtractor); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(attributesExtractor) + .addAttributesExtractor(new CassandraNetAttributesExtractor()) + .addAttributesExtractor(new CassandraKeyspaceExtractor()) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private CassandraSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSqlAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSqlAttributesExtractor.java new file mode 100644 index 000000000..fe17f7345 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSqlAttributesExtractor.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; + +import com.datastax.driver.core.ExecutionInfo; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.api.instrumenter.db.SqlAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class CassandraSqlAttributesExtractor + extends SqlAttributesExtractor { + + @Override + protected String system(CassandraRequest request) { + return SemanticAttributes.DbSystemValues.CASSANDRA; + } + + @Override + @Nullable + protected String user(CassandraRequest request) { + return null; + } + + @Override + @Nullable + protected String name(CassandraRequest request) { + return request.getSession().getLoggedKeyspace(); + } + + @Override + @Nullable + protected String connectionString(CassandraRequest request) { + return null; + } + + @Override + protected AttributeKey dbTableAttribute() { + return SemanticAttributes.DB_CASSANDRA_TABLE; + } + + @Override + @Nullable + protected String rawStatement(CassandraRequest request) { + return request.getStatement(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/TracingSession.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/TracingSession.java new file mode 100644 index 000000000..6bfaff9b0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/TracingSession.java @@ -0,0 +1,228 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; + +import static io.opentelemetry.javaagent.instrumentation.cassandra.v3_0.CassandraSingletons.instrumenter; + +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.CloseFuture; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.RegularStatement; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.ResultSetFuture; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.Statement; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.Map; + +public class TracingSession implements Session { + + private final Session session; + + public TracingSession(Session session) { + this.session = session; + } + + @Override + public String getLoggedKeyspace() { + return session.getLoggedKeyspace(); + } + + @Override + public Session init() { + return new TracingSession(session.init()); + } + + @Override + public ListenableFuture initAsync() { + return Futures.transform(session.initAsync(), TracingSession::new, Runnable::run); + } + + @Override + public ResultSet execute(String query) { + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + ResultSet resultSet; + try (Scope ignored = context.makeCurrent()) { + resultSet = session.execute(query); + } catch (Throwable t) { + instrumenter().end(context, request, null, t); + throw t; + } + instrumenter().end(context, request, resultSet.getExecutionInfo(), null); + return resultSet; + } + + @Override + public ResultSet execute(String query, Object... values) { + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + ResultSet resultSet; + try (Scope ignored = context.makeCurrent()) { + resultSet = session.execute(query, values); + } catch (Throwable t) { + instrumenter().end(context, request, null, t); + throw t; + } + instrumenter().end(context, request, resultSet.getExecutionInfo(), null); + return resultSet; + } + + @Override + public ResultSet execute(String query, Map values) { + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + ResultSet resultSet; + try (Scope ignored = context.makeCurrent()) { + resultSet = session.execute(query, values); + } catch (Throwable t) { + instrumenter().end(context, request, null, t); + throw t; + } + instrumenter().end(context, request, resultSet.getExecutionInfo(), null); + return resultSet; + } + + @Override + public ResultSet execute(Statement statement) { + String query = getQuery(statement); + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + ResultSet resultSet; + try (Scope ignored = context.makeCurrent()) { + resultSet = session.execute(statement); + } catch (Throwable t) { + instrumenter().end(context, request, null, t); + throw t; + } + instrumenter().end(context, request, resultSet.getExecutionInfo(), null); + return resultSet; + } + + @Override + public ResultSetFuture executeAsync(String query) { + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + try (Scope ignored = context.makeCurrent()) { + ResultSetFuture future = session.executeAsync(query); + addCallbackToEndSpan(future, context, request); + return future; + } + } + + @Override + public ResultSetFuture executeAsync(String query, Object... values) { + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + try (Scope ignored = context.makeCurrent()) { + ResultSetFuture future = session.executeAsync(query, values); + addCallbackToEndSpan(future, context, request); + return future; + } + } + + @Override + public ResultSetFuture executeAsync(String query, Map values) { + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + try (Scope ignored = context.makeCurrent()) { + ResultSetFuture future = session.executeAsync(query, values); + addCallbackToEndSpan(future, context, request); + return future; + } + } + + @Override + public ResultSetFuture executeAsync(Statement statement) { + String query = getQuery(statement); + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + try (Scope ignored = context.makeCurrent()) { + ResultSetFuture future = session.executeAsync(statement); + addCallbackToEndSpan(future, context, request); + return future; + } + } + + @Override + public PreparedStatement prepare(String query) { + return session.prepare(query); + } + + @Override + public PreparedStatement prepare(RegularStatement statement) { + return session.prepare(statement); + } + + @Override + public ListenableFuture prepareAsync(String query) { + return session.prepareAsync(query); + } + + @Override + public ListenableFuture prepareAsync(RegularStatement statement) { + return session.prepareAsync(statement); + } + + @Override + public CloseFuture closeAsync() { + return session.closeAsync(); + } + + @Override + public void close() { + session.close(); + } + + @Override + public boolean isClosed() { + return session.isClosed(); + } + + @Override + public Cluster getCluster() { + return session.getCluster(); + } + + @Override + public State getState() { + return session.getState(); + } + + private static String getQuery(Statement statement) { + String query = null; + if (statement instanceof BoundStatement) { + query = ((BoundStatement) statement).preparedStatement().getQueryString(); + } else if (statement instanceof RegularStatement) { + query = ((RegularStatement) statement).getQueryString(); + } + + return query == null ? "" : query; + } + + private static void addCallbackToEndSpan( + ResultSetFuture future, Context context, CassandraRequest request) { + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(ResultSet resultSet) { + instrumenter().end(context, request, resultSet.getExecutionInfo(), null); + } + + @Override + public void onFailure(Throwable t) { + instrumenter().end(context, request, null, t); + } + }, + Runnable::run); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/test/groovy/CassandraClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/test/groovy/CassandraClientTest.groovy new file mode 100644 index 000000000..30c813d07 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-3.0/javaagent/src/test/groovy/CassandraClientTest.groovy @@ -0,0 +1,152 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.datastax.driver.core.Cluster +import com.datastax.driver.core.Session +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import spock.lang.Shared + +class CassandraClientTest extends AgentInstrumentationSpecification { + private static final Logger log = LoggerFactory.getLogger(CassandraClientTest) + + @Shared + def executor = Executors.newCachedThreadPool() + + @Shared + GenericContainer cassandra + @Shared + int cassandraPort + @Shared + Cluster cluster + + def setupSpec() { + cassandra = new GenericContainer("cassandra:3") + .withExposedPorts(9042) + .withLogConsumer(new Slf4jLogConsumer(log)) + .withStartupTimeout(Duration.ofSeconds(120)) + cassandra.start() + + cassandraPort = cassandra.getMappedPort(9042) + + cluster = Cluster.builder() + .addContactPointsWithPorts(new InetSocketAddress("localhost", cassandraPort)) + .build() + } + + def cleanupSpec() { + cluster?.close() + cassandra.stop() + } + + def "test sync"() { + setup: + Session session = cluster.connect(keyspace) + + session.execute(statement) + + expect: + assertTraces(keyspace ? 2 : 1) { + if (keyspace) { + trace(0, 1) { + cassandraSpan(it, 0, "DB Query", "USE $keyspace") + } + } + trace(keyspace ? 1 : 0, 1) { + cassandraSpan(it, 0, spanName, expectedStatement, operation, keyspace, table) + } + } + + cleanup: + session.close() + + where: + keyspace | statement | expectedStatement | spanName | operation | table + null | "DROP KEYSPACE IF EXISTS sync_test" | "DROP KEYSPACE IF EXISTS sync_test" | "DB Query" | null | null + null | "CREATE KEYSPACE sync_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}" | "CREATE KEYSPACE sync_test WITH REPLICATION = {?:?, ?:?}" | "DB Query" | null | null + "sync_test" | "CREATE TABLE sync_test.users ( id UUID PRIMARY KEY, name text )" | "CREATE TABLE sync_test.users ( id UUID PRIMARY KEY, name text )" | "sync_test" | null | null + "sync_test" | "INSERT INTO sync_test.users (id, name) values (uuid(), 'alice')" | "INSERT INTO sync_test.users (id, name) values (uuid(), ?)" | "INSERT sync_test.users" | "INSERT" | "sync_test.users" + "sync_test" | "SELECT * FROM users where name = 'alice' ALLOW FILTERING" | "SELECT * FROM users where name = ? ALLOW FILTERING" | "SELECT sync_test.users" | "SELECT" | "users" + } + + def "test async"() { + setup: + def callbackExecuted = new AtomicBoolean() + Session session = cluster.connect(keyspace) + runUnderTrace("parent") { + def future = session.executeAsync(statement) + future.addListener({ -> + runUnderTrace("callbackListener") { + callbackExecuted.set(true) + } + }, executor) + } + + expect: + assertTraces(keyspace ? 2 : 1) { + if (keyspace) { + trace(0, 1) { + cassandraSpan(it, 0, "DB Query", "USE $keyspace") + } + } + trace(keyspace ? 1 : 0, 3) { + basicSpan(it, 0, "parent") + cassandraSpan(it, 1, spanName, expectedStatement, operation, keyspace, table, span(0)) + basicSpan(it, 2, "callbackListener", span(0)) + } + } + + cleanup: + session.close() + + where: + keyspace | statement | expectedStatement | spanName | operation | table + null | "DROP KEYSPACE IF EXISTS async_test" | "DROP KEYSPACE IF EXISTS async_test" | "DB Query" | null | null + null | "CREATE KEYSPACE async_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}" | "CREATE KEYSPACE async_test WITH REPLICATION = {?:?, ?:?}" | "DB Query" | null | null + "async_test" | "CREATE TABLE async_test.users ( id UUID PRIMARY KEY, name text )" | "CREATE TABLE async_test.users ( id UUID PRIMARY KEY, name text )" | "async_test" | null | null + "async_test" | "INSERT INTO async_test.users (id, name) values (uuid(), 'alice')" | "INSERT INTO async_test.users (id, name) values (uuid(), ?)" | "INSERT async_test.users" | "INSERT" | "async_test.users" + "async_test" | "SELECT * FROM users where name = 'alice' ALLOW FILTERING" | "SELECT * FROM users where name = ? ALLOW FILTERING" | "SELECT async_test.users" | "SELECT" | "users" + } + + def cassandraSpan(TraceAssert trace, int index, String spanName, String statement, + String operation = null, + String keyspace = null, + String table = null, + Object parentSpan = null) { + trace.span(index) { + name spanName + kind CLIENT + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + attributes { + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_PORT.key" cassandraPort + "$SemanticAttributes.DB_SYSTEM.key" "cassandra" + "$SemanticAttributes.DB_NAME.key" keyspace + "$SemanticAttributes.DB_STATEMENT.key" statement + "$SemanticAttributes.DB_OPERATION.key" operation + "$SemanticAttributes.DB_CASSANDRA_KEYSPACE.key" keyspace + "$SemanticAttributes.DB_CASSANDRA_TABLE.key" table + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/cassandra-4.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/cassandra-4.0-javaagent.gradle new file mode 100644 index 000000000..025036a54 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/cassandra-4.0-javaagent.gradle @@ -0,0 +1,19 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.datastax.oss" + module = "java-driver-core" + versions = "[4.0,)" + assertInverse = true + } +} + +dependencies { + library "com.datastax.oss:java-driver-core:4.0.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + latestDepTestLibrary "com.datastax.oss:java-driver-core:4.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraAttributesExtractor.java new file mode 100644 index 000000000..03f0f956c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraAttributesExtractor.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.Node; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class CassandraAttributesExtractor + extends AttributesExtractor { + + @Override + protected void onStart(AttributesBuilder attributes, CassandraRequest request) { + set( + attributes, + SemanticAttributes.DB_CASSANDRA_KEYSPACE, + request.getSession().getKeyspace().map(CqlIdentifier::toString).orElse(null)); + } + + @Override + protected void onEnd( + AttributesBuilder attributes, + CassandraRequest request, + @Nullable ExecutionInfo executionInfo) { + if (executionInfo == null) { + return; + } + + Node coordinator = executionInfo.getCoordinator(); + if (coordinator != null) { + if (coordinator.getDatacenter() != null) { + set( + attributes, + SemanticAttributes.DB_CASSANDRA_COORDINATOR_DC, + coordinator.getDatacenter()); + } + if (coordinator.getHostId() != null) { + set( + attributes, + SemanticAttributes.DB_CASSANDRA_COORDINATOR_ID, + coordinator.getHostId().toString()); + } + } + set( + attributes, + SemanticAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT, + (long) executionInfo.getSpeculativeExecutionCount()); + + Statement statement = executionInfo.getStatement(); + DriverExecutionProfile config = + request.getSession().getContext().getConfig().getDefaultProfile(); + if (statement.getConsistencyLevel() != null) { + set( + attributes, + SemanticAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL, + statement.getConsistencyLevel().name()); + } else { + set( + attributes, + SemanticAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL, + config.getString(DefaultDriverOption.REQUEST_CONSISTENCY)); + } + if (statement.getPageSize() > 0) { + set(attributes, SemanticAttributes.DB_CASSANDRA_PAGE_SIZE, (long) statement.getPageSize()); + } else { + int pageSize = config.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE); + if (pageSize > 0) { + set(attributes, SemanticAttributes.DB_CASSANDRA_PAGE_SIZE, (long) pageSize); + } + } + if (statement.isIdempotent() != null) { + set(attributes, SemanticAttributes.DB_CASSANDRA_IDEMPOTENCE, statement.isIdempotent()); + } else { + set( + attributes, + SemanticAttributes.DB_CASSANDRA_IDEMPOTENCE, + config.getBoolean(DefaultDriverOption.REQUEST_DEFAULT_IDEMPOTENCE)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraClientInstrumentationModule.java new file mode 100644 index 000000000..fad308fe8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class CassandraClientInstrumentationModule extends InstrumentationModule { + public CassandraClientInstrumentationModule() { + super("cassandra", "cassandra-4.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new SessionBuilderInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetAttributesExtractor.java new file mode 100644 index 000000000..73f0193f4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetAttributesExtractor.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class CassandraNetAttributesExtractor + extends InetSocketAddressNetAttributesExtractor { + + @Override + @Nullable + public String transport(CassandraRequest request) { + return null; + } + + @Override + public @Nullable InetSocketAddress getAddress( + CassandraRequest request, @Nullable ExecutionInfo executionInfo) { + if (executionInfo == null) { + return null; + } + Node coordinator = executionInfo.getCoordinator(); + if (coordinator == null) { + return null; + } + // resolve() returns an existing InetSocketAddress, it does not do a dns resolve, + // at least in the only current EndPoint implementation (DefaultEndPoint) + SocketAddress address = coordinator.getEndPoint().resolve(); + return address instanceof InetSocketAddress ? (InetSocketAddress) address : null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraRequest.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraRequest.java new file mode 100644 index 000000000..9c783cc3f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraRequest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import com.datastax.oss.driver.api.core.session.Session; +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class CassandraRequest { + + public static CassandraRequest create(Session session, String statement) { + return new AutoValue_CassandraRequest(session, statement); + } + + public abstract Session getSession(); + + public abstract String getStatement(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSingletons.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSingletons.java new file mode 100644 index 000000000..17347f21f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSingletons.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; + +public final class CassandraSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.cassandra-4.0"; + + // using ExecutionInfo because we can get that from ResultSet, AsyncResultSet and DriverException + private static final Instrumenter INSTRUMENTER; + + static { + DbAttributesExtractor attributesExtractor = + new CassandraSqlAttributesExtractor(); + SpanNameExtractor spanName = DbSpanNameExtractor.create(attributesExtractor); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(attributesExtractor) + .addAttributesExtractor(new CassandraNetAttributesExtractor()) + .addAttributesExtractor(new CassandraAttributesExtractor()) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private CassandraSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSqlAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSqlAttributesExtractor.java new file mode 100644 index 000000000..37661aab6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSqlAttributesExtractor.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.api.instrumenter.db.SqlAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class CassandraSqlAttributesExtractor + extends SqlAttributesExtractor { + + @Override + protected String system(CassandraRequest request) { + return SemanticAttributes.DbSystemValues.CASSANDRA; + } + + @Override + @Nullable + protected String user(CassandraRequest request) { + return null; + } + + @Override + @Nullable + protected String name(CassandraRequest request) { + return request.getSession().getKeyspace().map(CqlIdentifier::toString).orElse(null); + } + + @Override + @Nullable + protected String connectionString(CassandraRequest request) { + return null; + } + + @Override + protected AttributeKey dbTableAttribute() { + return SemanticAttributes.DB_CASSANDRA_TABLE; + } + + @Override + @Nullable + protected String rawStatement(CassandraRequest request) { + return request.getStatement(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CompletionStageFunction.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CompletionStageFunction.java new file mode 100644 index 000000000..14aa73c84 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CompletionStageFunction.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import com.datastax.oss.driver.api.core.CqlSession; +import java.util.function.Function; + +public class CompletionStageFunction implements Function { + + @Override + public Object apply(Object session) { + if (session == null) { + return null; + } + // This should cover ours and OT's TracingCqlSession + if (session.getClass().getName().endsWith("cassandra4.TracingCqlSession")) { + return session; + } + return new TracingCqlSession((CqlSession) session); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/SessionBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/SessionBuilderInstrumentation.java new file mode 100644 index 000000000..0520360c2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/SessionBuilderInstrumentation.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.concurrent.CompletionStage; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class SessionBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + // Note: Cassandra has a large driver and we instrument single class in it. + // The rest is ignored in AdditionalLibraryIgnoresMatcher + return named("com.datastax.oss.driver.api.core.session.SessionBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("buildAsync")).and(takesArguments(0)), + SessionBuilderInstrumentation.class.getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + /** + * Strategy: each time we build a connection to a Cassandra cluster, the + * com.datastax.oss.driver.api.core.session.SessionBuilder.buildAsync() method is called. The + * opentracing contribution is a simple wrapper, so we just have to wrap the new session. + * + * @param stage The fresh CompletionStage to patch. This stage produces session which is + * replaced with new session + */ + @Advice.OnMethodExit(suppress = Throwable.class) + public static void injectTracingSession( + @Advice.Return(readOnly = false) CompletionStage stage) { + stage = stage.thenApply(new CompletionStageFunction()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/TracingCqlSession.java b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/TracingCqlSession.java new file mode 100644 index 000000000..b6126c51f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/TracingCqlSession.java @@ -0,0 +1,244 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + +import static io.opentelemetry.javaagent.instrumentation.cassandra.v4_0.CassandraSingletons.instrumenter; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metrics.Metrics; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class TracingCqlSession implements CqlSession { + private final CqlSession session; + + public TracingCqlSession(CqlSession session) { + this.session = session; + } + + @Override + public PreparedStatement prepare(SimpleStatement statement) { + return session.prepare(statement); + } + + @Override + public PreparedStatement prepare(String query) { + return session.prepare(query); + } + + @Override + public PreparedStatement prepare(PrepareRequest request) { + return session.prepare(request); + } + + @Override + public CompletionStage prepareAsync(SimpleStatement statement) { + return session.prepareAsync(statement); + } + + @Override + public CompletionStage prepareAsync(String query) { + return session.prepareAsync(query); + } + + @Override + public CompletionStage prepareAsync(PrepareRequest request) { + return session.prepareAsync(request); + } + + @Override + public String getName() { + return session.getName(); + } + + @Override + public Metadata getMetadata() { + return session.getMetadata(); + } + + @Override + public boolean isSchemaMetadataEnabled() { + return session.isSchemaMetadataEnabled(); + } + + @Override + public CompletionStage setSchemaMetadataEnabled(@Nullable Boolean newValue) { + return session.setSchemaMetadataEnabled(newValue); + } + + @Override + public CompletionStage refreshSchemaAsync() { + return session.refreshSchemaAsync(); + } + + @Override + public Metadata refreshSchema() { + return session.refreshSchema(); + } + + @Override + public CompletionStage checkSchemaAgreementAsync() { + return session.checkSchemaAgreementAsync(); + } + + @Override + public boolean checkSchemaAgreement() { + return session.checkSchemaAgreement(); + } + + @Override + public DriverContext getContext() { + return session.getContext(); + } + + @Override + public Optional getKeyspace() { + return session.getKeyspace(); + } + + @Override + public Optional getMetrics() { + return session.getMetrics(); + } + + @Override + public CompletionStage closeFuture() { + return session.closeFuture(); + } + + @Override + public boolean isClosed() { + return session.isClosed(); + } + + @Override + public CompletionStage closeAsync() { + return session.closeAsync(); + } + + @Override + public CompletionStage forceCloseAsync() { + return session.forceCloseAsync(); + } + + @Override + public void close() { + session.close(); + } + + @Override + @Nullable + public RESULT execute( + REQUEST request, GenericType resultType) { + return session.execute(request, resultType); + } + + @Override + public ResultSet execute(String query) { + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + ResultSet resultSet; + try (Scope ignored = context.makeCurrent()) { + resultSet = session.execute(query); + } catch (RuntimeException e) { + instrumenter().end(context, request, getExecutionInfo(e), e); + throw e; + } + instrumenter().end(context, request, resultSet.getExecutionInfo(), null); + return resultSet; + } + + @Override + public ResultSet execute(Statement statement) { + String query = getQuery(statement); + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + ResultSet resultSet; + try (Scope ignored = context.makeCurrent()) { + resultSet = session.execute(statement); + } catch (RuntimeException e) { + instrumenter().end(context, request, getExecutionInfo(e), e); + throw e; + } + instrumenter().end(context, request, resultSet.getExecutionInfo(), null); + return resultSet; + } + + @Override + public CompletionStage executeAsync(Statement statement) { + String query = getQuery(statement); + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + try (Scope ignored = context.makeCurrent()) { + CompletionStage stage = session.executeAsync(statement); + return stage.whenComplete( + (asyncResultSet, throwable) -> + instrumenter() + .end(context, request, getExecutionInfo(asyncResultSet, throwable), throwable)); + } + } + + @Override + public CompletionStage executeAsync(String query) { + CassandraRequest request = CassandraRequest.create(session, query); + Context context = instrumenter().start(Context.current(), request); + try (Scope ignored = context.makeCurrent()) { + CompletionStage stage = session.executeAsync(query); + return stage.whenComplete( + (asyncResultSet, throwable) -> + instrumenter() + .end(context, request, getExecutionInfo(asyncResultSet, throwable), throwable)); + } + } + + private static String getQuery(Statement statement) { + String query = null; + if (statement instanceof SimpleStatement) { + query = ((SimpleStatement) statement).getQuery(); + } else if (statement instanceof BoundStatement) { + query = ((BoundStatement) statement).getPreparedStatement().getQuery(); + } + + return query == null ? "" : query; + } + + private static ExecutionInfo getExecutionInfo( + @Nullable AsyncResultSet asyncResultSet, @Nullable Throwable throwable) { + if (asyncResultSet != null) { + return asyncResultSet.getExecutionInfo(); + } else { + return getExecutionInfo(throwable); + } + } + + private static ExecutionInfo getExecutionInfo(@Nullable Throwable throwable) { + if (throwable instanceof DriverException) { + return ((DriverException) throwable).getExecutionInfo(); + } else if (throwable != null && throwable.getCause() instanceof DriverException) { + // TODO (trask) find out if this is needed and if so add comment explaining + return ((DriverException) throwable.getCause()).getExecutionInfo(); + } else { + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/test/groovy/CassandraClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/test/groovy/CassandraClientTest.groovy new file mode 100644 index 000000000..338d9dd2b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cassandra/cassandra-4.0/javaagent/src/test/groovy/CassandraClientTest.groovy @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.datastax.oss.driver.api.core.CqlSession +import com.datastax.oss.driver.api.core.config.DefaultDriverOption +import com.datastax.oss.driver.api.core.config.DriverConfigLoader +import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.time.Duration +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import spock.lang.Shared + +class CassandraClientTest extends AgentInstrumentationSpecification { + private static final Logger log = LoggerFactory.getLogger(CassandraClientTest) + + @Shared + GenericContainer cassandra + @Shared + int cassandraPort + + def setupSpec() { + cassandra = new GenericContainer("cassandra:4.0") + .withExposedPorts(9042) + .withLogConsumer(new Slf4jLogConsumer(log)) + .withStartupTimeout(Duration.ofSeconds(120)) + cassandra.start() + + cassandraPort = cassandra.getMappedPort(9042) + } + + def cleanupSpec() { + cassandra.stop() + } + + def "test sync"() { + setup: + CqlSession session = getSession(keyspace) + + session.execute(statement) + + expect: + assertTraces(1) { + trace(0, 1) { + cassandraSpan(it, 0, spanName, expectedStatement, operation, keyspace, table) + } + } + + cleanup: + session.close() + + where: + keyspace | statement | expectedStatement | spanName | operation | table + null | "DROP KEYSPACE IF EXISTS sync_test" | "DROP KEYSPACE IF EXISTS sync_test" | "DB Query" | null | null + null | "CREATE KEYSPACE sync_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}" | "CREATE KEYSPACE sync_test WITH REPLICATION = {?:?, ?:?}" | "DB Query" | null | null + "sync_test" | "CREATE TABLE sync_test.users ( id UUID PRIMARY KEY, name text )" | "CREATE TABLE sync_test.users ( id UUID PRIMARY KEY, name text )" | "sync_test" | null | null + "sync_test" | "INSERT INTO sync_test.users (id, name) values (uuid(), 'alice')" | "INSERT INTO sync_test.users (id, name) values (uuid(), ?)" | "INSERT sync_test.users" | "INSERT" | "sync_test.users" + "sync_test" | "SELECT * FROM users where name = 'alice' ALLOW FILTERING" | "SELECT * FROM users where name = ? ALLOW FILTERING" | "SELECT sync_test.users" | "SELECT" | "users" + } + + def "test async"() { + setup: + CqlSession session = getSession(keyspace) + + runUnderTrace("parent") { + session.executeAsync(statement).toCompletableFuture().get() + } + + expect: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + cassandraSpan(it, 1, spanName, expectedStatement, operation, keyspace, table, span(0)) + } + } + + cleanup: + session.close() + + where: + keyspace | statement | expectedStatement | spanName | operation | table + null | "DROP KEYSPACE IF EXISTS async_test" | "DROP KEYSPACE IF EXISTS async_test" | "DB Query" | null | null + null | "CREATE KEYSPACE async_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}" | "CREATE KEYSPACE async_test WITH REPLICATION = {?:?, ?:?}" | "DB Query" | null | null + "async_test" | "CREATE TABLE async_test.users ( id UUID PRIMARY KEY, name text )" | "CREATE TABLE async_test.users ( id UUID PRIMARY KEY, name text )" | "async_test" | null | null + "async_test" | "INSERT INTO async_test.users (id, name) values (uuid(), 'alice')" | "INSERT INTO async_test.users (id, name) values (uuid(), ?)" | "INSERT async_test.users" | "INSERT" | "async_test.users" + "async_test" | "SELECT * FROM users where name = 'alice' ALLOW FILTERING" | "SELECT * FROM users where name = ? ALLOW FILTERING" | "SELECT async_test.users" | "SELECT" | "users" + } + + def cassandraSpan(TraceAssert trace, int index, String spanName, String statement, String operation, String keyspace, String table, Object parentSpan = null) { + trace.span(index) { + name spanName + kind CLIENT + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + attributes { + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_PORT.key" cassandraPort + "$SemanticAttributes.DB_SYSTEM.key" "cassandra" + "$SemanticAttributes.DB_NAME.key" keyspace + "$SemanticAttributes.DB_STATEMENT.key" statement + "$SemanticAttributes.DB_OPERATION.key" operation + "$SemanticAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL.key" "LOCAL_ONE" + "$SemanticAttributes.DB_CASSANDRA_COORDINATOR_DC.key" "datacenter1" + "$SemanticAttributes.DB_CASSANDRA_COORDINATOR_ID.key" String + "$SemanticAttributes.DB_CASSANDRA_IDEMPOTENCE.key" Boolean + "$SemanticAttributes.DB_CASSANDRA_PAGE_SIZE.key" 5000 + "$SemanticAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT.key" 0 + "$SemanticAttributes.DB_CASSANDRA_KEYSPACE.key" keyspace + // the SqlStatementSanitizer can't handle CREATE statements yet + "$SemanticAttributes.DB_CASSANDRA_TABLE.key" table + } + } + } + + def getSession(String keyspace) { + DriverConfigLoader configLoader = DefaultDriverConfigLoader.builder() + .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(0)) + .build() + return CqlSession.builder() + .addContactPoint(new InetSocketAddress("localhost", cassandraPort)) + .withConfigLoader(configLoader) + .withLocalDatacenter("datacenter1") + .withKeyspace((String) keyspace) + .build() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cdi-testing/cdi-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/cdi-testing/cdi-testing.gradle new file mode 100644 index 000000000..18173071d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cdi-testing/cdi-testing.gradle @@ -0,0 +1,14 @@ +ext { + skipPublish = true +} +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + testLibrary "org.jboss.weld:weld-core:2.3.0.Final" + testLibrary "org.jboss.weld.se:weld-se:2.3.0.Final" + testLibrary "org.jboss.weld.se:weld-se-core:2.3.0.Final" + + latestDepTestLibrary "org.jboss.weld:weld-core:2.+" + latestDepTestLibrary "org.jboss.weld.se:weld-se:2.+" + latestDepTestLibrary "org.jboss.weld.se:weld-se-core:2.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cdi-testing/src/test/groovy/CDIContainerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/cdi-testing/src/test/groovy/CDIContainerTest.groovy new file mode 100644 index 000000000..3a34b98a9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cdi-testing/src/test/groovy/CDIContainerTest.groovy @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.jboss.weld.environment.se.Weld +import org.jboss.weld.environment.se.WeldContainer +import org.jboss.weld.environment.se.threading.RunnableDecorator + +class CDIContainerTest extends AgentInstrumentationSpecification { + + def "CDI container starts with agent"() { + given: + Weld builder = new Weld() + .disableDiscovery() + .addDecorator(RunnableDecorator) + .addBeanClass(TestBean) + + when: + WeldContainer container = builder.initialize() + + then: + container.isRunning() + + cleanup: + container?.shutdown() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/cdi-testing/src/test/java/TestBean.java b/opentelemetry-java-instrumentation/instrumentation/cdi-testing/src/test/java/TestBean.java new file mode 100644 index 000000000..d7fba0156 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/cdi-testing/src/test/java/TestBean.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +public class TestBean { + + private String someField; + + public String getSomeField() { + return someField; + } + + public void setSomeField(String someField) { + this.someField = someField; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent-unit-tests/couchbase-2.0-javaagent-unittests.gradle b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent-unit-tests/couchbase-2.0-javaagent-unittests.gradle new file mode 100644 index 000000000..c10828735 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent-unit-tests/couchbase-2.0-javaagent-unittests.gradle @@ -0,0 +1,10 @@ +apply plugin: "otel.java-conventions" + +dependencies { + testImplementation "org.codehaus.groovy:groovy-all" + testImplementation "org.spockframework:spock-core" + + testImplementation project(':instrumentation-api') + testImplementation project(':instrumentation:couchbase:couchbase-2.0:javaagent') + testImplementation "com.couchbase.client:java-client:2.5.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.groovy new file mode 100644 index 000000000..ec3bcd9ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.groovy @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0 + +import com.couchbase.client.java.analytics.AnalyticsQuery +import com.couchbase.client.java.query.N1qlQuery +import com.couchbase.client.java.query.Select +import com.couchbase.client.java.query.dsl.Expression +import com.couchbase.client.java.view.SpatialViewQuery +import com.couchbase.client.java.view.ViewQuery +import spock.lang.Specification +import spock.lang.Unroll + +class CouchbaseQuerySanitizerTest extends Specification { + @Unroll + def "should normalize #desc query"() { + when: + def normalized = CouchbaseQuerySanitizer.sanitize(query).getFullStatement() + + then: + // the analytics query ends up with trailing ';' in earlier couchbase version, but no trailing ';' in later couchbase version + normalized.replaceFirst(';$', '') == expected + + where: + desc | query | expected + "plain string" | "SELECT field1 FROM `test` WHERE field2 = 'asdf'" | "SELECT field1 FROM `test` WHERE field2 = ?" + "Statement" | Select.select("field1").from("test").where(Expression.path("field2").eq(Expression.s("asdf"))) | "SELECT field1 FROM test WHERE field2 = ?" + "N1QL" | N1qlQuery.simple("SELECT field1 FROM `test` WHERE field2 = 'asdf'") | "SELECT field1 FROM `test` WHERE field2 = ?" + "Analytics" | AnalyticsQuery.simple("SELECT field1 FROM `test` WHERE field2 = 'asdf'") | "SELECT field1 FROM `test` WHERE field2 = ?" + "View" | ViewQuery.from("design", "view").skip(10) | 'ViewQuery(design/view){params="skip=10"}' + "SpatialView" | SpatialViewQuery.from("design", "view").skip(10) | 'skip=10' + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/couchbase-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/couchbase-2.0-javaagent.gradle new file mode 100644 index 000000000..86dc4e7da --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/couchbase-2.0-javaagent.gradle @@ -0,0 +1,37 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + // Version 2.7.5 and 2.7.8 were not released properly and muzzle cannot test against it causing failure. + // So we have to skip them resulting in this verbose setup. + pass { + group = 'com.couchbase.client' + module = 'java-client' + versions = "[2.0.0,2.7.5)" + } + pass { + group = 'com.couchbase.client' + module = 'java-client' + versions = "[2.7.6,2.7.8)" + } + pass { + group = 'com.couchbase.client' + module = 'java-client' + versions = "[2.7.9,3.0.0)" + } + fail { + group = 'com.couchbase.client' + module = 'couchbase-client' + versions = "(,)" + } +} + +dependencies { + implementation project(':instrumentation:rxjava:rxjava-1.0:library') + + library "com.couchbase.client:java-client:2.0.0" + + testImplementation project(':instrumentation:couchbase:couchbase-testing') + + latestDepTestLibrary "org.springframework.data:spring-data-couchbase:3.+" + latestDepTestLibrary "com.couchbase.client:java-client:2.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseBucketInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseBucketInstrumentation.java new file mode 100644 index 000000000..c884f7747 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseBucketInstrumentation.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.couchbase.client.java.CouchbaseCluster; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import rx.Observable; + +public class CouchbaseBucketInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "com.couchbase.client.java.bucket.DefaultAsyncBucketManager", + "com.couchbase.client.java.CouchbaseAsyncBucket"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(returns(named("rx.Observable"))).and(not(named("query"))), + CouchbaseBucketInstrumentation.class.getName() + "$CouchbaseClientAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(returns(named("rx.Observable"))).and(named("query")), + CouchbaseBucketInstrumentation.class.getName() + "$CouchbaseClientQueryAdvice"); + } + + @SuppressWarnings("unused") + public static class CouchbaseClientAdvice { + + @Advice.OnMethodEnter + public static int trackCallDepth() { + return CallDepthThreadLocalMap.incrementCallDepth(CouchbaseCluster.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void subscribeResult( + @Advice.Enter int callDepth, + @Advice.Origin Method method, + @Advice.FieldValue("bucket") String bucket, + @Advice.Return(readOnly = false) Observable result) { + if (callDepth > 0) { + return; + } + CallDepthThreadLocalMap.reset(CouchbaseCluster.class); + result = Observable.create(CouchbaseOnSubscribe.create(result, bucket, method)); + } + } + + @SuppressWarnings("unused") + public static class CouchbaseClientQueryAdvice { + + @Advice.OnMethodEnter + public static int trackCallDepth() { + return CallDepthThreadLocalMap.incrementCallDepth(CouchbaseCluster.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void subscribeResult( + @Advice.Enter int callDepth, + @Advice.Origin Method method, + @Advice.FieldValue("bucket") String bucket, + @Advice.Argument(value = 0, optional = true) Object query, + @Advice.Return(readOnly = false) Observable result) { + if (callDepth > 0) { + return; + } + CallDepthThreadLocalMap.reset(CouchbaseCluster.class); + + if (query != null) { + // A query can be of many different types. We could track the creation of them and try to + // rewind back to when they were created from a string, but for now we rely on toString() + // returning something useful. That seems to be the case. If we're starting to see strange + // query texts, this is the place to look! + result = Observable.create(CouchbaseOnSubscribe.create(result, bucket, query)); + } else { + result = Observable.create(CouchbaseOnSubscribe.create(result, bucket, method)); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseClientTracer.java new file mode 100644 index 000000000..226b3987c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseClientTracer.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; + +import io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; + +public class CouchbaseClientTracer extends DatabaseClientTracer { + private static final CouchbaseClientTracer TRACER = new CouchbaseClientTracer(); + + private CouchbaseClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static CouchbaseClientTracer tracer() { + return TRACER; + } + + @Override + protected String spanName(Void connection, Method method, Void sanitizedStatement) { + Class declaringClass = method.getDeclaringClass(); + String className = + declaringClass.getSimpleName().replace("CouchbaseAsync", "").replace("DefaultAsync", ""); + return className + "." + method.getName(); + } + + @Override + protected Void sanitizeStatement(Method method) { + return null; + } + + @Override + protected String dbSystem(Void connection) { + return DbSystemValues.COUCHBASE; + } + + @Override + protected InetSocketAddress peerAddress(Void connection) { + return null; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.couchbase-2.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseClusterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseClusterInstrumentation.java new file mode 100644 index 000000000..6f3b8baff --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseClusterInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.couchbase.client.java.CouchbaseCluster; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import rx.Observable; + +public class CouchbaseClusterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "com.couchbase.client.java.cluster.DefaultAsyncClusterManager", + "com.couchbase.client.java.CouchbaseAsyncCluster"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(returns(named("rx.Observable"))).and(not(named("core"))), + CouchbaseClusterInstrumentation.class.getName() + "$CouchbaseClientAdvice"); + } + + @SuppressWarnings("unused") + public static class CouchbaseClientAdvice { + + @Advice.OnMethodEnter + public static int trackCallDepth() { + return CallDepthThreadLocalMap.incrementCallDepth(CouchbaseCluster.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void subscribeResult( + @Advice.Enter int callDepth, + @Advice.Origin Method method, + @Advice.Return(readOnly = false) Observable result) { + if (callDepth > 0) { + return; + } + CallDepthThreadLocalMap.reset(CouchbaseCluster.class); + + result = Observable.create(CouchbaseOnSubscribe.create(result, null, method)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseInstrumentationModule.java new file mode 100644 index 000000000..6cff8c4f1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class CouchbaseInstrumentationModule extends InstrumentationModule { + public CouchbaseInstrumentationModule() { + super("couchbase", "couchbase-2.0"); + } + + @Override + public boolean isHelperClass(String className) { + return className.equals("rx.__OpenTelemetryTracingUtil"); + } + + @Override + public List typeInstrumentations() { + return asList(new CouchbaseBucketInstrumentation(), new CouchbaseClusterInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseOnSubscribe.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseOnSubscribe.java new file mode 100644 index 000000000..4d10f19d9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseOnSubscribe.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer.conventionSpanName; +import static io.opentelemetry.javaagent.instrumentation.couchbase.v2_0.CouchbaseClientTracer.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.rxjava.TracedOnSubscribe; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; +import java.lang.reflect.Method; +import rx.Observable; + +public class CouchbaseOnSubscribe extends TracedOnSubscribe { + private final String bucket; + private final String query; + + public static CouchbaseOnSubscribe create( + Observable originalObservable, String bucket, Method method) { + Class declaringClass = method.getDeclaringClass(); + String className = + declaringClass.getSimpleName().replace("CouchbaseAsync", "").replace("DefaultAsync", ""); + String operation = className + "." + method.getName(); + return new CouchbaseOnSubscribe<>(originalObservable, operation, bucket, operation); + } + + public static CouchbaseOnSubscribe create( + Observable originalObservable, String bucket, Object query) { + SqlStatementInfo statement = CouchbaseQuerySanitizer.sanitize(query); + String spanName = + conventionSpanName( + bucket, statement.getOperation(), statement.getTable(), statement.getFullStatement()); + return new CouchbaseOnSubscribe<>( + originalObservable, spanName, bucket, statement.getFullStatement()); + } + + private CouchbaseOnSubscribe( + Observable originalObservable, String spanName, String bucket, String query) { + super(originalObservable, spanName, tracer(), CLIENT); + + this.bucket = bucket; + this.query = query; + } + + @Override + protected void decorateSpan(Span span) { + span.setAttribute(SemanticAttributes.DB_SYSTEM, DbSystemValues.COUCHBASE); + span.setAttribute(SemanticAttributes.DB_NAME, bucket); + span.setAttribute(SemanticAttributes.DB_STATEMENT, query); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizer.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizer.java new file mode 100644 index 000000000..db487f9a9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizer.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; + +import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class CouchbaseQuerySanitizer { + @Nullable private static final Class QUERY_CLASS; + @Nullable private static final Class STATEMENT_CLASS; + @Nullable private static final Class N1QL_QUERY_CLASS; + @Nullable private static final MethodHandle N1QL_GET_STATEMENT; + @Nullable private static final Class ANALYTICS_QUERY_CLASS; + @Nullable private static final MethodHandle ANALYTICS_GET_STATEMENT; + + static { + Class queryClass; + try { + queryClass = Class.forName("com.couchbase.client.java.query.Query"); + } catch (Exception e) { + queryClass = null; + } + QUERY_CLASS = queryClass; + + Class statementClass; + try { + statementClass = Class.forName("com.couchbase.client.java.query.Statement"); + } catch (Exception e) { + statementClass = null; + } + STATEMENT_CLASS = statementClass; + + Class n1qlQueryClass; + MethodHandle n1qlGetStatement; + try { + n1qlQueryClass = Class.forName("com.couchbase.client.java.query.N1qlQuery"); + n1qlGetStatement = + MethodHandles.publicLookup() + .findVirtual( + n1qlQueryClass, + "statement", + MethodType.methodType( + Class.forName("com.couchbase.client.java.query.Statement"))); + } catch (Exception e) { + n1qlQueryClass = null; + n1qlGetStatement = null; + } + N1QL_QUERY_CLASS = n1qlQueryClass; + N1QL_GET_STATEMENT = n1qlGetStatement; + + Class analyticsQueryClass; + MethodHandle analyticsGetStatement; + try { + analyticsQueryClass = Class.forName("com.couchbase.client.java.analytics.AnalyticsQuery"); + analyticsGetStatement = + MethodHandles.publicLookup() + .findVirtual(analyticsQueryClass, "statement", MethodType.methodType(String.class)); + } catch (Exception e) { + analyticsQueryClass = null; + analyticsGetStatement = null; + } + ANALYTICS_QUERY_CLASS = analyticsQueryClass; + ANALYTICS_GET_STATEMENT = analyticsGetStatement; + } + + public static SqlStatementInfo sanitize(Object query) { + if (query instanceof String) { + return sanitizeString((String) query); + } + // Query is present in Couchbase [2.0.0, 2.2.0) + // Statement is present starting from Couchbase 2.1.0 + if ((QUERY_CLASS != null && QUERY_CLASS.isAssignableFrom(query.getClass())) + || (STATEMENT_CLASS != null && STATEMENT_CLASS.isAssignableFrom(query.getClass()))) { + return sanitizeString(query.toString()); + } + // SpatialViewQuery is present starting from Couchbase 2.1.0 + String queryClassName = query.getClass().getName(); + if (queryClassName.equals("com.couchbase.client.java.view.ViewQuery") + || queryClassName.equals("com.couchbase.client.java.view.SpatialViewQuery")) { + return SqlStatementInfo.create(query.toString(), null, null); + } + // N1qlQuery is present starting from Couchbase 2.2.0 + if (N1QL_QUERY_CLASS != null && N1QL_QUERY_CLASS.isAssignableFrom(query.getClass())) { + String statement = getStatementString(N1QL_GET_STATEMENT, query); + if (statement != null) { + return sanitizeString(statement); + } + } + // AnalyticsQuery is present starting from Couchbase 2.4.3 + if (ANALYTICS_QUERY_CLASS != null && ANALYTICS_QUERY_CLASS.isAssignableFrom(query.getClass())) { + String statement = getStatementString(ANALYTICS_GET_STATEMENT, query); + if (statement != null) { + return sanitizeString(statement); + } + } + return SqlStatementInfo.create(query.getClass().getSimpleName(), null, null); + } + + private static String getStatementString(MethodHandle handle, Object query) { + if (handle == null) { + return null; + } + try { + return handle.invoke(query).toString(); + } catch (Throwable throwable) { + return null; + } + } + + private static SqlStatementInfo sanitizeString(String query) { + return SqlStatementSanitizer.sanitize(query); + } + + private CouchbaseQuerySanitizer() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/CouchbaseAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/CouchbaseAsyncClientTest.groovy new file mode 100644 index 000000000..43b302320 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/CouchbaseAsyncClientTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class CouchbaseAsyncClientTest extends AbstractCouchbaseAsyncClientTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/CouchbaseClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/CouchbaseClientTest.groovy new file mode 100644 index 000000000..aed568b0b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/CouchbaseClientTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class CouchbaseClientTest extends AbstractCouchbaseClientTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy new file mode 100644 index 000000000..b59b287da --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +class CouchbaseSpringRepositoryTest extends AbstractCouchbaseSpringRepositoryTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy new file mode 100644 index 000000000..3f355463f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.0/javaagent/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +class CouchbaseSpringTemplateTest extends AbstractCouchbaseSpringTemplateTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/couchbase-2.6-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/couchbase-2.6-javaagent.gradle new file mode 100644 index 000000000..52dc4b0d0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/couchbase-2.6-javaagent.gradle @@ -0,0 +1,37 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'com.couchbase.client' + module = 'java-client' + versions = "[2.6.0,3)" + // these versions were released as ".bundle" instead of ".jar" + skip('2.7.5', '2.7.8') + assertInverse = true + } + fail { + group = 'com.couchbase.client' + module = 'couchbase-client' + versions = "(,)" + } +} + +dependencies { + implementation project(':instrumentation:rxjava:rxjava-1.0:library') + + library "com.couchbase.client:java-client:2.6.0" + + testInstrumentation project(':instrumentation:couchbase:couchbase-2.0:javaagent') + testImplementation project(':instrumentation:couchbase:couchbase-testing') + + testLibrary "org.springframework.data:spring-data-couchbase:3.1.0.RELEASE" + testLibrary "com.couchbase.client:encryption:1.0.0" + + latestDepTestLibrary "org.springframework.data:spring-data-couchbase:3.1+" + latestDepTestLibrary "com.couchbase.client:java-client:2.+" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.couchbase.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseConfig.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseConfig.java new file mode 100644 index 000000000..4cf0454cc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseConfig.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_6; + +import io.opentelemetry.instrumentation.api.config.Config; + +public final class CouchbaseConfig { + + public static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.couchbase.experimental-span-attributes", false); + + private CouchbaseConfig() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseCoreInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseCoreInstrumentation.java new file mode 100644 index 000000000..333ec4942 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseCoreInstrumentation.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_6; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.couchbase.client.core.message.CouchbaseRequest; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class CouchbaseCoreInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.couchbase.client.core.CouchbaseCore"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(takesArgument(0, named("com.couchbase.client.core.message.CouchbaseRequest"))) + .and(named("send")), + CouchbaseCoreInstrumentation.class.getName() + "$CouchbaseCoreAdvice"); + } + + @SuppressWarnings("unused") + public static class CouchbaseCoreAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void addOperationIdToSpan(@Advice.Argument(0) CouchbaseRequest request) { + + Span parentSpan = Java8BytecodeBridge.currentSpan(); + if (parentSpan != null) { + // The scope from the initial rxJava subscribe is not available to the networking layer + // To transfer the span, the span is added to the context store + + ContextStore contextStore = + InstrumentationContext.get(CouchbaseRequest.class, Span.class); + + Span span = contextStore.get(request); + + if (span == null) { + span = parentSpan; + contextStore.put(request, span); + if (CouchbaseConfig.CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("couchbase.operation_id", request.operationId()); + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseInstrumentationModule.java new file mode 100644 index 000000000..2ff3f0937 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_6; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class CouchbaseInstrumentationModule extends InstrumentationModule { + public CouchbaseInstrumentationModule() { + super("couchbase", "couchbase-2.6"); + } + + @Override + public List typeInstrumentations() { + return asList(new CouchbaseCoreInstrumentation(), new CouchbaseNetworkInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseNetworkInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseNetworkInstrumentation.java new file mode 100644 index 000000000..69b4f6916 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_6/CouchbaseNetworkInstrumentation.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_6; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.couchbase.client.core.message.CouchbaseRequest; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class CouchbaseNetworkInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.couchbase.client.core.endpoint.AbstractGenericHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + // Exact class because private fields are used + return nameStartsWith("com.couchbase.client.") + .and(extendsClass(named("com.couchbase.client.core.endpoint.AbstractGenericHandler"))); + } + + @Override + public void transform(TypeTransformer transformer) { + // encode(ChannelHandlerContext ctx, REQUEST msg, List out) + transformer.applyAdviceToMethod( + isMethod() + .and(named("encode")) + .and(takesArguments(3)) + .and( + takesArgument( + 0, named("com.couchbase.client.deps.io.netty.channel.ChannelHandlerContext"))) + .and(takesArgument(2, List.class)), + CouchbaseNetworkInstrumentation.class.getName() + "$CouchbaseNetworkAdvice"); + } + + @SuppressWarnings("unused") + public static class CouchbaseNetworkAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void addNetworkTagsToSpan( + @Advice.FieldValue("remoteHostname") String remoteHostname, + @Advice.FieldValue("remoteSocket") String remoteSocket, + @Advice.FieldValue("localSocket") String localSocket, + @Advice.Argument(1) CouchbaseRequest request) { + ContextStore contextStore = + InstrumentationContext.get(CouchbaseRequest.class, Span.class); + + Span span = contextStore.get(request); + if (span != null) { + NetPeerAttributes.INSTANCE.setNetPeer(span, remoteHostname, null); + + if (remoteSocket != null) { + int splitIndex = remoteSocket.lastIndexOf(":"); + if (splitIndex != -1) { + span.setAttribute( + SemanticAttributes.NET_PEER_PORT, + (long) Integer.parseInt(remoteSocket.substring(splitIndex + 1))); + } + } + + if (CouchbaseConfig.CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("couchbase.local.address", localSocket); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseAsyncClient26Test.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseAsyncClient26Test.groovy new file mode 100644 index 000000000..182bf0759 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseAsyncClient26Test.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert + +class CouchbaseAsyncClient26Test extends AbstractCouchbaseAsyncClientTest { + + @Override + void assertCouchbaseCall(TraceAssert trace, int index, Object name, String bucketName = null, Object parentSpan = null, Object statement = null) { + CouchbaseSpanUtil.assertCouchbaseCall(trace, index, name, bucketName, parentSpan, statement) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseClient26Test.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseClient26Test.groovy new file mode 100644 index 000000000..2baea95d7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseClient26Test.groovy @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert + +class CouchbaseClient26Test extends AbstractCouchbaseClientTest { + @Override + void assertCouchbaseCall(TraceAssert trace, int index, Object name, String bucketName = null, Object parentSpan = null, Object statement = null) { + CouchbaseSpanUtil.assertCouchbaseCall(trace, index, name, bucketName, parentSpan, statement) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseSpanUtil.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseSpanUtil.groovy new file mode 100644 index 000000000..bee1718ed --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseSpanUtil.groovy @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class CouchbaseSpanUtil { + // Reusable span assertion method. Cannot directly override AbstractCouchbaseTest.assertCouchbaseSpan because + // Of the class hierarchy of these tests + static void assertCouchbaseCall(TraceAssert trace, int index, Object spanName, String bucketName = null, Object parentSpan = null, Object statement = null) { + trace.span(index) { + name spanName + kind CLIENT + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + attributes { + + // Because of caching, not all requests hit the server so these attributes may be absent + "${SemanticAttributes.NET_PEER_NAME.key}" { it == "localhost" || it == "127.0.0.1" || it == null } + "${SemanticAttributes.NET_PEER_PORT.key}" { it == null || Number } + + "${SemanticAttributes.DB_SYSTEM.key}" "couchbase" + if (bucketName != null) { + "${SemanticAttributes.DB_NAME.key}" bucketName + } + + // Because of caching, not all requests hit the server so this tag may be absent + "couchbase.local.address" { it == null || String } + + // Not all couchbase operations have operation id. Notably, 'ViewQuery's do not + // We assign a spanName of 'Bucket.query' and this is shared with n1ql queries + // that do have operation ids + "couchbase.operation_id" { it == null || String } + + "${SemanticAttributes.DB_STATEMENT.key}"(statement ?: spanName) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/springdata/CouchbaseSpringRepository26Test.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/springdata/CouchbaseSpringRepository26Test.groovy new file mode 100644 index 000000000..62b559680 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/springdata/CouchbaseSpringRepository26Test.groovy @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert + +class CouchbaseSpringRepository26Test extends AbstractCouchbaseSpringRepositoryTest { + + @Override + void assertCouchbaseCall(TraceAssert trace, int index, Object name, String bucketName = null, Object parentSpan = null, Object statement = null) { + CouchbaseSpanUtil.assertCouchbaseCall(trace, index, name, bucketName, parentSpan, statement) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/springdata/CouchbaseSpringTemplate26Test.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/springdata/CouchbaseSpringTemplate26Test.groovy new file mode 100644 index 000000000..80a12e9ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/springdata/CouchbaseSpringTemplate26Test.groovy @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert + +class CouchbaseSpringTemplate26Test extends AbstractCouchbaseSpringTemplateTest { + @Override + void assertCouchbaseCall(TraceAssert trace, int index, Object name, String bucketName = null, Object parentSpan = null, Object statement = null) { + CouchbaseSpanUtil.assertCouchbaseCall(trace, index, name, bucketName, parentSpan, statement) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/couchbase-3.1.6-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/couchbase-3.1.6-javaagent.gradle new file mode 100644 index 000000000..db5195249 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/couchbase-3.1.6-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.couchbase.client" + module = "java-client" + versions = "[3.1.6,)" + // these versions were released as ".bundle" instead of ".jar" + skip('2.7.5', '2.7.8') + assertInverse = true + } +} + +dependencies { + implementation("com.couchbase.client:tracing-opentelemetry:0.3.6") { + exclude(group: "com.couchbase.client", module: "core-io") + } + + library "com.couchbase.client:core-io:2.1.6" + + testLibrary "com.couchbase.client:java-client:3.1.6" + + testImplementation group: "org.testcontainers", name: "couchbase", version: versions["org.testcontainers"] +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1_6/CouchbaseEnvironmentInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1_6/CouchbaseEnvironmentInstrumentation.java new file mode 100644 index 000000000..5ccff7472 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1_6/CouchbaseEnvironmentInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v3_1_6; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.couchbase.client.core.env.CoreEnvironment; +import com.couchbase.client.tracing.opentelemetry.OpenTelemetryRequestTracer; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class CouchbaseEnvironmentInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.couchbase.client.core.env.CoreEnvironment$Builder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), + CouchbaseEnvironmentInstrumentation.class.getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This CoreEnvironment.Builder builder) { + builder.requestTracer(OpenTelemetryRequestTracer.wrap(GlobalOpenTelemetry.get())); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1_6/CouchbaseInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1_6/CouchbaseInstrumentationModule.java new file mode 100644 index 000000000..b3e0d9ea1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1_6/CouchbaseInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v3_1_6; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class CouchbaseInstrumentationModule extends InstrumentationModule { + public CouchbaseInstrumentationModule() { + super("couchbase", "couchbase-3.1.6"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("com.couchbase.client.tracing.opentelemetry"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // New class introduced in 3.1, the minimum version we support. + // NB: Couchbase does not provide any API guarantees on their core IO artifact so reconsider + // instrumenting it instead of each individual JVM artifact if this becomes unmaintainable. + return hasClassesNamed("com.couchbase.client.core.cnc.TracingIdentifiers"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new CouchbaseEnvironmentInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/test/groovy/CouchbaseClient316Test.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/test/groovy/CouchbaseClient316Test.groovy new file mode 100644 index 000000000..6f4f7f317 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1.6/javaagent/src/test/groovy/CouchbaseClient316Test.groovy @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.couchbase.client.core.error.DocumentNotFoundException +import com.couchbase.client.java.Cluster +import com.couchbase.client.java.Collection +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.time.Duration +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.containers.output.Slf4jLogConsumer +import org.testcontainers.couchbase.BucketDefinition +import org.testcontainers.couchbase.CouchbaseContainer +import org.testcontainers.couchbase.CouchbaseService +import spock.lang.Shared + +// Couchbase instrumentation is owned upstream so we don't assert on the contents of the spans, only +// that the instrumentation is properly registered by the agent, meaning some spans were generated. +class CouchbaseClient316Test extends AgentInstrumentationSpecification { + private static final Logger log = LoggerFactory.getLogger("couchbase-container") + + @Shared + CouchbaseContainer couchbase + @Shared + Cluster cluster + @Shared + Collection collection + + def setupSpec() { + couchbase = new CouchbaseContainer() + .withExposedPorts(8091) + .withEnabledServices(CouchbaseService.KV) + .withBucket(new BucketDefinition("test")) + .withLogConsumer(new Slf4jLogConsumer(log)) + .withStartupTimeout(Duration.ofSeconds(120)) + couchbase.start() + + cluster = Cluster.connect(couchbase.connectionString, couchbase.username, couchbase.password) + def bucket = cluster.bucket("test") + collection = bucket.defaultCollection() + bucket.waitUntilReady(Duration.ofSeconds(10)) + } + + def cleanupSpec() { + couchbase.stop() + } + + def "emits spans"() { + when: + try { + collection.get("id") + } catch (DocumentNotFoundException e) { + // Expected + } + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name(~/.*get/) + } + span(1) { + name(~/.*dispatch_to_server/) + } + } + } + + cleanup: + cluster.disconnect() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/couchbase-3.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/couchbase-3.1-javaagent.gradle new file mode 100644 index 000000000..41b7e2e56 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/couchbase-3.1-javaagent.gradle @@ -0,0 +1,27 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.couchbase.client" + module = "java-client" + versions = "[3.1,3.1.6)" + // these versions were released as ".bundle" instead of ".jar" + skip('2.7.5', '2.7.8') + assertInverse = true + } +} + +dependencies { + implementation("com.couchbase.client:tracing-opentelemetry:0.3.3") { + exclude(group: "com.couchbase.client", module: "core-io") + } + + library "com.couchbase.client:core-io:2.1.0" + + testLibrary "com.couchbase.client:java-client:3.1.0" + + testImplementation group: "org.testcontainers", name: "couchbase", version: versions["org.testcontainers"] + + latestDepTestLibrary "com.couchbase.client:java-client:3.1.5" + latestDepTestLibrary "com.couchbase.client:core-io:2.1.5" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseEnvironmentInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseEnvironmentInstrumentation.java new file mode 100644 index 000000000..ecc05ae8c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseEnvironmentInstrumentation.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v3_1; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.couchbase.client.core.env.CoreEnvironment; +import com.couchbase.client.tracing.opentelemetry.OpenTelemetryRequestTracer; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class CouchbaseEnvironmentInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.couchbase.client.core.env.CoreEnvironment$Builder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), + CouchbaseEnvironmentInstrumentation.class.getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This CoreEnvironment.Builder builder) { + builder.requestTracer( + OpenTelemetryRequestTracer.wrap( + GlobalOpenTelemetry.getTracer("io.opentelemetry.javaagent.couchbase-3.0"))); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseInstrumentationModule.java new file mode 100644 index 000000000..e3b3ce7ed --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v3_1; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class CouchbaseInstrumentationModule extends InstrumentationModule { + public CouchbaseInstrumentationModule() { + super("couchbase", "couchbase-3.1"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("com.couchbase.client.tracing.opentelemetry"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // New class introduced in 3.1, the minimum version we support. + // NB: Couchbase does not provide any API guarantees on their core IO artifact so reconsider + // instrumenting it instead of each individual JVM artifact if this becomes unmaintainable. + return hasClassesNamed("com.couchbase.client.core.cnc.TracingIdentifiers"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new CouchbaseEnvironmentInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/groovy/CouchbaseClient31Test.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/groovy/CouchbaseClient31Test.groovy new file mode 100644 index 000000000..e0072b2a3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/groovy/CouchbaseClient31Test.groovy @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.couchbase.client.core.error.DocumentNotFoundException +import com.couchbase.client.java.Cluster +import com.couchbase.client.java.Collection +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.time.Duration +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.containers.output.Slf4jLogConsumer +import org.testcontainers.couchbase.BucketDefinition +import org.testcontainers.couchbase.CouchbaseContainer +import org.testcontainers.couchbase.CouchbaseService +import spock.lang.Shared + +// Couchbase instrumentation is owned upstream so we don't assert on the contents of the spans, only +// that the instrumentation is properly registered by the agent, meaning some spans were generated. +class CouchbaseClient31Test extends AgentInstrumentationSpecification { + private static final Logger log = LoggerFactory.getLogger("couchbase-container") + + @Shared + CouchbaseContainer couchbase + @Shared + Cluster cluster + @Shared + Collection collection + + def setupSpec() { + couchbase = new CouchbaseContainer() + .withExposedPorts(8091) + .withEnabledServices(CouchbaseService.KV) + .withBucket(new BucketDefinition("test")) + .withLogConsumer(new Slf4jLogConsumer(log)) + .withStartupTimeout(Duration.ofSeconds(120)) + couchbase.start() + + cluster = Cluster.connect(couchbase.connectionString, couchbase.username, couchbase.password) + def bucket = cluster.bucket("test") + collection = bucket.defaultCollection() + bucket.waitUntilReady(Duration.ofSeconds(10)) + } + + def cleanupSpec() { + couchbase.stop() + } + + def "emits spans"() { + when: + try { + collection.get("id") + } catch (DocumentNotFoundException e) { + // Expected + } + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name(~/.*get/) + } + span(1) { + name(~/.*dispatch_to_server/) + } + } + } + + cleanup: + cluster.disconnect() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/couchbase-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/couchbase-testing.gradle new file mode 100644 index 000000000..e0aaadd62 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/couchbase-testing.gradle @@ -0,0 +1,14 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "com.couchbase.mock:CouchbaseMock:1.5.19" + // Earliest version that seems to allow queries with CouchbaseMock: + api "com.couchbase.client:java-client:2.5.0" + api "org.springframework.data:spring-data-couchbase:2.0.0.RELEASE" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/AbstractCouchbaseAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/AbstractCouchbaseAsyncClientTest.groovy new file mode 100644 index 000000000..d0f90dd8f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/AbstractCouchbaseAsyncClientTest.groovy @@ -0,0 +1,176 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.couchbase.client.java.AsyncCluster +import com.couchbase.client.java.CouchbaseAsyncCluster +import com.couchbase.client.java.document.JsonDocument +import com.couchbase.client.java.document.json.JsonObject +import com.couchbase.client.java.env.CouchbaseEnvironment +import com.couchbase.client.java.query.N1qlQuery +import java.util.concurrent.TimeUnit +import spock.lang.Unroll +import spock.util.concurrent.BlockingVariable +import util.AbstractCouchbaseTest + +@Unroll +abstract class AbstractCouchbaseAsyncClientTest extends AbstractCouchbaseTest { + static final int TIMEOUT = 10 + + def "test hasBucket #type"() { + setup: + def hasBucket = new BlockingVariable(TIMEOUT) + + when: + cluster.openBucket(bucketSettings.name(), bucketSettings.password()).subscribe({ bkt -> + manager.hasBucket(bucketSettings.name()).subscribe({ result -> hasBucket.set(result) }) + }) + + then: + assert hasBucket.get() + assertTraces(1) { + trace(0, 2) { + assertCouchbaseCall(it, 0, "Cluster.openBucket", null) + assertCouchbaseCall(it, 1, "ClusterManager.hasBucket", null, span(0)) + } + } + + cleanup: + cluster?.disconnect()?.timeout(TIMEOUT, TimeUnit.SECONDS)?.toBlocking()?.single() + environment.shutdown() + + where: + bucketSettings << [bucketCouchbase, bucketMemcache] + + environment = envBuilder(bucketSettings).build() + cluster = CouchbaseAsyncCluster.create(environment, Arrays.asList("127.0.0.1")) + manager = cluster.clusterManager(USERNAME, PASSWORD).toBlocking().single() + type = bucketSettings.type().name() + } + + def "test upsert #type"() { + setup: + JsonObject content = JsonObject.create().put("hello", "world") + def inserted = new BlockingVariable(TIMEOUT) + + when: + runUnderTrace("someTrace") { + // Connect to the bucket and open it + cluster.openBucket(bucketSettings.name(), bucketSettings.password()).subscribe({ bkt -> + bkt.upsert(JsonDocument.create("helloworld", content)).subscribe({ result -> inserted.set(result) }) + }) + } + + then: + inserted.get().content().getString("hello") == "world" + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + + assertCouchbaseCall(it, 1, "Cluster.openBucket", null, span(0)) + assertCouchbaseCall(it, 2, "Bucket.upsert", bucketSettings.name(), span(1)) + } + } + + cleanup: + cluster?.disconnect()?.timeout(TIMEOUT, TimeUnit.SECONDS)?.toBlocking()?.single() + environment.shutdown() + + where: + bucketSettings << [bucketCouchbase, bucketMemcache] + + environment = envBuilder(bucketSettings).build() + cluster = CouchbaseAsyncCluster.create(environment, Arrays.asList("127.0.0.1")) + type = bucketSettings.type().name() + } + + def "test upsert and get #type"() { + setup: + JsonObject content = JsonObject.create().put("hello", "world") + def inserted = new BlockingVariable(TIMEOUT) + def found = new BlockingVariable(TIMEOUT) + + when: + runUnderTrace("someTrace") { + cluster.openBucket(bucketSettings.name(), bucketSettings.password()).subscribe({ bkt -> + bkt.upsert(JsonDocument.create("helloworld", content)) + .subscribe({ result -> + inserted.set(result) + bkt.get("helloworld") + .subscribe({ searchResult -> found.set(searchResult) + }) + }) + }) + } + + // Create a JSON document and store it with the ID "helloworld" + then: + found.get() == inserted.get() + found.get().content().getString("hello") == "world" + + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "someTrace") + + assertCouchbaseCall(it, 1, "Cluster.openBucket", null, span(0)) + assertCouchbaseCall(it, 2, "Bucket.upsert", bucketSettings.name(), span(1)) + assertCouchbaseCall(it, 3, "Bucket.get", bucketSettings.name(), span(2)) + } + } + + cleanup: + cluster?.disconnect()?.timeout(TIMEOUT, TimeUnit.SECONDS)?.toBlocking()?.single() + environment.shutdown() + + where: + bucketSettings << [bucketCouchbase, bucketMemcache] + + environment = envBuilder(bucketSettings).build() + cluster = CouchbaseAsyncCluster.create(environment, Arrays.asList("127.0.0.1")) + type = bucketSettings.type().name() + } + + def "test query"() { + setup: + // Only couchbase buckets support queries. + CouchbaseEnvironment environment = envBuilder(bucketCouchbase).build() + AsyncCluster cluster = CouchbaseAsyncCluster.create(environment, Arrays.asList("127.0.0.1")) + def queryResult = new BlockingVariable(TIMEOUT) + + when: + // Mock expects this specific query. + // See com.couchbase.mock.http.query.QueryServer.handleString. + runUnderTrace("someTrace") { + cluster.openBucket(bucketCouchbase.name(), bucketCouchbase.password()).subscribe({ + bkt -> + bkt.query(N1qlQuery.simple("SELECT mockrow")) + .flatMap({ query -> query.rows() }) + .single() + .subscribe({ row -> queryResult.set(row.value()) }) + }) + } + + then: + queryResult.get().get("row") == "value" + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + + assertCouchbaseCall(it, 1, "Cluster.openBucket", null, span(0)) + + def dbName = bucketCouchbase.name() + assertCouchbaseCall(it, 2, "SELECT $dbName", dbName, span(1), 'SELECT mockrow') + } + } + + cleanup: + cluster?.disconnect()?.timeout(TIMEOUT, TimeUnit.SECONDS)?.toBlocking()?.single() + environment.shutdown() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/AbstractCouchbaseClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/AbstractCouchbaseClientTest.groovy new file mode 100644 index 000000000..54b551cd3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/AbstractCouchbaseClientTest.groovy @@ -0,0 +1,121 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.couchbase.client.java.Bucket +import com.couchbase.client.java.Cluster +import com.couchbase.client.java.CouchbaseCluster +import com.couchbase.client.java.document.JsonDocument +import com.couchbase.client.java.document.json.JsonObject +import com.couchbase.client.java.env.CouchbaseEnvironment +import com.couchbase.client.java.query.N1qlQuery +import spock.lang.Unroll +import util.AbstractCouchbaseTest + +@Unroll +abstract class AbstractCouchbaseClientTest extends AbstractCouchbaseTest { + def "test hasBucket #type"() { + when: + def hasBucket = manager.hasBucket(bucketSettings.name()) + + then: + assert hasBucket + assertTraces(1) { + trace(0, 1) { + assertCouchbaseCall(it, 0, "ClusterManager.hasBucket") + } + } + + cleanup: + cluster?.disconnect() + environment.shutdown() + + where: + bucketSettings << [bucketCouchbase, bucketMemcache] + + environment = envBuilder(bucketSettings).build() + cluster = CouchbaseCluster.create(environment, Arrays.asList("127.0.0.1")) + manager = cluster.clusterManager(USERNAME, PASSWORD) + type = bucketSettings.type().name() + } + + def "test upsert and get #type"() { + when: + // Connect to the bucket and open it + Bucket bkt = cluster.openBucket(bucketSettings.name(), bucketSettings.password()) + + // Create a JSON document and store it with the ID "helloworld" + JsonObject content = JsonObject.create().put("hello", "world") + + def inserted + def found + + runUnderTrace("someTrace") { + inserted = bkt.upsert(JsonDocument.create("helloworld", content)) + found = bkt.get("helloworld") + } + + then: + found == inserted + found.content().getString("hello") == "world" + + assertTraces(2) { + trace(0, 1) { + assertCouchbaseCall(it, 0, "Cluster.openBucket") + } + trace(1, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 1, "Bucket.upsert", bucketSettings.name(), span(0)) + assertCouchbaseCall(it, 2, "Bucket.get", bucketSettings.name(), span(0)) + } + } + + cleanup: + cluster?.disconnect() + environment.shutdown() + + where: + bucketSettings << [bucketCouchbase, bucketMemcache] + + environment = envBuilder(bucketSettings).build() + cluster = CouchbaseCluster.create(environment, Arrays.asList("127.0.0.1")) + type = bucketSettings.type().name() + } + + def "test query"() { + setup: + // Only couchbase buckets support queries. + CouchbaseEnvironment environment = envBuilder(bucketCouchbase).build() + Cluster cluster = CouchbaseCluster.create(environment, Arrays.asList("127.0.0.1")) + Bucket bkt = cluster.openBucket(bucketCouchbase.name(), bucketCouchbase.password()) + + when: + // Mock expects this specific query. + // See com.couchbase.mock.http.query.QueryServer.handleString. + def result = bkt.query(N1qlQuery.simple("SELECT mockrow")) + + then: + result.parseSuccess() + result.finalSuccess() + result.first().value().get("row") == "value" + + and: + assertTraces(2) { + trace(0, 1) { + assertCouchbaseCall(it, 0, "Cluster.openBucket") + } + trace(1, 1) { + def dbName = bucketCouchbase.name() + assertCouchbaseCall(it, 0, "SELECT $dbName", dbName, null, 'SELECT mockrow') + } + } + + cleanup: + cluster?.disconnect() + environment.shutdown() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/AbstractCouchbaseSpringRepositoryTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/AbstractCouchbaseSpringRepositoryTest.groovy new file mode 100644 index 000000000..b1e1a800f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/AbstractCouchbaseSpringRepositoryTest.groovy @@ -0,0 +1,191 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.couchbase.client.java.Cluster +import com.couchbase.client.java.CouchbaseCluster +import com.couchbase.client.java.env.CouchbaseEnvironment +import com.couchbase.client.java.view.DefaultView +import com.couchbase.client.java.view.DesignDocument +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.data.repository.CrudRepository +import spock.lang.Shared +import spock.lang.Unroll +import util.AbstractCouchbaseTest + +@Unroll +abstract class AbstractCouchbaseSpringRepositoryTest extends AbstractCouchbaseTest { + static final Closure FIND + static { + // This method is different in Spring Data 2+ + try { + CrudRepository.getMethod("findOne", Serializable) + FIND = { DocRepository repo, String id -> + repo.findOne(id) + } + } catch (NoSuchMethodException e) { + FIND = { DocRepository repo, String id -> + repo.findById(id).get() + } + } + } + @Shared + ConfigurableApplicationContext applicationContext + @Shared + DocRepository repo + + def setupSpec() { + CouchbaseEnvironment environment = envBuilder(bucketCouchbase).build() + Cluster couchbaseCluster = CouchbaseCluster.create(environment, Arrays.asList("127.0.0.1")) + + // Create view for SpringRepository's findAll() + couchbaseCluster.openBucket(bucketCouchbase.name(), bucketCouchbase.password()).bucketManager() + .insertDesignDocument( + DesignDocument.create("doc", Collections.singletonList(DefaultView.create("all", + ''' + function (doc, meta) { + if (doc._class == "springdata.Doc") { + emit(meta.id, null); + } + } + '''.stripIndent() + ))) + ) + CouchbaseConfig.setEnvironment(environment) + CouchbaseConfig.setBucketSettings(bucketCouchbase) + + // Close all buckets and disconnect + couchbaseCluster.disconnect() + + applicationContext = new AnnotationConfigApplicationContext(CouchbaseConfig) + repo = applicationContext.getBean(DocRepository) + } + + def cleanupSpec() { + applicationContext.close() + } + + def "test empty repo"() { + when: + def result = repo.findAll() + + then: + !result.iterator().hasNext() + + and: + assertTraces(1) { + trace(0, 1) { + def dbName = bucketCouchbase.name() + assertCouchbaseCall(it, 0, dbName, dbName, null, ~/^ViewQuery\(doc\/all\).*/) + } + } + } + + def "test save"() { + setup: + def doc = new Doc() + + when: + def result = repo.save(doc) + + then: + result == doc + assertTraces(1) { + trace(0, 1) { + assertCouchbaseCall(it, 0, "Bucket.upsert", bucketCouchbase.name()) + } + } + + cleanup: + clearExportedData() + repo.deleteAll() + ignoreTracesAndClear(2) + } + + def "test save and retrieve"() { + setup: + def doc = new Doc() + def result + + when: + runUnderTrace("someTrace") { + repo.save(doc) + result = FIND(repo, "1") + } + + then: // RETRIEVE + result == doc + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 1, "Bucket.upsert", bucketCouchbase.name(), span(0)) + assertCouchbaseCall(it, 2, "Bucket.get", bucketCouchbase.name(), span(0)) + } + } + + cleanup: + clearExportedData() + repo.deleteAll() + ignoreTracesAndClear(2) + } + + def "test save and update"() { + setup: + def doc = new Doc() + + when: + runUnderTrace("someTrace") { + repo.save(doc) + doc.data = "other data" + repo.save(doc) + } + + + then: + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 1, "Bucket.upsert", bucketCouchbase.name(), span(0)) + assertCouchbaseCall(it, 2, "Bucket.upsert", bucketCouchbase.name(), span(0)) + } + } + + cleanup: + clearExportedData() + repo.deleteAll() + ignoreTracesAndClear(2) + } + + def "save and delete"() { + setup: + def doc = new Doc() + def result + + when: // DELETE + runUnderTrace("someTrace") { + repo.save(doc) + repo.delete("1") + result = repo.findAll().iterator().hasNext() + } + + then: + assert !result + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "someTrace") + + def dbName = bucketCouchbase.name() + assertCouchbaseCall(it, 1, "Bucket.upsert", dbName, span(0)) + assertCouchbaseCall(it, 2, "Bucket.remove", dbName, span(0)) + assertCouchbaseCall(it, 3, dbName, dbName, span(0), ~/^ViewQuery\(doc\/all\).*/) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/AbstractCouchbaseSpringTemplateTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/AbstractCouchbaseSpringTemplateTest.groovy new file mode 100644 index 000000000..ec711410c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/AbstractCouchbaseSpringTemplateTest.groovy @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.couchbase.client.java.Bucket +import com.couchbase.client.java.Cluster +import com.couchbase.client.java.CouchbaseCluster +import com.couchbase.client.java.cluster.ClusterManager +import com.couchbase.client.java.env.CouchbaseEnvironment +import org.springframework.data.couchbase.core.CouchbaseTemplate +import spock.lang.Retry +import spock.lang.Shared +import spock.lang.Unroll +import util.AbstractCouchbaseTest + +@Retry(count = 10, delay = 500) +@Unroll +class AbstractCouchbaseSpringTemplateTest extends AbstractCouchbaseTest { + + @Shared + List templates + + @Shared + Cluster couchbaseCluster + + @Shared + Cluster memcacheCluster + + @Shared + protected CouchbaseEnvironment couchbaseEnvironment + @Shared + protected CouchbaseEnvironment memcacheEnvironment + + def setupSpec() { + couchbaseEnvironment = envBuilder(bucketCouchbase).build() + memcacheEnvironment = envBuilder(bucketMemcache).build() + + couchbaseCluster = CouchbaseCluster.create(couchbaseEnvironment, Arrays.asList("127.0.0.1")) + memcacheCluster = CouchbaseCluster.create(memcacheEnvironment, Arrays.asList("127.0.0.1")) + ClusterManager couchbaseManager = couchbaseCluster.clusterManager(USERNAME, PASSWORD) + ClusterManager memcacheManager = memcacheCluster.clusterManager(USERNAME, PASSWORD) + + Bucket bucketCouchbase = couchbaseCluster.openBucket(bucketCouchbase.name(), bucketCouchbase.password()) + Bucket bucketMemcache = memcacheCluster.openBucket(bucketMemcache.name(), bucketMemcache.password()) + + runUnderTrace("getting info") { + templates = [new CouchbaseTemplate(couchbaseManager.info(), bucketCouchbase), + new CouchbaseTemplate(memcacheManager.info(), bucketMemcache)] + } + } + + def cleanupSpec() { + couchbaseCluster?.disconnect() + memcacheCluster?.disconnect() + couchbaseEnvironment.shutdown() + memcacheEnvironment.shutdown() + } + + def "test write #name"() { + setup: + def doc = new Doc() + def result + + when: + runUnderTrace("someTrace") { + template.save(doc) + result = template.findById("1", Doc) + } + + + then: + result != null + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 1, "Bucket.upsert", name, span(0)) + assertCouchbaseCall(it, 2, "Bucket.get", name, span(0)) + } + } + + where: + template << templates + name = template.couchbaseBucket.name() + } + + def "test remove #name"() { + setup: + def doc = new Doc() + + when: + runUnderTrace("someTrace") { + template.save(doc) + template.remove(doc) + } + + + then: + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "someTrace") + assertCouchbaseCall(it, 1, "Bucket.upsert", name, span(0)) + assertCouchbaseCall(it, 2, "Bucket.remove", name, span(0)) + } + } + clearExportedData() + + when: + def result = template.findById("1", Doc) + + then: + result == null + assertTraces(1) { + trace(0, 1) { + assertCouchbaseCall(it, 0, "Bucket.get", name) + } + } + + where: + template << templates + name = template.couchbaseBucket.name() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/CouchbaseConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/CouchbaseConfig.groovy new file mode 100644 index 000000000..588944dbc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/CouchbaseConfig.groovy @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import static java.util.Objects.requireNonNull + +import com.couchbase.client.java.cluster.BucketSettings +import com.couchbase.client.java.env.CouchbaseEnvironment +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories + +@Configuration +@EnableCouchbaseRepositories(basePackages = "springdata") +@ComponentScan(basePackages = "springdata") +class CouchbaseConfig extends AbstractCouchbaseConfiguration { + + // These need to be set before this class can be used by Spring + static CouchbaseEnvironment environment + static BucketSettings bucketSettings + + @Override + protected CouchbaseEnvironment getEnvironment() { + return requireNonNull(environment) + } + + @Override + protected List getBootstrapHosts() { + return Collections.singletonList("127.0.0.1") + } + + @Override + protected String getBucketName() { + return bucketSettings.name() + } + + @Override + protected String getBucketPassword() { + return bucketSettings.password() + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/Doc.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/Doc.groovy new file mode 100644 index 000000000..4ae0b7707 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/Doc.groovy @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import groovy.transform.EqualsAndHashCode +import org.springframework.data.annotation.Id +import org.springframework.data.couchbase.core.mapping.Document + +@Document +@EqualsAndHashCode +class Doc { + @Id + private String id = "1" + private String data = "some data" + + String getId() { + return id + } + + void setId(String id) { + this.id = id + } + + String getData() { + return data + } + + void setData(String data) { + this.data = data + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/DocRepository.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/DocRepository.groovy new file mode 100644 index 000000000..520befd4a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/springdata/DocRepository.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import org.springframework.data.couchbase.repository.CouchbaseRepository + +interface DocRepository extends CouchbaseRepository {} diff --git a/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/util/AbstractCouchbaseTest.groovy b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/util/AbstractCouchbaseTest.groovy new file mode 100644 index 000000000..531c0cf27 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/couchbase/couchbase-testing/src/main/groovy/util/AbstractCouchbaseTest.groovy @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package util + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import com.couchbase.client.core.metrics.DefaultLatencyMetricsCollectorConfig +import com.couchbase.client.core.metrics.DefaultMetricsCollectorConfig +import com.couchbase.client.java.bucket.BucketType +import com.couchbase.client.java.cluster.BucketSettings +import com.couchbase.client.java.cluster.DefaultBucketSettings +import com.couchbase.client.java.env.DefaultCouchbaseEnvironment +import com.couchbase.mock.Bucket +import com.couchbase.mock.BucketConfiguration +import com.couchbase.mock.CouchbaseMock +import com.couchbase.mock.http.query.QueryServer +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.TimeUnit +import spock.lang.Shared + +abstract class AbstractCouchbaseTest extends AgentInstrumentationSpecification { + + static final USERNAME = "Administrator" + static final PASSWORD = "password" + + @Shared + private int port = PortUtils.findOpenPort() + + @Shared + private String testBucketName = this.getClass().simpleName + + @Shared + protected bucketCouchbase = DefaultBucketSettings.builder() + .enableFlush(true) + .name("$testBucketName-cb") + .password("test-pass") + .type(BucketType.COUCHBASE) + .quota(100) + .build() + + @Shared + protected bucketMemcache = DefaultBucketSettings.builder() + .enableFlush(true) + .name("$testBucketName-mem") + .password("test-pass") + .type(BucketType.MEMCACHED) + .quota(100) + .build() + + @Shared + CouchbaseMock mock + + def setupSpec() { + mock = new CouchbaseMock("127.0.0.1", port, 1, 1) + mock.httpServer.register("/query", new QueryServer()) + mock.start() + println "CouchbaseMock listening on localhost:$port" + + mock.createBucket(convert(bucketCouchbase)) + mock.createBucket(convert(bucketMemcache)) + } + + private static BucketConfiguration convert(BucketSettings bucketSettings) { + def configuration = new BucketConfiguration() + configuration.name = bucketSettings.name() + configuration.password = bucketSettings.password() + configuration.type = Bucket.BucketType.valueOf(bucketSettings.type().name()) + configuration.numNodes = 1 + configuration.numReplicas = 0 + return configuration + } + + def cleanupSpec() { + mock?.stop() + } + + protected DefaultCouchbaseEnvironment.Builder envBuilder(BucketSettings bucketSettings) { + // Couchbase seems to be really slow to start sometimes + def timeout = TimeUnit.SECONDS.toMillis(20) + return DefaultCouchbaseEnvironment.builder() + .bootstrapCarrierDirectPort(mock.getCarrierPort(bucketSettings.name())) + .bootstrapHttpDirectPort(port) + // settings to try to reduce variability in the tests: + .runtimeMetricsCollectorConfig(DefaultMetricsCollectorConfig.create(0, TimeUnit.DAYS)) + .networkLatencyMetricsCollectorConfig(DefaultLatencyMetricsCollectorConfig.create(0, TimeUnit.DAYS)) + .computationPoolSize(1) + .connectTimeout(timeout) + .disconnectTimeout(timeout) + .kvTimeout(timeout) + .managementTimeout(timeout) + .queryTimeout(timeout) + .viewTimeout(timeout) + .keepAliveTimeout(timeout) + .searchTimeout(timeout) + .analyticsTimeout(timeout) + .socketConnectTimeout(timeout.intValue()) + } + + void assertCouchbaseCall(TraceAssert trace, int index, Object spanName, String bucketName = null, Object parentSpan = null, Object statement = null) { + trace.span(index) { + name spanName + kind CLIENT + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "couchbase" + if (bucketName != null) { + "${SemanticAttributes.DB_NAME.key}" bucketName + } + "${SemanticAttributes.DB_STATEMENT.key}"(statement ?: spanName) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/docean-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/docean-javaagent.gradle new file mode 100644 index 000000000..add252774 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/docean-javaagent.gradle @@ -0,0 +1,6 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly("run.mone:docean:1.4-SNAPSHOT") + implementation("run.mone:opentelemetry-api") +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanInstrumentation.java new file mode 100644 index 000000000..e5e14400a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanInstrumentation.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.docean; + +import com.xiaomi.youpin.docean.mvc.HttpRequestMethod; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import java.lang.reflect.Method; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class DoceanInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xiaomi.youpin.docean.Mvc"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(ElementMatchers.named("callMethod").and(ElementMatchers.isPrivate()), DoceanInstrumentation.class.getName() + "$InvokeAdvice"); + } + + + public static class InvokeAdvice { + + @SuppressWarnings({"SystemOut", "CatchAndPrintStackTrace"}) + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Origin Method method, + @Advice.Origin Class clazz, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("request") DoceanRequest request, + @Advice.Argument(4) HttpRequestMethod hrmethod + ) { + try { + Context parentContext = currentContext(); + request = new DoceanRequest(); + request.setClazz(hrmethod.getObj().getClass().getSimpleName()); + request.setMethodName(hrmethod.getMethod().getName()); + context = DoceanSingletons.instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + + @SuppressWarnings("SystemOut") + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("request") DoceanRequest request) { + if (scope == null) { + System.out.println("docean scope null"); + return; + } + Span span = Span.fromContext(context); + Attributes attributes = Attributes.builder().put("method", request.getMethodName()).build(); + span.addEvent("docean_call_event", attributes); + scope.close(); + DoceanSingletons.instrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanInstrumentationModule.java new file mode 100644 index 000000000..6cf963a9f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.docean; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.List; + +import static java.util.Arrays.asList; + +@AutoService(InstrumentationModule.class) +public class DoceanInstrumentationModule extends InstrumentationModule { + public DoceanInstrumentationModule() { + super("docean"); + } + + @Override + public List typeInstrumentations() { + return asList(new DoceanInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanRequest.java b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanRequest.java new file mode 100644 index 000000000..4bc8b24ed --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanRequest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.docean; + + + +public class DoceanRequest { + + private String clazz; + + private String methodName; + + public String getClazz() { + return clazz; + } + + public void setClazz(String clazz) { + this.clazz = clazz; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanSingletons.java b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanSingletons.java new file mode 100644 index 000000000..a6260e0e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/docean/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/docean/DoceanSingletons.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.docean; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +public final class DoceanSingletons { + + private static final String INSTRUMENTATION_NAME = "com.xiaomi.mone.docean"; + + private static final Instrumenter INSTRUMENTER; + + static { + SpanNameExtractor spanName = request -> "Docean" + ":" + request.getMethodName(); + + AttributesExtractor extractor = new AttributesExtractor() { + @Override + protected void onStart(AttributesBuilder attributes, DoceanRequest o) { + attributes.put("class.name", o.getClazz()); + attributes.put("method.name", o.getMethodName()); + } + + @Override + protected void onEnd(AttributesBuilder attributes, DoceanRequest o, Void o2) { + + } + }; + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(extractor) + .newInstrumenter(SpanKindExtractor.alwaysInternal()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private DoceanSingletons() { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/dropwizard-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/dropwizard-testing.gradle new file mode 100644 index 000000000..b5e520023 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/dropwizard-testing.gradle @@ -0,0 +1,17 @@ +ext { + skipPublish = true +} +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + testInstrumentation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-jersey-2.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + + // First version with DropwizardTestSupport: + testImplementation "io.dropwizard:dropwizard-testing:0.8.0" + testImplementation "javax.xml.bind:jaxb-api:2.2.3" + testImplementation "com.fasterxml.jackson.module:jackson-module-afterburner" +} + +// Requires old Guava. Can't use enforcedPlatform since predates BOM +configurations.testRuntimeClasspath.resolutionStrategy.force "com.google.guava:guava:19.0" diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/src/test/groovy/DropwizardAsyncTest.groovy b/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/src/test/groovy/DropwizardAsyncTest.groovy new file mode 100644 index 000000000..c19b38975 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/src/test/groovy/DropwizardAsyncTest.groovy @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.dropwizard.Application +import io.dropwizard.Configuration +import io.dropwizard.setup.Bootstrap +import io.dropwizard.setup.Environment +import java.util.concurrent.Executors +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.PathParam +import javax.ws.rs.QueryParam +import javax.ws.rs.container.AsyncResponse +import javax.ws.rs.container.Suspended +import javax.ws.rs.core.Response + +class DropwizardAsyncTest extends DropwizardTest { + + Class testApp() { + AsyncTestApp + } + + Class testResource() { + AsyncServiceResource + } + + static class AsyncTestApp extends Application { + @Override + void initialize(Bootstrap bootstrap) { + } + + @Override + void run(Configuration configuration, Environment environment) { + environment.jersey().register(AsyncServiceResource) + } + } + + @Path("/") + static class AsyncServiceResource { + final executor = Executors.newSingleThreadExecutor() + + @GET + @Path("success") + void success(@Suspended final AsyncResponse asyncResponse) { + executor.execute { + controller(SUCCESS) { + asyncResponse.resume(Response.status(SUCCESS.status).entity(SUCCESS.body).build()) + } + } + } + + @GET + @Path("query") + Response query_param(@QueryParam("some") String param, @Suspended final AsyncResponse asyncResponse) { + executor.execute { + controller(QUERY_PARAM) { + asyncResponse.resume(Response.status(QUERY_PARAM.status).entity("some=$param".toString()).build()) + } + } + } + + @GET + @Path("redirect") + void redirect(@Suspended final AsyncResponse asyncResponse) { + executor.execute { + controller(REDIRECT) { + asyncResponse.resume(Response.status(REDIRECT.status).location(new URI(REDIRECT.body)).build()) + } + } + } + + @GET + @Path("error-status") + void error(@Suspended final AsyncResponse asyncResponse) { + executor.execute { + controller(ERROR) { + asyncResponse.resume(Response.status(ERROR.status).entity(ERROR.body).build()) + } + } + } + + @GET + @Path("exception") + void exception(@Suspended final AsyncResponse asyncResponse) { + executor.execute { + controller(EXCEPTION) { + def ex = new Exception(EXCEPTION.body) + asyncResponse.resume(ex) + throw ex + } + } + } + + @GET + @Path("path/{id}/param") + Response path_param(@PathParam("id") int param, @Suspended final AsyncResponse asyncResponse) { + executor.execute { + controller(PATH_PARAM) { + asyncResponse.resume(Response.status(PATH_PARAM.status).entity(param.toString()).build()) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/src/test/groovy/DropwizardTest.groovy b/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/src/test/groovy/DropwizardTest.groovy new file mode 100644 index 000000000..64a3b30ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-testing/src/test/groovy/DropwizardTest.groovy @@ -0,0 +1,198 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.dropwizard.Application +import io.dropwizard.Configuration +import io.dropwizard.setup.Bootstrap +import io.dropwizard.setup.Environment +import io.dropwizard.testing.ConfigOverride +import io.dropwizard.testing.DropwizardTestSupport +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.PathParam +import javax.ws.rs.QueryParam +import javax.ws.rs.core.Response + +class DropwizardTest extends HttpServerTest implements AgentTestTrait { + + @Override + DropwizardTestSupport startServer(int port) { + println "Port: $port" + def testSupport = new DropwizardTestSupport(testApp(), + null, + ConfigOverride.config("server.applicationConnectors[0].port", "$port"), + ConfigOverride.config("server.adminConnectors[0].port", PortUtils.findOpenPort().toString())) + testSupport.before() + return testSupport + } + + Class testApp() { + TestApp + } + + Class testResource() { + ServiceResource + } + + @Override + void stopServer(DropwizardTestSupport testSupport) { + testSupport.after() + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + endpoint != NOT_FOUND + } + + @Override + boolean testPathParam() { + true + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return "/path/{id}/param" + case NOT_FOUND: + return "/*" + default: + return endpoint.resolvePath(address).path + } + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name "${this.testResource().simpleName}.${endpoint.name().toLowerCase()}" + kind INTERNAL + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(Exception, EXCEPTION.body) + } + childOf((SpanData) parent) + } + } + + // this override is needed because dropwizard reports peer ip as the client ip + @Override + void serverSpan(TraceAssert trace, int index, String traceID = null, String parentID = null, String method = "GET", Long responseContentLength = null, ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name expectedServerSpanName(endpoint) + kind SERVER + if (endpoint.errored) { + status StatusCode.ERROR + } + if (parentID != null) { + traceId traceID + parentSpanId parentID + } else { + hasNoParent() + } + attributes { + // dropwizard reports peer ip as the client ip + "${SemanticAttributes.NET_PEER_IP.key}" TEST_CLIENT_IP + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" { it == "${endpoint.resolve(address)}" || it == "${endpoint.resolveWithoutFragment(address)}" } + "${SemanticAttributes.HTTP_METHOD.key}" method + "${SemanticAttributes.HTTP_STATUS_CODE.key}" endpoint.status + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT + "${SemanticAttributes.HTTP_CLIENT_IP.key}" TEST_CLIENT_IP + } + } + } + + static class TestApp extends Application { + @Override + void initialize(Bootstrap bootstrap) { + } + + @Override + void run(Configuration configuration, Environment environment) { + environment.jersey().register(ServiceResource) + } + } + + @Path("/ignored1") + static interface TestInterface {} + + @Path("/ignored2") + static abstract class AbstractClass implements TestInterface { + + @GET + @Path("success") + Response success() { + controller(SUCCESS) { + Response.status(SUCCESS.status).entity(SUCCESS.body).build() + } + } + + @GET + @Path("query") + Response query_param(@QueryParam("some") String param) { + controller(QUERY_PARAM) { + Response.status(QUERY_PARAM.status).entity("some=$param".toString()).build() + } + } + + @GET + @Path("redirect") + Response redirect() { + controller(REDIRECT) { + Response.status(REDIRECT.status).location(new URI(REDIRECT.body)).build() + } + } + } + + @Path("/ignored3") + static class ParentClass extends AbstractClass { + + @GET + @Path("error-status") + Response error() { + controller(ERROR) { + Response.status(ERROR.status).entity(ERROR.body).build() + } + } + + @GET + @Path("exception") + Response exception() { + controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + return null + } + + @GET + @Path("path/{id}/param") + Response path_param(@PathParam("id") int param) { + controller(PATH_PARAM) { + Response.status(PATH_PARAM.status).entity(param.toString()).build() + } + } + } + + @Path("/") + static class ServiceResource extends ParentClass {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/dropwizard-views-0.7-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/dropwizard-views-0.7-javaagent.gradle new file mode 100644 index 000000000..4149e7385 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/dropwizard-views-0.7-javaagent.gradle @@ -0,0 +1,16 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'io.dropwizard' + module = 'dropwizard-views' + versions = "(,)" + } +} + +dependencies { + compileOnly "io.dropwizard:dropwizard-views:0.7.0" + + testImplementation "io.dropwizard:dropwizard-views-freemarker:0.7.0" + testImplementation "io.dropwizard:dropwizard-views-mustache:0.7.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardInstrumentationModule.java new file mode 100644 index 000000000..e96072eca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.dropwizardviews; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class DropwizardInstrumentationModule extends InstrumentationModule { + public DropwizardInstrumentationModule() { + super("dropwizard-views"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new DropwizardRendererInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardRendererInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardRendererInstrumentation.java new file mode 100644 index 000000000..37bd996a8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardRendererInstrumentation.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.dropwizardviews; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.dropwizardviews.DropwizardTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.dropwizard.views.View; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DropwizardRendererInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.dropwizard.views.ViewRenderer"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.dropwizard.views.ViewRenderer")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("render")) + .and(takesArgument(0, named("io.dropwizard.views.View"))) + .and(isPublic()), + this.getClass().getName() + "$RenderAdvice"); + } + + @SuppressWarnings("unused") + public static class RenderAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) View view, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (Java8BytecodeBridge.currentSpan().getSpanContext().isValid()) { + context = tracer().startSpan("Render " + view.getTemplateName()); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + if (throwable == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardTracer.java b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardTracer.java new file mode 100644 index 000000000..ce3368aa3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/DropwizardTracer.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.dropwizardviews; + +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; + +public class DropwizardTracer extends BaseTracer { + private static final DropwizardTracer TRACER = new DropwizardTracer(); + + public static DropwizardTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.dropwizard-views-0.7"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/groovy/ViewRenderTest.groovy b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/groovy/ViewRenderTest.groovy new file mode 100644 index 000000000..e9a15c634 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/groovy/ViewRenderTest.groovy @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.dropwizard.views.View +import io.dropwizard.views.freemarker.FreemarkerViewRenderer +import io.dropwizard.views.mustache.MustacheViewRenderer +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.nio.charset.StandardCharsets + +class ViewRenderTest extends AgentInstrumentationSpecification { + + def "render #template succeeds with span"() { + setup: + def outputStream = new ByteArrayOutputStream() + + when: + runUnderTrace("parent") { + renderer.render(view, Locale.ENGLISH, outputStream) + } + + then: + outputStream.toString().contains("This is an example of a view") + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name "Render $template" + childOf span(0) + } + } + } + + where: + renderer | template + new FreemarkerViewRenderer() | "/views/ftl/utf8.ftl" + new MustacheViewRenderer() | "/views/mustache/utf8.mustache" + new FreemarkerViewRenderer() | "/views/ftl/utf8.ftl" + new MustacheViewRenderer() | "/views/mustache/utf8.mustache" + + view = new View(template, StandardCharsets.UTF_8) {} + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/ftl/iso88591.ftl b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/ftl/iso88591.ftl new file mode 100644 index 000000000..be7e5c9a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/ftl/iso88591.ftl @@ -0,0 +1,10 @@ + + + +

This is an example of a view containing ISO-8859-1 characters

+ + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/ftl/utf8.ftl b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/ftl/utf8.ftl new file mode 100644 index 000000000..86d499e6d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/ftl/utf8.ftl @@ -0,0 +1,9 @@ + + + +

This is an example of a view containing UTF-8 characters

+ +€€€€€€€€€€€€€€€€€€ + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/mustache/iso88591.mustache b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/mustache/iso88591.mustache new file mode 100644 index 000000000..be7e5c9a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/mustache/iso88591.mustache @@ -0,0 +1,10 @@ + + + +

This is an example of a view containing ISO-8859-1 characters

+ + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/mustache/utf8.mustache b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/mustache/utf8.mustache new file mode 100644 index 000000000..86d499e6d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/dropwizard-views-0.7/javaagent/src/test/resources/views/mustache/utf8.mustache @@ -0,0 +1,9 @@ + + + +

This is an example of a view containing UTF-8 characters

+ +€€€€€€€€€€€€€€€€€€ + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/elasticsearch-rest-5.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/elasticsearch-rest-5.0-javaagent.gradle new file mode 100644 index 000000000..045b231cc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/elasticsearch-rest-5.0-javaagent.gradle @@ -0,0 +1,38 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +muzzle { + pass { + group = "org.elasticsearch.client" + module = "rest" + versions = "[5.0,6.4)" + assertInverse = true + } + + pass { + group = "org.elasticsearch.client" + module = "elasticsearch-rest-client" + versions = "[5.0,6.4)" + } +} + +dependencies { + compileOnly "org.elasticsearch.client:rest:5.0.0" + + implementation project(':instrumentation:elasticsearch:elasticsearch-rest-common:library') + + testInstrumentation project(':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent') + testInstrumentation project(':instrumentation:apache-httpasyncclient-4.1:javaagent') + + testImplementation "org.apache.logging.log4j:log4j-core:2.11.0" + testImplementation "org.apache.logging.log4j:log4j-api:2.11.0" + + testImplementation "org.testcontainers:elasticsearch:${versions["org.testcontainers"]}" + testLibrary "org.elasticsearch.client:rest:5.0.0" + + latestDepTestLibrary "org.elasticsearch.client:elasticsearch-rest-client:6.3.+" +} + +tasks.withType(Test).configureEach { + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/Elasticsearch5RestClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/Elasticsearch5RestClientInstrumentationModule.java new file mode 100644 index 000000000..f9655758b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/Elasticsearch5RestClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v5_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Elasticsearch5RestClientInstrumentationModule extends InstrumentationModule { + public Elasticsearch5RestClientInstrumentationModule() { + super("elasticsearch-rest", "elasticsearch-rest-5.0", "elasticsearch"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RestClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/RestClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/RestClientInstrumentation.java new file mode 100644 index 000000000..6d84ea8a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/RestClientInstrumentation.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.elasticsearch.client.ResponseListener; + +public class RestClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.elasticsearch.client.RestClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("performRequestAsync", "performRequestAsyncNoCatch")) + .and(takesArguments(7)) + .and(takesArgument(0, String.class)) // method + .and(takesArgument(1, String.class)) // endpoint + .and(takesArgument(5, named("org.elasticsearch.client.ResponseListener"))), + this.getClass().getName() + "$PerformRequestAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class PerformRequestAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) String method, + @Advice.Argument(1) String endpoint, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(value = 5, readOnly = false) ResponseListener responseListener) { + + context = tracer().startSpan(currentContext(), null, method + " " + endpoint); + scope = context.makeCurrent(); + + responseListener = new RestResponseListener(responseListener, context); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + // span ended in RestResponseListener + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/groovy/Elasticsearch5RestClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/groovy/Elasticsearch5RestClientTest.groovy new file mode 100644 index 000000000..1056ca527 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/groovy/Elasticsearch5RestClientTest.groovy @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import groovy.json.JsonSlurper +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.apache.http.HttpHost +import org.apache.http.client.config.RequestConfig +import org.apache.http.util.EntityUtils +import org.elasticsearch.client.Response +import org.elasticsearch.client.RestClient +import org.elasticsearch.client.RestClientBuilder +import org.testcontainers.elasticsearch.ElasticsearchContainer +import spock.lang.Shared + +class Elasticsearch5RestClientTest extends AgentInstrumentationSpecification { + @Shared + ElasticsearchContainer elasticsearch + + @Shared + HttpHost httpHost + + @Shared + static RestClient client + + def setupSpec() { + if (!Boolean.getBoolean("testLatestDeps")) { + elasticsearch = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:5.6.16") + .withEnv("xpack.ml.enabled", "false") + .withEnv("xpack.security.enabled", "false") + } else { + elasticsearch = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.16") + } + elasticsearch.start() + + httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()) + client = RestClient.builder(httpHost) + .setMaxRetryTimeoutMillis(Integer.MAX_VALUE) + .setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { + @Override + RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder builder) { + return builder.setConnectTimeout(Integer.MAX_VALUE).setSocketTimeout(Integer.MAX_VALUE) + } + }) + .build() + } + + def cleanupSpec() { + elasticsearch.stop() + } + + def "test elasticsearch status"() { + setup: + Response response = client.performRequest("GET", "_cluster/health") + + Map result = new JsonSlurper().parseText(EntityUtils.toString(response.entity)) + + expect: + result.status == "green" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET _cluster/health" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GET _cluster/health" + "${SemanticAttributes.NET_PEER_NAME.key}" httpHost.hostName + "${SemanticAttributes.NET_PEER_PORT.key}" httpHost.port + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/elasticsearch-rest-6.4-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/elasticsearch-rest-6.4-javaagent.gradle new file mode 100644 index 000000000..d02ae71ee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/elasticsearch-rest-6.4-javaagent.gradle @@ -0,0 +1,36 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.elasticsearch.client" + module = "elasticsearch-rest-client" + versions = "[6.4,7.0)" + assertInverse = true + } + + fail { + group = "org.elasticsearch.client" + module = "rest" + versions = "(,)" + } +} + +dependencies { + library "org.elasticsearch.client:elasticsearch-rest-client:6.4.0" + + implementation project(':instrumentation:elasticsearch:elasticsearch-rest-common:library') + + testInstrumentation project(':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent') + testInstrumentation project(':instrumentation:apache-httpasyncclient-4.1:javaagent') + //TODO: review the following claim, we are not using embedded ES anymore + // Netty is used, but it adds complexity to the tests since we're using embedded ES. + //testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + + testImplementation "org.apache.logging.log4j:log4j-core:2.11.0" + testImplementation "org.apache.logging.log4j:log4j-api:2.11.0" + + testImplementation "org.testcontainers:elasticsearch:${versions["org.testcontainers"]}" + testLibrary "org.elasticsearch.client:elasticsearch-rest-client:6.4.0" + + latestDepTestLibrary "org.elasticsearch.client:elasticsearch-rest-client:6.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/Elasticsearch6RestClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/Elasticsearch6RestClientInstrumentationModule.java new file mode 100644 index 000000000..1ed72a088 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/Elasticsearch6RestClientInstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v6_4; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class Elasticsearch6RestClientInstrumentationModule extends InstrumentationModule { + public Elasticsearch6RestClientInstrumentationModule() { + super("elasticsearch-rest", "elasticsearch-rest-6.0", "elasticsearch"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // class introduced in 7.0.0 + return not(hasClassesNamed("org.elasticsearch.client.RestClient$InternalRequest")); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RestClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/RestClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/RestClientInstrumentation.java new file mode 100644 index 000000000..88d33d51d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/RestClientInstrumentation.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v6_4; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.ResponseListener; + +public class RestClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.elasticsearch.client.RestClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("performRequestAsyncNoCatch")) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.elasticsearch.client.Request"))) + .and(takesArgument(1, named("org.elasticsearch.client.ResponseListener"))), + this.getClass().getName() + "$PerformRequestAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class PerformRequestAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(value = 1, readOnly = false) ResponseListener responseListener, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = + tracer() + .startSpan(currentContext(), null, request.getMethod() + " " + request.getEndpoint()); + scope = context.makeCurrent(); + + responseListener = new RestResponseListener(responseListener, context); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + // span ended in RestResponseListener + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/groovy/Elasticsearch6RestClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/groovy/Elasticsearch6RestClientTest.groovy new file mode 100644 index 000000000..a3463001a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/groovy/Elasticsearch6RestClientTest.groovy @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import groovy.json.JsonSlurper +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.apache.http.HttpHost +import org.apache.http.client.config.RequestConfig +import org.apache.http.util.EntityUtils +import org.elasticsearch.client.Response +import org.elasticsearch.client.RestClient +import org.elasticsearch.client.RestClientBuilder +import org.testcontainers.elasticsearch.ElasticsearchContainer +import spock.lang.Shared + +class Elasticsearch6RestClientTest extends AgentInstrumentationSpecification { + @Shared + ElasticsearchContainer elasticsearch + + @Shared + HttpHost httpHost + + @Shared + RestClient client + + def setupSpec() { + elasticsearch = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.16") + elasticsearch.start() + + httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()) + client = RestClient.builder(httpHost) + .setMaxRetryTimeoutMillis(Integer.MAX_VALUE) + .setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { + @Override + RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder builder) { + return builder.setConnectTimeout(Integer.MAX_VALUE).setSocketTimeout(Integer.MAX_VALUE) + } + }) + .build() + + } + + def cleanupSpec() { + elasticsearch.stop() + } + + def "test elasticsearch status"() { + setup: + Response response = client.performRequest("GET", "_cluster/health") + + Map result = new JsonSlurper().parseText(EntityUtils.toString(response.entity)) + + expect: + result.status == "green" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET _cluster/health" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GET _cluster/health" + "${SemanticAttributes.NET_PEER_NAME.key}" httpHost.hostName + "${SemanticAttributes.NET_PEER_PORT.key}" httpHost.port + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/elasticsearch-rest-7.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/elasticsearch-rest-7.0-javaagent.gradle new file mode 100644 index 000000000..b9696e99b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/elasticsearch-rest-7.0-javaagent.gradle @@ -0,0 +1,33 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.elasticsearch.client" + module = "elasticsearch-rest-client" + versions = "[7.0,)" + assertInverse = true + } + + fail { + group = "org.elasticsearch.client" + module = "rest" + versions = "(,)" + } +} + +dependencies { + library "org.elasticsearch.client:elasticsearch-rest-client:7.0.0" + + implementation project(':instrumentation:elasticsearch:elasticsearch-rest-common:library') + + testInstrumentation project(':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent') + testInstrumentation project(':instrumentation:apache-httpasyncclient-4.1:javaagent') + //TODO: review the following claim, we are not using embedded ES anymore + // Netty is used, but it adds complexity to the tests since we're using embedded ES. + //testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + + testImplementation "org.apache.logging.log4j:log4j-core:2.11.0" + testImplementation "org.apache.logging.log4j:log4j-api:2.11.0" + + testImplementation "org.testcontainers:elasticsearch:${versions["org.testcontainers"]}" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/Elasticsearch7RestClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/Elasticsearch7RestClientInstrumentationModule.java new file mode 100644 index 000000000..d4bca3374 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/Elasticsearch7RestClientInstrumentationModule.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v7_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class Elasticsearch7RestClientInstrumentationModule extends InstrumentationModule { + public Elasticsearch7RestClientInstrumentationModule() { + super("elasticsearch-rest", "elasticsearch-rest-7.0", "elasticsearch"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // class introduced in 7.0.0 + return hasClassesNamed("org.elasticsearch.client.RestClient$InternalRequest"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RestClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java new file mode 100644 index 000000000..11bba6ef6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v7_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseListener; + +public class RestClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.elasticsearch.client.RestClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("performRequest")) + .and(takesArguments(1)) + .and(takesArgument(0, named("org.elasticsearch.client.Request"))), + this.getClass().getName() + "$PerformRequestAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("performRequestAsync")) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.elasticsearch.client.Request"))) + .and(takesArgument(1, named("org.elasticsearch.client.ResponseListener"))), + this.getClass().getName() + "$PerformRequestAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class PerformRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = + tracer() + .startSpan(currentContext(), null, request.getMethod() + " " + request.getEndpoint()); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Return(readOnly = false) Response response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + if (response != null) { + tracer().onResponse(context, response); + } + tracer().end(context); + } + } + } + + @SuppressWarnings("unused") + public static class PerformRequestAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(value = 1, readOnly = false) ResponseListener responseListener, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = + tracer() + .startSpan(currentContext(), null, request.getMethod() + " " + request.getEndpoint()); + scope = context.makeCurrent(); + + responseListener = new RestResponseListener(responseListener, context); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + // span ended in RestResponseListener + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/Elasticsearch7RestClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/Elasticsearch7RestClientTest.groovy new file mode 100644 index 000000000..2715cf363 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/Elasticsearch7RestClientTest.groovy @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import groovy.json.JsonSlurper +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CountDownLatch +import org.apache.http.HttpHost +import org.apache.http.client.config.RequestConfig +import org.apache.http.util.EntityUtils +import org.elasticsearch.client.Request +import org.elasticsearch.client.Response +import org.elasticsearch.client.ResponseListener +import org.elasticsearch.client.RestClient +import org.elasticsearch.client.RestClientBuilder +import org.testcontainers.elasticsearch.ElasticsearchContainer +import spock.lang.Shared + +class Elasticsearch7RestClientTest extends AgentInstrumentationSpecification { + @Shared + ElasticsearchContainer elasticsearch + + @Shared + HttpHost httpHost + + @Shared + RestClient client + + def setupSpec() { + elasticsearch = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2") + elasticsearch.start() + + httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()) + client = RestClient.builder(httpHost) + .setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { + @Override + RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder builder) { + return builder.setConnectTimeout(Integer.MAX_VALUE).setSocketTimeout(Integer.MAX_VALUE) + } + }) + .build() + } + + def cleanupSpec() { + elasticsearch.stop() + } + + def "test elasticsearch status"() { + setup: + Response response = client.performRequest(new Request("GET", "_cluster/health")) + + Map result = new JsonSlurper().parseText(EntityUtils.toString(response.entity)) + + expect: + result.status == "green" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET _cluster/health" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GET _cluster/health" + "${SemanticAttributes.NET_PEER_NAME.key}" httpHost.hostName + "${SemanticAttributes.NET_PEER_PORT.key}" httpHost.port + } + } + } + } + } + + def "test elasticsearch status async"() { + setup: + Response requestResponse = null + Exception exception = null + CountDownLatch countDownLatch = new CountDownLatch(1) + ResponseListener responseListener = new ResponseListener() { + @Override + void onSuccess(Response response) { + requestResponse = response + countDownLatch.countDown() + } + + @Override + void onFailure(Exception e) { + exception = e + countDownLatch.countDown() + } + } + client.performRequestAsync(new Request("GET", "_cluster/health"), responseListener) + countDownLatch.await() + + if (exception != null) { + throw exception + } + Map result = new JsonSlurper().parseText(EntityUtils.toString(requestResponse.entity)) + + expect: + result.status == "green" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET _cluster/health" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GET _cluster/health" + "${SemanticAttributes.NET_PEER_NAME.key}" httpHost.hostName + "${SemanticAttributes.NET_PEER_PORT.key}" httpHost.port + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/elasticsearch-rest-common-library.gradle b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/elasticsearch-rest-common-library.gradle new file mode 100644 index 000000000..72ed7a1af --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/elasticsearch-rest-common-library.gradle @@ -0,0 +1,5 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "org.elasticsearch.client:rest:5.0.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestClientTracer.java new file mode 100644 index 000000000..ba8858f6e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestClientTracer.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import org.elasticsearch.client.Response; + +public class ElasticsearchRestClientTracer extends DatabaseClientTracer { + private static final ElasticsearchRestClientTracer TRACER = new ElasticsearchRestClientTracer(); + + private ElasticsearchRestClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static ElasticsearchRestClientTracer tracer() { + return TRACER; + } + + public void onResponse(Context context, Response response) { + if (response != null && response.getHost() != null) { + Span span = Span.fromContext(context); + netPeerAttributes.setNetPeer(span, response.getHost().getHostName(), null); + span.setAttribute(SemanticAttributes.NET_PEER_PORT, (long) response.getHost().getPort()); + } + } + + @Override + protected String sanitizeStatement(String operation) { + return operation; + } + + @Override + protected String dbSystem(Void connection) { + return "elasticsearch"; + } + + @Override + protected InetSocketAddress peerAddress(Void connection) { + return null; + } + + @Override + protected String dbOperation(Void connection, String operation, String ignored) { + return operation; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.elasticsearch-rest-common"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/RestResponseListener.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/RestResponseListener.java new file mode 100644 index 000000000..c90430188 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/RestResponseListener.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestClientTracer.tracer; + +import io.opentelemetry.context.Context; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseListener; + +public class RestResponseListener implements ResponseListener { + + private final ResponseListener listener; + private final Context context; + + public RestResponseListener(ResponseListener listener, Context context) { + this.listener = listener; + this.context = context; + } + + @Override + public void onSuccess(Response response) { + if (response.getHost() != null) { + tracer().onResponse(context, response); + } + tracer().end(context); + + listener.onSuccess(response); + } + + @Override + public void onFailure(Exception e) { + tracer().endExceptionally(context, e); + listener.onFailure(e); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/elasticsearch-transport-5.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/elasticsearch-transport-5.0-javaagent.gradle new file mode 100644 index 000000000..a785a5ba3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/elasticsearch-transport-5.0-javaagent.gradle @@ -0,0 +1,44 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.elasticsearch.client" + module = "transport" + versions = "[5.0.0,5.3.0)" + // version 7.11.0 depends on org.elasticsearch:elasticsearch:7.11.0 which depends on + // org.elasticsearch:elasticsearch-plugin-classloader:7.11.0 which does not exist + skip('7.11.0') + assertInverse = true + } + pass { + group = "org.elasticsearch" + module = "elasticsearch" + versions = "[5.0.0,5.3.0)" + // version 7.11.0 depends on org.elasticsearch:elasticsearch:7.11.0 which depends on + // org.elasticsearch:elasticsearch-plugin-classloader:7.11.0 which does not exist + skip('7.11.0') + assertInverse = true + } +} + +dependencies { + compileOnly "org.elasticsearch.client:transport:5.0.0" + + implementation project(':instrumentation:elasticsearch:elasticsearch-transport-common:library') + + // Ensure no cross interference + testInstrumentation project(':instrumentation:elasticsearch:elasticsearch-rest-5.0:javaagent') + testInstrumentation project(':instrumentation:apache-httpasyncclient-4.1:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + + testImplementation "org.apache.logging.log4j:log4j-core:2.11.0" + testImplementation "org.apache.logging.log4j:log4j-api:2.11.0" + + testImplementation "org.elasticsearch.plugin:transport-netty3-client:5.0.0" + testImplementation "org.elasticsearch.client:transport:5.0.0" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.elasticsearch.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/AbstractClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/AbstractClientInstrumentation.java new file mode 100644 index 000000000..373993779 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/AbstractClientInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; + +public class AbstractClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // If we want to be more generic, we could instrument the interface instead: + // .and(safeHasSuperType(named("org.elasticsearch.client.ElasticsearchClient")))) + return named("org.elasticsearch.client.support.AbstractClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(takesArgument(0, named("org.elasticsearch.action.Action"))) + .and(takesArgument(1, named("org.elasticsearch.action.ActionRequest"))) + .and(takesArgument(2, named("org.elasticsearch.action.ActionListener"))), + this.getClass().getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Action action, + @Advice.Argument(1) ActionRequest actionRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(value = 2, readOnly = false) + ActionListener actionListener) { + + context = tracer().startSpan(currentContext(), null, action); + scope = context.makeCurrent(); + + tracer().onRequest(context, action.getClass(), actionRequest.getClass()); + actionListener = new TransportActionListener<>(actionRequest, actionListener, context); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/Elasticsearch5TransportClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/Elasticsearch5TransportClientInstrumentationModule.java new file mode 100644 index 000000000..0aed966de --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/Elasticsearch5TransportClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v5_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Elasticsearch5TransportClientInstrumentationModule extends InstrumentationModule { + public Elasticsearch5TransportClientInstrumentationModule() { + super("elasticsearch-transport", "elasticsearch-transport-5.0", "elasticsearch"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new AbstractClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/TransportActionListener.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/TransportActionListener.java new file mode 100644 index 000000000..c18cd0cee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/TransportActionListener.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportClientTracer.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.DocumentRequest; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.bulk.BulkShardResponse; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.broadcast.BroadcastResponse; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.action.support.replication.ReplicationResponse; + +public class TransportActionListener implements ActionListener { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty( + "otel.instrumentation.elasticsearch.experimental-span-attributes", false); + + private final ActionListener listener; + private final Context context; + + public TransportActionListener( + ActionRequest actionRequest, ActionListener listener, Context context) { + this.listener = listener; + this.context = context; + onRequest(actionRequest); + } + + private void onRequest(ActionRequest request) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span span = Span.fromContext(context); + + if (request instanceof IndicesRequest) { + IndicesRequest req = (IndicesRequest) request; + String[] indices = req.indices(); + if (indices != null && indices.length > 0) { + span.setAttribute("elasticsearch.request.indices", String.join(",", indices)); + } + } + if (request instanceof SearchRequest) { + SearchRequest req = (SearchRequest) request; + String[] types = req.types(); + if (types != null && types.length > 0) { + span.setAttribute("elasticsearch.request.search.types", String.join(",", types)); + } + } + if (request instanceof DocumentRequest) { + DocumentRequest req = (DocumentRequest) request; + span.setAttribute("elasticsearch.request.write.type", req.type()); + span.setAttribute("elasticsearch.request.write.routing", req.routing()); + } + } + } + + @Override + public void onResponse(T response) { + Span span = Span.fromContext(context); + + if (response.remoteAddress() != null) { + NetPeerAttributes.INSTANCE.setNetPeer( + span, response.remoteAddress().getHost(), response.remoteAddress().getAddress()); + span.setAttribute( + SemanticAttributes.NET_PEER_PORT, (long) response.remoteAddress().getPort()); + } + + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + if (response instanceof GetResponse) { + GetResponse resp = (GetResponse) response; + span.setAttribute("elasticsearch.type", resp.getType()); + span.setAttribute("elasticsearch.id", resp.getId()); + span.setAttribute("elasticsearch.version", resp.getVersion()); + } + + if (response instanceof BroadcastResponse) { + BroadcastResponse resp = (BroadcastResponse) response; + span.setAttribute("elasticsearch.shard.broadcast.total", resp.getTotalShards()); + span.setAttribute("elasticsearch.shard.broadcast.successful", resp.getSuccessfulShards()); + span.setAttribute("elasticsearch.shard.broadcast.failed", resp.getFailedShards()); + } + + if (response instanceof ReplicationResponse) { + ReplicationResponse resp = (ReplicationResponse) response; + span.setAttribute("elasticsearch.shard.replication.total", resp.getShardInfo().getTotal()); + span.setAttribute( + "elasticsearch.shard.replication.successful", resp.getShardInfo().getSuccessful()); + span.setAttribute( + "elasticsearch.shard.replication.failed", resp.getShardInfo().getFailed()); + } + + if (response instanceof IndexResponse) { + span.setAttribute( + "elasticsearch.response.status", ((IndexResponse) response).status().getStatus()); + } + + if (response instanceof BulkShardResponse) { + BulkShardResponse resp = (BulkShardResponse) response; + span.setAttribute("elasticsearch.shard.bulk.id", resp.getShardId().getId()); + span.setAttribute("elasticsearch.shard.bulk.index", resp.getShardId().getIndexName()); + } + + if (response instanceof BaseNodesResponse) { + BaseNodesResponse resp = (BaseNodesResponse) response; + if (resp.hasFailures()) { + span.setAttribute("elasticsearch.node.failures", resp.failures().size()); + } + span.setAttribute("elasticsearch.node.cluster.name", resp.getClusterName().value()); + } + } + + tracer().end(context); + listener.onResponse(response); + } + + @Override + public void onFailure(Exception e) { + tracer().endExceptionally(context, e); + listener.onFailure(e); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5NodeClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5NodeClientTest.groovy new file mode 100644 index 000000000..7cbb7f36e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5NodeClientTest.groovy @@ -0,0 +1,258 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest +import org.elasticsearch.common.io.FileSystemUtils +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.env.Environment +import org.elasticsearch.index.IndexNotFoundException +import org.elasticsearch.node.Node +import org.elasticsearch.node.internal.InternalSettingsPreparer +import org.elasticsearch.transport.Netty3Plugin +import spock.lang.Shared + +class Elasticsearch5NodeClientTest extends AgentInstrumentationSpecification { + public static final long TIMEOUT = 10000 // 10 seconds + + @Shared + Node testNode + @Shared + File esWorkingDir + @Shared + String clusterName = UUID.randomUUID().toString() + + def client = testNode.client() + + def setupSpec() { + + esWorkingDir = File.createTempDir("test-es-working-dir-", "") + esWorkingDir.deleteOnExit() + println "ES work dir: $esWorkingDir" + + def settings = Settings.builder() + .put("path.home", esWorkingDir.path) + // Since we use listeners to close spans this should make our span closing deterministic which is good for tests + .put("thread_pool.listener.size", 1) + .put("transport.type", "netty3") + .put("http.type", "netty3") + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("discovery.type", "local") + .build() + testNode = new Node(new Environment(InternalSettingsPreparer.prepareSettings(settings)), [Netty3Plugin]) + testNode.start() + runUnderTrace("setup") { + // this may potentially create multiple requests and therefore multiple spans, so we wrap this call + // into a top level trace to get exactly one trace in the result. + testNode.client().admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + } + ignoreTracesAndClear(1) + } + + def cleanupSpec() { + testNode?.close() + if (esWorkingDir != null) { + FileSystemUtils.deleteSubDirectories(esWorkingDir.toPath()) + esWorkingDir.delete() + } + } + + def "test elasticsearch status"() { + setup: + def result = client.admin().cluster().health(new ClusterHealthRequest()) + + def clusterHealthStatus = result.get().status + + expect: + clusterHealthStatus.name() == "GREEN" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + } + } + + def "test elasticsearch error"() { + when: + client.prepareGet(indexName, indexType, id).get() + + then: + thrown IndexNotFoundException + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GetAction" + status ERROR + errorEvent IndexNotFoundException, "no such index" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + } + } + } + } + + where: + indexName = "invalid-index" + indexType = "test-type" + id = "1" + } + + def "test elasticsearch get"() { + setup: + def indexResult = client.admin().indices().prepareCreate(indexName).get() + + expect: + indexResult.acknowledged + + when: + client.admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + def emptyResult = client.prepareGet(indexName, indexType, id).get() + + then: + !emptyResult.isExists() + emptyResult.id == id + emptyResult.type == indexType + emptyResult.index == indexName + + when: + def createResult = client.prepareIndex(indexName, indexType, id).setSource([:]).get() + + then: + createResult.id == id + createResult.type == indexType + createResult.index == indexName + createResult.status().status == 201 + + when: + def result = client.prepareGet(indexName, indexType, id).get() + + then: + result.isExists() + result.id == id + result.type == indexType + result.index == indexName + + and: + assertTraces(5) { + trace(0, 1) { + span(0) { + name "CreateIndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "CreateIndexAction" + "elasticsearch.action" "CreateIndexAction" + "elasticsearch.request" "CreateIndexRequest" + "elasticsearch.request.indices" indexName + } + } + } + trace(1, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + trace(2, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version"(-1) + } + } + } + trace(3, 2) { + span(0) { + name "IndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" indexType + "elasticsearch.response.status" 201 + "elasticsearch.shard.replication.total" 2 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.failed" 0 + } + } + span(1) { + name "PutMappingAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "PutMappingAction" + "elasticsearch.action" "PutMappingAction" + "elasticsearch.request" "PutMappingRequest" + } + } + } + trace(4, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version" 1 + } + } + } + } + + cleanup: + client.admin().indices().prepareDelete(indexName).get() + + where: + indexName = "test-index" + indexType = "test-type" + id = "1" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5TransportClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5TransportClientTest.groovy new file mode 100644 index 000000000..b47bd112b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5TransportClientTest.groovy @@ -0,0 +1,280 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest +import org.elasticsearch.client.transport.TransportClient +import org.elasticsearch.common.io.FileSystemUtils +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.common.transport.TransportAddress +import org.elasticsearch.env.Environment +import org.elasticsearch.index.IndexNotFoundException +import org.elasticsearch.node.Node +import org.elasticsearch.node.internal.InternalSettingsPreparer +import org.elasticsearch.transport.Netty3Plugin +import org.elasticsearch.transport.RemoteTransportException +import org.elasticsearch.transport.TransportService +import org.elasticsearch.transport.client.PreBuiltTransportClient +import spock.lang.Shared + +class Elasticsearch5TransportClientTest extends AgentInstrumentationSpecification { + public static final long TIMEOUT = 10000 // 10 seconds + + @Shared + TransportAddress tcpPublishAddress + @Shared + Node testNode + @Shared + File esWorkingDir + @Shared + String clusterName = UUID.randomUUID().toString() + + @Shared + TransportClient client + + def setupSpec() { + + esWorkingDir = File.createTempDir("test-es-working-dir-", "") + esWorkingDir.deleteOnExit() + println "ES work dir: $esWorkingDir" + + def settings = Settings.builder() + .put("path.home", esWorkingDir.path) + .put("transport.type", "netty3") + .put("http.type", "netty3") + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("discovery.type", "local") + .build() + testNode = new Node(new Environment(InternalSettingsPreparer.prepareSettings(settings)), [Netty3Plugin]) + testNode.start() + tcpPublishAddress = testNode.injector().getInstance(TransportService).boundAddress().publishAddress() + + client = new PreBuiltTransportClient( + Settings.builder() + // Since we use listeners to close spans this should make our span closing deterministic which is good for tests + .put("thread_pool.listener.size", 1) + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .build() + ) + client.addTransportAddress(tcpPublishAddress) + runUnderTrace("setup") { + // this may potentially create multiple requests and therefore multiple spans, so we wrap this call + // into a top level trace to get exactly one trace in the result. + client.admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + } + ignoreTracesAndClear(1) + } + + def cleanupSpec() { + testNode?.close() + if (esWorkingDir != null) { + FileSystemUtils.deleteSubDirectories(esWorkingDir.toPath()) + esWorkingDir.delete() + } + } + + def "test elasticsearch status"() { + setup: + def result = client.admin().cluster().health(new ClusterHealthRequest()) + + def clusterHealthStatus = result.get().status + + expect: + clusterHealthStatus.name() == "GREEN" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.host + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + } + } + + def "test elasticsearch error"() { + when: + client.prepareGet(indexName, indexType, id).get() + + then: + thrown IndexNotFoundException + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GetAction" + kind CLIENT + status ERROR + errorEvent RemoteTransportException, String + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + } + } + } + } + + where: + indexName = "invalid-index" + indexType = "test-type" + id = "1" + } + + def "test elasticsearch get"() { + setup: + def indexResult = client.admin().indices().prepareCreate(indexName).get() + + expect: + indexResult.acknowledged + + when: + def emptyResult = client.prepareGet(indexName, indexType, id).get() + + then: + !emptyResult.isExists() + emptyResult.id == id + emptyResult.type == indexType + emptyResult.index == indexName + + when: + def createResult = client.prepareIndex(indexName, indexType, id).setSource([:]).get() + + then: + createResult.id == id + createResult.type == indexType + createResult.index == indexName + createResult.status().status == 201 + + when: + def result = client.prepareGet(indexName, indexType, id).get() + + then: + result.isExists() + result.id == id + result.type == indexType + result.index == indexName + + and: + assertTraces(5) { + // PutMappingAction and IndexAction run in separate threads so their order can vary + traces.subList(2, 4).sort(orderByRootSpanName("PutMappingAction", "IndexAction")) + + trace(0, 1) { + span(0) { + name "CreateIndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.host + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "CreateIndexAction" + "elasticsearch.action" "CreateIndexAction" + "elasticsearch.request" "CreateIndexRequest" + "elasticsearch.request.indices" indexName + } + } + } + trace(1, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.host + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version"(-1) + } + } + } + trace(2, 1) { + span(0) { + name "PutMappingAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "PutMappingAction" + "elasticsearch.action" "PutMappingAction" + "elasticsearch.request" "PutMappingRequest" + } + } + } + trace(3, 1) { + span(0) { + name "IndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.host + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" indexType + "elasticsearch.response.status" 201 + "elasticsearch.shard.replication.total" 2 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.failed" 0 + } + } + } + trace(4, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version" 1 + } + } + } + } + + cleanup: + client.admin().indices().prepareDelete(indexName).get() + + where: + indexName = "test-index" + indexType = "test-type" + id = "1" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/elasticsearch-transport-5.3-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/elasticsearch-transport-5.3-javaagent.gradle new file mode 100644 index 000000000..a628fb779 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/elasticsearch-transport-5.3-javaagent.gradle @@ -0,0 +1,56 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.elasticsearch.client" + module = "transport" + versions = "[5.3.0,6.0.0)" + // version 7.11.0 depends on org.elasticsearch:elasticsearch:7.11.0 which depends on + // org.elasticsearch:elasticsearch-plugin-classloader:7.11.0 which does not exist + skip('7.11.0') + assertInverse = true + } + pass { + group = "org.elasticsearch" + module = "elasticsearch" + versions = "[5.3.0,6.0.0)" + // version 7.11.0 depends on org.elasticsearch:elasticsearch-plugin-classloader:7.11.0 + // which does not exist + skip('7.11.0') + assertInverse = true + } +} + +dependencies { + compileOnly("org.elasticsearch.client:transport:5.3.0") { + transitive = false + } + compileOnly("org.elasticsearch:elasticsearch:5.3.0") { + // We don't need all its transitive dependencies when compiling and run tests against 5.5.0 + transitive = false + } + + implementation project(':instrumentation:elasticsearch:elasticsearch-transport-common:library') + + testInstrumentation project(':instrumentation:apache-httpasyncclient-4.1:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:spring:spring-data-1.8:javaagent') + + testImplementation "org.apache.logging.log4j:log4j-core:2.11.0" + testImplementation "org.apache.logging.log4j:log4j-api:2.11.0" + + // Unfortunately spring-data-elasticsearch requires 5.5.0 + testLibrary "org.elasticsearch.client:transport:5.5.0" + testLibrary "org.elasticsearch.plugin:transport-netty3-client:5.3.0" + + testLibrary "org.springframework.data:spring-data-elasticsearch:3.0.0.RELEASE" + + latestDepTestLibrary "org.elasticsearch.plugin:transport-netty3-client:5.+" + latestDepTestLibrary "org.elasticsearch.client:transport:5.+" + latestDepTestLibrary "org.springframework.data:spring-data-elasticsearch:3.0.+" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.elasticsearch.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/AbstractClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/AbstractClientInstrumentation.java new file mode 100644 index 000000000..0190dfb1d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/AbstractClientInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v5_3; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; + +public class AbstractClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // If we want to be more generic, we could instrument the interface instead: + // .and(safeHasSuperType(named("org.elasticsearch.client.ElasticsearchClient")))) + return named("org.elasticsearch.client.support.AbstractClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(takesArgument(0, named("org.elasticsearch.action.Action"))) + .and(takesArgument(1, named("org.elasticsearch.action.ActionRequest"))) + .and(takesArgument(2, named("org.elasticsearch.action.ActionListener"))), + this.getClass().getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Action action, + @Advice.Argument(1) ActionRequest actionRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(value = 2, readOnly = false) + ActionListener actionListener) { + + context = tracer().startSpan(currentContext(), null, action); + scope = context.makeCurrent(); + + tracer().onRequest(context, action.getClass(), actionRequest.getClass()); + actionListener = new TransportActionListener<>(actionRequest, actionListener, context); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/Elasticsearch53TransportClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/Elasticsearch53TransportClientInstrumentationModule.java new file mode 100644 index 000000000..eda768845 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/Elasticsearch53TransportClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v5_3; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +/** Beginning in version 5.3.0, DocumentRequest was renamed to DocWriteRequest. */ +@AutoService(InstrumentationModule.class) +public class Elasticsearch53TransportClientInstrumentationModule extends InstrumentationModule { + public Elasticsearch53TransportClientInstrumentationModule() { + super("elasticsearch-transport", "elasticsearch-transport-5.3", "elasticsearch"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new AbstractClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/TransportActionListener.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/TransportActionListener.java new file mode 100644 index 000000000..f8398264d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/TransportActionListener.java @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v5_3; + +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportClientTracer.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.bulk.BulkShardResponse; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.broadcast.BroadcastResponse; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.action.support.replication.ReplicationResponse; + +public class TransportActionListener implements ActionListener { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty( + "otel.instrumentation.elasticsearch.experimental-span-attributes", false); + + private final ActionListener listener; + private final Context context; + + public TransportActionListener( + ActionRequest actionRequest, ActionListener listener, Context context) { + this.listener = listener; + this.context = context; + onRequest(actionRequest); + } + + private void onRequest(ActionRequest request) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span span = Span.fromContext(context); + + if (request instanceof IndicesRequest) { + IndicesRequest req = (IndicesRequest) request; + String[] indices = req.indices(); + if (indices != null && indices.length > 0) { + span.setAttribute("elasticsearch.request.indices", String.join(",", indices)); + } + } + if (request instanceof SearchRequest) { + SearchRequest req = (SearchRequest) request; + String[] types = req.types(); + if (types != null && types.length > 0) { + span.setAttribute("elasticsearch.request.search.types", String.join(",", types)); + } + } + if (request instanceof DocWriteRequest) { + DocWriteRequest req = (DocWriteRequest) request; + span.setAttribute("elasticsearch.request.write.type", req.type()); + span.setAttribute("elasticsearch.request.write.routing", req.routing()); + span.setAttribute("elasticsearch.request.write.version", req.version()); + } + } + } + + @Override + public void onResponse(T response) { + Span span = Span.fromContext(context); + + if (response.remoteAddress() != null) { + NetPeerAttributes.INSTANCE.setNetPeer( + span, response.remoteAddress().getHost(), response.remoteAddress().getAddress()); + span.setAttribute( + SemanticAttributes.NET_PEER_PORT, (long) response.remoteAddress().getPort()); + } + + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + if (response instanceof GetResponse) { + GetResponse resp = (GetResponse) response; + span.setAttribute("elasticsearch.type", resp.getType()); + span.setAttribute("elasticsearch.id", resp.getId()); + span.setAttribute("elasticsearch.version", resp.getVersion()); + } + + if (response instanceof BroadcastResponse) { + BroadcastResponse resp = (BroadcastResponse) response; + span.setAttribute("elasticsearch.shard.broadcast.total", resp.getTotalShards()); + span.setAttribute("elasticsearch.shard.broadcast.successful", resp.getSuccessfulShards()); + span.setAttribute("elasticsearch.shard.broadcast.failed", resp.getFailedShards()); + } + + if (response instanceof ReplicationResponse) { + ReplicationResponse resp = (ReplicationResponse) response; + span.setAttribute("elasticsearch.shard.replication.total", resp.getShardInfo().getTotal()); + span.setAttribute( + "elasticsearch.shard.replication.successful", resp.getShardInfo().getSuccessful()); + span.setAttribute( + "elasticsearch.shard.replication.failed", resp.getShardInfo().getFailed()); + } + + if (response instanceof IndexResponse) { + span.setAttribute( + "elasticsearch.response.status", ((IndexResponse) response).status().getStatus()); + } + + if (response instanceof BulkShardResponse) { + BulkShardResponse resp = (BulkShardResponse) response; + span.setAttribute("elasticsearch.shard.bulk.id", resp.getShardId().getId()); + span.setAttribute("elasticsearch.shard.bulk.index", resp.getShardId().getIndexName()); + } + + if (response instanceof BaseNodesResponse) { + BaseNodesResponse resp = (BaseNodesResponse) response; + if (resp.hasFailures()) { + span.setAttribute("elasticsearch.node.failures", resp.failures().size()); + } + span.setAttribute("elasticsearch.node.cluster.name", resp.getClusterName().value()); + } + } + + tracer().end(context); + listener.onResponse(response); + } + + @Override + public void onFailure(Exception e) { + tracer().endExceptionally(context, e); + listener.onFailure(e); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53NodeClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53NodeClientTest.groovy new file mode 100644 index 000000000..192bb1c58 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53NodeClientTest.groovy @@ -0,0 +1,262 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest +import org.elasticsearch.common.io.FileSystemUtils +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.env.Environment +import org.elasticsearch.index.IndexNotFoundException +import org.elasticsearch.node.InternalSettingsPreparer +import org.elasticsearch.node.Node +import org.elasticsearch.transport.Netty3Plugin +import spock.lang.Shared + +class Elasticsearch53NodeClientTest extends AgentInstrumentationSpecification { + public static final long TIMEOUT = 10000 // 10 seconds + + @Shared + Node testNode + @Shared + File esWorkingDir + @Shared + String clusterName = UUID.randomUUID().toString() + + def client = testNode.client() + + def setupSpec() { + + esWorkingDir = File.createTempDir("test-es-working-dir-", "") + esWorkingDir.deleteOnExit() + println "ES work dir: $esWorkingDir" + + def settings = Settings.builder() + .put("path.home", esWorkingDir.path) + // Since we use listeners to close spans this should make our span closing deterministic which is good for tests + .put("thread_pool.listener.size", 1) + .put("transport.type", "netty3") + .put("http.type", "netty3") + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("discovery.type", "single-node") + .build() + testNode = new Node(new Environment(InternalSettingsPreparer.prepareSettings(settings)), [Netty3Plugin]) + testNode.start() + runUnderTrace("setup") { + // this may potentially create multiple requests and therefore multiple spans, so we wrap this call + // into a top level trace to get exactly one trace in the result. + testNode.client().admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + // disable periodic refresh in InternalClusterInfoService as it creates spans that tests don't expect + testNode.client().admin().cluster().updateSettings(new ClusterUpdateSettingsRequest().transientSettings(["cluster.routing.allocation.disk.threshold_enabled": false])) + } + ignoreTracesAndClear(1) + } + + def cleanupSpec() { + testNode?.close() + if (esWorkingDir != null) { + FileSystemUtils.deleteSubDirectories(esWorkingDir.toPath()) + esWorkingDir.delete() + } + } + + def "test elasticsearch status"() { + setup: + def result = client.admin().cluster().health(new ClusterHealthRequest()) + + def clusterHealthStatus = result.get().status + + expect: + clusterHealthStatus.name() == "GREEN" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + } + } + + def "test elasticsearch error"() { + when: + client.prepareGet(indexName, indexType, id).get() + + then: + thrown IndexNotFoundException + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GetAction" + kind CLIENT + status ERROR + errorEvent IndexNotFoundException, "no such index" + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + } + } + } + } + + where: + indexName = "invalid-index" + indexType = "test-type" + id = "1" + } + + def "test elasticsearch get"() { + setup: + def indexResult = client.admin().indices().prepareCreate(indexName).get() + + expect: + indexResult.acknowledged + + when: + client.admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + def emptyResult = client.prepareGet(indexName, indexType, id).get() + + then: + !emptyResult.isExists() + emptyResult.id == id + emptyResult.type == indexType + emptyResult.index == indexName + + when: + def createResult = client.prepareIndex(indexName, indexType, id).setSource([:]).get() + + then: + createResult.id == id + createResult.type == indexType + createResult.index == indexName + createResult.status().status == 201 + + when: + def result = client.prepareGet(indexName, indexType, id).get() + + then: + result.isExists() + result.id == id + result.type == indexType + result.index == indexName + + and: + assertTraces(5) { + trace(0, 1) { + span(0) { + name "CreateIndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "CreateIndexAction" + "elasticsearch.action" "CreateIndexAction" + "elasticsearch.request" "CreateIndexRequest" + "elasticsearch.request.indices" indexName + } + } + } + trace(1, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + trace(2, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version"(-1) + } + } + } + trace(3, 2) { + span(0) { + name "IndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" indexType + "elasticsearch.request.write.version"(-3) + "elasticsearch.response.status" 201 + "elasticsearch.shard.replication.total" 2 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.failed" 0 + } + } + span(1) { + name "PutMappingAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "PutMappingAction" + "elasticsearch.action" "PutMappingAction" + "elasticsearch.request" "PutMappingRequest" + } + } + } + trace(4, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version" 1 + } + } + } + } + + cleanup: + client.admin().indices().prepareDelete(indexName).get() + + where: + indexName = "test-index" + indexType = "test-type" + id = "1" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53TransportClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53TransportClientTest.groovy new file mode 100644 index 000000000..f864bccfc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53TransportClientTest.groovy @@ -0,0 +1,286 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest +import org.elasticsearch.client.transport.TransportClient +import org.elasticsearch.common.io.FileSystemUtils +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.common.transport.TransportAddress +import org.elasticsearch.env.Environment +import org.elasticsearch.index.IndexNotFoundException +import org.elasticsearch.node.InternalSettingsPreparer +import org.elasticsearch.node.Node +import org.elasticsearch.transport.Netty3Plugin +import org.elasticsearch.transport.RemoteTransportException +import org.elasticsearch.transport.TransportService +import org.elasticsearch.transport.client.PreBuiltTransportClient +import spock.lang.Shared + +class Elasticsearch53TransportClientTest extends AgentInstrumentationSpecification { + public static final long TIMEOUT = 10000 // 10 seconds + + @Shared + TransportAddress tcpPublishAddress + + @Shared + Node testNode + @Shared + File esWorkingDir + + @Shared + TransportClient client + @Shared + String clusterName = UUID.randomUUID().toString() + + def setupSpec() { + + esWorkingDir = File.createTempDir("test-es-working-dir-", "") + esWorkingDir.deleteOnExit() + println "ES work dir: $esWorkingDir" + + def settings = Settings.builder() + .put("path.home", esWorkingDir.path) + .put("transport.type", "netty3") + .put("http.type", "netty3") + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("discovery.type", "single-node") + .build() + testNode = new Node(new Environment(InternalSettingsPreparer.prepareSettings(settings)), [Netty3Plugin]) + testNode.start() + tcpPublishAddress = testNode.injector().getInstance(TransportService).boundAddress().publishAddress() + + client = new PreBuiltTransportClient( + Settings.builder() + // Since we use listeners to close spans this should make our span closing deterministic which is good for tests + .put("thread_pool.listener.size", 1) + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .build() + ) + client.addTransportAddress(tcpPublishAddress) + runUnderTrace("setup") { + // this may potentially create multiple requests and therefore multiple spans, so we wrap this call + // into a top level trace to get exactly one trace in the result. + client.admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + // disable periodic refresh in InternalClusterInfoService as it creates spans that tests don't expect + client.admin().cluster().updateSettings(new ClusterUpdateSettingsRequest().transientSettings(["cluster.routing.allocation.disk.threshold_enabled": false])) + } + ignoreTracesAndClear(1) + } + + def cleanupSpec() { + client?.close() + testNode?.close() + if (esWorkingDir != null) { + FileSystemUtils.deleteSubDirectories(esWorkingDir.toPath()) + esWorkingDir.delete() + } + } + + def "test elasticsearch status"() { + setup: + def result = client.admin().cluster().health(new ClusterHealthRequest()) + + def clusterHealthStatus = result.get().status + + expect: + clusterHealthStatus.name() == "GREEN" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + } + } + + def "test elasticsearch error"() { + when: + client.prepareGet(indexName, indexType, id).get() + + then: + thrown IndexNotFoundException + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GetAction" + kind CLIENT + status ERROR + errorEvent RemoteTransportException, String + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + } + } + } + } + + where: + indexName = "invalid-index" + indexType = "test-type" + id = "1" + } + + def "test elasticsearch get"() { + setup: + def indexResult = client.admin().indices().prepareCreate(indexName).get() + + expect: + indexResult.acknowledged + + when: + def emptyResult = client.prepareGet(indexName, indexType, id).get() + + then: + !emptyResult.isExists() + emptyResult.id == id + emptyResult.type == indexType + emptyResult.index == indexName + + when: + def createResult = client.prepareIndex(indexName, indexType, id).setSource([:]).get() + + then: + createResult.id == id + createResult.type == indexType + createResult.index == indexName + createResult.status().status == 201 + + when: + def result = client.prepareGet(indexName, indexType, id).get() + + then: + result.isExists() + result.id == id + result.type == indexType + result.index == indexName + + and: + assertTraces(5) { + // PutMappingAction and IndexAction run in separate threads so their order can vary + traces.subList(2, 4).sort(orderByRootSpanName("PutMappingAction", "IndexAction")) + + trace(0, 1) { + span(0) { + name "CreateIndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "CreateIndexAction" + "elasticsearch.action" "CreateIndexAction" + "elasticsearch.request" "CreateIndexRequest" + "elasticsearch.request.indices" indexName + } + } + } + trace(1, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version"(-1) + } + } + } + trace(2, 1) { + span(0) { + name "PutMappingAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "PutMappingAction" + "elasticsearch.action" "PutMappingAction" + "elasticsearch.request" "PutMappingRequest" + } + } + } + trace(3, 1) { + span(0) { + name "IndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" indexType + "elasticsearch.request.write.version"(-3) + "elasticsearch.response.status" 201 + "elasticsearch.shard.replication.total" 2 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.failed" 0 + } + } + } + trace(4, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" tcpPublishAddress.host == tcpPublishAddress.address ? null : tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version" 1 + } + } + } + } + + cleanup: + client.admin().indices().prepareDelete(indexName).get() + + where: + indexName = "test-index" + indexType = "test-type" + id = "1" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Config.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Config.groovy new file mode 100644 index 000000000..2d761e504 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Config.groovy @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest +import org.elasticsearch.common.io.FileSystemUtils +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.env.Environment +import org.elasticsearch.node.InternalSettingsPreparer +import org.elasticsearch.node.Node +import org.elasticsearch.transport.Netty3Plugin +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.data.elasticsearch.core.ElasticsearchOperations +import org.springframework.data.elasticsearch.core.ElasticsearchTemplate +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories + +@Configuration +@EnableElasticsearchRepositories(basePackages = "springdata") +@ComponentScan(basePackages = "springdata") +class Config { + + @Bean + NodeBuilder nodeBuilder() { + return new NodeBuilder() + } + + @Bean(destroyMethod = "close") + Node elasticSearchNode() { + def tmpDir = File.createTempFile("test-es-working-dir-", "") + tmpDir.delete() + tmpDir.mkdir() + tmpDir.deleteOnExit() + + System.addShutdownHook { + if (tmpDir != null) { + FileSystemUtils.deleteSubDirectories(tmpDir.toPath()) + tmpDir.delete() + } + } + + def settings = Settings.builder() + .put("http.enabled", "false") + .put("path.data", tmpDir.toString()) + .put("path.home", tmpDir.toString()) + .put("thread_pool.listener.size", 1) + .put("transport.type", "netty3") + .put("http.type", "netty3") + .put("discovery.type", "single-node") + .build() + + println "ES work dir: $tmpDir" + + def testNode = new Node(new Environment(InternalSettingsPreparer.prepareSettings(settings)), [Netty3Plugin]) + testNode.start() + // disable periodic refresh in InternalClusterInfoService as it creates spans that tests don't expect + testNode.client().admin().cluster().updateSettings(new ClusterUpdateSettingsRequest().transientSettings(["cluster.routing.allocation.disk.threshold_enabled": false])) + + return testNode + } + + @Bean + ElasticsearchOperations elasticsearchTemplate(Node node) { + return new ElasticsearchTemplate(node.client()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Doc.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Doc.groovy new file mode 100644 index 000000000..b2b35f103 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Doc.groovy @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import groovy.transform.EqualsAndHashCode +import org.springframework.data.annotation.Id +import org.springframework.data.elasticsearch.annotations.Document + +@Document(indexName = "test-index") +@EqualsAndHashCode +class Doc { + @Id + private String id = "1" + private String data = "some data" + + String getId() { + return id + } + + void setId(String id) { + this.id = id + } + + String getData() { + return data + } + + void setData(String data) { + this.data = data + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/DocRepository.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/DocRepository.groovy new file mode 100644 index 000000000..dca62c102 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/DocRepository.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository + +interface DocRepository extends ElasticsearchRepository {} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringRepositoryTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringRepositoryTest.groovy new file mode 100644 index 000000000..e4e6fecb1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringRepositoryTest.groovy @@ -0,0 +1,355 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Shared + +class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecification { + // Setting up appContext & repo with @Shared doesn't allow + // spring-data instrumentation to applied. + // To change the timing without adding ugly checks everywhere - + // use a dynamic proxy. There's probably a more "groovy" way to do this. + + @Shared + LazyProxyInvoker lazyProxyInvoker = new LazyProxyInvoker() + + @Shared + DocRepository repo = Proxy.newProxyInstance( + getClass().getClassLoader(), + [DocRepository] as Class[], + lazyProxyInvoker) + + static class LazyProxyInvoker implements InvocationHandler { + def repo + def applicationContext + + DocRepository getOrCreateRepository() { + if (repo != null) { + return repo + } + + applicationContext = new AnnotationConfigApplicationContext(Config) + repo = applicationContext.getBean(DocRepository) + + return repo + } + + void close() { + applicationContext.close() + } + + @Override + Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return method.invoke(getOrCreateRepository(), args) + } + } + + def setup() { + repo.refresh() + clearExportedData() + runUnderTrace("delete") { + repo.deleteAll() + } + ignoreTracesAndClear(1) + } + + def cleanupSpec() { + lazyProxyInvoker.close() + } + + def "test empty repo"() { + when: + def result = repo.findAll() + + then: + !result.iterator().hasNext() + + and: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "CrudRepository.findAll" + kind INTERNAL + attributes { + } + } + span(1) { + name "SearchAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "SearchAction" + "elasticsearch.action" "SearchAction" + "elasticsearch.request" "SearchRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.search.types" "doc" + } + } + } + } + + where: + indexName = "test-index" + } + + def "test CRUD"() { + when: + def doc = new Doc() + + then: + repo.index(doc) == doc + + and: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "ElasticsearchRepository.index" + kind INTERNAL + attributes { + } + } + span(1) { + name "IndexAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" "doc" + "elasticsearch.request.write.version"(-3) + "elasticsearch.response.status" 201 + "elasticsearch.shard.replication.failed" 0 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.total" 2 + } + } + span(2) { + name "PutMappingAction" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "PutMappingAction" + "elasticsearch.action" "PutMappingAction" + "elasticsearch.request" "PutMappingRequest" + } + } + span(3) { + name "RefreshAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "RefreshAction" + "elasticsearch.action" "RefreshAction" + "elasticsearch.request" "RefreshRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.shard.broadcast.failed" 0 + "elasticsearch.shard.broadcast.successful" 5 + "elasticsearch.shard.broadcast.total" 10 + } + } + } + } + clearExportedData() + + and: + repo.findById("1").get() == doc + + and: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "CrudRepository.findById" + kind INTERNAL + attributes { + } + } + span(1) { + name "GetAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" "doc" + "elasticsearch.id" "1" + "elasticsearch.version" Number + } + } + } + } + clearExportedData() + + when: + doc.data = "other data" + + then: + repo.index(doc) == doc + repo.findById("1").get() == doc + + and: + assertTraces(2) { + trace(0, 3) { + span(0) { + name "ElasticsearchRepository.index" + kind INTERNAL + attributes { + } + } + span(1) { + name "IndexAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" "doc" + "elasticsearch.request.write.version"(-3) + "elasticsearch.response.status" 200 + "elasticsearch.shard.replication.failed" 0 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.total" 2 + } + } + span(2) { + name "RefreshAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "RefreshAction" + "elasticsearch.action" "RefreshAction" + "elasticsearch.request" "RefreshRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.shard.broadcast.failed" 0 + "elasticsearch.shard.broadcast.successful" 5 + "elasticsearch.shard.broadcast.total" 10 + } + } + } + trace(1, 2) { + span(0) { + name "CrudRepository.findById" + kind INTERNAL + attributes { + } + } + span(1) { + name "GetAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" "doc" + "elasticsearch.id" "1" + "elasticsearch.version" Number + } + } + } + } + clearExportedData() + + when: + repo.deleteById("1") + + then: + !repo.findAll().iterator().hasNext() + + and: + assertTraces(2) { + trace(0, 3) { + span(0) { + name "CrudRepository.deleteById" + kind INTERNAL + attributes { + } + } + span(1) { + name "DeleteAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "DeleteAction" + "elasticsearch.action" "DeleteAction" + "elasticsearch.request" "DeleteRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" "doc" + "elasticsearch.request.write.version"(-3) + "elasticsearch.shard.replication.failed" 0 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.total" 2 + } + } + span(2) { + name "RefreshAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "RefreshAction" + "elasticsearch.action" "RefreshAction" + "elasticsearch.request" "RefreshRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.shard.broadcast.failed" 0 + "elasticsearch.shard.broadcast.successful" 5 + "elasticsearch.shard.broadcast.total" 10 + } + } + } + + trace(1, 2) { + span(0) { + name "CrudRepository.findAll" + kind INTERNAL + attributes { + } + } + span(1) { + name "SearchAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "SearchAction" + "elasticsearch.action" "SearchAction" + "elasticsearch.request" "SearchRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.search.types" "doc" + } + } + } + } + + where: + indexName = "test-index" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringTemplateTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringTemplateTest.groovy new file mode 100644 index 000000000..5c1613e61 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringTemplateTest.groovy @@ -0,0 +1,327 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springdata + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.atomic.AtomicLong +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest +import org.elasticsearch.action.search.SearchResponse +import org.elasticsearch.common.io.FileSystemUtils +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.env.Environment +import org.elasticsearch.index.IndexNotFoundException +import org.elasticsearch.node.InternalSettingsPreparer +import org.elasticsearch.node.Node +import org.elasticsearch.search.aggregations.bucket.nested.InternalNested +import org.elasticsearch.search.aggregations.bucket.terms.Terms +import org.elasticsearch.transport.Netty3Plugin +import org.springframework.data.elasticsearch.core.ElasticsearchTemplate +import org.springframework.data.elasticsearch.core.ResultsExtractor +import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder +import org.springframework.data.elasticsearch.core.query.NativeSearchQuery +import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder +import spock.lang.Shared + +class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecification { + public static final long TIMEOUT = 10000 // 10 seconds + + @Shared + Node testNode + @Shared + File esWorkingDir + @Shared + String clusterName = UUID.randomUUID().toString() + + @Shared + ElasticsearchTemplate template + + def setupSpec() { + + esWorkingDir = File.createTempDir("test-es-working-dir-", "") + esWorkingDir.deleteOnExit() + println "ES work dir: $esWorkingDir" + + def settings = Settings.builder() + .put("path.home", esWorkingDir.path) + // Since we use listeners to close spans this should make our span closing deterministic which is good for tests + .put("thread_pool.listener.size", 1) + .put("transport.type", "netty3") + .put("http.type", "netty3") + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("discovery.type", "single-node") + .build() + testNode = new Node(new Environment(InternalSettingsPreparer.prepareSettings(settings)), [Netty3Plugin]) + testNode.start() + runUnderTrace("setup") { + // this may potentially create multiple requests and therefore multiple spans, so we wrap this call + // into a top level trace to get exactly one trace in the result. + testNode.client().admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + // disable periodic refresh in InternalClusterInfoService as it creates spans that tests don't expect + testNode.client().admin().cluster().updateSettings(new ClusterUpdateSettingsRequest().transientSettings(["cluster.routing.allocation.disk.threshold_enabled": false])) + } + waitForTraces(1) + + template = new ElasticsearchTemplate(testNode.client()) + } + + def cleanupSpec() { + testNode?.close() + if (esWorkingDir != null) { + FileSystemUtils.deleteSubDirectories(esWorkingDir.toPath()) + esWorkingDir.delete() + } + } + + def "test elasticsearch error"() { + when: + template.refresh(indexName) + + then: + thrown IndexNotFoundException + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RefreshAction" + kind CLIENT + status ERROR + errorEvent IndexNotFoundException, "no such index" + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "RefreshAction" + "elasticsearch.action" "RefreshAction" + "elasticsearch.request" "RefreshRequest" + "elasticsearch.request.indices" indexName + } + } + } + } + + where: + indexName = "invalid-index" + } + + def "test elasticsearch get"() { + expect: + template.createIndex(indexName) + template.getClient().admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + + when: + NativeSearchQuery query = new NativeSearchQueryBuilder() + .withIndices(indexName) + .withTypes(indexType) + .withIds([id]) + .build() + + then: + template.queryForIds(query) == [] + + when: + def result = template.index(IndexQueryBuilder.newInstance() + .withObject(new Doc()) + .withIndexName(indexName) + .withType(indexType) + .withId(id) + .build()) + template.refresh(Doc) + + then: + result == id + template.queryForList(query, Doc) == [new Doc()] + + and: + assertTraces(6) { + trace(0, 1) { + span(0) { + name "CreateIndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "CreateIndexAction" + "elasticsearch.action" "CreateIndexAction" + "elasticsearch.request" "CreateIndexRequest" + "elasticsearch.request.indices" indexName + } + } + } + trace(1, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + trace(2, 1) { + span(0) { + name "SearchAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "SearchAction" + "elasticsearch.action" "SearchAction" + "elasticsearch.request" "SearchRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.search.types" indexType + } + } + } + trace(3, 2) { + span(0) { + name "IndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" indexType + "elasticsearch.request.write.version"(-3) + "elasticsearch.response.status" 201 + "elasticsearch.shard.replication.failed" 0 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.total" 2 + } + } + span(1) { + name "PutMappingAction" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "PutMappingAction" + "elasticsearch.action" "PutMappingAction" + "elasticsearch.request" "PutMappingRequest" + } + } + } + trace(4, 1) { + span(0) { + name "RefreshAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "RefreshAction" + "elasticsearch.action" "RefreshAction" + "elasticsearch.request" "RefreshRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.shard.broadcast.failed" 0 + "elasticsearch.shard.broadcast.successful" 5 + "elasticsearch.shard.broadcast.total" 10 + } + } + } + trace(5, 1) { + span(0) { + name "SearchAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "SearchAction" + "elasticsearch.action" "SearchAction" + "elasticsearch.request" "SearchRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.search.types" indexType + } + } + } + } + + cleanup: + template.deleteIndex(indexName) + + where: + indexName = "test-index" + indexType = "test-type" + id = "1" + } + + def "test results extractor"() { + setup: + template.createIndex(indexName) + testNode.client().admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + + template.index(IndexQueryBuilder.newInstance() + .withObject(new Doc(id: 1, data: "doc a")) + .withIndexName(indexName) + .withId("a") + .build()) + template.index(IndexQueryBuilder.newInstance() + .withObject(new Doc(id: 2, data: "doc b")) + .withIndexName(indexName) + .withId("b") + .build()) + template.refresh(indexName) + ignoreTracesAndClear(5) + + and: + def query = new NativeSearchQueryBuilder().withIndices(indexName).build() + def hits = new AtomicLong() + List> results = [] + def bucketTags = [:] + + when: + template.query(query, new ResultsExtractor() { + + @Override + Doc extract(SearchResponse response) { + hits.addAndGet(response.getHits().totalHits()) + results.addAll(response.hits.collect { it.source }) + if (response.getAggregations() != null) { + InternalNested internalNested = response.getAggregations().get("tag") + if (internalNested != null) { + Terms terms = internalNested.getAggregations().get("count_agg") + Collection buckets = terms.getBuckets() + for (Terms.Bucket bucket : buckets) { + bucketTags.put(Integer.valueOf(bucket.getKeyAsString()), bucket.getDocCount()) + } + } + } + return null + } + }) + + then: + hits.get() == 2 + results[0] == [id: "2", data: "doc b"] + results[1] == [id: "1", data: "doc a"] + bucketTags == [:] + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SearchAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "SearchAction" + "elasticsearch.action" "SearchAction" + "elasticsearch.request" "SearchRequest" + "elasticsearch.request.indices" indexName + } + } + } + } + + cleanup: + template.deleteIndex(indexName) + + where: + indexName = "test-index-extract" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/elasticsearch-transport-6.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/elasticsearch-transport-6.0-javaagent.gradle new file mode 100644 index 000000000..5014d8785 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/elasticsearch-transport-6.0-javaagent.gradle @@ -0,0 +1,43 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.elasticsearch.client" + module = "transport" + versions = "[6.0.0,)" + // version 7.11.0 depends on org.elasticsearch:elasticsearch:7.11.0 which depends on + // org.elasticsearch:elasticsearch-plugin-classloader:7.11.0 which does not exist + skip('7.11.0') + assertInverse = true + } + pass { + group = "org.elasticsearch" + module = "elasticsearch" + versions = "[6.0.0,)" + // version 7.11.0 depends on org.elasticsearch:elasticsearch:7.11.0 which depends on + // org.elasticsearch:elasticsearch-plugin-classloader:7.11.0 which does not exist + skip('7.11.0') + assertInverse = true + } +} + +dependencies { + library "org.elasticsearch.client:transport:6.0.0" + + implementation project(':instrumentation:elasticsearch:elasticsearch-transport-common:library') + + // Ensure no cross interference + testInstrumentation project(':instrumentation:elasticsearch:elasticsearch-rest-5.0:javaagent') + testInstrumentation project(':instrumentation:apache-httpasyncclient-4.1:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + + testLibrary "org.elasticsearch.plugin:transport-netty4-client:6.0.0" + + testImplementation "org.apache.logging.log4j:log4j-core:2.11.0" + testImplementation "org.apache.logging.log4j:log4j-api:2.11.0" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.elasticsearch.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/AbstractClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/AbstractClientInstrumentation.java new file mode 100644 index 000000000..84d6dcfd5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/AbstractClientInstrumentation.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v6_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; + +public class AbstractClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // If we want to be more generic, we could instrument the interface instead: + // .and(safeHasSuperType(named("org.elasticsearch.client.ElasticsearchClient")))) + return named("org.elasticsearch.client.support.AbstractClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and( + takesArgument( + 0, + namedOneOf( + "org.elasticsearch.action.Action", "org.elasticsearch.action.ActionType"))) + .and(takesArgument(1, named("org.elasticsearch.action.ActionRequest"))) + .and(takesArgument(2, named("org.elasticsearch.action.ActionListener"))), + this.getClass().getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Object action, + @Advice.Argument(1) ActionRequest actionRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(value = 2, readOnly = false) + ActionListener actionListener) { + + context = tracer().startSpan(currentContext(), null, action); + scope = context.makeCurrent(); + tracer().onRequest(context, action.getClass(), actionRequest.getClass()); + actionListener = new TransportActionListener<>(actionRequest, actionListener, context); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportClientInstrumentationModule.java new file mode 100644 index 000000000..f888bc281 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportClientInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v6_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +/** + * Most of this class is identical to version 5's instrumentation, but they changed an interface to + * an abstract class, so the bytecode isn't directly compatible. + */ +@AutoService(InstrumentationModule.class) +public class Elasticsearch6TransportClientInstrumentationModule extends InstrumentationModule { + public Elasticsearch6TransportClientInstrumentationModule() { + super("elasticsearch-transport", "elasticsearch-transport-6.0", "elasticsearch"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new AbstractClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/TransportActionListener.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/TransportActionListener.java new file mode 100644 index 000000000..e9e46b77a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/TransportActionListener.java @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v6_0; + +import static io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportClientTracer.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.bulk.BulkShardResponse; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.broadcast.BroadcastResponse; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.action.support.replication.ReplicationResponse; + +/** + * Most of this class is identical to version 5's instrumentation, but they changed an interface to + * an abstract class, so the bytecode isn't directly compatible. + */ +public class TransportActionListener implements ActionListener { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty( + "otel.instrumentation.elasticsearch.experimental-span-attributes", false); + + private final ActionListener listener; + private final Context context; + + public TransportActionListener( + ActionRequest actionRequest, ActionListener listener, Context context) { + this.listener = listener; + this.context = context; + onRequest(actionRequest); + } + + private void onRequest(ActionRequest request) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span span = Span.fromContext(context); + if (request instanceof IndicesRequest) { + IndicesRequest req = (IndicesRequest) request; + String[] indices = req.indices(); + if (indices != null && indices.length > 0) { + span.setAttribute("elasticsearch.request.indices", String.join(",", indices)); + } + } + if (request instanceof SearchRequest) { + SearchRequest req = (SearchRequest) request; + String[] types = req.types(); + if (types != null && types.length > 0) { + span.setAttribute("elasticsearch.request.search.types", String.join(",", types)); + } + } + if (request instanceof DocWriteRequest) { + DocWriteRequest req = (DocWriteRequest) request; + span.setAttribute("elasticsearch.request.write.type", req.type()); + span.setAttribute("elasticsearch.request.write.routing", req.routing()); + span.setAttribute("elasticsearch.request.write.version", req.version()); + } + } + } + + @Override + public void onResponse(T response) { + Span span = Span.fromContext(context); + + if (response.remoteAddress() != null) { + NetPeerAttributes.INSTANCE.setNetPeer( + span, + response.remoteAddress().address().getHostName(), + response.remoteAddress().getAddress()); + span.setAttribute( + SemanticAttributes.NET_PEER_PORT, (long) response.remoteAddress().getPort()); + } + + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + if (response instanceof GetResponse) { + GetResponse resp = (GetResponse) response; + span.setAttribute("elasticsearch.type", resp.getType()); + span.setAttribute("elasticsearch.id", resp.getId()); + span.setAttribute("elasticsearch.version", resp.getVersion()); + } + + if (response instanceof BroadcastResponse) { + BroadcastResponse resp = (BroadcastResponse) response; + span.setAttribute("elasticsearch.shard.broadcast.total", resp.getTotalShards()); + span.setAttribute("elasticsearch.shard.broadcast.successful", resp.getSuccessfulShards()); + span.setAttribute("elasticsearch.shard.broadcast.failed", resp.getFailedShards()); + } + + if (response instanceof ReplicationResponse) { + ReplicationResponse resp = (ReplicationResponse) response; + span.setAttribute("elasticsearch.shard.replication.total", resp.getShardInfo().getTotal()); + span.setAttribute( + "elasticsearch.shard.replication.successful", resp.getShardInfo().getSuccessful()); + span.setAttribute( + "elasticsearch.shard.replication.failed", resp.getShardInfo().getFailed()); + } + + if (response instanceof IndexResponse) { + span.setAttribute( + "elasticsearch.response.status", ((IndexResponse) response).status().getStatus()); + } + + if (response instanceof BulkShardResponse) { + BulkShardResponse resp = (BulkShardResponse) response; + span.setAttribute("elasticsearch.shard.bulk.id", resp.getShardId().getId()); + span.setAttribute("elasticsearch.shard.bulk.index", resp.getShardId().getIndexName()); + } + + if (response instanceof BaseNodesResponse) { + BaseNodesResponse resp = (BaseNodesResponse) response; + if (resp.hasFailures()) { + span.setAttribute("elasticsearch.node.failures", resp.failures().size()); + } + span.setAttribute("elasticsearch.node.cluster.name", resp.getClusterName().value()); + } + } + + tracer().end(context); + listener.onResponse(response); + } + + @Override + public void onFailure(Exception e) { + tracer().endExceptionally(context, e); + listener.onFailure(e); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6NodeClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6NodeClientTest.groovy new file mode 100644 index 000000000..5aad4a85e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6NodeClientTest.groovy @@ -0,0 +1,244 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest +import org.elasticsearch.common.io.FileSystemUtils +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.index.IndexNotFoundException +import org.elasticsearch.node.Node +import spock.lang.Shared + +class Elasticsearch6NodeClientTest extends AgentInstrumentationSpecification { + public static final long TIMEOUT = 10000 // 10 seconds + + @Shared + Node testNode + @Shared + File esWorkingDir + @Shared + String clusterName = UUID.randomUUID().toString() + + def client = testNode.client() + + def setupSpec() { + + esWorkingDir = File.createTempDir("test-es-working-dir-", "") + esWorkingDir.deleteOnExit() + println "ES work dir: $esWorkingDir" + + def settings = Settings.builder() + .put("path.home", esWorkingDir.path) + // Since we use listeners to close spans this should make our span closing deterministic which is good for tests + .put("thread_pool.listener.size", 1) + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("discovery.type", "single-node") + .build() + testNode = NodeFactory.newNode(settings) + testNode.start() + runUnderTrace("setup") { + // this may potentially create multiple requests and therefore multiple spans, so we wrap this call + // into a top level trace to get exactly one trace in the result. + testNode.client().admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + // disable periodic refresh in InternalClusterInfoService as it creates spans that tests don't expect + testNode.client().admin().cluster().updateSettings(new ClusterUpdateSettingsRequest().transientSettings(["cluster.routing.allocation.disk.threshold_enabled": false])) + } + waitForTraces(1) + } + + def cleanupSpec() { + testNode?.close() + if (esWorkingDir != null) { + FileSystemUtils.deleteSubDirectories(esWorkingDir.toPath()) + esWorkingDir.delete() + } + } + + def "test elasticsearch status"() { + setup: + def result = client.admin().cluster().health(new ClusterHealthRequest()).get() + + def clusterHealthStatus = result.status + + expect: + clusterHealthStatus.name() == "GREEN" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + } + } + + def "test elasticsearch error"() { + when: + client.prepareGet(indexName, indexType, id).get() + + then: + thrown IndexNotFoundException + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GetAction" + kind CLIENT + status ERROR + errorEvent IndexNotFoundException, ~/no such index( \[invalid-index])?/ + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + } + } + } + } + + where: + indexName = "invalid-index" + indexType = "test-type" + id = "1" + } + + def "test elasticsearch get"() { + setup: + def indexResult = client.admin().indices().prepareCreate(indexName).get() + + expect: + indexResult.index() == indexName + + when: + def emptyResult = client.prepareGet(indexName, indexType, id).get() + + then: + !emptyResult.isExists() + emptyResult.id == id + emptyResult.type == indexType + emptyResult.index == indexName + + when: + def createResult = client.prepareIndex(indexName, indexType, id).setSource([:]).get() + + then: + createResult.id == id + createResult.type == indexType + createResult.index == indexName + createResult.status().status == 201 + + when: + def result = client.prepareGet(indexName, indexType, id).get() + + then: + result.isExists() + result.id == id + result.type == indexType + result.index == indexName + + and: + assertTraces(4) { + trace(0, 1) { + span(0) { + name "CreateIndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "CreateIndexAction" + "elasticsearch.action" "CreateIndexAction" + "elasticsearch.request" "CreateIndexRequest" + "elasticsearch.request.indices" indexName + } + } + } + trace(1, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version"(-1) + } + } + } + trace(2, 2) { + span(0) { + name "IndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" indexType + "elasticsearch.request.write.version"(-3) + "elasticsearch.response.status" 201 + "elasticsearch.shard.replication.total" 2 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.failed" 0 + } + } + span(1) { + name ~/(Auto)?PutMappingAction/ + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" ~/(Auto)?PutMappingAction/ + "elasticsearch.action" ~/(Auto)?PutMappingAction/ + "elasticsearch.request" "PutMappingRequest" + } + } + } + trace(3, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version" 1 + } + } + } + } + + cleanup: + client.admin().indices().prepareDelete(indexName).get() + + where: + indexName = "test-index" + indexType = "test-type" + id = "1" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6TransportClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6TransportClientTest.groovy new file mode 100644 index 000000000..9de3aca2d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6TransportClientTest.groovy @@ -0,0 +1,282 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest +import org.elasticsearch.client.transport.TransportClient +import org.elasticsearch.common.io.FileSystemUtils +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.common.transport.TransportAddress +import org.elasticsearch.index.IndexNotFoundException +import org.elasticsearch.node.Node +import org.elasticsearch.transport.RemoteTransportException +import org.elasticsearch.transport.TransportService +import org.elasticsearch.transport.client.PreBuiltTransportClient +import spock.lang.Shared + +class Elasticsearch6TransportClientTest extends AgentInstrumentationSpecification { + public static final long TIMEOUT = 10000 // 10 seconds + + @Shared + TransportAddress tcpPublishAddress + @Shared + Node testNode + @Shared + File esWorkingDir + @Shared + String clusterName = UUID.randomUUID().toString() + + @Shared + TransportClient client + + def setupSpec() { + esWorkingDir = File.createTempDir("test-es-working-dir-", "") + esWorkingDir.deleteOnExit() + println "ES work dir: $esWorkingDir" + + def settings = Settings.builder() + .put("path.home", esWorkingDir.path) + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("discovery.type", "single-node") + .build() + testNode = NodeFactory.newNode(settings) + testNode.start() + tcpPublishAddress = testNode.injector().getInstance(TransportService).boundAddress().publishAddress() + + client = new PreBuiltTransportClient( + Settings.builder() + // Since we use listeners to close spans this should make our span closing deterministic which is good for tests + .put("thread_pool.listener.size", 1) + .put(CLUSTER_NAME_SETTING.getKey(), clusterName) + .build() + ) + client.addTransportAddress(tcpPublishAddress) + runUnderTrace("setup") { + // this may potentially create multiple requests and therefore multiple spans, so we wrap this call + // into a top level trace to get exactly one trace in the result. + client.admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet(TIMEOUT) + // disable periodic refresh in InternalClusterInfoService as it creates spans that tests don't expect + client.admin().cluster().updateSettings(new ClusterUpdateSettingsRequest().transientSettings(["cluster.routing.allocation.disk.threshold_enabled": false])) + } + waitForTraces(1) + } + + def cleanupSpec() { + client?.close() + testNode?.close() + if (esWorkingDir != null) { + FileSystemUtils.deleteSubDirectories(esWorkingDir.toPath()) + esWorkingDir.delete() + } + } + + def "test elasticsearch status"() { + setup: + def result = client.admin().cluster().health(new ClusterHealthRequest()) + + def clusterHealthStatus = result.get().status + + expect: + clusterHealthStatus.name() == "GREEN" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "ClusterHealthAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "ClusterHealthAction" + "elasticsearch.action" "ClusterHealthAction" + "elasticsearch.request" "ClusterHealthRequest" + } + } + } + } + } + + def "test elasticsearch error"() { + when: + client.prepareGet(indexName, indexType, id).get() + + then: + thrown IndexNotFoundException + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GetAction" + kind CLIENT + status ERROR + errorEvent RemoteTransportException, String + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + } + } + } + } + + where: + indexName = "invalid-index" + indexType = "test-type" + id = "1" + } + + def "test elasticsearch get"() { + setup: + def indexResult = client.admin().indices().prepareCreate(indexName).get() + + expect: + indexResult.index() == indexName + + when: + def emptyResult = client.prepareGet(indexName, indexType, id).get() + + then: + !emptyResult.isExists() + emptyResult.id == id + emptyResult.type == indexType + emptyResult.index == indexName + + when: + def createResult = client.prepareIndex(indexName, indexType, id).setSource([:]).get() + + then: + createResult.id == id + createResult.type == indexType + createResult.index == indexName + createResult.status().status == 201 + + when: + def result = client.prepareGet(indexName, indexType, id).get() + + then: + result.isExists() + result.id == id + result.type == indexType + result.index == indexName + + and: + assertTraces(5) { + // PutMappingAction and IndexAction run in separate threads so their order can vary + traces.subList(2, 4).sort(orderByRootSpanName( + "PutMappingAction", // elasticsearch < 7 + "AutoPutMappingAction", // elasticsearch >= 7 + "IndexAction")) + + trace(0, 1) { + span(0) { + name "CreateIndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "CreateIndexAction" + "elasticsearch.action" "CreateIndexAction" + "elasticsearch.request" "CreateIndexRequest" + "elasticsearch.request.indices" indexName + } + } + } + trace(1, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version"(-1) + } + } + } + trace(2, 1) { + span(0) { + name ~/(Auto)?PutMappingAction/ + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" ~/(Auto)?PutMappingAction/ + "elasticsearch.action" ~/(Auto)?PutMappingAction/ + "elasticsearch.request" "PutMappingRequest" + } + } + } + trace(3, 1) { + span(0) { + name "IndexAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "IndexAction" + "elasticsearch.action" "IndexAction" + "elasticsearch.request" "IndexRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.request.write.type" indexType + "elasticsearch.request.write.version"(-3) + "elasticsearch.response.status" 201 + "elasticsearch.shard.replication.total" 2 + "elasticsearch.shard.replication.successful" 1 + "elasticsearch.shard.replication.failed" 0 + } + } + } + trace(4, 1) { + span(0) { + name "GetAction" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_IP.key}" tcpPublishAddress.address + "${SemanticAttributes.NET_PEER_PORT.key}" tcpPublishAddress.port + "${SemanticAttributes.DB_SYSTEM.key}" "elasticsearch" + "${SemanticAttributes.DB_OPERATION.key}" "GetAction" + "elasticsearch.action" "GetAction" + "elasticsearch.request" "GetRequest" + "elasticsearch.request.indices" indexName + "elasticsearch.type" indexType + "elasticsearch.id" "1" + "elasticsearch.version" 1 + } + } + } + } + + cleanup: + client.admin().indices().prepareDelete(indexName).get() + + where: + indexName = "test-index" + indexType = "test-type" + id = "1" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/NodeFactory.groovy b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/NodeFactory.groovy new file mode 100644 index 000000000..f6d0f6055 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/NodeFactory.groovy @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.elasticsearch.common.settings.Settings +import org.elasticsearch.node.InternalSettingsPreparer +import org.elasticsearch.node.Node +import org.elasticsearch.transport.Netty4Plugin + +class NodeFactory { + static Node newNode(Settings settings) { + def version = org.elasticsearch.Version.CURRENT + if (version.major >= 7) { + return new NodeV7(settings) + } else if (version.major == 6 && version.minor >= 5) { + return new NodeV65(settings) + } + return new NodeV6(settings) + } + + static class NodeV6 extends Node { + NodeV6(Settings settings) { + super(InternalSettingsPreparer.prepareEnvironment(settings, null), [Netty4Plugin]) + } + + protected void registerDerivedNodeNameWithLogger(String s) { + } + } + + static class NodeV65 extends Node { + NodeV65(Settings settings) { + super(InternalSettingsPreparer.prepareEnvironment(settings, null), [Netty4Plugin], true) + } + + protected void registerDerivedNodeNameWithLogger(String s) { + } + } + + static class NodeV7 extends Node { + NodeV7(Settings settings) { + super(InternalSettingsPreparer.prepareEnvironment(settings, Collections.emptyMap(), null, { "default node name" }), [Netty4Plugin], true) + } + + protected void registerDerivedNodeNameWithLogger(String s) { + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-common/library/elasticsearch-transport-common-library.gradle b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-common/library/elasticsearch-transport-common-library.gradle new file mode 100644 index 000000000..cc8517d77 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-common/library/elasticsearch-transport-common-library.gradle @@ -0,0 +1,3 @@ +apply plugin: "otel.library-instrumentation" + +// No dependencies, elasticsearch library not actually used here. \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportClientTracer.java new file mode 100644 index 000000000..69dcaa813 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/elasticsearch/elasticsearch-transport-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportClientTracer.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.InetSocketAddress; + +public class ElasticsearchTransportClientTracer extends DatabaseClientTracer { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty( + "otel.instrumentation.elasticsearch.experimental-span-attributes", false); + + private static final ElasticsearchTransportClientTracer TRACER = + new ElasticsearchTransportClientTracer(); + + private ElasticsearchTransportClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static ElasticsearchTransportClientTracer tracer() { + return TRACER; + } + + public void onRequest(Context context, Class action, Class request) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span span = Span.fromContext(context); + span.setAttribute("elasticsearch.action", action.getSimpleName()); + span.setAttribute("elasticsearch.request", request.getSimpleName()); + } + } + + @Override + protected String sanitizeStatement(Object action) { + return action.getClass().getSimpleName(); + } + + @Override + protected String dbSystem(Void connection) { + return "elasticsearch"; + } + + @Override + protected InetSocketAddress peerAddress(Void connection) { + return null; + } + + @Override + protected String dbOperation(Void connection, Object action, String operation) { + return operation; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.elasticsearch-transport-common"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/executors-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/executors-javaagent.gradle new file mode 100644 index 000000000..c5f484c81 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/executors-javaagent.gradle @@ -0,0 +1,12 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + coreJdk() + } +} + +tasks.withType(Test).configureEach { + jvmArgs "-Dotel.instrumentation.executors.include=ExecutorInstrumentationTest\$CustomThreadPoolExecutor" + jvmArgs "-Djava.awt.headless=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/AbstractExecutorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/AbstractExecutorInstrumentation.java new file mode 100644 index 000000000..21c5bf506 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/AbstractExecutorInstrumentation.java @@ -0,0 +1,142 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javaconcurrent; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.any; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Executor; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractExecutorInstrumentation implements TypeInstrumentation { + private static final Logger log = LoggerFactory.getLogger(AbstractExecutorInstrumentation.class); + + private static final String EXECUTORS_INCLUDE_PROPERTY_NAME = + "otel.instrumentation.executors.include"; + + private static final String EXECUTORS_INCLUDE_ALL_PROPERTY_NAME = + "otel.instrumentation.executors.include-all"; + + private static final boolean INCLUDE_ALL = + Config.get().getBoolean(EXECUTORS_INCLUDE_ALL_PROPERTY_NAME, false); + + /** + * Only apply executor instrumentation to allowed executors. To apply to all executors, use + * override setting above. + */ + private final Collection includeExecutors; + + /** + * Some frameworks have their executors defined as anon classes inside other classes. Referencing + * anon classes by name would be fragile, so instead we will use list of class prefix names. Since + * checking this list is more expensive (O(n)) we should try to keep it short. + */ + private final Collection includePrefixes; + + protected AbstractExecutorInstrumentation() { + if (INCLUDE_ALL) { + includeExecutors = Collections.emptyList(); + includePrefixes = Collections.emptyList(); + } else { + String[] includeExecutors = { + "akka.actor.ActorSystemImpl$$anon$1", + "akka.dispatch.BalancingDispatcher", + "akka.dispatch.Dispatcher", + "akka.dispatch.Dispatcher$LazyExecutorServiceDelegate", + "akka.dispatch.ExecutionContexts$sameThreadExecutionContext$", + "akka.dispatch.forkjoin.ForkJoinPool", + "akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinPool", + "akka.dispatch.MessageDispatcher", + "akka.dispatch.PinnedDispatcher", + "com.google.common.util.concurrent.AbstractListeningExecutorService", + "com.google.common.util.concurrent.MoreExecutors$ListeningDecorator", + "com.google.common.util.concurrent.MoreExecutors$ScheduledListeningDecorator", + "io.netty.channel.epoll.EpollEventLoop", + "io.netty.channel.epoll.EpollEventLoopGroup", + "io.netty.channel.MultithreadEventLoopGroup", + "io.netty.channel.nio.NioEventLoop", + "io.netty.channel.nio.NioEventLoopGroup", + "io.netty.channel.SingleThreadEventLoop", + "io.netty.util.concurrent.AbstractEventExecutor", + "io.netty.util.concurrent.AbstractEventExecutorGroup", + "io.netty.util.concurrent.AbstractScheduledEventExecutor", + "io.netty.util.concurrent.DefaultEventExecutor", + "io.netty.util.concurrent.DefaultEventExecutorGroup", + "io.netty.util.concurrent.GlobalEventExecutor", + "io.netty.util.concurrent.MultithreadEventExecutorGroup", + "io.netty.util.concurrent.SingleThreadEventExecutor", + "java.util.concurrent.AbstractExecutorService", + "java.util.concurrent.CompletableFuture$ThreadPerTaskExecutor", + "java.util.concurrent.Executors$DelegatedExecutorService", + "java.util.concurrent.Executors$FinalizableDelegatedExecutorService", + "java.util.concurrent.ForkJoinPool", + "java.util.concurrent.ScheduledThreadPoolExecutor", + "java.util.concurrent.ThreadPoolExecutor", + "org.eclipse.jetty.util.thread.QueuedThreadPool", // dispatch() is covered in the jetty + // module + "org.eclipse.jetty.util.thread.ReservedThreadExecutor", + "org.glassfish.grizzly.threadpool.GrizzlyExecutorService", + "play.api.libs.streams.Execution$trampoline$", + "play.shaded.ahc.io.netty.util.concurrent.ThreadPerTaskExecutor", + "scala.concurrent.forkjoin.ForkJoinPool", + "scala.concurrent.Future$InternalCallbackExecutor$", + "scala.concurrent.impl.ExecutionContextImpl", + }; + Set combined = new HashSet<>(Arrays.asList(includeExecutors)); + combined.addAll(Config.get().getList(EXECUTORS_INCLUDE_PROPERTY_NAME)); + this.includeExecutors = Collections.unmodifiableSet(combined); + + String[] includePrefixes = {"slick.util.AsyncExecutor$"}; + this.includePrefixes = Collections.unmodifiableCollection(Arrays.asList(includePrefixes)); + } + } + + @Override + public ElementMatcher typeMatcher() { + ElementMatcher.Junction matcher = any(); + ElementMatcher.Junction hasExecutorInterfaceMatcher = + implementsInterface(named(Executor.class.getName())); + if (!INCLUDE_ALL) { + matcher = + matcher.and( + new ElementMatcher() { + @Override + public boolean matches(TypeDescription target) { + boolean allowed = includeExecutors.contains(target.getName()); + + // Check for possible prefixes match only if not allowed already + if (!allowed) { + for (String name : includePrefixes) { + if (target.getName().startsWith(name)) { + allowed = true; + break; + } + } + } + + if (!allowed + && log.isDebugEnabled() + && hasExecutorInterfaceMatcher.matches(target)) { + log.debug("Skipping executor instrumentation for {}", target.getName()); + } + return allowed; + } + }); + } + return matcher.and(hasExecutorInterfaceMatcher); // Apply expensive matcher last. + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/CallableInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/CallableInstrumentation.java new file mode 100644 index 000000000..36bd1cc2b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/CallableInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javaconcurrent; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.AdviceUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import java.util.concurrent.Callable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class CallableInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named(Callable.class.getName())); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("call").and(takesArguments(0)).and(isPublic()), + CallableInstrumentation.class.getName() + "$CallableAdvice"); + } + + @SuppressWarnings("unused") + public static class CallableAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter(@Advice.This Callable thiz) { + ContextStore, State> contextStore = + InstrumentationContext.get(Callable.class, State.class); + return AdviceUtils.startTaskScope(contextStore, thiz); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/ExecutorInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/ExecutorInstrumentationModule.java new file mode 100644 index 000000000..105e396e1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/ExecutorInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javaconcurrent; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ExecutorInstrumentationModule extends InstrumentationModule { + public ExecutorInstrumentationModule() { + super("executor"); + } + + @Override + public List typeInstrumentations() { + return asList( + new CallableInstrumentation(), + new FutureInstrumentation(), + new JavaExecutorInstrumentation(), + new JavaForkJoinTaskInstrumentation(), + new RunnableInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/FutureInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/FutureInstrumentation.java new file mode 100644 index 000000000..437fa090a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/FutureInstrumentation.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javaconcurrent; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.concurrent.Future; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FutureInstrumentation implements TypeInstrumentation { + private static final Logger log = LoggerFactory.getLogger(FutureInstrumentation.class); + + /** + * Only apply executor instrumentation to allowed executors. In the future, this restriction may + * be lifted to include all executors. + */ + private static final Collection ALLOWED_FUTURES; + + static { + String[] allowed = { + "akka.dispatch.forkjoin.ForkJoinTask", + "akka.dispatch.forkjoin.ForkJoinTask$AdaptedCallable", + "akka.dispatch.forkjoin.ForkJoinTask$AdaptedRunnable", + "akka.dispatch.forkjoin.ForkJoinTask$AdaptedRunnableAction", + "akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask", + "akka.dispatch.Mailbox", + "com.google.common.util.concurrent.AbstractFuture", + "com.google.common.util.concurrent.AbstractFuture$TrustedFuture", + "com.google.common.util.concurrent.ListenableFutureTask", + "com.google.common.util.concurrent.SettableFuture", + "io.netty.util.concurrent.CompleteFuture", + "io.netty.util.concurrent.FailedFuture", + "io.netty.util.concurrent.ScheduledFutureTask", + "java.util.concurrent.CompletableFuture$BiApply", + "java.util.concurrent.CompletableFuture$BiCompletion", + "java.util.concurrent.CompletableFuture$BiRelay", + "java.util.concurrent.CompletableFuture$ThreadPerTaskExecutor", + "java.util.concurrent.CountedCompleter", + "java.util.concurrent.ExecutorCompletionService$QueueingFuture", + "java.util.concurrent.ForkJoinTask", + "java.util.concurrent.ForkJoinTask$AdaptedCallable", + "java.util.concurrent.ForkJoinTask$RunnableExecuteAction", + "java.util.concurrent.FutureTask", + "java.util.concurrent.RecursiveAction", + "java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask", + "scala.collection.parallel.AdaptiveWorkStealingForkJoinTasks$WrappedTask", + "scala.concurrent.forkjoin.ForkJoinTask", + "scala.concurrent.forkjoin.ForkJoinTask$AdaptedCallable", + "scala.concurrent.forkjoin.ForkJoinTask$AdaptedRunnable", + "scala.concurrent.forkjoin.ForkJoinTask$AdaptedRunnableAction", + "scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask", + }; + ALLOWED_FUTURES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(allowed))); + } + + @Override + public ElementMatcher typeMatcher() { + ElementMatcher.Junction hasFutureInterfaceMatcher = + implementsInterface(named(Future.class.getName())); + return new ElementMatcher.Junction.AbstractBase() { + @Override + public boolean matches(TypeDescription target) { + boolean allowed = ALLOWED_FUTURES.contains(target.getName()); + if (!allowed && log.isDebugEnabled() && hasFutureInterfaceMatcher.matches(target)) { + log.debug("Skipping future instrumentation for {}", target.getName()); + } + return allowed; + } + }.and(hasFutureInterfaceMatcher); // Apply expensive matcher last. + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("cancel").and(returns(boolean.class)), + FutureInstrumentation.class.getName() + "$CanceledFutureAdvice"); + } + + @SuppressWarnings("unused") + public static class CanceledFutureAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void exit(@Advice.This Future future) { + // Try to clear parent span even if future was not cancelled: + // the expectation is that parent span should be cleared after 'cancel' + // is called, one way or another + ContextStore, State> contextStore = + InstrumentationContext.get(Future.class, State.class); + State state = contextStore.get(future); + if (state != null) { + state.clearParentContext(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/JavaExecutorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/JavaExecutorInstrumentation.java new file mode 100644 index 000000000..eb47ace10 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/JavaExecutorInstrumentation.java @@ -0,0 +1,232 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javaconcurrent; + +import static net.bytebuddy.matcher.ElementMatchers.nameMatches; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.CallableWrapper; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.RunnableWrapper; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.Future; +import net.bytebuddy.asm.Advice; + +public class JavaExecutorInstrumentation extends AbstractExecutorInstrumentation { + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(takesArgument(0, Runnable.class)).and(takesArguments(1)), + JavaExecutorInstrumentation.class.getName() + "$SetExecuteRunnableStateAdvice"); + // Netty uses addTask as the actual core of their submission; there are non-standard variations + // like execute(Runnable,boolean) that aren't caught by standard instrumentation + transformer.applyAdviceToMethod( + named("addTask").and(takesArgument(0, Runnable.class)).and(takesArguments(1)), + JavaExecutorInstrumentation.class.getName() + "$SetExecuteRunnableStateAdvice"); + transformer.applyAdviceToMethod( + named("execute").and(takesArgument(0, ForkJoinTask.class)), + JavaExecutorInstrumentation.class.getName() + "$SetJavaForkJoinStateAdvice"); + transformer.applyAdviceToMethod( + named("submit").and(takesArgument(0, Runnable.class)), + JavaExecutorInstrumentation.class.getName() + "$SetSubmitRunnableStateAdvice"); + transformer.applyAdviceToMethod( + named("submit").and(takesArgument(0, Callable.class)), + JavaExecutorInstrumentation.class.getName() + "$SetCallableStateAdvice"); + transformer.applyAdviceToMethod( + named("submit").and(takesArgument(0, ForkJoinTask.class)), + JavaExecutorInstrumentation.class.getName() + "$SetJavaForkJoinStateAdvice"); + transformer.applyAdviceToMethod( + nameMatches("invoke(Any|All)$").and(takesArgument(0, Collection.class)), + JavaExecutorInstrumentation.class.getName() + + "$SetCallableStateForCallableCollectionAdvice"); + transformer.applyAdviceToMethod( + nameMatches("invoke").and(takesArgument(0, ForkJoinTask.class)), + JavaExecutorInstrumentation.class.getName() + "$SetJavaForkJoinStateAdvice"); + transformer.applyAdviceToMethod( + named("schedule").and(takesArgument(0, Runnable.class)), + JavaExecutorInstrumentation.class.getName() + "$SetSubmitRunnableStateAdvice"); + transformer.applyAdviceToMethod( + named("schedule").and(takesArgument(0, Callable.class)), + JavaExecutorInstrumentation.class.getName() + "$SetCallableStateAdvice"); + } + + @SuppressWarnings("unused") + public static class SetExecuteRunnableStateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterJobSubmit( + @Advice.Argument(value = 0, readOnly = false) Runnable task) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(task)) { + task = RunnableWrapper.wrapIfNeeded(task); + ContextStore contextStore = + InstrumentationContext.get(Runnable.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, task, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitJobSubmit( + @Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } + + @SuppressWarnings("unused") + public static class SetJavaForkJoinStateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterJobSubmit( + @Advice.Argument(value = 0, readOnly = false) ForkJoinTask task) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(task)) { + ContextStore, State> contextStore = + InstrumentationContext.get(ForkJoinTask.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, task, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitJobSubmit( + @Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } + + @SuppressWarnings("unused") + public static class SetSubmitRunnableStateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterJobSubmit( + @Advice.Argument(value = 0, readOnly = false) Runnable task) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(task)) { + task = RunnableWrapper.wrapIfNeeded(task); + ContextStore contextStore = + InstrumentationContext.get(Runnable.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, task, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitJobSubmit( + @Advice.Enter State state, + @Advice.Thrown Throwable throwable, + @Advice.Return Future future) { + if (state != null && future != null) { + ContextStore, State> contextStore = + InstrumentationContext.get(Future.class, State.class); + contextStore.put(future, state); + } + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } + + @SuppressWarnings("unused") + public static class SetCallableStateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterJobSubmit( + @Advice.Argument(value = 0, readOnly = false) Callable task) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(task)) { + task = CallableWrapper.wrapIfNeeded(task); + ContextStore, State> contextStore = + InstrumentationContext.get(Callable.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, task, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitJobSubmit( + @Advice.Enter State state, + @Advice.Thrown Throwable throwable, + @Advice.Return Future future) { + if (state != null && future != null) { + ContextStore, State> contextStore = + InstrumentationContext.get(Future.class, State.class); + contextStore.put(future, state); + } + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } + + @SuppressWarnings("unused") + public static class SetCallableStateForCallableCollectionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Collection submitEnter( + @Advice.Argument(value = 0, readOnly = false) Collection> tasks) { + if (tasks != null) { + Collection> wrappedTasks = new ArrayList<>(tasks.size()); + for (Callable task : tasks) { + if (task != null) { + Callable newTask = CallableWrapper.wrapIfNeeded(task); + wrappedTasks.add(newTask); + ContextStore, State> contextStore = + InstrumentationContext.get(Callable.class, State.class); + ExecutorInstrumentationUtils.setupState( + contextStore, newTask, Java8BytecodeBridge.currentContext()); + } + } + tasks = wrappedTasks; + return tasks; + } + return Collections.emptyList(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void submitExit( + @Advice.Enter Collection> wrappedTasks, + @Advice.Thrown Throwable throwable) { + /* + Note1: invokeAny doesn't return any futures so all we need to do for it + is to make sure we close all scopes in case of an exception. + Note2: invokeAll does return futures - but according to its documentation + it actually only returns after all futures have been completed - i.e. it blocks. + This means we do not need to setup any hooks on these futures, we just need to clear + any parent spans in case of an error. + (according to ExecutorService docs and AbstractExecutorService code) + */ + if (null != throwable) { + for (Callable task : wrappedTasks) { + if (task != null) { + ContextStore, State> contextStore = + InstrumentationContext.get(Callable.class, State.class); + State state = contextStore.get(task); + if (state != null) { + /* + Note: this may potentially clear somebody else's parent span if we didn't set it + up in setupState because it was already present before us. This should be safe but + may lead to non-attributed async work in some very rare cases. + Alternative is to not clear parent span here if we did not set it up in setupState + but this may potentially lead to memory leaks if callers do not properly handle + exceptions. + */ + state.clearParentContext(); + } + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/JavaForkJoinTaskInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/JavaForkJoinTaskInstrumentation.java new file mode 100644 index 000000000..97624c28b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/JavaForkJoinTaskInstrumentation.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javaconcurrent; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.AdviceUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Instrument {@link ForkJoinTask}. + * + *

Note: There are quite a few separate implementations of {@code ForkJoinTask}/{@code + * ForkJoinPool}: JVM, Akka, Scala, Netty to name a few. This class handles JVM version. + */ +public class JavaForkJoinTaskInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named(ForkJoinTask.class.getName())); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("exec").and(takesArguments(0)).and(not(isAbstract())), + JavaForkJoinTaskInstrumentation.class.getName() + "$ForkJoinTaskAdvice"); + } + + @SuppressWarnings("unused") + public static class ForkJoinTaskAdvice { + + /** + * When {@link ForkJoinTask} object is submitted to {@link ForkJoinPool} as {@link Runnable} or + * {@link Callable} it will not get wrapped, instead it will be casted to {@code ForkJoinTask} + * directly. This means state is still stored in {@code Runnable} or {@code Callable} and we + * need to use that state. + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter(@Advice.This ForkJoinTask thiz) { + ContextStore, State> contextStore = + InstrumentationContext.get(ForkJoinTask.class, State.class); + Scope scope = AdviceUtils.startTaskScope(contextStore, thiz); + if (thiz instanceof Runnable) { + ContextStore runnableContextStore = + InstrumentationContext.get(Runnable.class, State.class); + Scope newScope = AdviceUtils.startTaskScope(runnableContextStore, (Runnable) thiz); + if (null != newScope) { + if (null != scope) { + newScope.close(); + } else { + scope = newScope; + } + } + } + if (thiz instanceof Callable) { + ContextStore, State> callableContextStore = + InstrumentationContext.get(Callable.class, State.class); + Scope newScope = AdviceUtils.startTaskScope(callableContextStore, (Callable) thiz); + if (null != newScope) { + if (null != scope) { + newScope.close(); + } else { + scope = newScope; + } + } + } + return scope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/RunnableInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/RunnableInstrumentation.java new file mode 100644 index 000000000..c34bdfcb3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaconcurrent/RunnableInstrumentation.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javaconcurrent; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.AdviceUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RunnableInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named(Runnable.class.getName())); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("run").and(takesArguments(0)).and(isPublic()), + RunnableInstrumentation.class.getName() + "$RunnableAdvice"); + } + + @SuppressWarnings("unused") + public static class RunnableAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter(@Advice.This Runnable thiz) { + ContextStore contextStore = + InstrumentationContext.get(Runnable.class, State.class); + return AdviceUtils.startTaskScope(contextStore, thiz); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/CompletableFutureTest.groovy b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/CompletableFutureTest.groovy new file mode 100644 index 000000000..73ae632ef --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/CompletableFutureTest.groovy @@ -0,0 +1,240 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.function.Function +import java.util.function.Supplier +import spock.lang.Requires + +@Requires({ javaVersion >= 1.8 }) +class CompletableFutureTest extends AgentInstrumentationSpecification { + + def "CompletableFuture test"() { + setup: + def pool = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + def differentPool = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + def supplier = new Supplier() { + @Override + String get() { + runInternalSpan("supplier") + sleep(1000) + return "a" + } + } + + def function = new Function() { + @Override + String apply(String s) { + runInternalSpan("function") + return s + "c" + } + } + + def result = new Supplier() { + @Override + String get() { + runUnderTrace("parent") { + return CompletableFuture.supplyAsync(supplier, pool) + .thenCompose({ s -> CompletableFuture.supplyAsync(new AppendingSupplier(s), differentPool) }) + .thenApply(function) + .get() + } + } + }.get() + + expect: + result == "abc" + + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "supplier", span(0)) + basicSpan(it, 2, "appendingSupplier", span(0)) + basicSpan(it, 3, "function", span(0)) + } + } + + cleanup: + pool?.shutdown() + differentPool?.shutdown() + } + + def "test supplyAsync"() { + when: + CompletableFuture completableFuture = runUnderTrace("parent") { + def result = CompletableFuture.supplyAsync { + runUnderTrace("child") { + "done" + } + } + return result + } + + then: + completableFuture.get() == "done" + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "child", span(0)) + } + } + } + + def "test thenApply"() { + when: + CompletableFuture completableFuture = runUnderTrace("parent") { + CompletableFuture.supplyAsync { + "done" + }.thenApply { result -> + runUnderTrace("child") { + result + } + } + } + + then: + completableFuture.get() == "done" + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "child", span(0)) + } + } + } + + def "test thenApplyAsync"() { + when: + CompletableFuture completableFuture = runUnderTrace("parent") { + def result = CompletableFuture.supplyAsync { + "done" + }.thenApplyAsync { result -> + runUnderTrace("child") { + result + } + } + return result + } + + then: + completableFuture.get() == "done" + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "child", span(0)) + } + } + } + + def "test thenCompose"() { + when: + CompletableFuture completableFuture = runUnderTrace("parent") { + def result = CompletableFuture.supplyAsync { + "done" + }.thenCompose { result -> + CompletableFuture.supplyAsync { + runUnderTrace("child") { + result + } + } + } + return result + } + + then: + completableFuture.get() == "done" + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "child", span(0)) + } + } + } + + def "test thenComposeAsync"() { + when: + CompletableFuture completableFuture = runUnderTrace("parent") { + def result = CompletableFuture.supplyAsync { + "done" + }.thenComposeAsync { result -> + CompletableFuture.supplyAsync { + runUnderTrace("child") { + result + } + } + } + return result + } + + then: + completableFuture.get() == "done" + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "child", span(0)) + } + } + } + + def "test compose and apply"() { + when: + CompletableFuture completableFuture = runUnderTrace("parent") { + def result = CompletableFuture.supplyAsync { + "do" + }.thenCompose { result -> + CompletableFuture.supplyAsync { + result + "ne" + } + }.thenApplyAsync { result -> + runUnderTrace("child") { + result + } + } + return result + } + + then: + completableFuture.get() == "done" + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "child", span(0)) + } + } + } + + static class AppendingSupplier implements Supplier { + String letter + + AppendingSupplier(String letter) { + this.letter = letter + } + + @Override + String get() { + runInternalSpan("appendingSupplier") + return letter + "b" + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/ExecutorInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/ExecutorInstrumentationTest.groovy new file mode 100644 index 000000000..7d7f2dd0a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/ExecutorInstrumentationTest.groovy @@ -0,0 +1,344 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.lang.reflect.InvocationTargetException +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.ExecutorService +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.ForkJoinTask +import java.util.concurrent.Future +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import spock.lang.Shared + +class ExecutorInstrumentationTest extends AgentInstrumentationSpecification { + + @Shared + def executeRunnable = { e, c -> e.execute((Runnable) c) } + @Shared + def executeForkJoinTask = { e, c -> e.execute((ForkJoinTask) c) } + @Shared + def submitRunnable = { e, c -> e.submit((Runnable) c) } + @Shared + def submitCallable = { e, c -> e.submit((Callable) c) } + @Shared + def submitForkJoinTask = { e, c -> e.submit((ForkJoinTask) c) } + @Shared + def invokeAll = { e, c -> e.invokeAll([(Callable) c]) } + @Shared + def invokeAllTimeout = { e, c -> e.invokeAll([(Callable) c], 10, TimeUnit.SECONDS) } + @Shared + def invokeAny = { e, c -> e.invokeAny([(Callable) c]) } + @Shared + def invokeAnyTimeout = { e, c -> e.invokeAny([(Callable) c], 10, TimeUnit.SECONDS) } + @Shared + def invokeForkJoinTask = { e, c -> e.invoke((ForkJoinTask) c) } + @Shared + def scheduleRunnable = { e, c -> e.schedule((Runnable) c, 10, TimeUnit.MILLISECONDS) } + @Shared + def scheduleCallable = { e, c -> e.schedule((Callable) c, 10, TimeUnit.MILLISECONDS) } + + def "#poolName '#name' propagates"() { + setup: + def pool = poolImpl + def m = method + + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + // this child will have a span + def child1 = new JavaAsyncChild() + // this child won't + def child2 = new JavaAsyncChild(false, false) + m(pool, child1) + m(pool, child2) + child1.waitForCompletion() + child2.waitForCompletion() + } + } + }.run() + + expect: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "asyncChild", span(0)) + } + } + + cleanup: + if (pool.hasProperty("shutdown")) { + pool.shutdown() + pool.awaitTermination(10, TimeUnit.SECONDS) + } + + // Unfortunately, there's no simple way to test the cross product of methods/pools. + where: + name | method | poolImpl + "execute Runnable" | executeRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "submit Runnable" | submitRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "submit Callable" | submitCallable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "invokeAll" | invokeAll | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "invokeAll with timeout" | invokeAllTimeout | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "invokeAny" | invokeAny | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "invokeAny with timeout" | invokeAnyTimeout | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + + // Scheduled executor has additional methods and also may get disabled because it wraps tasks + "execute Runnable" | executeRunnable | new ScheduledThreadPoolExecutor(1) + "submit Runnable" | submitRunnable | new ScheduledThreadPoolExecutor(1) + "submit Callable" | submitCallable | new ScheduledThreadPoolExecutor(1) + "invokeAll" | invokeAll | new ScheduledThreadPoolExecutor(1) + "invokeAll with timeout" | invokeAllTimeout | new ScheduledThreadPoolExecutor(1) + "invokeAny" | invokeAny | new ScheduledThreadPoolExecutor(1) + "invokeAny with timeout" | invokeAnyTimeout | new ScheduledThreadPoolExecutor(1) + "schedule Runnable" | scheduleRunnable | new ScheduledThreadPoolExecutor(1) + "schedule Callable" | scheduleCallable | new ScheduledThreadPoolExecutor(1) + + // ForkJoinPool has additional set of method overloads for ForkJoinTask to deal with + "execute Runnable" | executeRunnable | new ForkJoinPool() + "execute ForkJoinTask" | executeForkJoinTask | new ForkJoinPool() + "submit Runnable" | submitRunnable | new ForkJoinPool() + "submit Callable" | submitCallable | new ForkJoinPool() + "submit ForkJoinTask" | submitForkJoinTask | new ForkJoinPool() + "invoke ForkJoinTask" | invokeForkJoinTask | new ForkJoinPool() + "invokeAll" | invokeAll | new ForkJoinPool() + "invokeAll with timeout" | invokeAllTimeout | new ForkJoinPool() + "invokeAny" | invokeAny | new ForkJoinPool() + "invokeAny with timeout" | invokeAnyTimeout | new ForkJoinPool() + + // CustomThreadPoolExecutor would normally be disabled except enabled above. + "execute Runnable" | executeRunnable | new CustomThreadPoolExecutor() + "submit Runnable" | submitRunnable | new CustomThreadPoolExecutor() + "submit Callable" | submitCallable | new CustomThreadPoolExecutor() + "invokeAll" | invokeAll | new CustomThreadPoolExecutor() + "invokeAll with timeout" | invokeAllTimeout | new CustomThreadPoolExecutor() + "invokeAny" | invokeAny | new CustomThreadPoolExecutor() + "invokeAny with timeout" | invokeAnyTimeout | new CustomThreadPoolExecutor() + + // Internal executor used by CompletableFuture + "execute Runnable" | executeRunnable | new CompletableFuture.ThreadPerTaskExecutor() + poolName = poolImpl.class.simpleName + } + + def "#poolName '#name' wrap lambdas"() { + setup: + ExecutorService pool = poolImpl + def m = method + def w = wrap + + JavaAsyncChild child = new JavaAsyncChild(true, true) + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + m(pool, w(child)) + } + } + }.run() + // We block in child to make sure spans close in predictable order + child.unblock() + child.waitForCompletion() + + expect: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "asyncChild", span(0)) + } + } + + cleanup: + pool.shutdown() + pool.awaitTermination(10, TimeUnit.SECONDS) + + where: + name | method | wrap | poolImpl + "execute Runnable" | executeRunnable | { LambdaGen.wrapRunnable(it) } | new ScheduledThreadPoolExecutor(1) + "submit Runnable" | submitRunnable | { LambdaGen.wrapRunnable(it) } | new ScheduledThreadPoolExecutor(1) + "submit Callable" | submitCallable | { LambdaGen.wrapCallable(it) } | new ScheduledThreadPoolExecutor(1) + "schedule Runnable" | scheduleRunnable | { LambdaGen.wrapRunnable(it) } | new ScheduledThreadPoolExecutor(1) + "schedule Callable" | scheduleCallable | { LambdaGen.wrapCallable(it) } | new ScheduledThreadPoolExecutor(1) + poolName = poolImpl.class.simpleName + } + + def "#poolName '#name' reports after canceled jobs"() { + setup: + ExecutorService pool = poolImpl + def m = method + List children = new ArrayList<>() + List jobFutures = new ArrayList<>() + + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + try { + for (int i = 0; i < 20; ++i) { + // Our current instrumentation instrumentation does not behave very well + // if we try to reuse Callable/Runnable. Namely we would be getting 'orphaned' + // child traces sometimes since state can contain only one parent span - and + // we do not really have a good way for attributing work to correct parent span + // if we reuse Callable/Runnable. + // Solution for now is to never reuse a Callable/Runnable. + JavaAsyncChild child = new JavaAsyncChild(false, true) + children.add(child) + try { + Future f = m(pool, child) + jobFutures.add(f) + } catch (InvocationTargetException e) { + throw e.getCause() + } + } + } catch (RejectedExecutionException e) { + } + + for (Future f : jobFutures) { + f.cancel(false) + } + for (JavaAsyncChild child : children) { + child.unblock() + } + } + } + }.run() + + + expect: + waitForTraces(1).size() == 1 + + pool.shutdown() + pool.awaitTermination(10, TimeUnit.SECONDS) + + where: + name | method | poolImpl + "submit Runnable" | submitRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "submit Callable" | submitCallable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + + // Scheduled executor has additional methods and also may get disabled because it wraps tasks + "submit Runnable" | submitRunnable | new ScheduledThreadPoolExecutor(1) + "submit Callable" | submitCallable | new ScheduledThreadPoolExecutor(1) + "schedule Runnable" | scheduleRunnable | new ScheduledThreadPoolExecutor(1) + "schedule Callable" | scheduleCallable | new ScheduledThreadPoolExecutor(1) + + // ForkJoinPool has additional set of method overloads for ForkJoinTask to deal with + "submit Runnable" | submitRunnable | new ForkJoinPool() + "submit Callable" | submitCallable | new ForkJoinPool() + poolName = poolImpl.class.simpleName + } + + static class CustomThreadPoolExecutor extends AbstractExecutorService { + volatile running = true + def workQueue = new LinkedBlockingQueue(10) + + def worker = new Runnable() { + void run() { + try { + while (running) { + def runnable = workQueue.take() + runnable.run() + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt() + } catch (Exception e) { + e.printStackTrace() + } + } + } + + def workerThread = new Thread(worker, "ExecutorTestThread") + + private CustomThreadPoolExecutor() { + workerThread.start() + } + + @Override + void shutdown() { + running = false + workerThread.interrupt() + } + + @Override + List shutdownNow() { + running = false + workerThread.interrupt() + return [] + } + + @Override + boolean isShutdown() { + return !running + } + + @Override + boolean isTerminated() { + return workerThread.isAlive() + } + + @Override + boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + workerThread.join(unit.toMillis(timeout)) + return true + } + + @Override + def Future submit(Callable task) { + def future = newTaskFor(task) + execute(future) + return future + } + + @Override + def Future submit(Runnable task, T result) { + def future = newTaskFor(task, result) + execute(future) + return future + } + + @Override + Future submit(Runnable task) { + def future = newTaskFor(task, null) + execute(future) + return future + } + + @Override + def List> invokeAll(Collection> tasks) throws InterruptedException { + return super.invokeAll(tasks) + } + + @Override + def List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { + return super.invokeAll(tasks) + } + + @Override + def T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + return super.invokeAny(tasks) + } + + @Override + def T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return super.invokeAny(tasks) + } + + @Override + void execute(Runnable command) { + workQueue.put(command) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/ModuleInjectionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/ModuleInjectionTest.groovy new file mode 100644 index 000000000..f9a8f69b2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/groovy/ModuleInjectionTest.groovy @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import javax.swing.* + +/** + * This class tests that we correctly add module references when instrumenting + */ +class ModuleInjectionTest extends AgentInstrumentationSpecification { + /** + * There's nothing special about RepaintManager other than + * it's in a module (java.desktop) that doesn't read the "unnamed module" and it + * creates an instrumented runnable in its constructor + */ + def "test instrumenting java.desktop class"() { + when: + new RepaintManager() + + then: + noExceptionThrown() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/java/JavaAsyncChild.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/java/JavaAsyncChild.java new file mode 100644 index 000000000..1eedd3454 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/java/JavaAsyncChild.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.atomic.AtomicBoolean; + +public class JavaAsyncChild extends ForkJoinTask implements Runnable, Callable { + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + private final AtomicBoolean blockThread; + private final boolean doTraceableWork; + private final CountDownLatch latch = new CountDownLatch(1); + + public JavaAsyncChild() { + this(/* doTraceableWork= */ true, /* blockThread= */ false); + } + + public JavaAsyncChild(boolean doTraceableWork, boolean blockThread) { + this.doTraceableWork = doTraceableWork; + this.blockThread = new AtomicBoolean(blockThread); + } + + @Override + public Object getRawResult() { + return null; + } + + @Override + protected void setRawResult(Object value) {} + + @Override + protected boolean exec() { + runImpl(); + return true; + } + + public void unblock() { + blockThread.set(false); + } + + @Override + public void run() { + runImpl(); + } + + @Override + public Object call() { + runImpl(); + return null; + } + + public void waitForCompletion() throws InterruptedException { + latch.await(); + } + + private void runImpl() { + while (blockThread.get()) { + // busy-wait to block thread + } + if (doTraceableWork) { + asyncChild(); + } + latch.countDown(); + } + + private static void asyncChild() { + tracer.spanBuilder("asyncChild").startSpan().end(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/java/LambdaGen.java b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/java/LambdaGen.java new file mode 100644 index 000000000..d715d8a09 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/executors/javaagent/src/test/java/LambdaGen.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.concurrent.Callable; + +class LambdaGen { + + static Callable wrapCallable(Callable callable) { + return () -> callable.call(); + } + + static Runnable wrapRunnable(Runnable runnable) { + return () -> runnable.run(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent-unit-tests/external-annotations-javaagent-unittests.gradle b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent-unit-tests/external-annotations-javaagent-unittests.gradle new file mode 100644 index 000000000..ec9f23c98 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent-unit-tests/external-annotations-javaagent-unittests.gradle @@ -0,0 +1,9 @@ +apply plugin: "otel.java-conventions" + +dependencies { + testImplementation project(':instrumentation-api') + testImplementation project(':javaagent-extension-api') + testImplementation project(':javaagent-tooling') + testImplementation "net.bytebuddy:byte-buddy" + testImplementation project(':instrumentation:external-annotations:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent-unit-tests/src/test/groovy/IncludeTest.groovy b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent-unit-tests/src/test/groovy/IncludeTest.groovy new file mode 100644 index 000000000..21107fd1a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent-unit-tests/src/test/groovy/IncludeTest.groovy @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.javaagent.instrumentation.extannotations.TraceAnnotationsInstrumentationModule.DEFAULT_ANNOTATIONS + +import io.opentelemetry.instrumentation.api.config.Config +import io.opentelemetry.instrumentation.api.config.ConfigBuilder +import io.opentelemetry.javaagent.instrumentation.extannotations.TraceAnnotationsInstrumentationModule +import spock.lang.Specification +import spock.lang.Unroll + +class IncludeTest extends Specification { + + @Unroll + def "test configuration #value"() { + setup: + Config config + if (value) { + config = new ConfigBuilder().readProperties([ + "otel.instrumentation.external-annotations.include": value + ]).build() + } else { + config = Config.create([:]) + } + + expect: + TraceAnnotationsInstrumentationModule.AnnotatedMethodsInstrumentation.configureAdditionalTraceAnnotations(config) == expected.toSet() + + where: + value | expected + null | DEFAULT_ANNOTATIONS.toList() + " " | [] + "some.Invalid[]" | [] + "some.package.ClassName " | ["some.package.ClassName"] + " some.package.Class\$Name" | ["some.package.Class\$Name"] + " ClassName " | ["ClassName"] + "ClassName" | ["ClassName"] + "Class\$1;Class\$2;" | ["Class\$1", "Class\$2"] + "Duplicate ;Duplicate ;Duplicate; " | ["Duplicate"] + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/external-annotations-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/external-annotations-javaagent.gradle new file mode 100644 index 000000000..6728964f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/external-annotations-javaagent.gradle @@ -0,0 +1,46 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + coreJdk = true + } +} + +dependencies { + compileOnly project(':javaagent-tooling') + + testImplementation "com.newrelic.agent.java:newrelic-api:5.14.0" + testImplementation("io.opentracing.contrib.dropwizard:dropwizard-opentracing:0.2.2") { + transitive = false + } + testImplementation "com.signalfx.public:signalfx-trace-api:0.48.0-sfx1" + //Old and new versions of kamon use different packages for Trace annotation + testImplementation("io.kamon:kamon-annotation_2.11:0.6.7") { + transitive = false + } + testImplementation "io.kamon:kamon-annotation-api:2.1.4" + testImplementation "com.appoptics.agent.java:appoptics-sdk:6.20.1" + testImplementation "com.tracelytics.agent.java:tracelytics-api:5.0.10" + testImplementation("org.springframework.cloud:spring-cloud-sleuth-core:2.2.4.RELEASE") { + transitive = false + } +} + +test { + filter { + excludeTestsMatching 'ConfiguredTraceAnnotationsTest' + excludeTestsMatching 'TracedMethodsExclusionTest' + } +} +test.finalizedBy(tasks.register("testIncludeProperty", Test) { + filter { + includeTestsMatching 'ConfiguredTraceAnnotationsTest' + } + jvmArgs "-Dotel.instrumentation.external-annotations.include=package.Class\$Name;OuterClass\$InterestingMethod" +}) +test.finalizedBy(tasks.register("testExcludeMethodsProperty", Test) { + filter { + includeTestsMatching 'TracedMethodsExclusionTest' + } + jvmArgs "-Dotel.instrumentation.external-annotations.exclude-methods=TracedMethodsExclusionTest\$TestClass[excluded,annotatedButExcluded]" +}) diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/Const.java b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/Const.java new file mode 100644 index 000000000..88169e6a6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/Const.java @@ -0,0 +1,9 @@ +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import io.opentelemetry.instrumentation.api.config.Config; + +public class Const { + + public static final int EVENT_TIME_THRESHOLD = Config.get().getInt("otel.trace.timeevent.threshold",1000); + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentation.java new file mode 100644 index 000000000..f230a37ad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentation.java @@ -0,0 +1,191 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.extannotations.ExternalAnnotationTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.none; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.ByteCodeElement; +import net.bytebuddy.description.NamedElement; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ExternalAnnotationInstrumentation implements TypeInstrumentation { + + private static final Logger log = + LoggerFactory.getLogger(ExternalAnnotationInstrumentationModule.class); + + private static final String PACKAGE_CLASS_NAME_REGEX = "[\\w.$]+"; + + static final String CONFIG_FORMAT = + "(?:\\s*" + + PACKAGE_CLASS_NAME_REGEX + + "\\s*;)*\\s*" + + PACKAGE_CLASS_NAME_REGEX + + "\\s*;?\\s*"; + + private static final List DEFAULT_ANNOTATIONS = + Arrays.asList( + "com.appoptics.api.ext.LogMethod", + "com.newrelic.api.agent.Trace", + "com.signalfx.tracing.api.Trace", + "com.tracelytics.api.ext.LogMethod", + "datadog.trace.api.Trace", + "io.opentracing.contrib.dropwizard.Trace", + "com.xiaomi.hera.trace.annotation.Trace", + "kamon.annotation.Trace", + "kamon.annotation.api.Trace", + "org.springframework.cloud.sleuth.annotation.NewSpan"); + + private static final String TRACE_ANNOTATIONS_CONFIG = + "otel.instrumentation.external-annotations.include"; + private static final String TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG = + "otel.instrumentation.external-annotations.exclude-methods"; + + private final ElementMatcher.Junction classLoaderOptimization; + private final ElementMatcher.Junction traceAnnotationMatcher; + /** This matcher matches all methods that should be excluded from transformation. */ + private final ElementMatcher.Junction excludedMethodsMatcher; + + public ExternalAnnotationInstrumentation() { + Set additionalTraceAnnotations = configureAdditionalTraceAnnotations(Config.get()); + + if (additionalTraceAnnotations.isEmpty()) { + classLoaderOptimization = none(); + traceAnnotationMatcher = none(); + } else { + ElementMatcher.Junction classLoaderMatcher = none(); + for (String annotationName : additionalTraceAnnotations) { + classLoaderMatcher = classLoaderMatcher.or(hasClassesNamed(annotationName)); + } + this.classLoaderOptimization = classLoaderMatcher; + this.traceAnnotationMatcher = namedOneOf(additionalTraceAnnotations.toArray(new String[0])); + } + + excludedMethodsMatcher = configureExcludedMethods(); + } + + @Override + public ElementMatcher classLoaderOptimization() { + return classLoaderOptimization; + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(declaresMethod(isAnnotatedWith(traceAnnotationMatcher))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isAnnotatedWith(traceAnnotationMatcher).and(not(excludedMethodsMatcher)), + ExternalAnnotationInstrumentation.class.getName() + "$ExternalAnnotationAdvice"); + } + + private static Set configureAdditionalTraceAnnotations(Config config) { + String configString = config.getString(TRACE_ANNOTATIONS_CONFIG); + if (configString == null) { + return Collections.unmodifiableSet(new HashSet<>(DEFAULT_ANNOTATIONS)); + } else if (configString.isEmpty()) { + return Collections.emptySet(); + } else if (!configString.matches(CONFIG_FORMAT)) { + log.warn( + "Invalid trace annotations config '{}'. Must match 'package.Annotation$Name;*'.", + configString); + return Collections.emptySet(); + } else { + Set annotations = new HashSet<>(); + String[] annotationClasses = configString.split(";", -1); + for (String annotationClass : annotationClasses) { + if (!annotationClass.trim().isEmpty()) { + annotations.add(annotationClass.trim()); + } + } + return Collections.unmodifiableSet(annotations); + } + } + + /** + * Returns a matcher for all methods that should be excluded from auto-instrumentation by + * annotation-based advices. + */ + private static ElementMatcher.Junction configureExcludedMethods() { + ElementMatcher.Junction result = none(); + + Map> excludedMethods = + MethodsConfigurationParser.parse( + Config.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); + for (Map.Entry> entry : excludedMethods.entrySet()) { + String className = entry.getKey(); + ElementMatcher.Junction classMather = + isDeclaredBy(ElementMatchers.named(className)); + + ElementMatcher.Junction excludedMethodsMatcher = none(); + for (String methodName : entry.getValue()) { + excludedMethodsMatcher = excludedMethodsMatcher.or(ElementMatchers.named(methodName)); + } + + result = result.or(classMather.and(excludedMethodsMatcher)); + } + + return result; + } + + @SuppressWarnings("unused") + public static class ExternalAnnotationAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startSpan(method); + if(context != null){ + Span.fromContext(context).setAttribute("hera.annotations",true); + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentationModule.java new file mode 100644 index 000000000..e576f210a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentationModule.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ExternalAnnotationInstrumentationModule extends InstrumentationModule { + + public ExternalAnnotationInstrumentationModule() { + super("external-annotations"); + } + + @Override + public List typeInstrumentations() { + return asList(new ExternalAnnotationInstrumentation(),new TraceEventTimeInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationTracer.java b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationTracer.java new file mode 100644 index 000000000..87bdc014c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationTracer.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import java.lang.reflect.Method; + +public class ExternalAnnotationTracer extends BaseTracer { + private static final ExternalAnnotationTracer TRACER = new ExternalAnnotationTracer(); + + public static ExternalAnnotationTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.external-annotations"; + } + + public Context startSpan(Method method) { + return startSpan(SpanNames.fromMethod(method)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceEventTimeInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceEventTimeInstrumentation.java new file mode 100644 index 000000000..702b1a8ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceEventTimeInstrumentation.java @@ -0,0 +1,75 @@ +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.NamedElement; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import java.lang.reflect.Method; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class TraceEventTimeInstrumentation implements TypeInstrumentation { + + private static final ElementMatcher.Junction traceAnnotationMatcher = named("com.xiaomi.hera.trace.annotation.TraceTimeEvent"); + + @Override + public ElementMatcher typeMatcher() { + + return safeHasSuperType(declaresMethod(isAnnotatedWith(traceAnnotationMatcher))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isAnnotatedWith(traceAnnotationMatcher), + TraceEventTimeInstrumentation.class.getName() + "$TraceTimeAdvice"); + } + + @SuppressWarnings({"unused"}) + public static class TraceTimeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("eventStartTime") long eventStartTime) { + context = currentContext(); + if (context == null) { + return; + } + eventStartTime = System.currentTimeMillis(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable, + @Advice.Origin Method method, + @Advice.Local("eventStartTime") long eventStartTime) { + if (context == null) { + return; + } + long duration = System.currentTimeMillis() - eventStartTime; + if (duration > Const.EVENT_TIME_THRESHOLD) { + String name = SpanNames.fromMethod(method); + AttributesBuilder put = Attributes.builder().put("name", name).put("time", duration+"ms"); + Span.fromContext(context).addEvent("trace_time", put.build()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/ConfiguredTraceAnnotationsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/ConfiguredTraceAnnotationsTest.groovy new file mode 100644 index 000000000..149666b3a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/ConfiguredTraceAnnotationsTest.groovy @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.test.annotation.SayTracedHello +import java.util.concurrent.Callable + +class ConfiguredTraceAnnotationsTest extends AgentInstrumentationSpecification { + + def "method with disabled NewRelic annotation should be ignored"() { + setup: + SayTracedHello.fromCallableWhenDisabled() + + expect: + traces.empty + } + + def "method with custom annotation should be traced"() { + expect: + new AnnotationTracedCallable().call() == "Hello!" + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "AnnotationTracedCallable.call" + attributes { + } + } + } + } + } + + static class AnnotationTracedCallable implements Callable { + @OuterClass.InterestingMethod + @Override + String call() throws Exception { + return "Hello!" + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TraceAnnotationsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TraceAnnotationsTest.groovy new file mode 100644 index 000000000..fc920b55c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TraceAnnotationsTest.groovy @@ -0,0 +1,132 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.test.annotation.SayTracedHello +import io.opentracing.contrib.dropwizard.Trace +import java.util.concurrent.Callable + +class TraceAnnotationsTest extends AgentInstrumentationSpecification { + + def "test simple case annotations"() { + setup: + // Test single span in new trace + SayTracedHello.sayHello() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SayTracedHello.sayHello" + hasNoParent() + attributes { + "myattr" "test" + } + } + } + } + } + + def "test complex case annotations"() { + when: + // Test new trace with 2 children spans + SayTracedHello.sayHelloSayHa() + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "SayTracedHello.sayHelloSayHa" + hasNoParent() + attributes { + "myattr" "test2" + } + } + span(1) { + name "SayTracedHello.sayHello" + childOf span(0) + attributes { + "myattr" "test" + } + } + span(2) { + name "SayTracedHello.sayHello" + childOf span(0) + attributes { + "myattr" "test" + } + } + } + } + } + + def "test exception exit"() { + setup: + Throwable error = null + try { + SayTracedHello.sayError() + } catch (final Throwable ex) { + error = ex + } + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SayTracedHello.sayError" + status ERROR + errorEvent(error.class) + } + } + } + } + + def "test anonymous class annotations"() { + setup: + // Test anonymous classes with package. + SayTracedHello.fromCallable() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SayTracedHello\$1.call" + attributes { + } + } + } + } + + when: + // Test anonymous classes with no package. + new Callable() { + @Trace + @Override + String call() throws Exception { + return "Howdy!" + } + }.call() + + then: + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SayTracedHello\$1.call" + attributes { + } + } + trace(1, 1) { + span(0) { + name "TraceAnnotationsTest\$1.call" + attributes { + } + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TraceProvidersTest.groovy b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TraceProvidersTest.groovy new file mode 100644 index 000000000..56d35ca67 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TraceProvidersTest.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.test.annotation.SayTracedHello + +/** + * This test verifies that Otel supports various 3rd-party trace annotations + */ +class TraceProvidersTest extends AgentInstrumentationSpecification { + + def "should support #provider"(String provider) { + setup: + new SayTracedHello()."${provider.toLowerCase()}"() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SayTracedHello.${provider.toLowerCase()}" + hasNoParent() + attributes { + "providerAttr" provider + } + } + } + } + + where: + provider << ["AppOptics", "Datadog", "Dropwizard", "KamonOld", "KamonNew", "NewRelic", "SignalFx", "Sleuth", "Tracelytics"] + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TracedMethodsExclusionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TracedMethodsExclusionTest.groovy new file mode 100644 index 000000000..a0fffe454 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/groovy/TracedMethodsExclusionTest.groovy @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentracing.contrib.dropwizard.Trace + +class TracedMethodsExclusionTest extends AgentInstrumentationSpecification { + + static class TestClass { + //This method is not mentioned in any configuration + String notMentioned() { + return "Hello!" + } + + //This method is both configured to be traced and to be excluded. Should NOT be traced. + String excluded() { + return "Hello!" + } + + //This method is annotated with annotation which usually results in a captured span + @Trace + String annotated() { + return "Hello!" + } + + //This method is annotated with annotation which usually results in a captured span, but is configured to be + //excluded. + @Trace + String annotatedButExcluded() { + return "Hello!" + } + } + + + //Baseline and assumption validation + def "Calling these methods should be traced"() { + expect: + new TestClass().annotated() == "Hello!" + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TestClass.annotated" + attributes { + } + } + } + } + } + + def "Not explicitly configured method should not be traced"() { + expect: + new TestClass().notMentioned() == "Hello!" + + and: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + } + + def "Method which is both annotated and excluded for tracing should NOT be traced"() { + expect: + new TestClass().excluded() == "Hello!" + + and: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + } + + def "Method exclusion should override tracing annotations"() { + expect: + new TestClass().annotatedButExcluded() == "Hello!" + + and: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/java/OuterClass.java b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/java/OuterClass.java new file mode 100644 index 000000000..5fb43c233 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/java/OuterClass.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +public class OuterClass { + + @Retention(RUNTIME) + @Target(METHOD) + public @interface InterestingMethod {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/test/annotation/SayTracedHello.java b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/test/annotation/SayTracedHello.java new file mode 100644 index 000000000..e3a196bbb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/test/annotation/SayTracedHello.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test.annotation; + +import io.opentelemetry.api.trace.Span; +import java.util.concurrent.Callable; + +// To better see which library is tested +@SuppressWarnings("UnnecessarilyFullyQualified") +public class SayTracedHello { + + @com.appoptics.api.ext.LogMethod + public String appoptics() { + Span.current().setAttribute("providerAttr", "AppOptics"); + return "hello!"; + } + + @com.newrelic.api.agent.Trace + public String newrelic() { + Span.current().setAttribute("providerAttr", "NewRelic"); + return "hello!"; + } + + @com.signalfx.tracing.api.Trace + public String signalfx() { + Span.current().setAttribute("providerAttr", "SignalFx"); + return "hello!"; + } + + @com.tracelytics.api.ext.LogMethod + public String tracelytics() { + Span.current().setAttribute("providerAttr", "Tracelytics"); + return "hello!"; + } + + @datadog.trace.api.Trace + public String datadog() { + Span.current().setAttribute("providerAttr", "Datadog"); + return "hello!"; + } + + @io.opentracing.contrib.dropwizard.Trace + public String dropwizard() { + Span.current().setAttribute("providerAttr", "Dropwizard"); + return "hello!"; + } + + @kamon.annotation.Trace("spanName") + public String kamonold() { + Span.current().setAttribute("providerAttr", "KamonOld"); + return "hello!"; + } + + @kamon.annotation.api.Trace + public String kamonnew() { + Span.current().setAttribute("providerAttr", "KamonNew"); + return "hello!"; + } + + @org.springframework.cloud.sleuth.annotation.NewSpan + public String sleuth() { + Span.current().setAttribute("providerAttr", "Sleuth"); + return "hello!"; + } + + @io.opentracing.contrib.dropwizard.Trace + public static String sayHello() { + Span.current().setAttribute("myattr", "test"); + return "hello!"; + } + + @io.opentracing.contrib.dropwizard.Trace + public static String sayHelloSayHa() { + Span.current().setAttribute("myattr", "test2"); + return sayHello() + sayHello(); + } + + @io.opentracing.contrib.dropwizard.Trace + public static String sayError() { + throw new IllegalStateException(); + } + + public static String fromCallable() { + return new Callable() { + @com.newrelic.api.agent.Trace + @Override + public String call() { + return "Howdy!"; + } + }.call(); + } + + public static String fromCallableWhenDisabled() { + return new Callable() { + @com.newrelic.api.agent.Trace + @Override + public String call() { + return "Howdy!"; + } + }.call(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/finatra-2.9-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/finatra-2.9-javaagent.gradle new file mode 100644 index 000000000..bf73eb639 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/finatra-2.9-javaagent.gradle @@ -0,0 +1,61 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: "otel.scala-conventions" + +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + // We need separate test sources to compile against latest Finatra. + latestDepTest +} + +muzzle { + // There are some weird library issues below 2.9 so can't assert inverse + pass { + group = 'com.twitter' + module = 'finatra-http_2.11' + versions = '[2.9.0,]' + excludeDependency "io.netty:netty-transport-native-epoll" + } + + pass { + group = 'com.twitter' + module = 'finatra-http_2.12' + versions = '[2.9.0,]' + excludeDependency "io.netty:netty-transport-native-epoll" + } +} + +dependencies { + // TODO(anuraaga): Something about library configuration doesn't work well with scala compilation + // here. + compileOnly "com.twitter:finatra-http_2.11:2.9.0" + + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + + if (!testLatestDeps) { + // Requires old version of Jackson + testImplementation enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.9.10") + } + testImplementation("com.twitter:finatra-http_2.11:19.12.0") { + // Finatra POM references linux-aarch64 version of this which we don't need. Including it + // prevents us from managing Netty version because the classifier name changed to linux-aarch_64 + // in recent releases. So we exclude and force the linux-x86_64 classifier instead. + exclude group: "io.netty", module: "netty-transport-native-epoll" + } + testImplementation "io.netty:netty-transport-native-epoll:4.1.51.Final:linux-x86_64" + // Required for older versions of finatra on JDKs >= 11 + testImplementation "com.sun.activation:javax.activation:1.2.0" + + latestDepTestImplementation("com.twitter:finatra-http_2.11:+") { + exclude group: "io.netty", module: "netty-transport-native-epoll" + } +} + +compileLatestDepTestGroovy { + classpath += files(sourceSets.latestDepTest.scala.classesDirectory) +} + +if (testLatestDeps) { + // Separate task + test.enabled = false +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/groovy/FinatraServerLatestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/groovy/FinatraServerLatestTest.groovy new file mode 100644 index 000000000..ed6187ac6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/groovy/FinatraServerLatestTest.groovy @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import com.twitter.app.lifecycle.Event +import com.twitter.app.lifecycle.Observer +import com.twitter.finatra.http.HttpServer +import com.twitter.util.Await +import com.twitter.util.Closable +import com.twitter.util.Duration +import com.twitter.util.Promise +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData + +class FinatraServerLatestTest extends HttpServerTest implements AgentTestTrait { + private static final Duration TIMEOUT = Duration.fromSeconds(5) + private static final Duration STARTUP_TIMEOUT = Duration.fromSeconds(20) + + static closeAndWait(Closable closable) { + if (closable != null) { + Await.ready(closable.close(), TIMEOUT) + } + } + + @Override + HttpServer startServer(int port) { + HttpServer testServer = new FinatraServer() + + // Starting the server is blocking so start it in a separate thread + Thread startupThread = new Thread({ + testServer.main("-admin.port=:0", "-http.port=:" + port) + }) + startupThread.setDaemon(true) + startupThread.start() + + Promise startupPromise = new Promise<>() + + testServer.withObserver(new Observer() { + @Override + void onSuccess(Event event) { + if (event == testServer.startupCompletionEvent()) { + startupPromise.setValue(true) + } + } + + void onEntry(Event event) { + + } + + @Override + void onFailure(Event stage, Throwable throwable) { + if (stage != Event.Close$.MODULE$) { + startupPromise.setException(throwable) + } + } + }) + + Await.result(startupPromise, STARTUP_TIMEOUT) + + return testServer + } + + @Override + void stopServer(HttpServer httpServer) { + Await.ready(httpServer.close(), TIMEOUT) + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + endpoint != NOT_FOUND + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return endpoint == NOT_FOUND ? "HTTP GET" : super.expectedServerSpanName(endpoint) + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name "FinatraController" + kind INTERNAL + childOf(parent as SpanData) + // Finatra doesn't propagate the stack trace or exception to the instrumentation + // so the normal errorAttributes() method can't be used + attributes { + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/FinatraController.scala b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/FinatraController.scala new file mode 100644 index 000000000..b752bed61 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/FinatraController.scala @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.twitter.finagle.http.{Request, Response} +import com.twitter.finatra.http.Controller +import com.twitter.util.Future +import groovy.lang.Closure +import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint._ +import io.opentelemetry.instrumentation.test.base.HttpServerTest.controller + +class FinatraController extends Controller { + any(SUCCESS.getPath) { request: Request => + controller(SUCCESS, new Closure[Response](null) { + override def call(): Response = { + response.ok(SUCCESS.getBody) + } + }) + } + + any(ERROR.getPath) { request: Request => + controller(ERROR, new Closure[Response](null) { + override def call(): Response = { + response.internalServerError(ERROR.getBody) + } + }) + } + + any(QUERY_PARAM.getPath) { request: Request => + controller(QUERY_PARAM, new Closure[Response](null) { + override def call(): Response = { + response.ok(QUERY_PARAM.getBody) + } + }) + } + + any(EXCEPTION.getPath) { request: Request => + controller(EXCEPTION, new Closure[Future[Response]](null) { + override def call(): Future[Response] = { + throw new Exception(EXCEPTION.getBody) + } + }) + } + + any(REDIRECT.getPath) { request: Request => + controller(REDIRECT, new Closure[Response](null) { + override def call(): Response = { + response.found.location(REDIRECT.getBody) + } + }) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/FinatraServer.scala b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/FinatraServer.scala new file mode 100644 index 000000000..9a7405e55 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/FinatraServer.scala @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.twitter.finagle.http.Request +import com.twitter.finatra.http.HttpServer +import com.twitter.finatra.http.filters.ExceptionMappingFilter +import com.twitter.finatra.http.routing.HttpRouter + +class FinatraServer extends HttpServer { + override protected def configureHttp(router: HttpRouter): Unit = { + router + .filter[ExceptionMappingFilter[Request]] + .add[FinatraController] + .exceptionMapper[ResponseSettingExceptionMapper] + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/ResponseSettingExceptionMapper.scala b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/ResponseSettingExceptionMapper.scala new file mode 100644 index 000000000..dbc7841e4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/ResponseSettingExceptionMapper.scala @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.twitter.finagle.http.{Request, Response} +import com.twitter.finatra.http.exceptions.ExceptionMapper +import com.twitter.finatra.http.response.ResponseBuilder +import javax.inject.{Inject, Singleton} + +@Singleton +class ResponseSettingExceptionMapper @Inject()(response: ResponseBuilder) + extends ExceptionMapper[Exception] { + + override def toResponse(request: Request, exception: Exception): Response = { + response.internalServerError(exception.getMessage) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraInstrumentationModule.java new file mode 100644 index 000000000..9e6b02b34 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finatra; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class FinatraInstrumentationModule extends InstrumentationModule { + public FinatraInstrumentationModule() { + super("finatra", "finatra-2.9"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new FinatraRouteInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraResponseListener.java b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraResponseListener.java new file mode 100644 index 000000000..e615337a5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraResponseListener.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finatra; + +import static io.opentelemetry.javaagent.instrumentation.finatra.FinatraTracer.tracer; + +import com.twitter.finagle.http.Response; +import com.twitter.util.FutureEventListener; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class FinatraResponseListener implements FutureEventListener { + private final Context context; + private final Scope scope; + + public FinatraResponseListener(Context context, Scope scope) { + this.context = context; + this.scope = scope; + } + + @Override + public void onSuccess(Response response) { + scope.close(); + tracer().end(context); + } + + @Override + public void onFailure(Throwable cause) { + scope.close(); + tracer().endExceptionally(context, cause); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraRouteInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraRouteInstrumentation.java new file mode 100644 index 000000000..808d67043 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraRouteInstrumentation.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finatra; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.finatra.FinatraTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.twitter.finagle.http.Response; +import com.twitter.finatra.http.contexts.RouteInfo; +import com.twitter.util.Future; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import scala.Some; + +public class FinatraRouteInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.twitter.finatra.http.internal.routing.Route"); + } + + @Override + public ElementMatcher typeMatcher() { + return nameStartsWith("com.twitter.finatra.") + .and(extendsClass(named("com.twitter.finatra.http.internal.routing.Route"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("handleMatch")) + .and(takesArguments(2)) + .and(takesArgument(0, named("com.twitter.finagle.http.Request"))), + this.getClass().getName() + "$HandleMatchAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleMatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void nameSpan( + @Advice.FieldValue("routeInfo") RouteInfo routeInfo, + @Advice.FieldValue("clazz") Class clazz, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = Java8BytecodeBridge.currentContext(); + Span serverSpan = ServerSpan.fromContextOrNull(parentContext); + if (serverSpan != null) { + serverSpan.updateName(routeInfo.path()); + } + + context = tracer().startSpan(parentContext, clazz); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void setupCallback( + @Advice.Thrown Throwable throwable, + @Advice.Return Some> responseOption, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope == null) { + return; + } + + if (throwable != null) { + scope.close(); + tracer().endExceptionally(context, throwable); + return; + } + + responseOption.get().addEventListener(new FinatraResponseListener(context, scope)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraTracer.java b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraTracer.java new file mode 100644 index 000000000..d9c08ef79 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraTracer.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finatra; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ClassNames; + +public class FinatraTracer extends BaseTracer { + private static final FinatraTracer TRACER = new FinatraTracer(); + + public static FinatraTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.finatra-2.9"; + } + + public Context startSpan(Context parentContext, Class clazz) { + return super.startSpan(parentContext, ClassNames.simpleName(clazz), SpanKind.INTERNAL); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/groovy/FinatraServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/groovy/FinatraServerTest.groovy new file mode 100644 index 000000000..715f99811 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/groovy/FinatraServerTest.groovy @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import com.twitter.finatra.http.HttpServer +import com.twitter.util.Await +import com.twitter.util.Closable +import com.twitter.util.Duration +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.TimeoutException + +class FinatraServerTest extends HttpServerTest implements AgentTestTrait { + private static final Duration TIMEOUT = Duration.fromSeconds(5) + private static final long STARTUP_TIMEOUT = 40 * 1000 + + static closeAndWait(Closable closable) { + if (closable != null) { + Await.ready(closable.close(), TIMEOUT) + } + } + + @Override + HttpServer startServer(int port) { + HttpServer testServer = new FinatraServer() + + // Starting the server is blocking so start it in a separate thread + Thread startupThread = new Thread({ + testServer.main("-admin.port=:0", "-http.port=:" + port) + }) + startupThread.setDaemon(true) + startupThread.start() + + long startupDeadline = System.currentTimeMillis() + STARTUP_TIMEOUT + while (!testServer.started()) { + if (System.currentTimeMillis() > startupDeadline) { + throw new TimeoutException("Timed out waiting for server startup") + } + } + + return testServer + } + + @Override + void stopServer(HttpServer httpServer) { + Await.ready(httpServer.close(), TIMEOUT) + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + endpoint != NOT_FOUND + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return endpoint == NOT_FOUND ? "HTTP GET" : super.expectedServerSpanName(endpoint) + } + + @Override + boolean testPathParam() { + true + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name "FinatraController" + kind INTERNAL + childOf(parent as SpanData) + // Finatra doesn't propagate the stack trace or exception to the instrumentation + // so the normal errorAttributes() method can't be used + attributes { + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/FinatraController.scala b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/FinatraController.scala new file mode 100644 index 000000000..fbc801d90 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/FinatraController.scala @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.twitter.finagle.http.{Request, Response} +import com.twitter.finatra.http.Controller +import com.twitter.util.Future +import groovy.lang.Closure +import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint._ +import io.opentelemetry.instrumentation.test.base.HttpServerTest.controller + +class FinatraController extends Controller { + any(SUCCESS.getPath) { request: Request => + controller(SUCCESS, new Closure[Response](null) { + override def call(): Response = { + response.ok(SUCCESS.getBody) + } + }) + } + + any(ERROR.getPath) { request: Request => + controller(ERROR, new Closure[Response](null) { + override def call(): Response = { + response.internalServerError(ERROR.getBody) + } + }) + } + + any(QUERY_PARAM.getPath) { request: Request => + controller(QUERY_PARAM, new Closure[Response](null) { + override def call(): Response = { + response.ok(QUERY_PARAM.getBody) + } + }) + } + + any(EXCEPTION.getPath) { request: Request => + controller(EXCEPTION, new Closure[Future[Response]](null) { + override def call(): Future[Response] = { + throw new Exception(EXCEPTION.getBody) + } + }) + } + + any(REDIRECT.getPath) { request: Request => + controller(REDIRECT, new Closure[Response](null) { + override def call(): Response = { + response.found.location(REDIRECT.getBody) + } + }) + } + + any("/path/:id/param") { request: Request => + controller(PATH_PARAM, new Closure[Response](null) { + override def call(): Response = { + response.ok(request.params("id")) + } + }) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/FinatraServer.scala b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/FinatraServer.scala new file mode 100644 index 000000000..9a7405e55 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/FinatraServer.scala @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.twitter.finagle.http.Request +import com.twitter.finatra.http.HttpServer +import com.twitter.finatra.http.filters.ExceptionMappingFilter +import com.twitter.finatra.http.routing.HttpRouter + +class FinatraServer extends HttpServer { + override protected def configureHttp(router: HttpRouter): Unit = { + router + .filter[ExceptionMappingFilter[Request]] + .add[FinatraController] + .exceptionMapper[ResponseSettingExceptionMapper] + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/ResponseSettingExceptionMapper.scala b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/ResponseSettingExceptionMapper.scala new file mode 100644 index 000000000..dbc7841e4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/finatra-2.9/javaagent/src/test/scala/ResponseSettingExceptionMapper.scala @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.twitter.finagle.http.{Request, Response} +import com.twitter.finatra.http.exceptions.ExceptionMapper +import com.twitter.finatra.http.response.ResponseBuilder +import javax.inject.{Inject, Singleton} + +@Singleton +class ResponseSettingExceptionMapper @Inject()(response: ResponseBuilder) + extends ExceptionMapper[Exception] { + + override def toResponse(request: Request, exception: Exception): Response = { + response.internalServerError(exception.getMessage) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/geode-1.4-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/geode-1.4-javaagent.gradle new file mode 100644 index 000000000..f944b1eac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/geode-1.4-javaagent.gradle @@ -0,0 +1,16 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.geode" + module = "geode-core" + versions = "[1.4.0,)" + } +} + +dependencies { + library "org.apache.geode:geode-core:1.4.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeDbAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeDbAttributesExtractor.java new file mode 100644 index 000000000..f2319aaf7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeDbAttributesExtractor.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.geode; + +import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class GeodeDbAttributesExtractor extends DbAttributesExtractor { + @Override + protected String system(GeodeRequest request) { + return SemanticAttributes.DbSystemValues.GEODE; + } + + @Override + @Nullable + protected String user(GeodeRequest request) { + return null; + } + + @Override + protected String name(GeodeRequest request) { + return request.getRegion().getName(); + } + + @Override + @Nullable + protected String connectionString(GeodeRequest request) { + return null; + } + + @Override + @Nullable + protected String statement(GeodeRequest request) { + // sanitized statement is cached + return SqlStatementSanitizer.sanitize(request.getQuery()).getFullStatement(); + } + + @Override + @Nullable + protected String operation(GeodeRequest request) { + return request.getOperation(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeInstrumentationModule.java new file mode 100644 index 000000000..32377314f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.geode; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class GeodeInstrumentationModule extends InstrumentationModule { + public GeodeInstrumentationModule() { + super("geode", "geode-1.4"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new GeodeRegionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeRegionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeRegionInstrumentation.java new file mode 100644 index 000000000..ee4752ed8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeRegionInstrumentation.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.geode; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.geode.GeodeSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.geode.cache.Region; + +public class GeodeRegionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.geode.cache.Region"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.apache.geode.cache.Region")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and( + namedOneOf( + "clear", + "create", + "destroy", + "entrySet", + "get", + "getAll", + "invalidate", + "replace") + .or(nameStartsWith("contains")) + .or(nameStartsWith("keySet")) + .or(nameStartsWith("put")) + .or(nameStartsWith("remove"))), + this.getClass().getName() + "$SimpleAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("existsValue", "query", "selectValue")) + .and(takesArgument(0, String.class)), + this.getClass().getName() + "$QueryAdvice"); + } + + @SuppressWarnings("unused") + public static class SimpleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Region region, + @Advice.Origin Method method, + @Advice.Local("otelRequest") GeodeRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = currentContext(); + request = GeodeRequest.create(region, method.getName(), null); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") GeodeRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + instrumenter().end(context, request, null, throwable); + } + } + + @SuppressWarnings("unused") + public static class QueryAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Region region, + @Advice.Origin Method method, + @Advice.Argument(0) String query, + @Advice.Local("otelRequest") GeodeRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = currentContext(); + request = GeodeRequest.create(region, method.getName(), query); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") GeodeRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + instrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeRequest.java b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeRequest.java new file mode 100644 index 000000000..fbe541df2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.geode; + +import com.google.auto.value.AutoValue; +import org.apache.geode.cache.Region; +import org.checkerframework.checker.nullness.qual.Nullable; + +@AutoValue +public abstract class GeodeRequest { + + public static GeodeRequest create(Region region, String operation, @Nullable String query) { + return new AutoValue_GeodeRequest(region, operation, query); + } + + public abstract Region getRegion(); + + public abstract String getOperation(); + + @Nullable + public abstract String getQuery(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeSingletons.java b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeSingletons.java new file mode 100644 index 000000000..4e2f55860 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeSingletons.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.geode; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; + +public final class GeodeSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.geode-1.4"; + + private static final Instrumenter INSTRUMENTER; + + static { + DbAttributesExtractor attributesExtractor = + new GeodeDbAttributesExtractor(); + SpanNameExtractor spanName = DbSpanNameExtractor.create(attributesExtractor); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(attributesExtractor) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private GeodeSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/test/groovy/PutGetTest.groovy b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/test/groovy/PutGetTest.groovy new file mode 100644 index 000000000..05f44ba67 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/geode-1.4/javaagent/src/test/groovy/PutGetTest.groovy @@ -0,0 +1,197 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.apache.geode.DataSerializable +import org.apache.geode.cache.client.ClientCacheFactory +import org.apache.geode.cache.client.ClientRegionShortcut +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +class PutGetTest extends AgentInstrumentationSpecification { + @Shared + def cache = new ClientCacheFactory().create() + + @Shared + def regionFactory = cache.createClientRegionFactory(ClientRegionShortcut.LOCAL) + + @Shared + def region = regionFactory.create("test-region") + + def "test put and get"() { + when: + def cacheValue = runUnderTrace("someTrace") { + region.clear() + region.put(key, value) + region.get(key) + } + + then: + cacheValue == value + assertGeodeTrace("get", null) + + where: + key | value + 'Hello' | 'World' + 'Humpty' | 'Dumpty' + '1' | 'One' + 'One' | '1' + } + + def "test put and remove"() { + when: + runUnderTrace("someTrace") { + region.clear() + region.put(key, value) + region.remove(key) + } + + then: + region.size() == 0 + assertGeodeTrace("remove", null) + + where: + key | value + 'Hello' | 'World' + 'Humpty' | 'Dumpty' + '1' | 'One' + 'One' | '1' + } + + def "test query"() { + when: + def cacheValue = runUnderTrace("someTrace") { + region.clear() + region.put(key, value) + region.query("SELECT * FROM /test-region") + } + + then: + cacheValue.asList().size() + assertGeodeTrace("query", "SELECT * FROM /test-region") + + where: + key | value + 'Hello' | 'World' + 'Humpty' | 'Dumpty' + '1' | 'One' + 'One' | '1' + } + + def "test existsValue"() { + when: + def exists = runUnderTrace("someTrace") { + region.clear() + region.put(key, value) + region.existsValue("SELECT * FROM /test-region") + } + + then: + exists + assertGeodeTrace("existsValue", "SELECT * FROM /test-region") + + where: + key | value + 'Hello' | 'World' + 'Humpty' | 'Dumpty' + '1' | 'One' + 'One' | '1' + } + + def assertGeodeTrace(String verb, String query) { + assertTraces(1) { + trace(0, 4) { + span(0) { + name "someTrace" + kind INTERNAL + } + span(1) { + name "clear test-region" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "geode" + "$SemanticAttributes.DB_NAME.key" "test-region" + "$SemanticAttributes.DB_OPERATION.key" "clear" + } + } + span(2) { + name "put test-region" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "geode" + "$SemanticAttributes.DB_NAME.key" "test-region" + "$SemanticAttributes.DB_OPERATION.key" "put" + } + } + span(3) { + name "$verb test-region" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "geode" + "$SemanticAttributes.DB_NAME.key" "test-region" + "$SemanticAttributes.DB_OPERATION.key" verb + if (query != null) { + "$SemanticAttributes.DB_STATEMENT.key" query + } + } + } + } + } + return true + } + + def "should sanitize geode query"() { + given: + def value = new Card(cardNumber: '1234432156788765', expDate: '10/2020') + + region.clear() + region.put(1, value) + ignoreTracesAndClear(2) + + when: + def results = region.query("SELECT * FROM /test-region p WHERE p.expDate = '10/2020'") + + then: + results.toList() == [value] + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "query test-region" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "geode" + "$SemanticAttributes.DB_NAME.key" "test-region" + "$SemanticAttributes.DB_OPERATION.key" "query" + "$SemanticAttributes.DB_STATEMENT.key" "SELECT * FROM /test-region p WHERE p.expDate = ?" + } + } + } + } + } + + static class Card implements DataSerializable { + String cardNumber + String expDate + + @Override + void toData(DataOutput dataOutput) throws IOException { + dataOutput.writeUTF(cardNumber) + dataOutput.writeUTF(expDate) + } + + @Override + void fromData(DataInput dataInput) throws IOException, ClassNotFoundException { + cardNumber = dataInput.readUTF() + expDate = dataInput.readUTF() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/google-http-client-1.19-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/google-http-client-1.19-javaagent.gradle new file mode 100644 index 000000000..8be9f73c9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/google-http-client-1.19-javaagent.gradle @@ -0,0 +1,15 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.google.http-client" + module = "google-http-client" + + // 1.19.0 is the first release. The versions before are betas and RCs + versions = "[1.19.0,)" + } +} + +dependencies { + library "com.google.http-client:google-http-client:1.19.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientInstrumentationModule.java new file mode 100644 index 000000000..45e0b242a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.googlehttpclient; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class GoogleHttpClientInstrumentationModule extends InstrumentationModule { + public GoogleHttpClientInstrumentationModule() { + super("google-http-client", "google-http-client-1.19"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new GoogleHttpRequestInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientTracer.java new file mode 100644 index 000000000..7c79a6b33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientTracer.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.googlehttpclient; + +import static io.opentelemetry.javaagent.instrumentation.googlehttpclient.HeadersInjectAdapter.SETTER; + +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import java.net.URISyntaxException; + +public class GoogleHttpClientTracer + extends HttpClientTracer { + private static final GoogleHttpClientTracer TRACER = new GoogleHttpClientTracer(); + + private GoogleHttpClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static GoogleHttpClientTracer tracer() { + return TRACER; + } + + public Context startSpan(Context parentContext, HttpRequest request) { + return startSpan(parentContext, request, request.getHeaders()); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.google-http-client-1.19"; + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.getRequestMethod(); + } + + @Override + protected URI url(HttpRequest httpRequest) throws URISyntaxException { + // Google uses %20 (space) instead of "+" for spaces in the fragment + // Add "+" back for consistency with the other http client instrumentations + String url = httpRequest.getUrl().build(); + String fixedUrl = url.replaceAll("%20", "+"); + return new URI(fixedUrl); + } + + @Override + protected Integer status(HttpResponse httpResponse) { + return httpResponse.getStatusCode(); + } + + @Override + protected String requestHeader(HttpRequest httpRequest, String name) { + return header(httpRequest.getHeaders(), name); + } + + @Override + protected String responseHeader(HttpResponse httpResponse, String name) { + return header(httpResponse.getHeaders(), name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + private static String header(HttpHeaders headers, String name) { + return headers.getFirstHeaderStringValue(name); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpRequestInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpRequestInstrumentation.java new file mode 100644 index 000000000..18e23a74a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpRequestInstrumentation.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.googlehttpclient; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.googlehttpclient.GoogleHttpClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import java.util.concurrent.Executor; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class GoogleHttpRequestInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // HttpRequest is a final class. Only need to instrument it exactly + // Note: the rest of com.google.api is ignored in AdditionalLibraryIgnoresMatcher to speed + // things up + return named("com.google.api.client.http.HttpRequest"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("execute")).and(takesArguments(0)), + this.getClass().getName() + "$ExecuteAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("executeAsync")) + .and(takesArguments(1)) + .and(takesArgument(0, Executor.class)), + this.getClass().getName() + "$ExecuteAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.This HttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = InstrumentationContext.get(HttpRequest.class, Context.class).get(request); + if (context != null) { + // span was created by GoogleHttpClientAsyncAdvice instrumentation below + // (executeAsync ends up calling execute from a separate thread) + // so make it current and end it in method exit + scope = context.makeCurrent(); + return; + } + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + context = tracer().startSpan(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return HttpResponse response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + tracer().endMaybeExceptionally(context, response, throwable); + } + } + + @SuppressWarnings("unused") + public static class ExecuteAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.This HttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, request); + scope = context.makeCurrent(); + + InstrumentationContext.get(HttpRequest.class, Context.class).put(request, context); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/HeadersInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/HeadersInjectAdapter.java new file mode 100644 index 000000000..370b70f1f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/HeadersInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.googlehttpclient; + +import com.google.api.client.http.HttpHeaders; +import io.opentelemetry.context.propagation.TextMapSetter; + +public class HeadersInjectAdapter implements TextMapSetter { + + public static final HeadersInjectAdapter SETTER = new HeadersInjectAdapter(); + + @Override + public void set(HttpHeaders carrier, String key, String value) { + carrier.put(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/AbstractGoogleHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/AbstractGoogleHttpClientTest.groovy new file mode 100644 index 000000000..617a9988c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/AbstractGoogleHttpClientTest.groovy @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import com.google.api.client.http.GenericUrl +import com.google.api.client.http.HttpRequest +import com.google.api.client.http.HttpResponse +import com.google.api.client.http.javanet.NetHttpTransport +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import spock.lang.Shared + +abstract class AbstractGoogleHttpClientTest extends HttpClientTest implements AgentTestTrait { + + @Shared + def requestFactory = new NetHttpTransport().createRequestFactory() + + @Override + boolean testCallback() { + // executeAsync does not actually allow asynchronous execution since it returns a standard + // Future which cannot have callbacks attached. We instrument execute and executeAsync + // differently so test both but do not need to run our normal asynchronous tests, which check + // context propagation, as there is no possible context propagation. + return false + } + + @Override + HttpRequest buildRequest(String method, URI uri, Map headers) { + def genericUrl = new GenericUrl(uri) + + def request = requestFactory.buildRequest(method, genericUrl, null) + request.connectTimeout = CONNECT_TIMEOUT_MS + + // GenericData::putAll method converts all known http headers to List + // and lowercase all other headers + def ci = request.getHeaders().getClassInfo() + request.getHeaders().putAll(headers.collectEntries { name, value + -> [(name): (ci.getFieldInfo(name) != null ? [value] : value.toLowerCase())] + }) + + request.setThrowExceptionOnExecuteError(false) + return request + } + + @Override + int sendRequest(HttpRequest request, String method, URI uri, Map headers) { + return sendRequest(request).getStatusCode() + } + + abstract HttpResponse sendRequest(HttpRequest request) + + @Override + boolean testCircularRedirects() { + // Circular redirects don't throw an exception with Google Http Client + return false + } + + def "error traces when exception is not thrown"() { + given: + def uri = resolveAddress("/error") + + when: + def responseCode = doRequest(method, uri) + + then: + responseCode == 500 + assertTraces(1) { + trace(0, 2) { + span(0) { + kind CLIENT + status ERROR + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "${uri}" + "${SemanticAttributes.HTTP_METHOD.key}" method + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 500 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + } + } + serverSpan(it, 1, span(0)) + } + } + + where: + method = "GET" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/GoogleHttpClientAsyncTest.groovy b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/GoogleHttpClientAsyncTest.groovy new file mode 100644 index 000000000..14b1f6ba4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/GoogleHttpClientAsyncTest.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.google.api.client.http.HttpRequest +import com.google.api.client.http.HttpResponse + +class GoogleHttpClientAsyncTest extends AbstractGoogleHttpClientTest { + @Override + HttpResponse sendRequest(HttpRequest request) { + return request.executeAsync().get() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/GoogleHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/GoogleHttpClientTest.groovy new file mode 100644 index 000000000..6efa376fb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/google-http-client-1.19/javaagent/src/test/groovy/GoogleHttpClientTest.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.google.api.client.http.HttpRequest +import com.google.api.client.http.HttpResponse + +class GoogleHttpClientTest extends AbstractGoogleHttpClientTest { + @Override + HttpResponse sendRequest(HttpRequest request) { + return request.execute() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/grails-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/grails-3.0-javaagent.gradle new file mode 100644 index 000000000..4efa08ca6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/grails-3.0-javaagent.gradle @@ -0,0 +1,50 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.grails" + module = "grails-web-url-mappings" + versions = "[3.0,)" + // version 3.1.15 depends on org.grails:grails-datastore-core:5.0.13.BUILD-SNAPSHOT + // which (obviously) does not exist + // version 3.3.6 depends on org.grails:grails-datastore-core:6.1.10.BUILD-SNAPSHOT + // which (also obviously) does not exist + skip('3.1.15', '3.3.6') + assertInverse = true + } +} + +repositories { + mavenCentral() + maven { + url "https://repo.grails.org/artifactory/core" + mavenContent { + releasesOnly() + } + } + mavenLocal() +} + +// first version where our tests work +def grailsVersion = '3.0.6' +def springBootVersion = '1.2.5.RELEASE' + +dependencies { + library("org.grails:grails-plugin-url-mappings:$grailsVersion") + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + testInstrumentation project(':instrumentation:tomcat:tomcat-7.0:javaagent') + testInstrumentation project(':instrumentation:spring:spring-webmvc-3.1:javaagent') + + testLibrary "org.springframework.boot:spring-boot-autoconfigure:$springBootVersion" + testLibrary "org.springframework.boot:spring-boot-starter-tomcat:$springBootVersion" + + testImplementation(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + + latestDepTestLibrary("org.grails:grails-plugin-url-mappings:4.0.+") + latestDepTestLibrary "org.springframework.boot:spring-boot-autoconfigure:2.1.17.RELEASE" + latestDepTestLibrary "org.springframework.boot:spring-boot-starter-tomcat:2.1.17.RELEASE" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/DefaultGrailsControllerClassInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/DefaultGrailsControllerClassInstrumentation.java new file mode 100644 index 000000000..ed307a877 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/DefaultGrailsControllerClassInstrumentation.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grails; + +import static io.opentelemetry.javaagent.instrumentation.grails.GrailsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DefaultGrailsControllerClassInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.grails.core.DefaultGrailsControllerClass"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("invoke")) + .and(takesArgument(0, named(Object.class.getName()))) + .and(takesArgument(1, named(String.class.getName()))) + .and(takesArguments(2)), + DefaultGrailsControllerClassInstrumentation.class.getName() + "$ControllerAdvice"); + } + + @SuppressWarnings("unused") + public static class ControllerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startSpan( + @Advice.Argument(0) Object controller, + @Advice.Argument(1) String action, + @Advice.FieldValue("defaultActionName") String defaultActionName, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = tracer().startSpan(controller, action != null ? action : defaultActionName); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + if (throwable == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsInstrumentationModule.java new file mode 100644 index 000000000..caca5eff0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsInstrumentationModule.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grails; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class GrailsInstrumentationModule extends InstrumentationModule { + public GrailsInstrumentationModule() { + super("grails", "grails-3.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new DefaultGrailsControllerClassInstrumentation(), + new UrlMappingsInfoHandlerAdapterInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsTracer.java b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsTracer.java new file mode 100644 index 000000000..6471c237f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsTracer.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grails; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTROLLER; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import org.grails.web.mapping.mvc.GrailsControllerUrlMappingInfo; + +public class GrailsTracer extends BaseTracer { + + private static final GrailsTracer TRACER = new GrailsTracer(); + + public static GrailsTracer tracer() { + return TRACER; + } + + public Context startSpan(Object controller, String action) { + return startSpan(SpanNames.fromMethod(controller.getClass(), action)); + } + + public void updateServerSpanName(Context context, GrailsControllerUrlMappingInfo info) { + ServerSpanNaming.updateServerSpanName( + context, CONTROLLER, () -> getServerSpanName(context, info)); + } + + private static String getServerSpanName(Context context, GrailsControllerUrlMappingInfo info) { + String action = + info.getActionName() != null + ? info.getActionName() + : info.getControllerClass().getDefaultAction(); + return ServletContextPath.prepend(context, "/" + info.getControllerName() + "/" + action); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.grails-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/UrlMappingsInfoHandlerAdapterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/UrlMappingsInfoHandlerAdapterInstrumentation.java new file mode 100644 index 000000000..6d2e28389 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/UrlMappingsInfoHandlerAdapterInstrumentation.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grails; + +import static io.opentelemetry.javaagent.instrumentation.grails.GrailsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.grails.web.mapping.mvc.GrailsControllerUrlMappingInfo; + +public class UrlMappingsInfoHandlerAdapterInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.grails.web.mapping.mvc.UrlMappingsInfoHandlerAdapter"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("handle")) + .and(takesArgument(2, named(Object.class.getName()))) + .and(takesArguments(3)), + UrlMappingsInfoHandlerAdapterInstrumentation.class.getName() + "$ServerSpanNameAdvice"); + } + + @SuppressWarnings("unused") + public static class ServerSpanNameAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void nameSpan(@Advice.Argument(2) Object handler) { + + if (handler instanceof GrailsControllerUrlMappingInfo) { + Context parentContext = Java8BytecodeBridge.currentContext(); + tracer().updateServerSpanName(parentContext, (GrailsControllerUrlMappingInfo) handler); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/ErrorController.groovy b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/ErrorController.groovy new file mode 100644 index 000000000..6ca586eb4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/ErrorController.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR + +import grails.artefact.Controller +import grails.web.Action + +class ErrorController implements Controller { + + @Action + def index() { + render ERROR.body + } + + @Action + def notFound() { + response.sendError(404, "Not Found") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/GrailsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/GrailsTest.groovy new file mode 100644 index 000000000..1db64f85f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/GrailsTest.groovy @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration +import groovy.transform.CompileStatic +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.web.ServerProperties +import org.springframework.context.ConfigurableApplicationContext + +class GrailsTest extends HttpServerTest implements AgentTestTrait { + + @CompileStatic + @SpringBootApplication + static class TestApplication extends GrailsAutoConfiguration { + static ConfigurableApplicationContext start(int port, String contextPath) { + GrailsApp grailsApp = new GrailsApp(TestApplication) + // context path configuration property name changes between spring boot versions + def contextPathKey = "server.context-path" + try { + ServerProperties.getDeclaredMethod("getServlet") + contextPathKey = "server.servlet.contextPath" + } catch (NoSuchMethodException ignore) { + } + Map properties = new HashMap<>() + properties.put("server.port", port) + properties.put(contextPathKey, contextPath) + grailsApp.setDefaultProperties(properties) + return grailsApp.run() + } + + @Override + Collection classes() { + return Arrays.asList(TestController, ErrorController, UrlMappings) + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + if (endpoint == PATH_PARAM) { + return getContextPath() + "/test/path" + } else if (endpoint == QUERY_PARAM) { + return getContextPath() + "/test/query" + } else if (endpoint == ERROR) { + return getContextPath() + "/test/error" + } else if (endpoint == NOT_FOUND) { + return getContextPath() + "/**" + } + return getContextPath() + "/test" + endpoint.path + } + + @Override + ConfigurableApplicationContext startServer(int port) { + return TestApplication.start(port, getContextPath()) + } + + @Override + void stopServer(ConfigurableApplicationContext ctx) { + ctx.close() + } + + @Override + String getContextPath() { + return "/xyz" + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + true + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == ERROR || endpoint == NOT_FOUND + } + + @Override + boolean hasErrorPageSpans(ServerEndpoint endpoint) { + endpoint == ERROR || endpoint == EXCEPTION || endpoint == NOT_FOUND + } + + @Override + int getErrorPageSpansCount(ServerEndpoint endpoint) { + endpoint == NOT_FOUND ? 2 : 1 + } + + @Override + boolean testPathParam() { + true + } + + @Override + void errorPageSpans(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint) { + trace.span(index) { + name endpoint == NOT_FOUND ? "ErrorController.notFound" : "ErrorController.index" + kind INTERNAL + attributes { + } + } + if (endpoint == NOT_FOUND) { + trace.span(index + 1) { + name ~/\.sendError$/ + kind INTERNAL + attributes { + } + } + } + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint) { + trace.span(index) { + name endpoint == REDIRECT ? ~/\.sendRedirect$/ : ~/\.sendError$/ + kind INTERNAL + attributes { + } + } + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint) { + trace.span(index) { + if (endpoint == QUERY_PARAM) { + name "TestController.query" + } else if (endpoint == PATH_PARAM) { + name "TestController.path" + } else if (endpoint == NOT_FOUND) { + name "ResourceHttpRequestHandler.handleRequest" + } else { + name "TestController.${endpoint.name().toLowerCase()}" + } + kind INTERNAL + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(Exception, EXCEPTION.body) + } + childOf((SpanData) parent) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/TestController.groovy b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/TestController.groovy new file mode 100644 index 000000000..49390ad5a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/TestController.groovy @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import grails.artefact.Controller +import grails.web.Action +import io.opentelemetry.instrumentation.test.base.HttpServerTest + +class TestController implements Controller { + + @Action + def index() { + render "Hello World!" + } + + @Action + def success() { + HttpServerTest.controller(SUCCESS) { + render SUCCESS.body + } + } + + @Action + def query() { + HttpServerTest.controller(QUERY_PARAM) { + render "some=${params.some}" + } + } + + @Action + def redirect() { + HttpServerTest.controller(REDIRECT) { + response.sendRedirect(REDIRECT.body) + } + } + + @Action + def error() { + HttpServerTest.controller(ERROR) { + response.sendError(ERROR.status, "unused") + } + } + + @Action + def exception() { + HttpServerTest.controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + + @Action + def path() { + HttpServerTest.controller(PATH_PARAM) { + render params.id + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/UrlMappings.groovy b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/UrlMappings.groovy new file mode 100644 index 000000000..7e79a7d45 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/groovy/test/UrlMappings.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +class UrlMappings { + + static mappings = { + "/success"(controller: 'test', action: 'success') + "/query"(controller: 'test', action: 'query') + "/redirect"(controller: 'test', action: 'redirect') + "/error-status"(controller: 'test', action: 'error') + "/exception"(controller: 'test', action: 'exception') + "/path/$id/param"(controller: 'test', action: 'path') + + "500"(controller: 'error') + "404"(controller: 'error', action: 'notFound') + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/resources/application.yml b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/resources/application.yml new file mode 100644 index 000000000..730dce4bb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grails-3.0/javaagent/src/test/resources/application.yml @@ -0,0 +1,8 @@ +--- +grails: + profile: web + env: production +spring: + groovy: + template: + check-template-location: false \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/grizzly-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/grizzly-2.0-javaagent.gradle new file mode 100644 index 000000000..2a2b02f2a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/grizzly-2.0-javaagent.gradle @@ -0,0 +1,34 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.glassfish.grizzly" + module = 'grizzly-http' + versions = "[2.0,)" + assertInverse = true + } +} + +dependencies { + compileOnly "org.glassfish.grizzly:grizzly-http:2.0" + + testImplementation "javax.xml.bind:jaxb-api:2.2.3" + testImplementation "javax.ws.rs:javax.ws.rs-api:2.0" + testLibrary "org.glassfish.jersey.containers:jersey-container-grizzly2-http:2.0" + + latestDepTestLibrary "org.glassfish.jersey.containers:jersey-container-grizzly2-http:2.+" + latestDepTestLibrary "org.glassfish.jersey.inject:jersey-hk2:2.+" +} + +tasks.withType(Test).configureEach { + jvmArgs "-Dotel.instrumentation.grizzly.enabled=true" +} + +tasks.withType(Test).configureEach { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2640 + jvmArgs "-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=false" +} + + +// Requires old Guava. Can't use enforcedPlatform since predates BOM +configurations.testRuntimeClasspath.resolutionStrategy.force "com.google.guava:guava:19.0" diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/DefaultFilterChainInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/DefaultFilterChainInstrumentation.java new file mode 100644 index 000000000..63b4c921b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/DefaultFilterChainInstrumentation.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grizzly; + +import static io.opentelemetry.javaagent.instrumentation.grizzly.GrizzlyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPrivate; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.glassfish.grizzly.filterchain.FilterChainContext; + +public class DefaultFilterChainInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.glassfish.grizzly.filterchain.DefaultFilterChain"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPrivate()) + .and(named("notifyFailure")) + .and(takesArgument(0, named("org.glassfish.grizzly.filterchain.FilterChainContext"))) + .and(takesArgument(1, Throwable.class)), + DefaultFilterChainInstrumentation.class.getName() + "$NotifyFailureAdvice"); + } + + @SuppressWarnings("unused") + public static class NotifyFailureAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onFail( + @Advice.Argument(0) FilterChainContext ctx, @Advice.Argument(1) Throwable throwable) { + Context context = tracer().getServerContext(ctx); + if (context != null) { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/ExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/ExtractAdapter.java new file mode 100644 index 000000000..c85e21cd2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/ExtractAdapter.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grizzly; + +import io.opentelemetry.context.propagation.TextMapGetter; +import org.glassfish.grizzly.http.HttpRequestPacket; + +public class ExtractAdapter implements TextMapGetter { + public static final ExtractAdapter GETTER = new ExtractAdapter(); + + @Override + public Iterable keys(HttpRequestPacket request) { + return request.getHeaders().names(); + } + + @Override + public String get(HttpRequestPacket request, String key) { + return request.getHeader(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/FilterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/FilterInstrumentation.java new file mode 100644 index 000000000..8c6accdf2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/FilterInstrumentation.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grizzly; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.grizzly.GrizzlyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.hasSuperClass; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; +import org.glassfish.grizzly.filterchain.BaseFilter; +import org.glassfish.grizzly.filterchain.FilterChainContext; + +public class FilterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.glassfish.grizzly.filterchain.BaseFilter"); + } + + @Override + public ElementMatcher typeMatcher() { + return hasSuperClass(named("org.glassfish.grizzly.filterchain.BaseFilter")) + // HttpCodecFilter is instrumented in the server instrumentation + .and(not(ElementMatchers.named("org.glassfish.grizzly.http.HttpCodecFilter"))) + .and(not(ElementMatchers.named("org.glassfish.grizzly.http.HttpServerFilter"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handleRead") + .and(takesArgument(0, named("org.glassfish.grizzly.filterchain.FilterChainContext"))) + .and(isPublic()), + FilterInstrumentation.class.getName() + "$HandleReadAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleReadAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This BaseFilter it, + @Advice.Argument(0) FilterChainContext ctx, + @Advice.Local("otelScope") Scope scope) { + if (Java8BytecodeBridge.currentSpan().getSpanContext().isValid()) { + return; + } + + Context context = tracer().getServerContext(ctx); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.This BaseFilter it, @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyHttpServerTracer.java new file mode 100644 index 000000000..87578c5d8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyHttpServerTracer.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grizzly; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import java.net.URI; +import java.net.URISyntaxException; +import org.glassfish.grizzly.filterchain.FilterChainContext; +import org.glassfish.grizzly.http.HttpRequestPacket; +import org.glassfish.grizzly.http.HttpResponsePacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class GrizzlyHttpServerTracer + extends HttpServerTracer< + HttpRequestPacket, HttpResponsePacket, HttpRequestPacket, FilterChainContext> { + + private static final Logger log = LoggerFactory.getLogger(GrizzlyHttpServerTracer.class); + + private static final GrizzlyHttpServerTracer TRACER = new GrizzlyHttpServerTracer(); + + public static GrizzlyHttpServerTracer tracer() { + return TRACER; + } + + @Override + protected String method(HttpRequestPacket httpRequest) { + return httpRequest.getMethod().getMethodString(); + } + + @Override + protected String requestHeader(HttpRequestPacket httpRequestPacket, String name) { + return httpRequestPacket.getHeader(name); + } + + @Override + protected int responseStatus(HttpResponsePacket httpResponsePacket) { + return httpResponsePacket.getStatus(); + } + + @Override + protected void attachServerContext(Context context, FilterChainContext filterChainContext) { + filterChainContext.getAttributes().setAttribute(CONTEXT_ATTRIBUTE, context); + } + + @Override + public Context getServerContext(FilterChainContext filterChainContext) { + Object attribute = filterChainContext.getAttributes().getAttribute(CONTEXT_ATTRIBUTE); + return attribute instanceof Context ? (Context) attribute : null; + } + + @Override + protected String url(HttpRequestPacket httpRequest) { + try { + return new URI( + (httpRequest.isSecure() ? "https://" : "http://") + + httpRequest.serverName() + + ":" + + httpRequest.getServerPort() + + httpRequest.getRequestURI() + + (httpRequest.getQueryString() != null + ? "?" + httpRequest.getQueryString() + : "")) + .toString(); + } catch (URISyntaxException e) { + log.warn("Failed to construct request URI", e); + return null; + } + } + + @Override + protected String peerHostIP(HttpRequestPacket httpRequest) { + return httpRequest.getRemoteAddress(); + } + + @Override + protected String flavor(HttpRequestPacket connection, HttpRequestPacket request) { + return connection.getProtocolString(); + } + + @Override + protected TextMapGetter getGetter() { + return ExtractAdapter.GETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.grizzly-2.0"; + } + + @Override + protected Integer peerPort(HttpRequestPacket httpRequest) { + return httpRequest.getRemotePort(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyInstrumentationModule.java new file mode 100644 index 000000000..11e4dcc18 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyInstrumentationModule.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grizzly; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class GrizzlyInstrumentationModule extends InstrumentationModule { + public GrizzlyInstrumentationModule() { + super("grizzly", "grizzly-2.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new DefaultFilterChainInstrumentation(), + new FilterInstrumentation(), + new HttpCodecFilterInstrumentation(), + new HttpServerFilterInstrumentation(), + new HttpHandlerInstrumentation()); + } + + @Override + protected boolean defaultEnabled() { + return false; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpCodecFilterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpCodecFilterInstrumentation.java new file mode 100644 index 000000000..0e8501558 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpCodecFilterInstrumentation.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grizzly; + +import static io.opentelemetry.javaagent.instrumentation.grizzly.GrizzlyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.glassfish.grizzly.filterchain.FilterChainContext; +import org.glassfish.grizzly.http.HttpHeader; +import org.glassfish.grizzly.http.HttpPacketParsing; +import org.glassfish.grizzly.http.HttpRequestPacket; + +public class HttpCodecFilterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.glassfish.grizzly.http.HttpCodecFilter"); + } + + @Override + public void transform(TypeTransformer transformer) { + // this is for 2.3.20+ + transformer.applyAdviceToMethod( + named("handleRead") + .and(takesArgument(0, named("org.glassfish.grizzly.filterchain.FilterChainContext"))) + .and(takesArgument(1, named("org.glassfish.grizzly.http.HttpHeader"))) + .and(isPublic()), + HttpCodecFilterInstrumentation.class.getName() + "$HandleReadAdvice"); + // this is for 2.3 through 2.3.19 + transformer.applyAdviceToMethod( + named("handleRead") + .and(takesArgument(0, named("org.glassfish.grizzly.filterchain.FilterChainContext"))) + .and(takesArgument(1, named("org.glassfish.grizzly.http.HttpPacketParsing"))) + .and(isPublic()), + HttpCodecFilterInstrumentation.class.getName() + "$HandleReadOldAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleReadAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Origin Method method, + @Advice.Argument(0) FilterChainContext ctx, + @Advice.Argument(1) HttpHeader httpHeader) { + Context context = tracer().getServerContext(ctx); + + // only create a span if there isn't another one attached to the current ctx + // and if the httpHeader has been parsed into a HttpRequestPacket + if (context != null || !(httpHeader instanceof HttpRequestPacket)) { + return; + } + HttpRequestPacket httpRequest = (HttpRequestPacket) httpHeader; + + // We don't want to attach the new context to this thread, as actual request will continue on + // some other thread where we will read and attach it via tracer().getServerContext(ctx). + tracer().startSpan(httpRequest, httpRequest, ctx, method); + } + } + + @SuppressWarnings("unused") + public static class HandleReadOldAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Origin Method method, + @Advice.Argument(0) FilterChainContext ctx, + @Advice.Argument(1) HttpPacketParsing httpHeader) { + Context context = tracer().getServerContext(ctx); + + // only create a span if there isn't another one attached to the current ctx + // and if the httpHeader has been parsed into a HttpRequestPacket + if (context != null || !(httpHeader instanceof HttpRequestPacket)) { + return; + } + HttpRequestPacket httpRequest = (HttpRequestPacket) httpHeader; + + // We don't want to attach the new context to this thread, as actual request will continue on + // some other thread where we will read and attach it via tracer().getServerContext(ctx). + tracer().startSpan(httpRequest, httpRequest, ctx, method); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpHandlerInstrumentation.java new file mode 100644 index 000000000..29abe48b0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpHandlerInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grizzly; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.grizzly.GrizzlyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HttpHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.glassfish.grizzly.http.server.HttpHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("org.glassfish.grizzly.http.server.HttpHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("service")) + .and(takesArgument(0, named("org.glassfish.grizzly.http.server.Request"))) + .and(takesArgument(1, named("org.glassfish.grizzly.http.server.Response"))), + HttpHandlerInstrumentation.class.getName() + "$ServiceAdvice"); + } + + @SuppressWarnings("unused") + public static class ServiceAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Thrown Throwable throwable) { + if (throwable != null) { + tracer().onException(Java8BytecodeBridge.currentContext(), throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpServerFilterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpServerFilterInstrumentation.java new file mode 100644 index 000000000..514059f95 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpServerFilterInstrumentation.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grizzly; + +import static io.opentelemetry.javaagent.instrumentation.grizzly.GrizzlyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPrivate; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.glassfish.grizzly.filterchain.FilterChainContext; +import org.glassfish.grizzly.http.HttpResponsePacket; + +public class HttpServerFilterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.glassfish.grizzly.http.HttpServerFilter"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("prepareResponse") + .and(takesArgument(0, named("org.glassfish.grizzly.filterchain.FilterChainContext"))) + .and(takesArgument(1, named("org.glassfish.grizzly.http.HttpRequestPacket"))) + .and(takesArgument(2, named("org.glassfish.grizzly.http.HttpResponsePacket"))) + .and(takesArgument(3, named("org.glassfish.grizzly.http.HttpContent"))) + .and(isPrivate()), + HttpServerFilterInstrumentation.class.getName() + "$PrepareResponseAdvice"); + } + + @SuppressWarnings("unused") + public static class PrepareResponseAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) FilterChainContext ctx, + @Advice.Argument(2) HttpResponsePacket response) { + Context context = tracer().getServerContext(ctx); + if (context != null) { + tracer().end(context, response); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyAsyncTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyAsyncTest.groovy new file mode 100644 index 000000000..ec45ec2cd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyAsyncTest.groovy @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.QueryParam +import javax.ws.rs.container.AsyncResponse +import javax.ws.rs.container.Suspended +import javax.ws.rs.core.Response +import org.glassfish.grizzly.http.server.HttpServer +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory +import org.glassfish.jersey.server.ResourceConfig + +class GrizzlyAsyncTest extends GrizzlyTest { + + @Override + HttpServer startServer(int port) { + ResourceConfig rc = new ResourceConfig() + rc.register(SimpleExceptionMapper) + rc.register(AsyncServiceResource) + GrizzlyHttpServerFactory.createHttpServer(new URI("http://localhost:$port"), rc) + } + + @Override + boolean testException() { + // justification: exception is handled by jersey + false + } + + @Path("/") + static class AsyncServiceResource { + private ExecutorService executor = Executors.newSingleThreadExecutor() + + @GET + @Path("success") + void success(@Suspended AsyncResponse ar) { + executor.execute { + controller(SUCCESS) { + ar.resume(Response.status(SUCCESS.status).entity(SUCCESS.body).build()) + } + } + } + + @GET + @Path("query") + Response query_param(@QueryParam("some") String param, @Suspended AsyncResponse ar) { + controller(QUERY_PARAM) { + ar.resume(Response.status(QUERY_PARAM.status).entity("some=$param".toString()).build()) + } + } + + @GET + @Path("redirect") + void redirect(@Suspended AsyncResponse ar) { + executor.execute { + controller(REDIRECT) { + ar.resume(Response.status(REDIRECT.status).location(new URI(REDIRECT.body)).build()) + } + } + } + + @GET + @Path("error-status") + void error(@Suspended AsyncResponse ar) { + executor.execute { + controller(ERROR) { + ar.resume(Response.status(ERROR.status).entity(ERROR.body).build()) + } + } + } + + @GET + @Path("exception") + void exception(@Suspended AsyncResponse ar) { + executor.execute { + try { + controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } catch (Exception e) { + ar.resume(e) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyFilterchainServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyFilterchainServerTest.groovy new file mode 100644 index 000000000..6fe984c83 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyFilterchainServerTest.groovy @@ -0,0 +1,205 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static java.lang.String.valueOf +import static java.nio.charset.Charset.defaultCharset +import static java.util.concurrent.TimeUnit.MILLISECONDS +import static org.glassfish.grizzly.memory.Buffers.wrap + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import java.util.concurrent.Executors +import org.glassfish.grizzly.filterchain.BaseFilter +import org.glassfish.grizzly.filterchain.FilterChain +import org.glassfish.grizzly.filterchain.FilterChainBuilder +import org.glassfish.grizzly.filterchain.FilterChainContext +import org.glassfish.grizzly.filterchain.NextAction +import org.glassfish.grizzly.filterchain.TransportFilter +import org.glassfish.grizzly.http.HttpContent +import org.glassfish.grizzly.http.HttpHeader +import org.glassfish.grizzly.http.HttpRequestPacket +import org.glassfish.grizzly.http.HttpResponsePacket +import org.glassfish.grizzly.http.HttpServerFilter +import org.glassfish.grizzly.http.server.HttpServer +import org.glassfish.grizzly.nio.transport.TCPNIOServerConnection +import org.glassfish.grizzly.nio.transport.TCPNIOTransport +import org.glassfish.grizzly.nio.transport.TCPNIOTransportBuilder +import org.glassfish.grizzly.utils.DelayedExecutor +import org.glassfish.grizzly.utils.IdleTimeoutFilter + +class GrizzlyFilterchainServerTest extends HttpServerTest implements AgentTestTrait { + + private TCPNIOTransport transport + private TCPNIOServerConnection serverConnection + + @Override + HttpServer startServer(int port) { + FilterChain filterChain = setUpFilterChain() + setUpTransport(filterChain) + + serverConnection = transport.bind("127.0.0.1", port) + transport.start() + return null + } + + @Override + void stopServer(HttpServer httpServer) { + transport.shutdownNow() + } + + @Override + boolean testException() { + // justification: grizzly async closes the channel which + // looks like a ConnectException to the client when this happens + false + } + + void setUpTransport(FilterChain filterChain) { + TCPNIOTransportBuilder transportBuilder = TCPNIOTransportBuilder.newInstance() + .setOptimizedForMultiplexing(true) + + transportBuilder.setTcpNoDelay(true) + transportBuilder.setKeepAlive(false) + transportBuilder.setReuseAddress(true) + transportBuilder.setServerConnectionBackLog(50) + transportBuilder.setServerSocketSoTimeout(80000) + + transport = transportBuilder.build() + transport.setProcessor(filterChain) + } + + FilterChain setUpFilterChain() { + return FilterChainBuilder.stateless() + .add(createTransportFilter()) + .add(createIdleTimeoutFilter()) + .add(new HttpServerFilter()) + .add(new LastFilter()) + .build() + } + + TransportFilter createTransportFilter() { + return new TransportFilter() + } + + IdleTimeoutFilter createIdleTimeoutFilter() { + return new IdleTimeoutFilter(new DelayedExecutor(Executors.newCachedThreadPool()), 80000, MILLISECONDS) + } + + static class LastFilter extends BaseFilter { + + @Override + NextAction handleRead(final FilterChainContext ctx) throws IOException { + if (ctx.getMessage() instanceof HttpContent) { + HttpContent httpContent = ctx.getMessage() + HttpHeader httpHeader = httpContent.getHttpHeader() + if (httpHeader instanceof HttpRequestPacket) { + HttpRequestPacket request = (HttpRequestPacket) httpContent.getHttpHeader() + ResponseParameters responseParameters = buildResponse(request) + HttpResponsePacket.Builder builder = HttpResponsePacket.builder(request) + .status(responseParameters.getStatus()) + .header("Content-Length", valueOf(responseParameters.getResponseBody().length)) + responseParameters.fillHeaders(builder) + HttpResponsePacket responsePacket = builder.build() + controller(responseParameters.getEndpoint()) { + ctx.write(HttpContent.builder(responsePacket) + .content(wrap(ctx.getMemoryManager(), responseParameters.getResponseBody())) + .build()) + } + } + } + return ctx.getStopAction() + } + + ResponseParameters buildResponse(HttpRequestPacket request) { + String uri = request.getRequestURI() + String requestParams = request.getQueryString() + String fullPath = uri + (requestParams != null ? "?" + requestParams : "") + + Map headers = new HashMap<>() + + HttpServerTest.ServerEndpoint endpoint + switch (fullPath) { + case "/success": + endpoint = SUCCESS + break + case "/redirect": + endpoint = REDIRECT + headers.put("location", REDIRECT.body) + break + case "/error-status": + endpoint = ERROR + break + case "/exception": + throw new Exception(EXCEPTION.body) + case "/query?some=query": + endpoint = QUERY_PARAM + break + case "/path/123/param": + endpoint = PATH_PARAM + break + case "/authRequired": + endpoint = AUTH_REQUIRED + break + default: + endpoint = NOT_FOUND + break + } + + int status = endpoint.status + String responseBody = endpoint == REDIRECT ? "" : endpoint.body + + byte[] responseBodyBytes = responseBody.getBytes(defaultCharset()) + return new ResponseParameters(endpoint, status, responseBodyBytes, headers) + } + + static class ResponseParameters { + Map headers + HttpServerTest.ServerEndpoint endpoint + int status + byte[] responseBody + + ResponseParameters(HttpServerTest.ServerEndpoint endpoint, + int status, + byte[] responseBody, + Map headers) { + this.endpoint = endpoint + this.status = status + this.responseBody = responseBody + this.headers = headers + } + + int getStatus() { + return status + } + + byte[] getResponseBody() { + return responseBody + } + + HttpServerTest.ServerEndpoint getEndpoint() { + return endpoint + } + + void fillHeaders(HttpResponsePacket.Builder builder) { + for (Map.Entry header : headers.entrySet()) { + builder.header(header.getKey(), header.getValue()) + } + } + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "HttpCodecFilter.handleRead" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyIOStrategyTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyIOStrategyTest.groovy new file mode 100644 index 000000000..a11275b18 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyIOStrategyTest.groovy @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.glassfish.grizzly.IOStrategy +import org.glassfish.grizzly.http.server.HttpServer +import org.glassfish.grizzly.strategies.LeaderFollowerNIOStrategy +import org.glassfish.grizzly.strategies.SameThreadIOStrategy +import org.glassfish.grizzly.strategies.SimpleDynamicNIOStrategy +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory +import org.glassfish.jersey.server.ResourceConfig + +abstract class GrizzlyIOStrategyTest extends GrizzlyTest { + @Override + HttpServer startServer(int port) { + ResourceConfig rc = new ResourceConfig() + rc.register(SimpleExceptionMapper) + rc.register(ServiceResource) + + def server = GrizzlyHttpServerFactory.createHttpServer(new URI("http://localhost:$port"), rc, false) + // Default in NIOTransportBuilder is WorkerThreadIOStrategy, so don't need to retest that. + server.getListener("grizzly").getTransport().setIOStrategy(strategy()) + // jersey doesn't propagate exceptions up to the grizzly handler + // so we use a standalone HttpHandler to test exception capture + server.getServerConfiguration().addHttpHandler(new ExceptionHttpHandler(), "/exception") + server.start() + + return server + } + + abstract IOStrategy strategy() +} + +class LeaderFollowerTest extends GrizzlyIOStrategyTest { + + @Override + IOStrategy strategy() { + return LeaderFollowerNIOStrategy.instance + } +} + +class SameThreadTest extends GrizzlyIOStrategyTest { + + @Override + IOStrategy strategy() { + return SameThreadIOStrategy.instance + } +} + +class SimpleDynamicTest extends GrizzlyIOStrategyTest { + + @Override + IOStrategy strategy() { + return SimpleDynamicNIOStrategy.instance + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyTest.groovy new file mode 100644 index 000000000..0c144e428 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grizzly-2.0/javaagent/src/test/groovy/GrizzlyTest.groovy @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import javax.ws.rs.GET +import javax.ws.rs.NotFoundException +import javax.ws.rs.Path +import javax.ws.rs.QueryParam +import javax.ws.rs.core.Response +import javax.ws.rs.ext.ExceptionMapper +import org.glassfish.grizzly.http.server.HttpHandler +import org.glassfish.grizzly.http.server.HttpServer +import org.glassfish.grizzly.http.server.Request +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory +import org.glassfish.jersey.server.ResourceConfig + +class GrizzlyTest extends HttpServerTest implements AgentTestTrait { + + @Override + HttpServer startServer(int port) { + ResourceConfig rc = new ResourceConfig() + rc.register(SimpleExceptionMapper) + rc.register(ServiceResource) + + def server = GrizzlyHttpServerFactory.createHttpServer(new URI("http://localhost:$port"), rc, false) + // jersey doesn't propagate exceptions up to the grizzly handler + // so we use a standalone HttpHandler to test exception capture + server.getServerConfiguration().addHttpHandler(new ExceptionHttpHandler(), "/exception") + server.start() + + return server + } + + @Override + void stopServer(HttpServer server) { + server.stop() + } + + static class SimpleExceptionMapper implements ExceptionMapper { + + @Override + Response toResponse(Throwable exception) { + if (exception instanceof NotFoundException) { + return exception.getResponse() + } + Response.status(500).entity(exception.message).build() + } + } + + @Path("/") + static class ServiceResource { + + @GET + @Path("success") + Response success() { + controller(SUCCESS) { + Response.status(SUCCESS.status).entity(SUCCESS.body).build() + } + } + + @GET + @Path("query") + Response query_param(@QueryParam("some") String param) { + controller(QUERY_PARAM) { + Response.status(QUERY_PARAM.status).entity("some=$param".toString()).build() + } + } + + @GET + @Path("redirect") + Response redirect() { + controller(REDIRECT) { + Response.status(REDIRECT.status).location(new URI(REDIRECT.body)).build() + } + } + + @GET + @Path("error-status") + Response error() { + controller(ERROR) { + Response.status(ERROR.status).entity(ERROR.body).build() + } + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "HttpCodecFilter.handleRead" + } + + static class ExceptionHttpHandler extends HttpHandler { + + @Override + void service(Request request, org.glassfish.grizzly.http.server.Response response) throws Exception { + controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/grpc-1.6-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/grpc-1.6-javaagent.gradle new file mode 100644 index 000000000..5c048a2f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/grpc-1.6-javaagent.gradle @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.grpc" + module = "grpc-core" + versions = "[1.6.0,)" + assertInverse = true + } +} + +def grpcVersion = '1.6.0' + +dependencies { + implementation project(':instrumentation:grpc-1.6:library') + + library "io.grpc:grpc-core:${grpcVersion}" + + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + + testLibrary "io.grpc:grpc-netty:${grpcVersion}" + testLibrary "io.grpc:grpc-protobuf:${grpcVersion}" + testLibrary "io.grpc:grpc-services:${grpcVersion}" + testLibrary "io.grpc:grpc-stub:${grpcVersion}" + + testImplementation project(':instrumentation:grpc-1.6:testing') +} + +test { + // The agent context debug mechanism isn't compatible with the bridge approach which may add a + // gRPC context to the root. + jvmArgs "-Dotel.javaagent.experimental.thread-propagation-debugger.enabled=false" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java new file mode 100644 index 000000000..64f0cfa2d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grpc.v1_6; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.declaresField; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.grpc.ClientInterceptor; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class GrpcClientBuilderBuildInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.grpc.ManagedChannelBuilder"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("io.grpc.ManagedChannelBuilder")) + .and(declaresField(named("interceptors"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("build")), + GrpcClientBuilderBuildInstrumentation.class.getName() + "$AddInterceptorAdvice"); + } + + @SuppressWarnings("unused") + public static class AddInterceptorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void addInterceptor( + @Advice.FieldValue("interceptors") List interceptors) { + interceptors.add(0, GrpcSingletons.CLIENT_INTERCEPTOR); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcContextInstrumentation.java new file mode 100644 index 000000000..aeca7598e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcContextInstrumentation.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grpc.v1_6; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.grpc.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class GrpcContextInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.grpc.Context"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isStatic()) + .and(named("storage")) + .and(returns(named("io.grpc.Context$Storage"))), + GrpcContextInstrumentation.class.getName() + "$ContextBridgeAdvice"); + } + + @SuppressWarnings("unused") + public static class ContextBridgeAdvice { + + @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) + public static Object onEnter() { + return null; + } + + @Advice.OnMethodExit + public static void onExit(@Advice.Return(readOnly = false) Context.Storage storage) { + storage = GrpcSingletons.STORAGE; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcInstrumentationModule.java new file mode 100644 index 000000000..381e0a810 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grpc.v1_6; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class GrpcInstrumentationModule extends InstrumentationModule { + public GrpcInstrumentationModule() { + super("grpc", "grpc-1.5"); + } + + @Override + public List typeInstrumentations() { + return asList( + new GrpcClientBuilderBuildInstrumentation(), + new GrpcContextInstrumentation(), + new GrpcServerBuilderInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java new file mode 100644 index 000000000..c353eaca0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grpc.v1_6; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.grpc.ServerBuilder; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class GrpcServerBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.grpc.ServerBuilder"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("io.grpc.ServerBuilder")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")).and(takesArguments(0)), + GrpcServerBuilderInstrumentation.class.getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This ServerBuilder serverBuilder) { + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(ServerBuilder.class); + if (callDepth == 0) { + serverBuilder.intercept(GrpcSingletons.SERVER_INTERCEPTOR); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.This ServerBuilder serverBuilder) { + CallDepthThreadLocalMap.decrementCallDepth(ServerBuilder.class); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java new file mode 100644 index 000000000..64d070578 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grpc.v1_6; + +import io.grpc.ClientInterceptor; +import io.grpc.Context; +import io.grpc.ServerInterceptor; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTracing; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.ContextStorageBridge; + +// Holds singleton references. +public final class GrpcSingletons { + private static final GrpcTracing TRACING = + GrpcTracing.newBuilder(GlobalOpenTelemetry.get()) + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBoolean( + "otel.instrumentation.grpc.experimental-span-attributes", false)) + .build(); + + public static final ClientInterceptor CLIENT_INTERCEPTOR = TRACING.newClientInterceptor(); + + public static final ServerInterceptor SERVER_INTERCEPTOR = TRACING.newServerInterceptor(); + + public static final Context.Storage STORAGE = new ContextStorageBridge(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcStreamingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcStreamingTest.groovy new file mode 100644 index 000000000..82133c6ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcStreamingTest.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grpc.v1_6 + +import io.grpc.ManagedChannelBuilder +import io.grpc.ServerBuilder +import io.opentelemetry.instrumentation.grpc.v1_6.AbstractGrpcStreamingTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class GrpcStreamingTest extends AbstractGrpcStreamingTest implements AgentTestTrait { + @Override + ServerBuilder configureServer(ServerBuilder server) { + return server + } + + @Override + ManagedChannelBuilder configureClient(ManagedChannelBuilder client) { + return client + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcTest.groovy new file mode 100644 index 000000000..c3ce41401 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcTest.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.grpc.v1_6 + +import io.grpc.ManagedChannelBuilder +import io.grpc.ServerBuilder +import io.opentelemetry.instrumentation.grpc.v1_6.AbstractGrpcTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class GrpcTest extends AbstractGrpcTest implements AgentTestTrait { + @Override + ServerBuilder configureServer(ServerBuilder server) { + return server + } + + @Override + ManagedChannelBuilder configureClient(ManagedChannelBuilder client) { + return client + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/grpc-1.6-library.gradle b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/grpc-1.6-library.gradle new file mode 100644 index 000000000..f58291f99 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/grpc-1.6-library.gradle @@ -0,0 +1,15 @@ +apply plugin: "otel.library-instrumentation" + +def grpcVersion = '1.6.0' + +dependencies { + library "io.grpc:grpc-core:${grpcVersion}" + + testLibrary "io.grpc:grpc-netty:${grpcVersion}" + testLibrary "io.grpc:grpc-protobuf:${grpcVersion}" + testLibrary "io.grpc:grpc-services:${grpcVersion}" + testLibrary "io.grpc:grpc-stub:${grpcVersion}" + + testImplementation "org.assertj:assertj-core" + testImplementation project(':instrumentation:grpc-1.6:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/grpc/override/ContextStorageOverride.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/grpc/override/ContextStorageOverride.java new file mode 100644 index 000000000..ac20ff025 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/grpc/override/ContextStorageOverride.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.grpc.override; + +import io.grpc.Context; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.ContextStorageBridge; + +/** + * Override class for gRPC to pick up this class to replace the default {@link Context.Storage} with + * an OpenTelemetry bridge. + */ +public final class ContextStorageOverride extends Context.Storage { + + private static final Context.Storage delegate = new ContextStorageBridge(); + + @Override + public Context doAttach(Context toAttach) { + return delegate.doAttach(toAttach); + } + + @Override + public void detach(Context toDetach, Context toRestore) { + delegate.detach(toDetach, toRestore); + } + + @Override + public Context current() { + return delegate.current(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcAttributesExtractor.java new file mode 100644 index 000000000..9372a9fc7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcAttributesExtractor.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Status; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class GrpcAttributesExtractor extends AttributesExtractor { + @Override + protected void onStart(AttributesBuilder attributes, GrpcRequest grpcRequest) { + // No request attributes + } + + @Override + protected void onEnd(AttributesBuilder attributes, GrpcRequest request, @Nullable Status status) { + if (status != null) { + attributes.put(SemanticAttributes.RPC_GRPC_STATUS_CODE, status.getCode().value()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcExtractAdapter.java new file mode 100644 index 000000000..03ece6225 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcExtractAdapter.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Metadata; +import io.opentelemetry.context.propagation.TextMapGetter; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class GrpcExtractAdapter implements TextMapGetter { + + static final GrpcExtractAdapter GETTER = new GrpcExtractAdapter(); + + @Override + public Iterable keys(GrpcRequest request) { + return request.getMetadata().keys(); + } + + @Override + @Nullable + public String get(@Nullable GrpcRequest request, String key) { + if (request == null) { + return null; + } + return request.getMetadata().get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcHelper.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcHelper.java new file mode 100644 index 000000000..789d9f1bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcHelper.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.opentelemetry.api.common.AttributeKey; + +final class GrpcHelper { + + static final AttributeKey MESSAGE_TYPE = AttributeKey.stringKey("message.type"); + static final AttributeKey MESSAGE_ID = AttributeKey.longKey("message.id"); + + private GrpcHelper() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcInjectAdapter.java new file mode 100644 index 000000000..88aaf83f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Metadata; +import io.opentelemetry.context.propagation.TextMapSetter; + +final class GrpcInjectAdapter implements TextMapSetter { + + static final GrpcInjectAdapter SETTER = new GrpcInjectAdapter(); + + @Override + public void set(Metadata carrier, String key, String value) { + carrier.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetAttributesExtractor.java new file mode 100644 index 000000000..c0d66b4bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetAttributesExtractor.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Status; +import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class GrpcNetAttributesExtractor + extends InetSocketAddressNetAttributesExtractor { + @Override + @Nullable + public InetSocketAddress getAddress(GrpcRequest request, @Nullable Status status) { + SocketAddress address = request.getRemoteAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } + + @Override + public String transport(GrpcRequest request) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java new file mode 100644 index 000000000..32bb18157 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import java.net.SocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class GrpcRequest { + + private final MethodDescriptor method; + @Nullable private final Metadata metadata; + + @Nullable private volatile SocketAddress remoteAddress; + + GrpcRequest( + MethodDescriptor method, + @Nullable Metadata metadata, + @Nullable SocketAddress remoteAddress) { + this.method = method; + this.metadata = metadata; + this.remoteAddress = remoteAddress; + } + + MethodDescriptor getMethod() { + return method; + } + + Metadata getMetadata() { + return metadata; + } + + SocketAddress getRemoteAddress() { + return remoteAddress; + } + + void setRemoteAddress(SocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesExtractor.java new file mode 100644 index 000000000..3388fdf76 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesExtractor.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Status; +import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class GrpcRpcAttributesExtractor extends RpcAttributesExtractor { + @Override + protected String system(GrpcRequest request) { + return "grpc"; + } + + @Override + @Nullable + protected String service(GrpcRequest request) { + String fullMethodName = request.getMethod().getFullMethodName(); + int slashIndex = fullMethodName.lastIndexOf('/'); + if (slashIndex == -1) { + return null; + } + return fullMethodName.substring(0, slashIndex); + } + + @Override + @Nullable + protected String method(GrpcRequest request) { + String fullMethodName = request.getMethod().getFullMethodName(); + int slashIndex = fullMethodName.lastIndexOf('/'); + if (slashIndex == -1) { + return null; + } + return fullMethodName.substring(slashIndex + 1); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanNameExtractor.java new file mode 100644 index 000000000..35a08ba0f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanNameExtractor.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +// Small optimization to avoid RpcSpanNameExtractor because gRPC provides the span name directly. +final class GrpcSpanNameExtractor implements SpanNameExtractor { + @Override + public String extract(GrpcRequest request) { + return request.getMethod().getFullMethodName(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanStatusExtractor.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanStatusExtractor.java new file mode 100644 index 000000000..4ec6ddc11 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanStatusExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class GrpcSpanStatusExtractor implements SpanStatusExtractor { + @Override + public StatusCode extract(GrpcRequest request, Status status, @Nullable Throwable error, SpanKind spanKind) { + if (status == null) { + if (error instanceof StatusRuntimeException) { + status = ((StatusRuntimeException) error).getStatus(); + } else if (error instanceof StatusException) { + status = ((StatusException) error).getStatus(); + } + } + if (status != null) { + if (status.isOk()) { + return StatusCode.UNSET; + } + return StatusCode.ERROR; + } + return SpanStatusExtractor.getDefault().extract(request, status, error, spanKind); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTracing.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTracing.java new file mode 100644 index 000000000..2ed8cf35b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTracing.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.ClientInterceptor; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +/** Entrypoint for tracing gRPC servers or clients. */ +public final class GrpcTracing { + + /** Returns a new {@link GrpcTracing} configured with the given {@link OpenTelemetry}. */ + public static GrpcTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + /** Returns a new {@link GrpcTracingBuilder} configured with the given {@link OpenTelemetry}. */ + public static GrpcTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new GrpcTracingBuilder(openTelemetry); + } + + private final Instrumenter serverInstrumenter; + private final Instrumenter clientInstrumenter; + private final ContextPropagators propagators; + private final boolean captureExperimentalSpanAttributes; + + GrpcTracing( + Instrumenter serverInstrumenter, + Instrumenter clientInstrumenter, + ContextPropagators propagators, + boolean captureExperimentalSpanAttributes) { + this.serverInstrumenter = serverInstrumenter; + this.clientInstrumenter = clientInstrumenter; + this.propagators = propagators; + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + /** + * Returns a new {@link ClientInterceptor} for use with methods like {@link + * io.grpc.ManagedChannelBuilder#intercept(ClientInterceptor...)}. + */ + public ClientInterceptor newClientInterceptor() { + return new TracingClientInterceptor(clientInstrumenter, propagators); + } + + /** + * Returns a new {@link ServerInterceptor} for use with methods like {@link + * io.grpc.ServerBuilder#intercept(ServerInterceptor)}. + */ + public ServerInterceptor newServerInterceptor() { + return new TracingServerInterceptor(serverInstrumenter, captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTracingBuilder.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTracingBuilder.java new file mode 100644 index 000000000..98f5b339a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTracingBuilder.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Status; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import java.util.ArrayList; +import java.util.List; + +/** A builder of {@link GrpcTracing}. */ +public final class GrpcTracingBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.grpc-1.6"; + + private final OpenTelemetry openTelemetry; + + private final List> + additionalExtractors = new ArrayList<>(); + + private boolean captureExperimentalSpanAttributes; + + GrpcTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. The {@link AttributesExtractor} will be executed after all default extractors. + */ + public GrpcTracingBuilder addAttributeExtractor( + AttributesExtractor attributesExtractor) { + additionalExtractors.add(attributesExtractor); + return this; + } + + /** + * Sets whether experimental attributes should be set to spans. These attributes may be changed or + * removed in the future, so only enable this if you know you do not require attributes filled by + * this instrumentation to be stable across versions + */ + public GrpcTracingBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + /** Returns a new {@link GrpcTracing} with the settings of this {@link GrpcTracingBuilder}. */ + public GrpcTracing build() { + InstrumenterBuilder instrumenterBuilder = + Instrumenter.newBuilder(openTelemetry, INSTRUMENTATION_NAME, new GrpcSpanNameExtractor()); + instrumenterBuilder + .setSpanStatusExtractor(new GrpcSpanStatusExtractor()) + .addAttributesExtractors( + new GrpcNetAttributesExtractor(), + new GrpcRpcAttributesExtractor(), + new GrpcAttributesExtractor()) + .addAttributesExtractors(additionalExtractors); + return new GrpcTracing( + instrumenterBuilder.newServerInstrumenter(GrpcExtractAdapter.GETTER), + // gRPC client interceptors require two phases, one to set up request and one to execute. + // So we go ahead and inject manually in this instrumentation. + instrumenterBuilder.newInstrumenter(SpanKindExtractor.alwaysClient()), + openTelemetry.getPropagators(), + captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java new file mode 100644 index 000000000..71af9f89b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import static io.opentelemetry.instrumentation.grpc.v1_6.GrpcInjectAdapter.SETTER; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientCall.Listener; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +final class TracingClientInterceptor implements ClientInterceptor { + + @SuppressWarnings("rawtypes") + private static final AtomicLongFieldUpdater MESSAGE_ID_UPDATER = + AtomicLongFieldUpdater.newUpdater(TracingClientCallListener.class, "messageId"); + + private final Instrumenter instrumenter; + private final ContextPropagators propagators; + + TracingClientInterceptor( + Instrumenter instrumenter, ContextPropagators propagators) { + this.instrumenter = instrumenter; + this.propagators = propagators; + } + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + GrpcRequest request = new GrpcRequest(method, null, null); + Context context = instrumenter.start(Context.current(), request); + final ClientCall result; + try (Scope ignored = context.makeCurrent()) { + try { + // call other interceptors + result = next.newCall(method, callOptions); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + + SocketAddress address = result.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + request.setRemoteAddress(address); + + return new TracingClientCall<>(result, context, request); + } + + final class TracingClientCall + extends ForwardingClientCall.SimpleForwardingClientCall { + + private final Context context; + private final GrpcRequest request; + + TracingClientCall( + ClientCall delegate, Context context, GrpcRequest request) { + super(delegate); + this.context = context; + this.request = request; + } + + @Override + public void start(Listener responseListener, Metadata headers) { + propagators.getTextMapPropagator().inject(context, headers, SETTER); + try (Scope ignored = context.makeCurrent()) { + super.start(new TracingClientCallListener<>(responseListener, context, request), headers); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + + @Override + public void sendMessage(REQUEST message) { + try (Scope ignored = context.makeCurrent()) { + super.sendMessage(message); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + } + + final class TracingClientCallListener + extends ForwardingClientCallListener.SimpleForwardingClientCallListener { + + private final Context context; + private final GrpcRequest request; + + // Used by MESSAGE_ID_UPDATER + @SuppressWarnings("UnusedVariable") + volatile long messageId; + + TracingClientCallListener(Listener delegate, Context context, GrpcRequest request) { + super(delegate); + this.context = context; + this.request = request; + } + + @Override + public void onMessage(RESPONSE message) { +// Span span = Span.fromContext(context); +// Attributes attributes = +// Attributes.of( +// GrpcHelper.MESSAGE_TYPE, +// "SENT", +// GrpcHelper.MESSAGE_ID, +// MESSAGE_ID_UPDATER.incrementAndGet(this)); +// span.addEvent("message", attributes); + try (Scope ignored = context.makeCurrent()) { + delegate().onMessage(message); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + + @Override + public void onClose(Status status, Metadata trailers) { + try (Scope ignored = context.makeCurrent()) { + delegate().onClose(status, trailers); + } catch (Throwable e) { + instrumenter.end(context, request, status, e); + throw e; + } + instrumenter.end(context, request, status, status.getCause()); + } + + @Override + public void onReady() { + try (Scope ignored = context.makeCurrent()) { + delegate().onReady(); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java new file mode 100644 index 000000000..6c0f383c9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java @@ -0,0 +1,163 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Contexts; +import io.grpc.ForwardingServerCall; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +final class TracingServerInterceptor implements ServerInterceptor { + + @SuppressWarnings("rawtypes") + private static final AtomicLongFieldUpdater MESSAGE_ID_UPDATER = + AtomicLongFieldUpdater.newUpdater(TracingServerCallListener.class, "messageId"); + + private final Instrumenter instrumenter; + private final boolean captureExperimentalSpanAttributes; + + TracingServerInterceptor( + Instrumenter instrumenter, boolean captureExperimentalSpanAttributes) { + this.instrumenter = instrumenter; + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next) { + GrpcRequest request = + new GrpcRequest( + call.getMethodDescriptor(), + headers, + call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)); + Context context = instrumenter.start(Context.current(), request); + + try (Scope ignored = context.makeCurrent()) { + return new TracingServerCallListener<>( + Contexts.interceptCall( + io.grpc.Context.current(), + new TracingServerCall<>(call, context, request), + headers, + next), + context, + request); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + + final class TracingServerCall + extends ForwardingServerCall.SimpleForwardingServerCall { + private final Context context; + private final GrpcRequest request; + + TracingServerCall( + ServerCall delegate, Context context, GrpcRequest request) { + super(delegate); + this.context = context; + this.request = request; + } + + @Override + public void close(Status status, Metadata trailers) { + try { + delegate().close(status, trailers); + } catch (Throwable e) { + instrumenter.end(context, request, status, e); + throw e; + } + instrumenter.end(context, request, status, status.getCause()); + } + } + + final class TracingServerCallListener + extends ForwardingServerCallListener.SimpleForwardingServerCallListener { + private final Context context; + private final GrpcRequest request; + + // Used by MESSAGE_ID_UPDATER + @SuppressWarnings("UnusedVariable") + volatile long messageId; + + TracingServerCallListener(Listener delegate, Context context, GrpcRequest request) { + super(delegate); + this.context = context; + this.request = request; + } + + @Override + public void onMessage(REQUEST message) { + // TODO(anuraaga): Restore +// Attributes attributes = +// Attributes.of( +// GrpcHelper.MESSAGE_TYPE, +// "RECEIVED", +// GrpcHelper.MESSAGE_ID, +// MESSAGE_ID_UPDATER.incrementAndGet(this)); +// Span.fromContext(context).addEvent("message", attributes); + delegate().onMessage(message); + } + + @Override + public void onHalfClose() { + try { + delegate().onHalfClose(); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + + @Override + public void onCancel() { + try { + delegate().onCancel(); + if (captureExperimentalSpanAttributes) { + Span.fromContext(context).setAttribute("grpc.canceled", true); + } + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + instrumenter.end(context, request, null, null); + } + + @Override + public void onComplete() { + try { + delegate().onComplete(); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + + @Override + public void onReady() { + try { + delegate().onReady(); + } catch (Throwable e) { + instrumenter.end(context, request, null, e); + throw e; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ContextStorageBridge.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ContextStorageBridge.java new file mode 100644 index 000000000..114a8e62c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ContextStorageBridge.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6.internal; + +import io.grpc.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link Context.Storage} override which uses OpenTelemetry context as the backing store. Both gRPC + * and OpenTelemetry contexts refer to each other to ensure that both OTel context propagation + * mechanisms and gRPC context propagation mechanisms can be used interchangably. + */ +public final class ContextStorageBridge extends Context.Storage { + + private static final Logger logger = Logger.getLogger(ContextStorageBridge.class.getName()); + + private static final ContextKey GRPC_CONTEXT = ContextKey.named("grpc-context"); + private static final Context.Key OTEL_CONTEXT = + Context.key("otel-context"); + private static final Context.Key OTEL_SCOPE = Context.key("otel-scope"); + + @Override + public Context doAttach(Context toAttach) { + io.opentelemetry.context.Context otelContext = io.opentelemetry.context.Context.current(); + Context current = otelContext.get(GRPC_CONTEXT); + + if (current == null) { + current = Context.ROOT; + } + + if (current == toAttach) { + return current.withValue(OTEL_SCOPE, Scope.noop()); + } + + io.opentelemetry.context.Context base = OTEL_CONTEXT.get(toAttach); + final io.opentelemetry.context.Context newOtelContext; + if (base != null) { + // gRPC context which has an OTel context associated with it via a call to + // ContextStorageOverride.current(). Using it as the base allows it to be propagated together + // with the gRPC context. + newOtelContext = base.with(GRPC_CONTEXT, toAttach); + } else { + // gRPC context without an OTel context associated with it. This is only possible when + // attaching a context directly created by Context.ROOT, e.g., Context.ROOT.with(...) which + // is not common. We go ahead and assume the gRPC context can be reset while using the current + // OTel context. + newOtelContext = io.opentelemetry.context.Context.current().with(GRPC_CONTEXT, toAttach); + } + + Scope scope = newOtelContext.makeCurrent(); + return current.withValue(OTEL_SCOPE, scope); + } + + @Override + public void detach(Context toDetach, Context toRestore) { + Scope scope = OTEL_SCOPE.get(toRestore); + if (scope == null) { + logger.log( + Level.SEVERE, + "Detaching context which was not attached.", + new Throwable().fillInStackTrace()); + } else { + scope.close(); + } + } + + @Override + public Context current() { + io.opentelemetry.context.Context otelContext = io.opentelemetry.context.Context.current(); + Context current = otelContext.get(GRPC_CONTEXT); + if (current == null) { + return Context.ROOT.withValue(OTEL_CONTEXT, otelContext); + } + // Store the current OTel context in the gRPC context so that gRPC context propagation + // mechanisms will also propagate the OTel context. + io.opentelemetry.context.Context previousOtelContext = OTEL_CONTEXT.get(current); + if (previousOtelContext != otelContext) { + // This context has already been previously attached and associated with an OTel context. Just + // create a new context referring to the current OTel context to reflect the current stack. + // The previous context is unaffected and will continue to live in its own stack. + return current.withValue(OTEL_CONTEXT, otelContext); + } + return current; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/groovy/io/opentelemetry/instrumentation/grpc/v1_6/GrpcStreamingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/groovy/io/opentelemetry/instrumentation/grpc/v1_6/GrpcStreamingTest.groovy new file mode 100644 index 000000000..f2c23762f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/groovy/io/opentelemetry/instrumentation/grpc/v1_6/GrpcStreamingTest.groovy @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6 + +import io.grpc.ManagedChannelBuilder +import io.grpc.ServerBuilder +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class GrpcStreamingTest extends AbstractGrpcStreamingTest implements LibraryTestTrait { + @Override + ServerBuilder configureServer(ServerBuilder server) { + return server.intercept(GrpcTracing.create(getOpenTelemetry()).newServerInterceptor()) + } + + @Override + ManagedChannelBuilder configureClient(ManagedChannelBuilder client) { + return client.intercept(GrpcTracing.create(getOpenTelemetry()).newClientInterceptor()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/groovy/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/groovy/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.groovy new file mode 100644 index 000000000..5663043b4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/groovy/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.groovy @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6 + +import io.grpc.ManagedChannelBuilder +import io.grpc.ServerBuilder +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class GrpcTest extends AbstractGrpcTest implements LibraryTestTrait { + @Override + ServerBuilder configureServer(ServerBuilder server) { + return server.intercept(GrpcTracing.create(getOpenTelemetry()).newServerInterceptor()) + } + + @Override + ManagedChannelBuilder configureClient(ManagedChannelBuilder client) { + return client.intercept(GrpcTracing.create(getOpenTelemetry()).newClientInterceptor()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/ContextBridgeTest.java b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/ContextBridgeTest.java new file mode 100644 index 000000000..e61776fdb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/ContextBridgeTest.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class ContextBridgeTest { + private static final ContextKey ANIMAL = ContextKey.named("animal"); + + private static final io.grpc.Context.Key FOOD = io.grpc.Context.key("food"); + private static final io.grpc.Context.Key COUNTRY = io.grpc.Context.key("country"); + + private static ExecutorService otherThread; + + @BeforeAll + static void setUp() { + otherThread = Executors.newSingleThreadExecutor(); + } + + @AfterAll + static void tearDown() { + otherThread.shutdown(); + } + + @Test + void grpcOtelMix() { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + assertThat(COUNTRY.get()).isNull(); + io.grpc.Context root = grpcContext.attach(); + try { + assertThat(COUNTRY.get()).isEqualTo("japan"); + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + assertThat(COUNTRY.get()).isEqualTo("japan"); + + io.grpc.Context context2 = io.grpc.Context.current().withValue(FOOD, "cheese"); + assertThat(FOOD.get()).isNull(); + io.grpc.Context toRestore = context2.attach(); + try { + assertThat(FOOD.get()).isEqualTo("cheese"); + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } finally { + context2.detach(toRestore); + } + } + } finally { + grpcContext.detach(root); + } + } + + @Test + void grpcWrap() throws Exception { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + io.grpc.Context root = grpcContext.attach(); + try { + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + AtomicReference grpcValue = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + grpcValue.set(COUNTRY.get()); + otelValue.set(Context.current().get(ANIMAL)); + }; + otherThread.submit(runnable).get(); + assertThat(grpcValue).hasValue(null); + assertThat(otelValue).hasValue(null); + + otherThread.submit(io.grpc.Context.current().wrap(runnable)).get(); + assertThat(grpcValue).hasValue("japan"); + assertThat(otelValue).hasValue("cat"); + } + } finally { + grpcContext.detach(root); + } + } + + @Test + void otelWrap() throws Exception { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + io.grpc.Context root = grpcContext.attach(); + try { + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + AtomicReference grpcValue = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + grpcValue.set(COUNTRY.get()); + otelValue.set(Context.current().get(ANIMAL)); + }; + otherThread.submit(runnable).get(); + assertThat(grpcValue).hasValue(null); + assertThat(otelValue).hasValue(null); + + otherThread.submit(Context.current().wrap(runnable)).get(); + assertThat(grpcValue).hasValue("japan"); + assertThat(otelValue).hasValue("cat"); + } + } finally { + grpcContext.detach(root); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/grpc-1.6-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/grpc-1.6-testing.gradle new file mode 100644 index 000000000..baf4cbc86 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/grpc-1.6-testing.gradle @@ -0,0 +1,48 @@ +plugins { + id "java-library" + id "com.google.protobuf" version "0.8.16" +} + +apply plugin: "otel.java-conventions" + +def grpcVersion = '1.6.0' + +protobuf { + protoc { + // Download compiler rather than using locally installed version: + artifact = 'com.google.protobuf:protoc:3.3.0' + if (osdetector.os == "osx") { + // Always use x86_64 version as ARM binary is not available + artifact += ":osx-x86_64" + } + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + if (osdetector.os == "osx") { + // Always use x86_64 version as ARM binary is not available + artifact += ":osx-x86_64" + } + } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } +} + +dependencies { + api project(':testing-common') + + api "io.grpc:grpc-core:${grpcVersion}" + api "io.grpc:grpc-protobuf:${grpcVersion}" + api "io.grpc:grpc-services:${grpcVersion}" + api "io.grpc:grpc-stub:${grpcVersion}" + + implementation "javax.annotation:javax.annotation-api:1.3.2" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/groovy/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcStreamingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/groovy/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcStreamingTest.groovy new file mode 100644 index 000000000..c3407c138 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/groovy/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcStreamingTest.groovy @@ -0,0 +1,176 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import example.GreeterGrpc +import example.Helloworld +import io.grpc.BindableService +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.grpc.Server +import io.grpc.ServerBuilder +import io.grpc.Status +import io.grpc.stub.StreamObserver +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import spock.lang.Unroll + +@Unroll +abstract class AbstractGrpcStreamingTest extends InstrumentationSpecification { + + abstract ServerBuilder configureServer(ServerBuilder server) + + abstract ManagedChannelBuilder configureClient(ManagedChannelBuilder client) + + def "test conversation #paramName"() { + setup: + def msgCount = serverMessageCount + def serverReceived = new CopyOnWriteArrayList<>() + def clientReceived = new CopyOnWriteArrayList<>() + def error = new AtomicReference() + + BindableService greeter = new GreeterGrpc.GreeterImplBase() { + @Override + StreamObserver conversation(StreamObserver observer) { + return new StreamObserver() { + @Override + void onNext(Helloworld.Response value) { + + serverReceived << value.message + + (1..msgCount).each { + observer.onNext(value) + } + } + + @Override + void onError(Throwable t) { + error.set(t) + observer.onError(t) + } + + @Override + void onCompleted() { + observer.onCompleted() + } + } + } + } + def port = PortUtils.findOpenPort() + Server server = configureServer(ServerBuilder.forPort(port).addService(greeter)).build().start() + ManagedChannelBuilder channelBuilder = configureClient(ManagedChannelBuilder.forAddress("localhost", port)) + + // Depending on the version of gRPC usePlainText may or may not take an argument. + try { + channelBuilder.usePlaintext() + } catch (MissingMethodException e) { + channelBuilder.usePlaintext(true) + } + ManagedChannel channel = channelBuilder.build() + GreeterGrpc.GreeterStub client = GreeterGrpc.newStub(channel).withWaitForReady() + + when: + def observer = client.conversation(new StreamObserver() { + @Override + void onNext(Helloworld.Response value) { + clientReceived << value.message + } + + @Override + void onError(Throwable t) { + error.set(t) + } + + @Override + void onCompleted() { + } + }) + + clientRange.each { + def message = Helloworld.Response.newBuilder().setMessage("call $it").build() + observer.onNext(message) + } + observer.onCompleted() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "example.Greeter/Conversation" + kind CLIENT + hasNoParent() + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "Conversation" + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" Status.OK.code.value() + } + (1..(clientMessageCount * serverMessageCount)).each { + def messageId = it + event(it - 1) { + eventName "message" + attributes { + "message.type" "SENT" + "message.id" messageId + } + } + } + } + span(1) { + name "example.Greeter/Conversation" + kind SERVER + childOf span(0) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "Conversation" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" Status.OK.code.value() + } + clientRange.each { + def messageId = it + event(it - 1) { + eventName "message" + attributes { + "message.type" "RECEIVED" + "message.id" messageId + } + } + } + } + } + } + error.get() == null + serverReceived == clientRange.collect { "call $it" } + clientReceived == serverRange.collect { clientRange.collect { "call $it" } }.flatten().sort() + + cleanup: + channel?.shutdownNow()?.awaitTermination(10, TimeUnit.SECONDS) + server?.shutdownNow()?.awaitTermination() + + where: + paramName | clientMessageCount | serverMessageCount + "A" | 1 | 1 + "B" | 2 | 1 + "C" | 1 | 2 + "D" | 2 | 2 + "E" | 3 | 3 + + clientRange = 1..clientMessageCount + serverRange = 1..serverMessageCount + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/groovy/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.groovy b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/groovy/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.groovy new file mode 100644 index 000000000..b056b6cd6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/groovy/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.groovy @@ -0,0 +1,681 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import example.GreeterGrpc +import example.Helloworld +import io.grpc.BindableService +import io.grpc.CallOptions +import io.grpc.Channel +import io.grpc.ClientCall +import io.grpc.ClientInterceptor +import io.grpc.Context +import io.grpc.Contexts +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.grpc.Metadata +import io.grpc.MethodDescriptor +import io.grpc.Server +import io.grpc.ServerBuilder +import io.grpc.ServerCall +import io.grpc.ServerCallHandler +import io.grpc.ServerInterceptor +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.grpc.protobuf.services.ProtoReflectionService +import io.grpc.reflection.v1alpha.ServerReflectionGrpc +import io.grpc.reflection.v1alpha.ServerReflectionRequest +import io.grpc.reflection.v1alpha.ServerReflectionResponse +import io.grpc.stub.StreamObserver +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import spock.lang.Unroll + +@Unroll +abstract class AbstractGrpcTest extends InstrumentationSpecification { + + abstract ServerBuilder configureServer(ServerBuilder server) + + abstract ManagedChannelBuilder configureClient(ManagedChannelBuilder client) + + def "test request-response #paramName"() { + setup: + BindableService greeter = new GreeterGrpc.GreeterImplBase() { + @Override + void sayHello( + final Helloworld.Request req, final StreamObserver responseObserver) { + final Helloworld.Response reply = Helloworld.Response.newBuilder().setMessage("Hello $req.name").build() + responseObserver.onNext(reply) + responseObserver.onCompleted() + } + } + def port = PortUtils.findOpenPort() + Server server = configureServer(ServerBuilder.forPort(port).addService(greeter)).build().start() + ManagedChannelBuilder channelBuilder = configureClient(ManagedChannelBuilder.forAddress("localhost", port)) + + // Depending on the version of gRPC usePlainText may or may not take an argument. + try { + channelBuilder.usePlaintext() + } catch (MissingMethodException e) { + channelBuilder.usePlaintext(true) + } + ManagedChannel channel = channelBuilder.build() + GreeterGrpc.GreeterBlockingStub client = GreeterGrpc.newBlockingStub(channel) + + when: + def response = runUnderTrace("parent") { + client.sayHello(Helloworld.Request.newBuilder().setName(paramName).build()) + } + + then: + response.message == "Hello $paramName" + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + name "example.Greeter/SayHello" + kind CLIENT + childOf span(0) + event(0) { + eventName "message" + attributes { + "message.type" "SENT" + "message.id" 1 + } + } + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE}" Status.Code.OK.value() + } + } + span(2) { + name "example.Greeter/SayHello" + kind SERVER + childOf span(1) + event(0) { + eventName "message" + attributes { + "message.type" "RECEIVED" + "message.id" 1 + } + } + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" Status.Code.OK.value() + } + } + } + } + + cleanup: + channel?.shutdownNow()?.awaitTermination(10, TimeUnit.SECONDS) + server?.shutdownNow()?.awaitTermination() + + where: + paramName << ["some name", "some other name"] + } + + def "test error - #paramName"() { + setup: + def error = grpcStatus.asException() + BindableService greeter = new GreeterGrpc.GreeterImplBase() { + @Override + void sayHello( + final Helloworld.Request req, final StreamObserver responseObserver) { + responseObserver.onError(error) + } + } + def port = PortUtils.findOpenPort() + Server server = configureServer(ServerBuilder.forPort(port).addService(greeter)).build().start() + ManagedChannelBuilder channelBuilder = configureClient(ManagedChannelBuilder.forAddress("localhost", port)) + + // Depending on the version of gRPC usePlainText may or may not take an argument. + try { + channelBuilder.usePlaintext() + } catch (MissingMethodException e) { + channelBuilder.usePlaintext(true) + } + ManagedChannel channel = channelBuilder.build() + GreeterGrpc.GreeterBlockingStub client = GreeterGrpc.newBlockingStub(channel) + + when: + client.sayHello(Helloworld.Request.newBuilder().setName(paramName).build()) + + then: + def e = thrown(StatusRuntimeException) + e.status.code == grpcStatus.code + e.status.description == grpcStatus.description + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "example.Greeter/SayHello" + kind CLIENT + hasNoParent() + status ERROR + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" grpcStatus.code.value() + } + } + span(1) { + name "example.Greeter/SayHello" + kind SERVER + childOf span(0) + status ERROR + event(0) { + eventName "message" + attributes { + "message.type" "RECEIVED" + "message.id" 1 + } + } + if (grpcStatus.cause != null) { + errorEvent grpcStatus.cause.class, grpcStatus.cause.message, 1 + } + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_TRANSPORT}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE}" grpcStatus.code.value() + } + } + } + } + + cleanup: + channel?.shutdownNow()?.awaitTermination(10, TimeUnit.SECONDS) + server?.shutdownNow()?.awaitTermination() + + where: + paramName | grpcStatus + "Runtime - cause" | Status.UNKNOWN.withCause(new RuntimeException("some error")) + "Status - cause" | Status.PERMISSION_DENIED.withCause(new RuntimeException("some error")) + "StatusRuntime - cause" | Status.UNIMPLEMENTED.withCause(new RuntimeException("some error")) + "Runtime - description" | Status.UNKNOWN.withDescription("some description") + "Status - description" | Status.PERMISSION_DENIED.withDescription("some description") + "StatusRuntime - description" | Status.UNIMPLEMENTED.withDescription("some description") + } + + def "test error thrown - #paramName"() { + setup: + def error = grpcStatus.asRuntimeException() + BindableService greeter = new GreeterGrpc.GreeterImplBase() { + @Override + void sayHello( + final Helloworld.Request req, final StreamObserver responseObserver) { + throw error + } + } + def port = PortUtils.findOpenPort() + Server server = configureServer(ServerBuilder.forPort(port).addService(greeter)).build().start() + ManagedChannelBuilder channelBuilder = configureClient(ManagedChannelBuilder.forAddress("localhost", port)) + + // Depending on the version of gRPC usePlainText may or may not take an argument. + try { + channelBuilder.usePlaintext() + } catch (MissingMethodException e) { + channelBuilder.usePlaintext(true) + } + ManagedChannel channel = channelBuilder.build() + GreeterGrpc.GreeterBlockingStub client = GreeterGrpc.newBlockingStub(channel) + + when: + client.sayHello(Helloworld.Request.newBuilder().setName(paramName).build()) + + then: + def e = thrown(StatusRuntimeException) + // gRPC doesn't appear to propagate server exceptions that are thrown, not onError. + e.status.code == Status.UNKNOWN.code + e.status.description == null + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "example.Greeter/SayHello" + kind CLIENT + hasNoParent() + status ERROR + // NB: Exceptions thrown on the server don't appear to be propagated to the client, at + // least for the version we test against. + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" Status.UNKNOWN.code.value() + } + } + span(1) { + name "example.Greeter/SayHello" + kind SERVER + childOf span(0) + status ERROR + event(0) { + eventName "message" + attributes { + "message.type" "RECEIVED" + "message.id" 1 + } + } + errorEvent grpcStatus.asRuntimeException().class, grpcStatus.asRuntimeException().message, 1 + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + } + } + } + } + + cleanup: + channel?.shutdownNow()?.awaitTermination(10, TimeUnit.SECONDS) + server?.shutdownNow()?.awaitTermination() + + where: + paramName | grpcStatus + "Runtime - cause" | Status.UNKNOWN.withCause(new RuntimeException("some error")) + "Status - cause" | Status.PERMISSION_DENIED.withCause(new RuntimeException("some error")) + "StatusRuntime - cause" | Status.UNIMPLEMENTED.withCause(new RuntimeException("some error")) + "Runtime - description" | Status.UNKNOWN.withDescription("some description") + "Status - description" | Status.PERMISSION_DENIED.withDescription("some description") + "StatusRuntime - description" | Status.UNIMPLEMENTED.withDescription("some description") + } + + def "test user context preserved"() { + setup: + Context.Key key = Context.key("cat") + BindableService greeter = new GreeterGrpc.GreeterImplBase() { + @Override + void sayHello( + final Helloworld.Request req, final StreamObserver responseObserver) { + if (key.get() != "meow") { + responseObserver.onError(new AssertionError((Object) "context not preserved")) + return + } + if (!io.opentelemetry.api.trace.Span.fromContext(io.opentelemetry.context.Context.current()).getSpanContext().isValid()) { + responseObserver.onError(new AssertionError((Object) "span not attached")) + return + } + final Helloworld.Response reply = Helloworld.Response.newBuilder().setMessage("Hello $req.name").build() + responseObserver.onNext(reply) + responseObserver.onCompleted() + } + } + def port = PortUtils.findOpenPort() + Server server + server = configureServer(ServerBuilder.forPort(port) + .addService(greeter) + .intercept(new ServerInterceptor() { + @Override + ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + if (!io.opentelemetry.api.trace.Span.fromContext(io.opentelemetry.context.Context.current()).getSpanContext().isValid()) { + throw new AssertionError((Object) "span not attached in server interceptor") + } + def ctx = Context.current().withValue(key, "meow") + return Contexts.interceptCall(ctx, call, headers, next) + } + })) + .build().start() + ManagedChannelBuilder channelBuilder + channelBuilder = configureClient(ManagedChannelBuilder.forAddress("localhost", port)) + .intercept(new ClientInterceptor() { + @Override + ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { + if (!io.opentelemetry.api.trace.Span.fromContext(io.opentelemetry.context.Context.current()).getSpanContext().isValid()) { + throw new AssertionError((Object) "span not attached in client interceptor") + } + def ctx = Context.current().withValue(key, "meow") + def oldCtx = ctx.attach() + try { + return next.newCall(method, callOptions) + } finally { + ctx.detach(oldCtx) + } + } + }) + + // Depending on the version of gRPC usePlainText may or may not take an argument. + try { + channelBuilder.usePlaintext() + } catch (MissingMethodException e) { + channelBuilder.usePlaintext(true) + } + ManagedChannel channel = channelBuilder.build() + def client = GreeterGrpc.newStub(channel) + + when: + AtomicReference response = new AtomicReference<>() + AtomicReference error = new AtomicReference<>() + CountDownLatch latch = new CountDownLatch(1) + runUnderTrace("parent") { + client.sayHello( + Helloworld.Request.newBuilder().setName("test").build(), + new StreamObserver() { + @Override + void onNext(Helloworld.Response r) { + if (key.get() != "meow") { + error.set(new AssertionError((Object) "context not preserved")) + return + } + if (!io.opentelemetry.api.trace.Span.fromContext(io.opentelemetry.context.Context.current()).getSpanContext().isValid()) { + error.set(new AssertionError((Object) "span not attached")) + return + } + response.set(r) + } + + @Override + void onError(Throwable throwable) { + error.set(throwable) + } + + @Override + void onCompleted() { + latch.countDown() + } + }) + } + + latch.await(10, TimeUnit.SECONDS) + + then: + error.get() == null + response.get().message == "Hello test" + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + name "example.Greeter/SayHello" + kind CLIENT + childOf span(0) + event(0) { + eventName "message" + attributes { + "message.type" "SENT" + "message.id" 1 + } + } + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" Status.OK.code.value() + } + } + span(2) { + name "example.Greeter/SayHello" + kind SERVER + childOf span(1) + event(0) { + eventName "message" + attributes { + "message.type" "RECEIVED" + "message.id" 1 + } + } + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" Status.OK.code.value() + } + } + } + } + + cleanup: + channel?.shutdownNow()?.awaitTermination(10, TimeUnit.SECONDS) + server?.shutdownNow()?.awaitTermination() + } + + // Regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2285 + def "client error thrown"() { + setup: + BindableService greeter = new GreeterGrpc.GreeterImplBase() { + @Override + void sayHello( + final Helloworld.Request req, final StreamObserver responseObserver) { + // Send a response but don't complete so client can fail itself + responseObserver.onNext(Helloworld.Response.getDefaultInstance()) + } + } + def port = PortUtils.findOpenPort() + Server server + server = configureServer(ServerBuilder.forPort(port) + .addService(greeter)) + .build().start() + ManagedChannelBuilder channelBuilder + channelBuilder = configureClient(ManagedChannelBuilder.forAddress("localhost", port)) + + // Depending on the version of gRPC usePlainText may or may not take an argument. + try { + channelBuilder.usePlaintext() + } catch (MissingMethodException e) { + channelBuilder.usePlaintext(true) + } + ManagedChannel channel = channelBuilder.build() + def client = GreeterGrpc.newStub(channel) + + when: + AtomicReference error = new AtomicReference<>() + CountDownLatch latch = new CountDownLatch(1) + runUnderTrace("parent") { + client.sayHello( + Helloworld.Request.newBuilder().setName("test").build(), + new StreamObserver() { + @Override + void onNext(Helloworld.Response r) { + throw new IllegalStateException("illegal") + } + + @Override + void onError(Throwable throwable) { + error.set(throwable) + latch.countDown() + } + + @Override + void onCompleted() { + latch.countDown() + } + }) + } + + latch.await(10, TimeUnit.SECONDS) + + then: + error.get() != null + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + name "example.Greeter/SayHello" + kind CLIENT + childOf span(0) + status ERROR + event(0) { + eventName "message" + attributes { + "message.type" "SENT" + "message.id" 1 + } + } + errorEvent(IllegalStateException, "illegal", 1) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + } + } + span(2) { + name "example.Greeter/SayHello" + kind SERVER + childOf span(1) + event(0) { + eventName "message" + attributes { + "message.type" "RECEIVED" + "message.id" 1 + } + } + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "example.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "SayHello" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + } + } + } + } + + cleanup: + channel?.shutdownNow()?.awaitTermination(10, TimeUnit.SECONDS) + server?.shutdownNow()?.awaitTermination() + } + + def "test reflection service"() { + setup: + def service = ProtoReflectionService.newInstance() + def port = PortUtils.findOpenPort() + Server server = configureServer(ServerBuilder.forPort(port).addService(service)).build().start() + ManagedChannelBuilder channelBuilder = configureClient(ManagedChannelBuilder.forAddress("localhost", port)) + + // Depending on the version of gRPC usePlainText may or may not take an argument. + try { + channelBuilder.usePlaintext() + } catch (MissingMethodException e) { + channelBuilder.usePlaintext(true) + } + ManagedChannel channel = channelBuilder.build() + ServerReflectionGrpc.ServerReflectionStub client = ServerReflectionGrpc.newStub(channel) + + when: + AtomicReference error = new AtomicReference<>() + AtomicReference response = new AtomicReference<>() + CountDownLatch latch = new CountDownLatch(1) + def request = client.serverReflectionInfo(new StreamObserver() { + @Override + void onNext(ServerReflectionResponse serverReflectionResponse) { + response.set(serverReflectionResponse) + } + + @Override + void onError(Throwable throwable) { + error.set(throwable) + latch.countDown() + } + + @Override + void onCompleted() { + latch.countDown() + } + }) + + request.onNext(ServerReflectionRequest.newBuilder() + .setListServices("The content will not be checked?") + .build()) + request.onCompleted() + + latch.await(10, TimeUnit.SECONDS) + + then: + error.get() == null + response.get().listServicesResponse.getService(0).name == "grpc.reflection.v1alpha.ServerReflection" + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" + kind CLIENT + hasNoParent() + event(0) { + eventName "message" + attributes { + "message.type" "SENT" + "message.id" 1 + } + } + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "grpc.reflection.v1alpha.ServerReflection" + "${SemanticAttributes.RPC_METHOD.key}" "ServerReflectionInfo" + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" Status.OK.code.value() + } + } + span(1) { + name "grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" + kind SERVER + childOf span(0) + event(0) { + eventName "message" + attributes { + "message.type" "RECEIVED" + "message.id" 1 + } + } + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "grpc" + "${SemanticAttributes.RPC_SERVICE.key}" "grpc.reflection.v1alpha.ServerReflection" + "${SemanticAttributes.RPC_METHOD.key}" "ServerReflectionInfo" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_TRANSPORT.key}" SemanticAttributes.NetTransportValues.IP_TCP + "${SemanticAttributes.RPC_GRPC_STATUS_CODE.key}" Status.OK.code.value() + } + } + } + } + + cleanup: + channel?.shutdownNow()?.awaitTermination(10, TimeUnit.SECONDS) + server?.shutdownNow()?.awaitTermination() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/proto/helloworld.proto b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/proto/helloworld.proto new file mode 100644 index 000000000..412aec011 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/grpc-1.6/testing/src/main/proto/helloworld.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package example; + +service Greeter { + rpc SayHello (Request) returns (Response) { + } + + rpc Conversation (stream Response) returns (stream Response) { + } +} + +message Request { + string name = 1; +} + +message Response { + string message = 1; +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/guava-10.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/guava-10.0-javaagent.gradle new file mode 100644 index 000000000..342e2563e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/guava-10.0-javaagent.gradle @@ -0,0 +1,23 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.google.guava" + module = "guava" + versions = "[10.0,]" + assertInverse = true + } +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.guava.experimental-span-attributes=true" +} + +dependencies { + library "com.google.guava:guava:10.0" + + implementation project(':instrumentation:guava-10.0:library') + + testImplementation "io.opentelemetry:opentelemetry-extension-annotations" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/GuavaInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/GuavaInstrumentationModule.java new file mode 100644 index 000000000..8330ca67f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/GuavaInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.guava; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class GuavaInstrumentationModule extends InstrumentationModule { + + public GuavaInstrumentationModule() { + super("guava", "guava-10.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new GuavaListenableFutureInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/GuavaListenableFutureInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/GuavaListenableFutureInstrumentation.java new file mode 100644 index 000000000..4ebf83ccf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/GuavaListenableFutureInstrumentation.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.guava; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.RunnableWrapper; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import java.util.concurrent.Executor; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +public class GuavaListenableFutureInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.google.common.util.concurrent.AbstractFuture"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$AbstractFutureAdvice"); + transformer.applyAdviceToMethod( + named("addListener").and(ElementMatchers.takesArguments(Runnable.class, Executor.class)), + this.getClass().getName() + "$AddListenerAdvice"); + } + + @SuppressWarnings("unused") + public static class AbstractFutureAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onConstruction() { + InstrumentationHelper.initialize(); + } + } + + @SuppressWarnings("unused") + public static class AddListenerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State addListenerEnter( + @Advice.Argument(value = 0, readOnly = false) Runnable task) { + Context context = Java8BytecodeBridge.currentContext(); + Runnable newTask = RunnableWrapper.wrapIfNeeded(task); + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(newTask)) { + task = newTask; + ContextStore contextStore = + InstrumentationContext.get(Runnable.class, State.class); + return ExecutorInstrumentationUtils.setupState(contextStore, newTask, context); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void addListenerExit( + @Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/InstrumentationHelper.java b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/InstrumentationHelper.java new file mode 100644 index 000000000..d7bf1af54 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/InstrumentationHelper.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.guava; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies; +import io.opentelemetry.instrumentation.guava.GuavaAsyncSpanEndStrategy; + +public final class InstrumentationHelper { + static { + registerAsyncSpanEndStrategy(); + } + + private static void registerAsyncSpanEndStrategy() { + AsyncSpanEndStrategies.getInstance() + .registerStrategy( + GuavaAsyncSpanEndStrategy.newBuilder() + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBooleanProperty( + "otel.instrumentation.guava.experimental-span-attributes", false)) + .build()); + } + + /** + * This method is invoked to trigger the runtime system to execute the static initializer block + * ensuring that the {@link GuavaAsyncSpanEndStrategy} is registered exactly once. + */ + public static void initialize() {} + + private InstrumentationHelper() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/groovy/GuavaWithSpanInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/groovy/GuavaWithSpanInstrumentationTest.groovy new file mode 100644 index 000000000..477219b99 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/groovy/GuavaWithSpanInstrumentationTest.groovy @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.SettableFuture +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.guava.TracedWithSpan +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +class GuavaWithSpanInstrumentationTest extends AgentInstrumentationSpecification { + + def "should capture span for already done ListenableFuture"() { + setup: + new TracedWithSpan().listenableFuture(Futures.immediateFuture("Value")) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.listenableFuture" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already failed ListenableFuture"() { + setup: + def error = new IllegalArgumentException("Boom") + new TracedWithSpan().listenableFuture(Futures.immediateFailedFuture(error)) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.listenableFuture" + kind SpanKind.INTERNAL + hasNoParent() + status StatusCode.ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually done ListenableFuture"() { + setup: + def future = SettableFuture.create() + new TracedWithSpan().listenableFuture(future) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + future.set("Value") + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.listenableFuture" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually failed ListenableFuture"() { + setup: + def error = new IllegalArgumentException("Boom") + def future = SettableFuture.create() + new TracedWithSpan().listenableFuture(future) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + future.setException(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.listenableFuture" + kind SpanKind.INTERNAL + hasNoParent() + status StatusCode.ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled ListenableFuture"() { + setup: + def future = SettableFuture.create() + new TracedWithSpan().listenableFuture(future) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + future.cancel(true) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.listenableFuture" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + "guava.canceled" true + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/groovy/ListenableFutureTest.groovy b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/groovy/ListenableFutureTest.groovy new file mode 100644 index 000000000..0e06d4ef3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/groovy/ListenableFutureTest.groovy @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.SettableFuture +import io.opentelemetry.instrumentation.test.base.AbstractPromiseTest +import java.util.concurrent.Executors +import spock.lang.Shared + +class ListenableFutureTest extends AbstractPromiseTest, ListenableFuture> { + @Shared + def executor = Executors.newFixedThreadPool(1) + + @Override + SettableFuture newPromise() { + return SettableFuture.create() + } + + @Override + ListenableFuture map(SettableFuture promise, Closure callback) { + return Futures.transform(promise, callback, executor) + } + + @Override + void onComplete(ListenableFuture promise, Closure callback) { + promise.addListener({ -> callback(promise.get()) }, executor) + } + + + @Override + void complete(SettableFuture promise, boolean value) { + promise.set(value) + } + + @Override + Boolean get(SettableFuture promise) { + return promise.get() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/java/io/opentelemetry/instrumentation/guava/TracedWithSpan.java b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/java/io/opentelemetry/instrumentation/guava/TracedWithSpan.java new file mode 100644 index 000000000..89f52ed13 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/javaagent/src/test/java/io/opentelemetry/instrumentation/guava/TracedWithSpan.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.guava; + +import com.google.common.util.concurrent.ListenableFuture; +import io.opentelemetry.extension.annotations.WithSpan; + +public class TracedWithSpan { + @WithSpan + public ListenableFuture listenableFuture(ListenableFuture future) { + return future; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/guava-10.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/guava-10.0-library.gradle new file mode 100644 index 000000000..59e36017a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/guava-10.0-library.gradle @@ -0,0 +1,5 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library "com.google.guava:guava:10.0" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategy.java b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategy.java new file mode 100644 index 000000000..b87d725fc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategy.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.guava; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.Uninterruptibles; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy; + +public final class GuavaAsyncSpanEndStrategy implements AsyncSpanEndStrategy { + private static final AttributeKey CANCELED_ATTRIBUTE_KEY = + AttributeKey.booleanKey("guava.canceled"); + + public static GuavaAsyncSpanEndStrategy create() { + return newBuilder().build(); + } + + public static GuavaAsyncSpanEndStrategyBuilder newBuilder() { + return new GuavaAsyncSpanEndStrategyBuilder(); + } + + private final boolean captureExperimentalSpanAttributes; + + GuavaAsyncSpanEndStrategy(boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + @Override + public boolean supports(Class returnType) { + return ListenableFuture.class.isAssignableFrom(returnType); + } + + @Override + public Object end(BaseTracer tracer, Context context, Object returnValue) { + ListenableFuture future = (ListenableFuture) returnValue; + if (future.isDone()) { + endSpan(tracer, context, future); + } else { + future.addListener(() -> endSpan(tracer, context, future), Runnable::run); + } + return future; + } + + private void endSpan(BaseTracer tracer, Context context, ListenableFuture future) { + if (future.isCancelled()) { + if (captureExperimentalSpanAttributes) { + Span.fromContext(context).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + tracer.end(context); + } else { + try { + Uninterruptibles.getUninterruptibly(future); + tracer.end(context); + } catch (Throwable exception) { + tracer.endExceptionally(context, exception); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategyBuilder.java b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategyBuilder.java new file mode 100644 index 000000000..26bcc9c65 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategyBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.guava; + +public final class GuavaAsyncSpanEndStrategyBuilder { + private boolean captureExperimentalSpanAttributes = false; + + GuavaAsyncSpanEndStrategyBuilder() {} + + public GuavaAsyncSpanEndStrategyBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + public GuavaAsyncSpanEndStrategy build() { + return new GuavaAsyncSpanEndStrategy(captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/test/groovy/GuavaAsyncSpanEndStrategyTest.groovy b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/test/groovy/GuavaAsyncSpanEndStrategyTest.groovy new file mode 100644 index 000000000..64849d62e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/guava-10.0/library/src/test/groovy/GuavaAsyncSpanEndStrategyTest.groovy @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.SettableFuture +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.api.tracer.BaseTracer +import io.opentelemetry.instrumentation.guava.GuavaAsyncSpanEndStrategy +import spock.lang.Specification + +class GuavaAsyncSpanEndStrategyTest extends Specification { + BaseTracer tracer + + Context context + + Span span + + def underTest = GuavaAsyncSpanEndStrategy.create() + + def underTestWithExperimentalAttributes = GuavaAsyncSpanEndStrategy.newBuilder() + .setCaptureExperimentalSpanAttributes(true) + .build() + + void setup() { + tracer = Mock() + context = Mock() + span = Mock() + span.storeInContext(_) >> { callRealMethod() } + } + + def "ListenableFuture is supported"() { + expect: + underTest.supports(ListenableFuture) + } + + def "SettableFuture is also supported"() { + expect: + underTest.supports(SettableFuture) + } + + def "ends span on already done future"() { + when: + underTest.end(tracer, context, Futures.immediateFuture("Value")) + + then: + 1 * tracer.end(context) + } + + def "ends span on already failed future"() { + given: + def exception = new IllegalStateException() + + when: + underTest.end(tracer, context, Futures.immediateFailedFuture(exception)) + + then: + 1 * tracer.endExceptionally(context, { it.getCause() == exception }) + } + + def "ends span on eventually done future"() { + given: + def future = SettableFuture.create() + + when: + underTest.end(tracer, context, future) + + then: + 0 * tracer._ + + when: + future.set("Value") + + then: + 1 * tracer.end(context) + } + + def "ends span on eventually failed future"() { + given: + def future = SettableFuture.create() + def exception = new IllegalStateException() + + when: + underTest.end(tracer, context, future) + + then: + 0 * tracer._ + + when: + future.setException(exception) + + then: + 1 * tracer.endExceptionally(context, { it.getCause() == exception }) + } + + def "ends span on eventually canceled future"() { + given: + def future = SettableFuture.create() + def context = span.storeInContext(Context.root()) + + when: + underTest.end(tracer, context, future) + + then: + 0 * tracer._ + + when: + future.cancel(true) + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span on eventually canceled future and capturing experimental span attributes"() { + given: + def future = SettableFuture.create() + def context = span.storeInContext(Context.root()) + + when: + underTestWithExperimentalAttributes.end(tracer, context, future) + + then: + 0 * tracer._ + + when: + future.cancel(true) + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "guava.canceled" }, true) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/gwt-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/gwt-2.0-javaagent.gradle new file mode 100644 index 000000000..315dee379 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/gwt-2.0-javaagent.gradle @@ -0,0 +1,97 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.google.gwt" + module = "gwt-servlet" + versions = "[2.0.0,)" + assertInverse = true + } +} + +sourceSets { + testapp { + java + resources { + srcDirs("src/webapp") + } + java.outputDir = file("$buildDir/testapp/classes") + compileClasspath += sourceSets.main.compileClasspath + } +} + +dependencies { + // these are needed for compileGwt task + if (findProperty('testLatestDeps')) { + compileOnly 'com.google.gwt:gwt-user:+' + compileOnly 'com.google.gwt:gwt-dev:+' + } else { + compileOnly 'com.google.gwt:gwt-user:2.0.0' + compileOnly 'com.google.gwt:gwt-dev:2.0.0' + } + + library 'com.google.gwt:gwt-servlet:2.0.0' + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') + + testImplementation "org.testcontainers:selenium:${versions["org.testcontainers"]}" + testImplementation 'org.seleniumhq.selenium:selenium-java:3.141.59' + + testImplementation(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + testImplementation "org.eclipse.jetty:jetty-webapp:9.4.35.v20201120" +} + +def warDir = "$buildDir/testapp/war" + +task compileGwt(dependsOn: classes, type: JavaExec) { + // versions before 2.9 require java8 + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + } + + def extraDir = "$buildDir/testapp/extra" + + outputs.cacheIf { true } + + outputs.dir extraDir + outputs.dir warDir + + main = 'com.google.gwt.dev.Compiler' + + classpath { + [ + sourceSets.testapp.java.srcDirs, + sourceSets.testapp.compileClasspath + ] + } + + args = [ + 'test.gwt.Greeting', // gwt module + '-war', warDir, + '-logLevel', 'INFO', + '-localWorkers', '2', + '-compileReport', + '-extra', extraDir, + '-draftCompile' // makes compile a bit faster + ] +} + +task copyTestWebapp(type: Copy) { + dependsOn compileGwt + + from file("src/testapp/webapp") + from warDir + + into file("$buildDir/testapp/web") +} + +test.dependsOn sourceSets.testapp.output, copyTestWebapp + +test { + // add test app classes to classpath + classpath = project.sourceSets.test.runtimeClasspath + files("$buildDir/testapp/classes") +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtInstrumentationModule.java new file mode 100644 index 000000000..cbabf7cd2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtInstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.gwt; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class GwtInstrumentationModule extends InstrumentationModule { + + public GwtInstrumentationModule() { + super("gwt", "gwt-2.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // class added in gwt 2.0 + return hasClassesNamed("com.google.gwt.uibinder.client.UiBinder"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new GwtRpcInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcAttributesExtractor.java new file mode 100644 index 000000000..7922fa639 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcAttributesExtractor.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.gwt; + +import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesExtractor; +import java.lang.reflect.Method; + +final class GwtRpcAttributesExtractor extends RpcAttributesExtractor { + @Override + protected String system(Method method) { + return "gwt"; + } + + @Override + protected String service(Method method) { + return method.getDeclaringClass().getName(); + } + + @Override + protected String method(Method method) { + return method.getName(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcInstrumentation.java new file mode 100644 index 000000000..ca67f078f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcInstrumentation.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.gwt; + +import static io.opentelemetry.javaagent.instrumentation.gwt.GwtSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class GwtRpcInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.google.gwt.user.server.rpc.RPC"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invokeAndEncodeResponse") + .and(takesArguments(5)) + .and(takesArgument(0, Object.class)) + .and(takesArgument(1, Method.class)) + .and(takesArgument(2, Object[].class)) + .and(takesArgument(3, named("com.google.gwt.user.server.rpc.SerializationPolicy"))) + .and(takesArgument(4, int.class)), + this.getClass().getName() + "$InvokeAndEncodeResponseAdvice"); + + // encodeResponseForFailure is called by invokeAndEncodeResponse in case of failure + transformer.applyAdviceToMethod( + named("encodeResponseForFailure") + .and(takesArguments(4)) + .and(takesArgument(0, Method.class)) + .and(takesArgument(1, Throwable.class)) + .and(takesArgument(2, named("com.google.gwt.user.server.rpc.SerializationPolicy"))) + .and(takesArgument(3, int.class)), + this.getClass().getName() + "$EncodeResponseForFailureAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAndEncodeResponseAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(1) Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = + instrumenter() + .start(Java8BytecodeBridge.currentContext(), method) + .with(GwtSingletons.RPC_CONTEXT_KEY, true); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Argument(1) Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + scope.close(); + + instrumenter().end(context, method, null, throwable); + } + } + + @SuppressWarnings("unused") + public static class EncodeResponseForFailureAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(1) Throwable throwable) { + if (throwable == null) { + return; + } + Context context = Java8BytecodeBridge.currentContext(); + if (context.get(GwtSingletons.RPC_CONTEXT_KEY) == null) { + // not inside rpc invocation + return; + } + Java8BytecodeBridge.spanFromContext(context).recordException(throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtSingletons.java b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtSingletons.java new file mode 100644 index 000000000..8a2334ae1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtSingletons.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.gwt; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcSpanNameExtractor; +import java.lang.reflect.Method; + +public final class GwtSingletons { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.gwt-2.0"; + + public static final ContextKey RPC_CONTEXT_KEY = + ContextKey.named("opentelemetry-gwt-rpc-context-key"); + + private static final Instrumenter INSTRUMENTER; + + static { + RpcAttributesExtractor rpcAttributes = new GwtRpcAttributesExtractor(); + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), + INSTRUMENTATION_NAME, + RpcSpanNameExtractor.create(rpcAttributes)) + .addAttributesExtractor(rpcAttributes) + // TODO(anuraaga): This should be a server span, but we currently have no way to merge + // with the HTTP instrumentation's server span. + .newInstrumenter(); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private GwtSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/test/groovy/GwtTest.groovy b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/test/groovy/GwtTest.groovy new file mode 100644 index 000000000..fce27cc33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/test/groovy/GwtTest.groovy @@ -0,0 +1,168 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.TimeUnit +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.util.resource.Resource +import org.eclipse.jetty.webapp.WebAppContext +import org.openqa.selenium.chrome.ChromeOptions +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.Testcontainers +import org.testcontainers.containers.BrowserWebDriverContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import spock.lang.Shared + +class GwtTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + private static final Logger logger = LoggerFactory.getLogger(GwtTest) + + @Shared + BrowserWebDriverContainer chrome + + @Override + Server startServer(int port) { + WebAppContext webAppContext = new WebAppContext() + webAppContext.setContextPath(getContextPath()) + webAppContext.setBaseResource(Resource.newResource(new File("build/testapp/web"))) + + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + jettyServer.setHandler(webAppContext) + jettyServer.start() + + return jettyServer + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + def setupSpec() { + Testcontainers.exposeHostPorts(port) + + chrome = new BrowserWebDriverContainer<>() + .withCapabilities(new ChromeOptions()) + .withLogConsumer(new Slf4jLogConsumer(logger)) + chrome.start() + + address = new URI("http://host.testcontainers.internal:$port" + getContextPath() + "/") + } + + def cleanupSpec() { + chrome?.stop() + } + + @Override + String getContextPath() { + return "/xyz" + } + + def getDriver() { + def driver = chrome.getWebDriver() + driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS) + return driver + } + + def "test gwt"() { + setup: + def driver = getDriver() + + // fetch the test page + driver.get(address.resolve("greeting.html").toString()) + + expect: + // wait for page to load + driver.findElementByClassName("greeting.button") + assertTraces(4) { + // /xyz/greeting.html + trace(0, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + // /xyz/greeting/greeting.nocache.js + trace(1, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + // /xyz/greeting/1B105441581A8F41E49D5DF3FB5B55BA.cache.html + trace(2, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + // /favicon.ico + trace(3, 1) { + serverSpan(it, 0, "HTTP GET") + } + } + clearExportedData() + + when: + // click a button to trigger calling java code + driver.findElementByClassName("greeting.button").click() + + then: + // wait for response + "Hello, Otel" == driver.findElementByClassName("message.received").getText() + assertTraces(1) { + trace(0, 2) { + serverSpan(it, 0, getContextPath() + "/greeting/greet") + span(1) { + name "test.gwt.shared.MessageService/sendMessage" + kind SpanKind.INTERNAL + childOf(span(0)) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "gwt" + "${SemanticAttributes.RPC_SERVICE.key}" "test.gwt.shared.MessageService" + "${SemanticAttributes.RPC_METHOD.key}" "sendMessage" + } + } + } + } + clearExportedData() + + when: + // click a button to trigger calling java code + driver.findElementByClassName("error.button").click() + + then: + // wait for response + "Error" == driver.findElementByClassName("error.received").getText() + assertTraces(1) { + trace(0, 2) { + serverSpan(it, 0, getContextPath() + "/greeting/greet") + span(1) { + name "test.gwt.shared.MessageService/sendMessage" + kind SpanKind.INTERNAL + childOf(span(0)) + errorEvent(IOException) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "gwt" + "${SemanticAttributes.RPC_SERVICE.key}" "test.gwt.shared.MessageService" + "${SemanticAttributes.RPC_METHOD.key}" "sendMessage" + } + } + } + } + + cleanup: + driver.close() + } + + static serverSpan(TraceAssert trace, int index, String spanName) { + trace.span(index) { + hasNoParent() + + name spanName + kind SpanKind.SERVER + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/Greeting.gwt.xml b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/Greeting.gwt.xml new file mode 100644 index 000000000..2e3d99f1f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/Greeting.gwt.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/client/GreetingEntryPoint.java b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/client/GreetingEntryPoint.java new file mode 100644 index 000000000..b8034de4e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/client/GreetingEntryPoint.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.gwt.client; + +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RootPanel; +import test.gwt.shared.MessageService; +import test.gwt.shared.MessageServiceAsync; + +public class GreetingEntryPoint implements EntryPoint { + private final MessageServiceAsync messageServiceAsync = GWT.create(MessageService.class); + + @Override + public void onModuleLoad() { + Button greetingButton = new Button("Greeting"); + greetingButton.addStyleName("greeting.button"); + + Button errorButton = new Button("Error"); + errorButton.addStyleName("error.button"); + + RootPanel.get("buttonContainer").add(greetingButton); + RootPanel.get("buttonContainer").add(errorButton); + + final Label messageLabel = new Label(); + RootPanel.get("messageContainer").add(messageLabel); + + class MyHandler implements ClickHandler { + private final String message; + + MyHandler(String message) { + this.message = message; + } + + @Override + public void onClick(ClickEvent event) { + sendMessageToServer(); + } + + private void sendMessageToServer() { + messageLabel.setText(""); + messageLabel.setStyleName(""); + + messageServiceAsync.sendMessage( + message, + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + messageLabel.setText("Error"); + messageLabel.addStyleName("error.received"); + } + + @Override + public void onSuccess(String result) { + messageLabel.setText(result); + messageLabel.addStyleName("message.received"); + } + }); + } + } + + greetingButton.addClickHandler(new MyHandler("Otel")); + errorButton.addClickHandler(new MyHandler("Error")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/server/MessageServiceImpl.java b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/server/MessageServiceImpl.java new file mode 100644 index 000000000..95fb0ce17 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/server/MessageServiceImpl.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.gwt.server; + +import com.google.gwt.user.server.rpc.RemoteServiceServlet; +import java.io.IOException; +import test.gwt.shared.MessageService; + +/** The server-side implementation of the RPC service. */ +@SuppressWarnings("serial") +public class MessageServiceImpl extends RemoteServiceServlet implements MessageService { + + @Override + public String sendMessage(String message) throws IOException { + if (message == null || "Error".equals(message)) { + throw new IOException(); + } + + return "Hello, " + message; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/shared/MessageService.java b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/shared/MessageService.java new file mode 100644 index 000000000..0a514c3d1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/shared/MessageService.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.gwt.shared; + +import com.google.gwt.user.client.rpc.RemoteService; +import com.google.gwt.user.client.rpc.RemoteServiceRelativePath; +import java.io.IOException; + +/** The client-side stub for the RPC service. */ +@RemoteServiceRelativePath("greet") +public interface MessageService extends RemoteService { + String sendMessage(String message) throws IOException; +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/shared/MessageServiceAsync.java b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/shared/MessageServiceAsync.java new file mode 100644 index 000000000..fd64d9bce --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/java/test/gwt/shared/MessageServiceAsync.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.gwt.shared; + +import com.google.gwt.user.client.rpc.AsyncCallback; + +/** The async counterpart of MessageService. */ +public interface MessageServiceAsync { + void sendMessage(String input, AsyncCallback callback); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..215442e2b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/WEB-INF/web.xml @@ -0,0 +1,20 @@ + + + + + greetServlet + test.gwt.server.MessageServiceImpl + + + + greetServlet + /greeting/greet + + + + greeting.html + + diff --git a/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/greeting.html b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/greeting.html new file mode 100644 index 000000000..acd563c8c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/greeting.html @@ -0,0 +1,13 @@ + + + + +Example + + + + +

+

+ + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/hera-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/hera-javaagent.gradle new file mode 100644 index 000000000..e693ed2dd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/hera-javaagent.gradle @@ -0,0 +1 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/HeraContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/HeraContextInstrumentation.java new file mode 100644 index 000000000..4d6ed63ca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/HeraContextInstrumentation.java @@ -0,0 +1,222 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hera; + +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Map; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.named; + +@SuppressWarnings("CatchAndPrintStackTrace") +public class +HeraContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xiaomi.hera.trace.context.HeraContextUtil"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(ElementMatchers.named("getHeraContext") + .and(ElementMatchers.isPublic()) + , HeraContextInstrumentation.class.getName() + "$GetContextInvokeAdvice"); + + transformer.applyAdviceToMethod(ElementMatchers.named("get") + .and(ElementMatchers.isPublic()) + , HeraContextInstrumentation.class.getName() + "$GetInvokeAdvice"); + + transformer.applyAdviceToMethod(ElementMatchers.named("set") + .and(ElementMatchers.isPublic()) + , HeraContextInstrumentation.class.getName() + "$SetInvokeAdvice"); + } + + + public static class GetContextInvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + } + + @SuppressWarnings("SystemOut") + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Return(readOnly = false) Map result) { + if (context == null) { + context = currentContext(); + if (context == null) { + System.out.println("java8 context is null"); + return; + } + } + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + result = spanContext.getHeraContext(); + return; + } + + } + + public static class GetInvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) String key, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + } + + @SuppressWarnings("SystemOut") + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Argument(0) String key, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Return(readOnly = false) String result) { + if (key == null || key.isEmpty()) { + return; + } + if (context == null) { + context = currentContext(); + if (context == null) { + System.out.println("java8 context is null"); + return; + } + } + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + Map heraContextMap = spanContext.getHeraContext(); + if (heraContextMap != null && heraContextMap.size() > 0) { + String heraContext = heraContextMap.get(HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + if (heraContext != null && !heraContext.isEmpty()) { + String[] split = heraContext.split(HeraContext.ENTRY_SPLIT); + for (String entry : split) { + String[] kv = entry.split(HeraContext.KEY_VALUE_SPLIT); + if (key.equals(kv[0])) { + result = kv[1]; + } + } + } + } + if (result != null) { + try { + result = URLDecoder.decode(result, "UTF-8"); + } catch (Throwable e) { + e.printStackTrace(); + } + } + return; + } + } + + public static class SetInvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) String key, + @Advice.Argument(1) String value, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + } + + @SuppressWarnings("SystemOut") + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Argument(0) String key, + @Advice.Argument(1) String value, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Return(readOnly = false) boolean result) { + if (key == null || key.isEmpty()) { + return; + } + if (context == null) { + context = currentContext(); + if (context == null) { + System.out.println("java8 context is null"); + return; + } + } + String encodeStr = value; + if (encodeStr != null) { + try { + encodeStr = URLEncoder.encode(encodeStr, "UTF-8"); + } catch (Throwable t) { + t.printStackTrace(); + return; + } + } + StringBuilder sb = new StringBuilder(key).append(HeraContext.KEY_VALUE_SPLIT).append(encodeStr); + try { + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + Map heraContextMap = spanContext.getHeraContext(); + String heraContext = heraContextMap.get(HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + if (heraContext == null || heraContext.isEmpty()) { + heraContextMap.put(HeraContext.HERA_CONTEXT_PROPAGATOR_KEY, sb.toString()); + result = true; + } else { + String[] entrys = heraContext.split(HeraContext.ENTRY_SPLIT); + if (entrys.length >= HeraContext.LIMIT_LENGTH) { + System.out.println("set HeraContext size break bounds"); + result = false; + } else { + // the key duplicate validation + if (heraContext.contains(key)) { + StringBuilder newSb = new StringBuilder(); + for (String entry : entrys) { + String[] keyValue = entry.split(HeraContext.KEY_VALUE_SPLIT); + if (keyValue[1].equals(key)) { + newSb.append(HeraContext.ENTRY_SPLIT).append(key).append(HeraContext.KEY_VALUE_SPLIT).append(encodeStr); + } else { + if (newSb.length() == 0) { + newSb.append(entry); + } else { + newSb.append(HeraContext.ENTRY_SPLIT).append(entry); + } + } + } + heraContextMap.put(HeraContext.HERA_CONTEXT_PROPAGATOR_KEY, newSb.toString()); + } else { + heraContextMap.put(HeraContext.HERA_CONTEXT_PROPAGATOR_KEY, new StringBuilder(heraContext).append(HeraContext.ENTRY_SPLIT).append(sb).toString()); + } + result = true; + } + } + } catch (Throwable t) { + System.out.println("set HeraContext error : " + t.getMessage()); + } + return; + } + + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/HeraInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/HeraInstrumentationModule.java new file mode 100644 index 000000000..04347c62f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/HeraInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hera; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.List; + +import static java.util.Arrays.asList; + +@AutoService(InstrumentationModule.class) +public class HeraInstrumentationModule extends InstrumentationModule { + public HeraInstrumentationModule() { + super("hera"); + } + + @Override + public List typeInstrumentations() { + return asList(new TraceIdInstrumentation(),new HeraContextInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/TraceIdInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/TraceIdInstrumentation.java new file mode 100644 index 000000000..3729a2822 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hera/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hera/TraceIdInstrumentation.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hera; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class TraceIdInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xiaomi.hera.trace.context.TraceIdUtil"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + ElementMatchers.named("traceId") + .and(ElementMatchers.isPublic()), + this.getClass().getName() + "$GetTraceIdAdvice"); + transformer.applyAdviceToMethod( + ElementMatchers.named("spanId") + .and(ElementMatchers.isPublic()), + this.getClass().getName() + "$GetSpanIdAdvice"); + } + + + public static class GetTraceIdAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope + ) { + } + + @SuppressWarnings({"SystemOut", "CatchAndPrintStackTrace"}) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Return(readOnly = false) String traceId) { + try { + Context parentContext = currentContext(); + traceId = Span.fromContext(parentContext).getSpanContext().getTraceId(); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + } + + public static class GetSpanIdAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope + ) { + } + + @SuppressWarnings({"SystemOut", "CatchAndPrintStackTrace"}) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Return(readOnly = false) String spanId) { + try { + Context parentContext = currentContext(); + spanId = Span.fromContext(parentContext).getSpanContext().getSpanId(); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/hibernate-3.3-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/hibernate-3.3-javaagent.gradle new file mode 100644 index 000000000..6fbb66f38 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/hibernate-3.3-javaagent.gradle @@ -0,0 +1,46 @@ +/* + * Instrumentation for Hibernate between 3.5 and 4. + * Has the same logic as the Hibernate 4+ instrumentation, but is copied rather than sharing a codebase. This is because + * the root interface for Session/StatelessSession - SharedSessionContract - isn't present before version 4. So the + * instrumentation isn't able to reference it. + */ + +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.hibernate" + module = "hibernate-core" + versions = "[3.3.0.GA,4.0.0.Final)" + assertInverse = true + } +} + +dependencies { + library "org.hibernate:hibernate-core:3.3.0.GA" + + implementation project(':instrumentation:hibernate:hibernate-common:javaagent') + + testInstrumentation project(':instrumentation:jdbc:javaagent') + // Added to ensure cross compatibility: + testInstrumentation project(':instrumentation:hibernate:hibernate-4.0:javaagent') + testInstrumentation project(':instrumentation:hibernate:hibernate-procedure-call-4.3:javaagent') + + testLibrary "org.hibernate:hibernate-core:3.3.0.SP1" + testImplementation "org.hibernate:hibernate-annotations:3.4.0.GA" + testImplementation "javassist:javassist:+" + testImplementation "com.h2database:h2:1.4.197" + testImplementation "javax.xml.bind:jaxb-api:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-core:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-impl:2.2.11" + testImplementation "javax.activation:activation:1.1.1" + + latestDepTestLibrary "org.hibernate:hibernate-core:3.+" +} + +if (findProperty('testLatestDeps')) { + configurations { + // Needed for test, but for latestDepTest this would otherwise bundle a second incompatible version of hibernate-core. + testImplementation.exclude group: 'org.hibernate', module: 'hibernate-annotations' + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaInstrumentation.java new file mode 100644 index 000000000..ee8118798 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaInstrumentation.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Criteria; + +public class CriteriaInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.Criteria"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.Criteria")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(namedOneOf("list", "uniqueResult", "scroll")), + CriteriaInstrumentation.class.getName() + "$CriteriaMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class CriteriaMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startMethod( + @Advice.This Criteria criteria, + @Advice.Origin("#m") String name, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + ContextStore contextStore = + InstrumentationContext.get(Criteria.class, Context.class); + + context = SessionMethodUtils.startSpanFrom(contextStore, criteria, "Criteria." + name, null); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endMethod( + @Advice.Thrown Throwable throwable, + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object entity, + @Advice.Origin("#m") String name, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + SessionMethodUtils.end(context, throwable, "Criteria." + name, entity); + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/HibernateInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/HibernateInstrumentationModule.java new file mode 100644 index 000000000..1da7a3c45 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/HibernateInstrumentationModule.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class HibernateInstrumentationModule extends InstrumentationModule { + + public HibernateInstrumentationModule() { + super("hibernate", "hibernate-3.3"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + // Not in 4.0 + "org.hibernate.classic.Validatable", + // Not before 3.3.0.GA + "org.hibernate.transaction.JBossTransactionManagerLookup"); + } + + @Override + public List typeInstrumentations() { + return asList( + new CriteriaInstrumentation(), + new QueryInstrumentation(), + new SessionFactoryInstrumentation(), + new SessionInstrumentation(), + new TransactionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryInstrumentation.java new file mode 100644 index 000000000..807e25c7f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Query; + +public class QueryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.Query"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.Query")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(namedOneOf("list", "executeUpdate", "uniqueResult", "iterate", "scroll")), + QueryInstrumentation.class.getName() + "$QueryMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class QueryMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startMethod( + @Advice.This Query query, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + ContextStore contextStore = + InstrumentationContext.get(Query.class, Context.class); + + context = SessionMethodUtils.startSpanFromQuery(contextStore, query, query.getQueryString()); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endMethod( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + SessionMethodUtils.end(context, throwable, null, null); + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionFactoryInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionFactoryInstrumentation.java new file mode 100644 index 000000000..dc3b37b81 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionFactoryInstrumentation.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.hibernate.HibernateTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Session; +import org.hibernate.StatelessSession; + +public class SessionFactoryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.SessionFactory"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.SessionFactory")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("openSession", "openStatelessSession")) + .and(takesArguments(0)) + .and( + returns( + namedOneOf("org.hibernate.Session", "org.hibernate.StatelessSession") + .or(implementsInterface(named("org.hibernate.Session"))))), + SessionFactoryInstrumentation.class.getName() + "$SessionFactoryAdvice"); + } + + @SuppressWarnings("unused") + public static class SessionFactoryAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void openSession(@Advice.Return Object session) { + + Context parentContext = Java8BytecodeBridge.currentContext(); + Context context = tracer().startSpan(parentContext, "Session"); + + if (session instanceof Session) { + ContextStore contextStore = + InstrumentationContext.get(Session.class, Context.class); + contextStore.putIfAbsent((Session) session, context); + } else if (session instanceof StatelessSession) { + ContextStore contextStore = + InstrumentationContext.get(StatelessSession.class, Context.class); + contextStore.putIfAbsent((StatelessSession) session, context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionInstrumentation.java new file mode 100644 index 000000000..951c705ef --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionInstrumentation.java @@ -0,0 +1,247 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.hibernate.HibernateTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils.SCOPE_ONLY_METHODS; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Criteria; +import org.hibernate.Query; +import org.hibernate.Session; +import org.hibernate.StatelessSession; +import org.hibernate.Transaction; + +public class SessionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.Session", "org.hibernate.StatelessSession"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface( + namedOneOf("org.hibernate.Session", "org.hibernate.StatelessSession")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("close")).and(takesArguments(0)), + SessionInstrumentation.class.getName() + "$SessionCloseAdvice"); + + // Session synchronous methods we want to instrument. + transformer.applyAdviceToMethod( + isMethod() + .and( + namedOneOf( + "save", + "replicate", + "saveOrUpdate", + "update", + "merge", + "persist", + "lock", + "refresh", + "insert", + "delete", + // Lazy-load methods. + "immediateLoad", + "internalLoad")), + SessionInstrumentation.class.getName() + "$SessionMethodAdvice"); + + // Handle the non-generic 'get' separately. + transformer.applyAdviceToMethod( + isMethod().and(named("get")).and(returns(Object.class)).and(takesArgument(0, String.class)), + SessionInstrumentation.class.getName() + "$SessionMethodAdvice"); + + // These methods return some object that we want to instrument, and so the Advice will pin the + // current Span to the returned object using a ContextStore. + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("beginTransaction", "getTransaction")) + .and(returns(named("org.hibernate.Transaction"))), + SessionInstrumentation.class.getName() + "$GetTransactionAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(returns(implementsInterface(named("org.hibernate.Query")))), + SessionInstrumentation.class.getName() + "$GetQueryAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(returns(implementsInterface(named("org.hibernate.Criteria")))), + SessionInstrumentation.class.getName() + "$GetCriteriaAdvice"); + } + + @SuppressWarnings("unused") + public static class SessionCloseAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void closeSession( + @Advice.This Object session, @Advice.Thrown Throwable throwable) { + + Context sessionContext = null; + if (session instanceof Session) { + ContextStore contextStore = + InstrumentationContext.get(Session.class, Context.class); + sessionContext = contextStore.get((Session) session); + } else if (session instanceof StatelessSession) { + ContextStore contextStore = + InstrumentationContext.get(StatelessSession.class, Context.class); + sessionContext = contextStore.get((StatelessSession) session); + } + + if (sessionContext == null) { + return; + } + if (throwable != null) { + tracer().endExceptionally(sessionContext, throwable); + } else { + tracer().end(sessionContext); + } + } + } + + @SuppressWarnings("unused") + public static class SessionMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startMethod( + @Advice.This Object session, + @Advice.Origin("#m") String name, + @Advice.Argument(0) Object entity, + @Advice.Local("otelContext") Context spanContext, + @Advice.Local("otelScope") Scope scope) { + + Context sessionContext = null; + if (session instanceof Session) { + ContextStore contextStore = + InstrumentationContext.get(Session.class, Context.class); + sessionContext = contextStore.get((Session) session); + } else if (session instanceof StatelessSession) { + ContextStore contextStore = + InstrumentationContext.get(StatelessSession.class, Context.class); + sessionContext = contextStore.get((StatelessSession) session); + } + + if (sessionContext == null) { + return; // No state found. We aren't in a Session. + } + + if (CallDepthThreadLocalMap.incrementCallDepth(SessionMethodUtils.class) > 0) { + return; // This method call is being traced already. + } + + if (!SCOPE_ONLY_METHODS.contains(name)) { + spanContext = tracer().startSpan(sessionContext, "Session." + name, entity); + scope = spanContext.makeCurrent(); + } else { + scope = sessionContext.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endMethod( + @Advice.Thrown Throwable throwable, + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object returned, + @Advice.Origin("#m") String name, + @Advice.Local("otelContext") Context spanContext, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + scope.close(); + SessionMethodUtils.end(spanContext, throwable, "Session." + name, returned); + } + } + } + + @SuppressWarnings("unused") + public static class GetQueryAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void getQuery(@Advice.This Object session, @Advice.Return Query query) { + + ContextStore queryContextStore = + InstrumentationContext.get(Query.class, Context.class); + if (session instanceof Session) { + ContextStore sessionContextStore = + InstrumentationContext.get(Session.class, Context.class); + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, (Session) session, queryContextStore, query); + } else if (session instanceof StatelessSession) { + ContextStore sessionContextStore = + InstrumentationContext.get(StatelessSession.class, Context.class); + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, (StatelessSession) session, queryContextStore, query); + } + } + } + + @SuppressWarnings("unused") + public static class GetTransactionAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void getTransaction( + @Advice.This Object session, @Advice.Return Transaction transaction) { + + ContextStore transactionContextStore = + InstrumentationContext.get(Transaction.class, Context.class); + + if (session instanceof Session) { + ContextStore sessionContextStore = + InstrumentationContext.get(Session.class, Context.class); + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, (Session) session, transactionContextStore, transaction); + } else if (session instanceof StatelessSession) { + ContextStore sessionContextStore = + InstrumentationContext.get(StatelessSession.class, Context.class); + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, (StatelessSession) session, transactionContextStore, transaction); + } + } + } + + @SuppressWarnings("unused") + public static class GetCriteriaAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void getCriteria(@Advice.This Object session, @Advice.Return Criteria criteria) { + + ContextStore criteriaContextStore = + InstrumentationContext.get(Criteria.class, Context.class); + if (session instanceof Session) { + ContextStore sessionContextStore = + InstrumentationContext.get(Session.class, Context.class); + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, (Session) session, criteriaContextStore, criteria); + } else if (session instanceof StatelessSession) { + ContextStore sessionContextStore = + InstrumentationContext.get(StatelessSession.class, Context.class); + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, (StatelessSession) session, criteriaContextStore, criteria); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/TransactionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/TransactionInstrumentation.java new file mode 100644 index 000000000..9581aff1b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/TransactionInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Transaction; + +public class TransactionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.Transaction"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.Transaction")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("commit")).and(takesArguments(0)), + TransactionInstrumentation.class.getName() + "$TransactionCommitAdvice"); + } + + @SuppressWarnings("unused") + public static class TransactionCommitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startCommit( + @Advice.This Transaction transaction, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + ContextStore contextStore = + InstrumentationContext.get(Transaction.class, Context.class); + + context = + SessionMethodUtils.startSpanFrom(contextStore, transaction, "Transaction.commit", null); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endCommit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + SessionMethodUtils.end(context, throwable, null, null); + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/AbstractHibernateTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/AbstractHibernateTest.groovy new file mode 100644 index 000000000..10bd08ba5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/AbstractHibernateTest.groovy @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.hibernate.Session +import org.hibernate.SessionFactory +import org.hibernate.cfg.AnnotationConfiguration +import spock.lang.Shared + +abstract class AbstractHibernateTest extends AgentInstrumentationSpecification { + + @Shared + protected SessionFactory sessionFactory + + @Shared + protected List prepopulated + + def setupSpec() { + sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory() + + // Pre-populate the DB, so delete/update can be tested. + Session writer = sessionFactory.openSession() + writer.beginTransaction() + prepopulated = new ArrayList<>() + for (int i = 0; i < 2; i++) { + prepopulated.add(new Value("Hello :) " + i)) + writer.save(prepopulated.get(i)) + } + writer.getTransaction().commit() + writer.close() + } + + def cleanupSpec() { + if (sessionFactory != null) { + sessionFactory.close() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/CriteriaTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/CriteriaTest.groovy new file mode 100644 index 000000000..946748153 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/CriteriaTest.groovy @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.hibernate.Criteria +import org.hibernate.Session +import org.hibernate.criterion.Order +import org.hibernate.criterion.Restrictions + +class CriteriaTest extends AbstractHibernateTest { + + def "test criteria.#methodName"() { + setup: + Session session = sessionFactory.openSession() + session.beginTransaction() + Criteria criteria = session.createCriteria(Value) + .add(Restrictions.like("name", "Hello")) + .addOrder(Order.desc("name")) + interaction.call(criteria) + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Criteria.$methodName" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + + where: + methodName | interaction + "list" | { c -> c.list() } + "uniqueResult" | { c -> c.uniqueResult() } + "scroll" | { c -> c.scroll() } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/QueryTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/QueryTest.groovy new file mode 100644 index 000000000..ac0bc2e60 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/QueryTest.groovy @@ -0,0 +1,187 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.hibernate.Query +import org.hibernate.Session + +class QueryTest extends AbstractHibernateTest { + + def "test hibernate query.#queryMethodName single call"() { + setup: + + // With Transaction + Session session = sessionFactory.openSession() + session.beginTransaction() + queryInteraction(session) + session.getTransaction().commit() + session.close() + + // Without Transaction + if (!requiresTransaction) { + session = sessionFactory.openSession() + queryInteraction(session) + session.close() + } + + expect: + assertTraces(requiresTransaction ? 1 : 2) { + // With Transaction + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name expectedSpanName + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" String + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + if (!requiresTransaction) { + // Without Transaction + trace(1, 3) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name expectedSpanName + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + } + } + } + + where: + queryMethodName | expectedSpanName | requiresTransaction | queryInteraction + "Query.list" | "SELECT Value" | false | { sess -> + Query q = sess.createQuery("from Value") + q.list() + } + "Query.executeUpdate" | "UPDATE Value" | true | { sess -> + Query q = sess.createQuery("update Value set name = ?") + q.setParameter(0, "alyx") + q.executeUpdate() + } + "Query.uniqueResult" | "SELECT Value" | false | { sess -> + Query q = sess.createQuery("from Value where id = ?") + q.setParameter(0, 1L) + q.uniqueResult() + } + "Query.iterate" | "SELECT Value" | false | { sess -> + Query q = sess.createQuery("from Value") + q.iterate() + } + "Query.scroll" | "SELECT Value" | false | { sess -> + Query q = sess.createQuery("from Value") + q.scroll() + } + } + + def "test hibernate query.iterate"() { + setup: + + Session session = sessionFactory.openSession() + session.beginTransaction() + Query q = session.createQuery("from Value") + Iterator it = q.iterate() + while (it.hasNext()) { + it.next() + } + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "SELECT Value" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/SessionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/SessionTest.groovy new file mode 100644 index 000000000..b7986bb1d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/SessionTest.groovy @@ -0,0 +1,567 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.hibernate.LockMode +import org.hibernate.MappingException +import org.hibernate.Query +import org.hibernate.ReplicationMode +import org.hibernate.Session +import spock.lang.Shared + +class SessionTest extends AbstractHibernateTest { + + @Shared + private Closure sessionBuilder = { return sessionFactory.openSession() } + @Shared + private Closure statelessSessionBuilder = { return sessionFactory.openStatelessSession() } + + + def "test hibernate action #testName"() { + setup: + + // Test for each implementation of Session. + for (def buildSession : sessionImplementations) { + def session = buildSession() + session.beginTransaction() + + try { + sessionMethodTest.call(session, prepopulated.get(0)) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + } + + expect: + assertTraces(sessionImplementations.size()) { + for (int i = 0; i < sessionImplementations.size(); i++) { + trace(i, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.$methodName $resource" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + } + + where: + testName | methodName | resource | sessionImplementations | sessionMethodTest + "lock" | "lock" | "Value" | [sessionBuilder] | { sesh, val -> + sesh.lock(val, LockMode.READ) + } + "refresh" | "refresh" | "Value" | [sessionBuilder, statelessSessionBuilder] | { sesh, val -> + sesh.refresh(val) + } + "get" | "get" | "Value" | [sessionBuilder, statelessSessionBuilder] | { sesh, val -> + sesh.get("Value", val.getId()) + } + } + + def "test hibernate statless action #testName"() { + setup: + + // Test for each implementation of Session. + def session = statelessSessionBuilder() + session.beginTransaction() + + try { + sessionMethodTest.call(session, prepopulated.get(0)) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.$methodName $resource" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(3) { + kind CLIENT + childOf span(2) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" String + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + } + } + + where: + testName | methodName | resource | sessionMethodTest + "insert" | "insert" | "Value" | { sesh, val -> + sesh.insert("Value", new Value("insert me")) + } + "update" | "update" | "Value" | { sesh, val -> + val.setName("New name") + sesh.update(val) + } + "update by entityName" | "update" | "Value" | { sesh, val -> + val.setName("New name") + sesh.update("Value", val) + } + "delete" | "delete" | "Value" | { sesh, val -> + sesh.delete(val) + } + } + + def "test hibernate replicate: #testName"() { + setup: + + // Test for each implementation of Session. + def session = sessionFactory.openSession() + session.beginTransaction() + + try { + sessionMethodTest.call(session, prepopulated.get(0)) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.$methodName $resource" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(4) { + kind CLIENT + childOf span(3) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" String + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + } + + } + + where: + testName | methodName | resource | sessionMethodTest + "replicate" | "replicate" | "Value" | { sesh, val -> + Value replicated = new Value(val.getName() + " replicated") + replicated.setId(val.getId()) + sesh.replicate(replicated, ReplicationMode.OVERWRITE) + } + "replicate by entityName" | "replicate" | "Value" | { sesh, val -> + Value replicated = new Value(val.getName() + " replicated") + replicated.setId(val.getId()) + sesh.replicate("Value", replicated, ReplicationMode.OVERWRITE) + } + } + + def "test hibernate failed replicate"() { + setup: + + // Test for each implementation of Session. + def session = sessionFactory.openSession() + session.beginTransaction() + + try { + session.replicate(new Long(123) /* Not a valid entity */, ReplicationMode.OVERWRITE) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.replicate" + kind INTERNAL + childOf span(0) + status ERROR + errorEvent(MappingException, "Unknown entity: java.lang.Long") + } + span(2) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + + } + } + + + def "test hibernate commit action #testName"() { + setup: + + def session = sessionBuilder() + session.beginTransaction() + + try { + sessionMethodTest.call(session, prepopulated.get(0)) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.$methodName $resource" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(3) { + kind CLIENT + childOf span(2) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" String + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + } + } + + where: + testName | methodName | resource | sessionMethodTest + "save" | "save" | "Value" | { sesh, val -> + sesh.save(new Value("Another value")) + } + "saveOrUpdate save" | "saveOrUpdate" | "Value" | { sesh, val -> + sesh.saveOrUpdate(new Value("Value")) + } + "saveOrUpdate update" | "saveOrUpdate" | "Value" | { sesh, val -> + val.setName("New name") + sesh.saveOrUpdate(val) + } + "merge" | "merge" | "Value" | { sesh, val -> + sesh.merge(new Value("merge me in")) + } + "persist" | "persist" | "Value" | { sesh, val -> + sesh.persist(new Value("merge me in")) + } + "update (Session)" | "update" | "Value" | { sesh, val -> + val.setName("New name") + sesh.update(val) + } + "update by entityName (Session)" | "update" | "Value" | { sesh, val -> + val.setName("New name") + sesh.update("Value", val) + } + "delete (Session)" | "delete" | "Value" | { sesh, val -> + sesh.delete(val) + } + } + + + def "test attaches State to query created via #queryMethodName"() { + setup: + Session session = sessionFactory.openSession() + session.beginTransaction() + Query query = queryBuildMethod(session) + query.list() + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name expectedSpanName + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + + where: + queryMethodName | expectedSpanName | queryBuildMethod + "createQuery" | "SELECT Value" | { sess -> sess.createQuery("from Value") } + "getNamedQuery" | "SELECT Value" | { sess -> sess.getNamedQuery("TestNamedQuery") } + "createSQLQuery" | "SELECT Value" | { sess -> sess.createSQLQuery("SELECT * FROM Value") } + } + + + def "test hibernate overlapping Sessions"() { + setup: + + runUnderTrace("overlapping Sessions") { + def session1 = sessionFactory.openSession() + session1.beginTransaction() + def session2 = sessionFactory.openStatelessSession() + def session3 = sessionFactory.openSession() + + def value1 = new Value("Value 1") + session1.save(value1) + session2.insert(new Value("Value 2")) + session3.save(new Value("Value 3")) + session1.delete(value1) + + session2.close() + session1.getTransaction().commit() + session1.close() + session3.close() + } + + expect: + assertTraces(1) { + trace(0, 11) { + span(0) { + name "overlapping Sessions" + attributes { + } + } + span(1) { + name "Session" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "Session.save Value" + kind INTERNAL + childOf span(1) + attributes { + } + } + span(3) { + name "Session.delete Value" + kind INTERNAL + childOf span(1) + attributes { + } + } + span(4) { + name "Transaction.commit" + kind INTERNAL + childOf span(1) + attributes { + } + } + span(5) { + name "INSERT db1.Value" + kind CLIENT + childOf span(4) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^insert / + "${SemanticAttributes.DB_OPERATION.key}" "INSERT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(6) { + name "DELETE db1.Value" + kind CLIENT + childOf span(4) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^delete / + "${SemanticAttributes.DB_OPERATION.key}" "DELETE" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(7) { + name "Session" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(8) { + name "Session.insert Value" + kind INTERNAL + childOf span(7) + attributes { + } + } + span(9) { + name "Session" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(10) { + name "Session.save Value" + kind INTERNAL + childOf span(9) + attributes { + } + } + } + } + } +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/Value.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/Value.java new file mode 100644 index 000000000..a9de87173 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/Value.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NamedQuery; + +@Entity +@Table +@NamedQuery(name = "TestNamedQuery", query = "from Value") +public class Value { + + private Long id; + private String name; + + public Value() {} + + public Value(String name) { + this.name = name; + } + + @Id + @GeneratedValue(generator = "increment") + @GenericGenerator(name = "increment", strategy = "increment") + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String title) { + name = title; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/resources/hibernate.cfg.xml b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/resources/hibernate.cfg.xml new file mode 100644 index 000000000..8cf25d386 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/resources/hibernate.cfg.xml @@ -0,0 +1,31 @@ + + + + + + + + false + + + org.h2.Driver + jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE + sa + + org.hibernate.dialect.H2Dialect + + 3 + org.hibernate.cache.internal.NoCacheProvider + true + + + create + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/hibernate-4.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/hibernate-4.0-javaagent.gradle new file mode 100644 index 000000000..9ccb43df6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/hibernate-4.0-javaagent.gradle @@ -0,0 +1,62 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +muzzle { + pass { + group = "org.hibernate" + module = "hibernate-core" + versions = "[4.0.0.Final,)" + assertInverse = true + } +} + +testSets { + version5Test { + dirName = 'test' + } + version6Test { + dirName = 'hibernate6Test' + } + + latestDepTest { + dirName = 'test' + } +} + +test.dependsOn version5Test, version6Test + +dependencies { + compileOnly "org.hibernate:hibernate-core:4.0.0.Final" + + implementation project(':instrumentation:hibernate:hibernate-common:javaagent') + + testInstrumentation project(':instrumentation:jdbc:javaagent') + // Added to ensure cross compatibility: + testInstrumentation project(':instrumentation:hibernate:hibernate-3.3:javaagent') + testInstrumentation project(':instrumentation:hibernate:hibernate-procedure-call-4.3:javaagent') + + testImplementation "com.h2database:h2:1.4.197" + testImplementation "javax.xml.bind:jaxb-api:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-core:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-impl:2.2.11" + testImplementation "javax.activation:activation:1.1.1" + testImplementation "org.hsqldb:hsqldb:2.0.0" + //First version to work with Java 14 + testImplementation "org.springframework.data:spring-data-jpa:1.8.0.RELEASE" + + testImplementation "org.hibernate:hibernate-core:4.0.0.Final" + testImplementation "org.hibernate:hibernate-entitymanager:4.0.0.Final" + + version5TestImplementation "org.hibernate:hibernate-core:5.0.0.Final" + version5TestImplementation "org.hibernate:hibernate-entitymanager:5.0.0.Final" + version5TestImplementation "org.springframework.data:spring-data-jpa:2.3.0.RELEASE" + + version6TestImplementation "org.hibernate:hibernate-core:6.0.0.Alpha6" + version6TestImplementation "org.hibernate:hibernate-entitymanager:6.0.0.Alpha6" + version6TestImplementation "org.springframework.data:spring-data-jpa:2.3.0.RELEASE" + + // hibernate 6 is alpha so use 5 as latest version + latestDepTestImplementation "org.hibernate:hibernate-core:5.+" + latestDepTestImplementation "org.hibernate:hibernate-entitymanager:5.+" + latestDepTestImplementation "org.springframework.data:spring-data-jpa:(2.4.0,)" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/groovy/SpringJpaTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/groovy/SpringJpaTest.groovy new file mode 100644 index 000000000..b5093956a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/groovy/SpringJpaTest.groovy @@ -0,0 +1,198 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Shared +import spring.jpa.Customer +import spring.jpa.CustomerRepository +import spring.jpa.PersistenceConfig + +/** + * Unfortunately this test verifies that our hibernate instrumentation doesn't currently work with Spring Data Repositories. + */ +class SpringJpaTest extends AgentInstrumentationSpecification { + + @Shared + def context = new AnnotationConfigApplicationContext(PersistenceConfig) + + @Shared + def repo = context.getBean(CustomerRepository) + + def "test CRUD"() { + setup: + def customer = new Customer("Bob", "Anonymous") + + expect: + customer.id == null + !repo.findAll().iterator().hasNext() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SELECT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName(.*)from Customer(.*)/ + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + clearExportedData() + + when: + repo.save(customer) + def savedId = customer.id + + then: + customer.id != null + // Behavior changed in new version: + def extraTrace = traces.size() == 2 + assertTraces(extraTrace ? 2 : 1) { + if (extraTrace) { + trace(0, 1) { + span(0) { + name "test" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_STATEMENT.key}" "call next value for hibernate_sequence" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + } + } + } + } + trace(extraTrace ? 1 : 0, 1) { + span(0) { + name "INSERT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/insert into Customer \(.*\) values \(.*, \?, \?\)/ + "${SemanticAttributes.DB_OPERATION.key}" "INSERT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + clearExportedData() + + when: + customer.firstName = "Bill" + repo.save(customer) + + then: + customer.id == savedId + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SELECT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + trace(1, 1) { + span(0) { + name "UPDATE test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" "update Customer set firstName=?, lastName=? where id=?" + "${SemanticAttributes.DB_OPERATION.key}" "UPDATE" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + clearExportedData() + + when: + customer = repo.findByLastName("Anonymous")[0] + + then: + customer.id == savedId + customer.firstName == "Bill" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SELECT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)(where ([^.]+)\.lastName( ?)=( ?)\?|)/ + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + clearExportedData() + + when: + repo.delete(customer) + + then: + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SELECT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + trace(1, 1) { + span(0) { + name "DELETE test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" "delete from Customer where id=?" + "${SemanticAttributes.DB_OPERATION.key}" "DELETE" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/Value.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/Value.java new file mode 100644 index 000000000..f89b45096 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/Value.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NamedQuery; + +@Entity +@Table +@NamedQuery(name = "TestNamedQuery", query = "FROM Value") +public class Value { + + private Long id; + private String name; + + public Value() {} + + public Value(String name) { + this.name = name; + } + + @Id + @GeneratedValue(generator = "increment") + @GenericGenerator(name = "increment", strategy = "increment") + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String title) { + name = title; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/Customer.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/Customer.java new file mode 100644 index 000000000..27e7d6894 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/Customer.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import java.util.Objects; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String firstName; + private String lastName; + + protected Customer() {} + + public Customer(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public String toString() { + return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Customer)) { + return false; + } + Customer other = (Customer) obj; + return Objects.equals(id, other.id) + && Objects.equals(firstName, other.firstName) + && Objects.equals(lastName, other.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/CustomerRepository.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/CustomerRepository.java new file mode 100644 index 000000000..9662f2a4b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/CustomerRepository.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import java.util.List; +import org.springframework.data.repository.CrudRepository; + +public interface CustomerRepository extends CrudRepository { + + List findByLastName(String lastName); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/PersistenceConfig.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/PersistenceConfig.java new file mode 100644 index 000000000..d161e7526 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/hibernate6Test/java/spring/jpa/PersistenceConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import java.util.Properties; +import javax.sql.DataSource; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; + +@EnableJpaRepositories(basePackages = "spring/jpa") +public class PersistenceConfig { + + @Bean(name = "transactionManager") + public PlatformTransactionManager dbTransactionManager() { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); + return transactionManager; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setDatabase(Database.HSQL); + vendorAdapter.setGenerateDdl(true); + + LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); + em.setDataSource(dataSource()); + em.setPackagesToScan("spring/jpa"); + em.setJpaVendorAdapter(vendorAdapter); + em.setJpaProperties(additionalProperties()); + + return em; + } + + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:test"); + dataSource.setUsername("sa"); + dataSource.setPassword("1"); + return dataSource; + } + + private static Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.show_sql", "true"); + properties.setProperty("hibernate.hbm2ddl.auto", "create"); + properties.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); + return properties; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaInstrumentation.java new file mode 100644 index 000000000..a1b20701e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaInstrumentation.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Criteria; + +public class CriteriaInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.Criteria"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.Criteria")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(namedOneOf("list", "uniqueResult", "scroll")), + CriteriaInstrumentation.class.getName() + "$CriteriaMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class CriteriaMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startMethod( + @Advice.This Criteria criteria, + @Advice.Origin("#m") String name, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + ContextStore contextStore = + InstrumentationContext.get(Criteria.class, Context.class); + + context = SessionMethodUtils.startSpanFrom(contextStore, criteria, "Criteria." + name, null); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endMethod( + @Advice.Thrown Throwable throwable, + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object entity, + @Advice.Origin("#m") String name, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + scope.close(); + SessionMethodUtils.end(context, throwable, "Criteria." + name, entity); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/HibernateInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/HibernateInstrumentationModule.java new file mode 100644 index 000000000..8902bd828 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/HibernateInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class HibernateInstrumentationModule extends InstrumentationModule { + + public HibernateInstrumentationModule() { + super("hibernate", "hibernate-4.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new CriteriaInstrumentation(), + new QueryInstrumentation(), + new SessionFactoryInstrumentation(), + new SessionInstrumentation(), + new TransactionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryInstrumentation.java new file mode 100644 index 000000000..a426ec52b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Query; + +public class QueryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.Query"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.Query")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(namedOneOf("list", "executeUpdate", "uniqueResult", "iterate", "scroll")), + QueryInstrumentation.class.getName() + "$QueryMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class QueryMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startMethod( + @Advice.This Query query, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + ContextStore contextStore = + InstrumentationContext.get(Query.class, Context.class); + + context = SessionMethodUtils.startSpanFromQuery(contextStore, query, query.getQueryString()); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endMethod( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + scope.close(); + SessionMethodUtils.end(context, throwable, null, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionFactoryInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionFactoryInstrumentation.java new file mode 100644 index 000000000..a34e9a09e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionFactoryInstrumentation.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.hibernate.HibernateTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.SharedSessionContract; + +public class SessionFactoryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.SessionFactory"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.SessionFactory")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("openSession", "openStatelessSession")) + .and(takesArguments(0)) + .and(returns(namedOneOf("org.hibernate.Session", "org.hibernate.StatelessSession"))), + SessionFactoryInstrumentation.class.getName() + "$SessionFactoryAdvice"); + } + + @SuppressWarnings("unused") + public static class SessionFactoryAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void openSession(@Advice.Return SharedSessionContract session) { + + Context parentContext = Java8BytecodeBridge.currentContext(); + Context context = tracer().startSpan(parentContext, "Session"); + + ContextStore contextStore = + InstrumentationContext.get(SharedSessionContract.class, Context.class); + contextStore.putIfAbsent(session, context); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionInstrumentation.java new file mode 100644 index 000000000..7991ac18c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionInstrumentation.java @@ -0,0 +1,212 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.hibernate.HibernateTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils.SCOPE_ONLY_METHODS; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Criteria; +import org.hibernate.Query; +import org.hibernate.SharedSessionContract; +import org.hibernate.Transaction; + +public class SessionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.SharedSessionContract"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.SharedSessionContract")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("close")).and(takesArguments(0)), + SessionInstrumentation.class.getName() + "$SessionCloseAdvice"); + + // Session synchronous methods we want to instrument. + transformer.applyAdviceToMethod( + isMethod() + .and( + namedOneOf( + "save", + "replicate", + "saveOrUpdate", + "update", + "merge", + "persist", + "lock", + "refresh", + "insert", + "delete", + // Lazy-load methods. + "immediateLoad", + "internalLoad")), + SessionInstrumentation.class.getName() + "$SessionMethodAdvice"); + // Handle the non-generic 'get' separately. + transformer.applyAdviceToMethod( + isMethod().and(named("get")).and(returns(Object.class)).and(takesArgument(0, String.class)), + SessionInstrumentation.class.getName() + "$SessionMethodAdvice"); + + // These methods return some object that we want to instrument, and so the Advice will pin the + // current Span to the returned object using a ContextStore. + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("beginTransaction", "getTransaction")) + .and(returns(named("org.hibernate.Transaction"))), + SessionInstrumentation.class.getName() + "$GetTransactionAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(returns(implementsInterface(named("org.hibernate.Query")))), + SessionInstrumentation.class.getName() + "$GetQueryAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(returns(implementsInterface(named("org.hibernate.Criteria")))), + SessionInstrumentation.class.getName() + "$GetCriteriaAdvice"); + } + + @SuppressWarnings("unused") + public static class SessionCloseAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void closeSession( + @Advice.This SharedSessionContract session, @Advice.Thrown Throwable throwable) { + + ContextStore contextStore = + InstrumentationContext.get(SharedSessionContract.class, Context.class); + Context sessionContext = contextStore.get(session); + if (sessionContext == null) { + return; + } + if (throwable != null) { + tracer().endExceptionally(sessionContext, throwable); + } else { + tracer().end(sessionContext); + } + } + } + + @SuppressWarnings("unused") + public static class SessionMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startMethod( + @Advice.This SharedSessionContract session, + @Advice.Origin("#m") String name, + @Advice.Argument(0) Object entity, + @Advice.Local("otelContext") Context spanContext, + @Advice.Local("otelScope") Scope scope) { + + ContextStore contextStore = + InstrumentationContext.get(SharedSessionContract.class, Context.class); + Context sessionContext = contextStore.get(session); + + if (sessionContext == null) { + return; // No state found. We aren't in a Session. + } + + if (CallDepthThreadLocalMap.incrementCallDepth(SessionMethodUtils.class) > 0) { + return; // This method call is being traced already. + } + + if (!SCOPE_ONLY_METHODS.contains(name)) { + spanContext = tracer().startSpan(sessionContext, "Session." + name, entity); + scope = spanContext.makeCurrent(); + } else { + scope = sessionContext.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endMethod( + @Advice.Thrown Throwable throwable, + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object returned, + @Advice.Origin("#m") String name, + @Advice.Local("otelContext") Context spanContext, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + scope.close(); + SessionMethodUtils.end(spanContext, throwable, "Session." + name, returned); + } + } + } + + @SuppressWarnings("unused") + public static class GetQueryAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void getQuery( + @Advice.This SharedSessionContract session, @Advice.Return Query query) { + + ContextStore sessionContextStore = + InstrumentationContext.get(SharedSessionContract.class, Context.class); + ContextStore queryContextStore = + InstrumentationContext.get(Query.class, Context.class); + + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, session, queryContextStore, query); + } + } + + @SuppressWarnings("unused") + public static class GetTransactionAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void getTransaction( + @Advice.This SharedSessionContract session, @Advice.Return Transaction transaction) { + + ContextStore sessionContextStore = + InstrumentationContext.get(SharedSessionContract.class, Context.class); + ContextStore transactionContextStore = + InstrumentationContext.get(Transaction.class, Context.class); + + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, session, transactionContextStore, transaction); + } + } + + @SuppressWarnings("unused") + public static class GetCriteriaAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void getCriteria( + @Advice.This SharedSessionContract session, @Advice.Return Criteria criteria) { + + ContextStore sessionContextStore = + InstrumentationContext.get(SharedSessionContract.class, Context.class); + ContextStore criteriaContextStore = + InstrumentationContext.get(Criteria.class, Context.class); + + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, session, criteriaContextStore, criteria); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/TransactionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/TransactionInstrumentation.java new file mode 100644 index 000000000..3d02d57dd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/TransactionInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.Transaction; + +public class TransactionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.Transaction"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.Transaction")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("commit")).and(takesArguments(0)), + TransactionInstrumentation.class.getName() + "$TransactionCommitAdvice"); + } + + @SuppressWarnings("unused") + public static class TransactionCommitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startCommit( + @Advice.This Transaction transaction, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + ContextStore contextStore = + InstrumentationContext.get(Transaction.class, Context.class); + + context = + SessionMethodUtils.startSpanFrom(contextStore, transaction, "Transaction.commit", null); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endCommit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + scope.close(); + SessionMethodUtils.end(context, throwable, null, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/AbstractHibernateTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/AbstractHibernateTest.groovy new file mode 100644 index 000000000..09b6d5842 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/AbstractHibernateTest.groovy @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.hibernate.Session +import org.hibernate.SessionFactory +import org.hibernate.cfg.Configuration +import spock.lang.Shared + +abstract class AbstractHibernateTest extends AgentInstrumentationSpecification { + + @Shared + protected SessionFactory sessionFactory + + @Shared + protected List prepopulated + + def setupSpec() { + sessionFactory = new Configuration().configure().buildSessionFactory() + // Pre-populate the DB, so delete/update can be tested. + Session writer = sessionFactory.openSession() + writer.beginTransaction() + prepopulated = new ArrayList<>() + for (int i = 0; i < 2; i++) { + prepopulated.add(new Value("Hello :) " + i)) + writer.save(prepopulated.get(i)) + } + writer.getTransaction().commit() + writer.close() + } + + def cleanupSpec() { + if (sessionFactory != null) { + sessionFactory.close() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/CriteriaTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/CriteriaTest.groovy new file mode 100644 index 000000000..946748153 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/CriteriaTest.groovy @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.hibernate.Criteria +import org.hibernate.Session +import org.hibernate.criterion.Order +import org.hibernate.criterion.Restrictions + +class CriteriaTest extends AbstractHibernateTest { + + def "test criteria.#methodName"() { + setup: + Session session = sessionFactory.openSession() + session.beginTransaction() + Criteria criteria = session.createCriteria(Value) + .add(Restrictions.like("name", "Hello")) + .addOrder(Order.desc("name")) + interaction.call(criteria) + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Criteria.$methodName" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + + where: + methodName | interaction + "list" | { c -> c.list() } + "uniqueResult" | { c -> c.uniqueResult() } + "scroll" | { c -> c.scroll() } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/QueryTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/QueryTest.groovy new file mode 100644 index 000000000..0841d35bb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/QueryTest.groovy @@ -0,0 +1,187 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.hibernate.Query +import org.hibernate.Session + +class QueryTest extends AbstractHibernateTest { + + def "test hibernate query.#queryMethodName single call"() { + setup: + + // With Transaction + Session session = sessionFactory.openSession() + session.beginTransaction() + queryInteraction(session) + session.getTransaction().commit() + session.close() + + // Without Transaction + if (!requiresTransaction) { + session = sessionFactory.openSession() + queryInteraction(session) + session.close() + } + + expect: + assertTraces(requiresTransaction ? 1 : 2) { + // With Transaction + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name expectedSpanName + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" String + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + if (!requiresTransaction) { + // Without Transaction + trace(1, 3) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name expectedSpanName + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + } + } + } + + where: + queryMethodName | expectedSpanName | requiresTransaction | queryInteraction + "query/list" | "SELECT Value" | false | { sess -> + Query q = sess.createQuery("from Value") + q.list() + } + "query/executeUpdate" | "UPDATE Value" | true | { sess -> + Query q = sess.createQuery("update Value set name = :name") + q.setParameter("name", "alyx") + q.executeUpdate() + } + "query/uniqueResult" | "SELECT Value" | false | { sess -> + Query q = sess.createQuery("from Value where id = :id") + q.setParameter("id", 1L) + q.uniqueResult() + } + "iterate" | "SELECT Value" | false | { sess -> + Query q = sess.createQuery("from Value") + q.iterate() + } + "query/scroll" | "SELECT Value" | false | { sess -> + Query q = sess.createQuery("from Value") + q.scroll() + } + } + + def "test hibernate query.iterate"() { + setup: + + Session session = sessionFactory.openSession() + session.beginTransaction() + Query q = session.createQuery("from Value") + Iterator it = q.iterate() + while (it.hasNext()) { + it.next() + } + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "SELECT Value" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SessionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SessionTest.groovy new file mode 100644 index 000000000..5eb5a9fa8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SessionTest.groovy @@ -0,0 +1,520 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.hibernate.LockMode +import org.hibernate.MappingException +import org.hibernate.Query +import org.hibernate.ReplicationMode +import org.hibernate.Session +import spock.lang.Shared + +class SessionTest extends AbstractHibernateTest { + + @Shared + private Closure sessionBuilder = { return sessionFactory.openSession() } + @Shared + private Closure statelessSessionBuilder = { return sessionFactory.openStatelessSession() } + + + def "test hibernate action #testName"() { + setup: + + // Test for each implementation of Session. + for (def buildSession : sessionImplementations) { + def session = buildSession() + session.beginTransaction() + + try { + sessionMethodTest.call(session, prepopulated.get(0)) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + } + + expect: + assertTraces(sessionImplementations.size()) { + for (int i = 0; i < sessionImplementations.size(); i++) { + trace(i, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.$methodName $resource" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + childOf span(1) + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" String + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + } + + where: + testName | methodName | resource | sessionImplementations | sessionMethodTest + "lock" | "lock" | "Value" | [sessionBuilder] | { sesh, val -> + sesh.lock(val, LockMode.READ) + } + "refresh" | "refresh" | "Value" | [sessionBuilder, statelessSessionBuilder] | { sesh, val -> + sesh.refresh(val) + } + "get" | "get" | "Value" | [sessionBuilder, statelessSessionBuilder] | { sesh, val -> + sesh.get("Value", val.getId()) + } + "insert" | "insert" | "Value" | [statelessSessionBuilder] | { sesh, val -> + sesh.insert("Value", new Value("insert me")) + } + "update (StatelessSession)" | "update" | "Value" | [statelessSessionBuilder] | { sesh, val -> + val.setName("New name") + sesh.update(val) + } + "update by entityName (StatelessSession)" | "update" | "Value" | [statelessSessionBuilder] | { sesh, val -> + val.setName("New name") + sesh.update("Value", val) + } + "delete (Session)" | "delete" | "Value" | [statelessSessionBuilder] | { sesh, val -> + sesh.delete(val) + } + } + + def "test hibernate replicate: #testName"() { + setup: + + // Test for each implementation of Session. + def session = sessionFactory.openSession() + session.beginTransaction() + + try { + sessionMethodTest.call(session, prepopulated.get(0)) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.$methodName $resource" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "SELECT db1.Value" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(4) { + kind CLIENT + childOf span(3) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" String + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + } + + } + + where: + testName | methodName | resource | sessionMethodTest + "replicate" | "replicate" | "Value" | { sesh, val -> + Value replicated = new Value(val.getName() + " replicated") + replicated.setId(val.getId()) + sesh.replicate(replicated, ReplicationMode.OVERWRITE) + } + "replicate by entityName" | "replicate" | "Value" | { sesh, val -> + Value replicated = new Value(val.getName() + " replicated") + replicated.setId(val.getId()) + sesh.replicate("Value", replicated, ReplicationMode.OVERWRITE) + } + } + + def "test hibernate failed replicate"() { + setup: + + // Test for each implementation of Session. + def session = sessionFactory.openSession() + session.beginTransaction() + + try { + session.replicate(new Long(123) /* Not a valid entity */, ReplicationMode.OVERWRITE) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.replicate" + kind INTERNAL + childOf span(0) + status ERROR + errorEvent(MappingException, "Unknown entity: java.lang.Long") + } + span(2) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + + } + } + + + def "test hibernate commit action #testName"() { + setup: + + def session = sessionBuilder() + session.beginTransaction() + + try { + sessionMethodTest.call(session, prepopulated.get(0)) + } catch (Exception e) { + // We expected this, we should see the error field set on the span. + } + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "Session.$methodName $resource" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(3) { + kind CLIENT + childOf span(2) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" String + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + } + } + + where: + testName | methodName | resource | sessionMethodTest + "save" | "save" | "Value" | { sesh, val -> + sesh.save(new Value("Another value")) + } + "saveOrUpdate save" | "saveOrUpdate" | "Value" | { sesh, val -> + sesh.saveOrUpdate(new Value("Value")) + } + "saveOrUpdate update" | "saveOrUpdate" | "Value" | { sesh, val -> + val.setName("New name") + sesh.saveOrUpdate(val) + } + "merge" | "merge" | "Value" | { sesh, val -> + sesh.merge(new Value("merge me in")) + } + "persist" | "persist" | "Value" | { sesh, val -> + sesh.persist(new Value("merge me in")) + } + "update (Session)" | "update" | "Value" | { sesh, val -> + val.setName("New name") + sesh.update(val) + } + "update by entityName (Session)" | "update" | "Value" | { sesh, val -> + val.setName("New name") + sesh.update("Value", val) + } + "delete (Session)" | "delete" | "Value" | { sesh, val -> + sesh.delete(val) + } + } + + + def "test attaches State to query created via #queryMethodName"() { + setup: + Session session = sessionFactory.openSession() + session.beginTransaction() + Query query = queryBuildMethod(session) + query.list() + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name resource + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" String + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(3) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + + where: + queryMethodName | resource | queryBuildMethod + "createQuery" | "SELECT Value" | { sess -> sess.createQuery("from Value") } + "getNamedQuery" | "SELECT Value" | { sess -> sess.getNamedQuery("TestNamedQuery") } + "createSQLQuery" | "SELECT Value" | { sess -> sess.createSQLQuery("SELECT * FROM Value") } + } + + + def "test hibernate overlapping Sessions"() { + setup: + + runUnderTrace("overlapping Sessions") { + def session1 = sessionFactory.openSession() + session1.beginTransaction() + def session2 = sessionFactory.openStatelessSession() + def session3 = sessionFactory.openSession() + + def value1 = new Value("Value 1") + session1.save(value1) + session2.insert(new Value("Value 2")) + session3.save(new Value("Value 3")) + session1.delete(value1) + + session2.close() + session1.getTransaction().commit() + session1.close() + session3.close() + } + + expect: + assertTraces(1) { + trace(0, 12) { + span(0) { + name "overlapping Sessions" + attributes { + } + } + span(1) { + name "Session" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "Session.save Value" + kind INTERNAL + childOf span(1) + attributes { + } + } + span(3) { + name "Session.delete Value" + kind INTERNAL + childOf span(1) + attributes { + } + } + span(4) { + name "Transaction.commit" + kind INTERNAL + childOf span(1) + attributes { + } + } + span(5) { + name "INSERT db1.Value" + kind CLIENT + childOf span(4) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^insert / + "${SemanticAttributes.DB_OPERATION.key}" "INSERT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(6) { + name "DELETE db1.Value" + kind CLIENT + childOf span(4) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^delete / + "${SemanticAttributes.DB_OPERATION.key}" "DELETE" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(7) { + name "Session" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(8) { + name "Session.insert Value" + kind INTERNAL + childOf span(7) + attributes { + } + } + span(9) { + name "INSERT db1.Value" + kind CLIENT + childOf span(8) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "h2" + "${SemanticAttributes.DB_NAME.key}" "db1" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "h2:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^insert / + "${SemanticAttributes.DB_OPERATION.key}" "INSERT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Value" + } + } + span(10) { + name "Session" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(11) { + name "Session.save Value" + kind INTERNAL + childOf span(10) + attributes { + } + } + } + } + } +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SpringJpaTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SpringJpaTest.groovy new file mode 100644 index 000000000..b5093956a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SpringJpaTest.groovy @@ -0,0 +1,198 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Shared +import spring.jpa.Customer +import spring.jpa.CustomerRepository +import spring.jpa.PersistenceConfig + +/** + * Unfortunately this test verifies that our hibernate instrumentation doesn't currently work with Spring Data Repositories. + */ +class SpringJpaTest extends AgentInstrumentationSpecification { + + @Shared + def context = new AnnotationConfigApplicationContext(PersistenceConfig) + + @Shared + def repo = context.getBean(CustomerRepository) + + def "test CRUD"() { + setup: + def customer = new Customer("Bob", "Anonymous") + + expect: + customer.id == null + !repo.findAll().iterator().hasNext() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SELECT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName(.*)from Customer(.*)/ + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + clearExportedData() + + when: + repo.save(customer) + def savedId = customer.id + + then: + customer.id != null + // Behavior changed in new version: + def extraTrace = traces.size() == 2 + assertTraces(extraTrace ? 2 : 1) { + if (extraTrace) { + trace(0, 1) { + span(0) { + name "test" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_STATEMENT.key}" "call next value for hibernate_sequence" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + } + } + } + } + trace(extraTrace ? 1 : 0, 1) { + span(0) { + name "INSERT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/insert into Customer \(.*\) values \(.*, \?, \?\)/ + "${SemanticAttributes.DB_OPERATION.key}" "INSERT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + clearExportedData() + + when: + customer.firstName = "Bill" + repo.save(customer) + + then: + customer.id == savedId + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SELECT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + trace(1, 1) { + span(0) { + name "UPDATE test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" "update Customer set firstName=?, lastName=? where id=?" + "${SemanticAttributes.DB_OPERATION.key}" "UPDATE" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + clearExportedData() + + when: + customer = repo.findByLastName("Anonymous")[0] + + then: + customer.id == savedId + customer.firstName == "Bill" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SELECT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)(where ([^.]+)\.lastName( ?)=( ?)\?|)/ + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + clearExportedData() + + when: + repo.delete(customer) + + then: + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SELECT test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + trace(1, 1) { + span(0) { + name "DELETE test.Customer" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" "delete from Customer where id=?" + "${SemanticAttributes.DB_OPERATION.key}" "DELETE" + "${SemanticAttributes.DB_SQL_TABLE.key}" "Customer" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/Value.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/Value.java new file mode 100644 index 000000000..a9de87173 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/Value.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NamedQuery; + +@Entity +@Table +@NamedQuery(name = "TestNamedQuery", query = "from Value") +public class Value { + + private Long id; + private String name; + + public Value() {} + + public Value(String name) { + this.name = name; + } + + @Id + @GeneratedValue(generator = "increment") + @GenericGenerator(name = "increment", strategy = "increment") + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String title) { + name = title; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/Customer.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/Customer.java new file mode 100644 index 000000000..27e7d6894 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/Customer.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import java.util.Objects; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String firstName; + private String lastName; + + protected Customer() {} + + public Customer(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public String toString() { + return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Customer)) { + return false; + } + Customer other = (Customer) obj; + return Objects.equals(id, other.id) + && Objects.equals(firstName, other.firstName) + && Objects.equals(lastName, other.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/CustomerRepository.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/CustomerRepository.java new file mode 100644 index 000000000..9662f2a4b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/CustomerRepository.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import java.util.List; +import org.springframework.data.repository.CrudRepository; + +public interface CustomerRepository extends CrudRepository { + + List findByLastName(String lastName); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/PersistenceConfig.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/PersistenceConfig.java new file mode 100644 index 000000000..d161e7526 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/PersistenceConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import java.util.Properties; +import javax.sql.DataSource; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; + +@EnableJpaRepositories(basePackages = "spring/jpa") +public class PersistenceConfig { + + @Bean(name = "transactionManager") + public PlatformTransactionManager dbTransactionManager() { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); + return transactionManager; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setDatabase(Database.HSQL); + vendorAdapter.setGenerateDdl(true); + + LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); + em.setDataSource(dataSource()); + em.setPackagesToScan("spring/jpa"); + em.setJpaVendorAdapter(vendorAdapter); + em.setJpaProperties(additionalProperties()); + + return em; + } + + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:test"); + dataSource.setUsername("sa"); + dataSource.setPassword("1"); + return dataSource; + } + + private static Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.show_sql", "true"); + properties.setProperty("hibernate.hbm2ddl.auto", "create"); + properties.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); + return properties; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/hibernate.cfg.xml b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/hibernate.cfg.xml new file mode 100644 index 000000000..fe5e5de6b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/hibernate.cfg.xml @@ -0,0 +1,29 @@ + + + + + + + + + org.h2.Driver + jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE + sa + + org.hibernate.dialect.H2Dialect + + 3 + org.hibernate.cache.internal.NoCacheProvider + true + + + create + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/hibernate-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/hibernate-common-javaagent.gradle new file mode 100644 index 000000000..e6a518bdc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/hibernate-common-javaagent.gradle @@ -0,0 +1,9 @@ +/* + * Classes that are common to all versions of the Hibernate instrumentation. + */ + +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly project(':javaagent-api') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateTracer.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateTracer.java new file mode 100644 index 000000000..167cb5e5c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateTracer.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class HibernateTracer extends BaseTracer { + private static final HibernateTracer TRACER = new HibernateTracer(); + + public static HibernateTracer tracer() { + return TRACER; + } + + public Context startSpan(Context parentContext, String operationName, Object entity) { + return startSpan(parentContext, spanNameForOperation(operationName, entity)); + } + + public Context startSpan(Context parentContext, String spanName) { + return startSpan(parentContext, spanName, SpanKind.INTERNAL); + } + + private String spanNameForOperation(String operationName, Object entity) { + if (entity != null) { + String entityName = entityName(entity); + if (entityName != null) { + return operationName + " " + entityName; + } + } + return operationName; + } + + String entityName(Object entity) { + if (entity == null) { + return null; + } + String name = null; + Set annotations = new HashSet<>(); + for (Annotation annotation : entity.getClass().getDeclaredAnnotations()) { + annotations.add(annotation.annotationType().getName()); + } + + if (entity instanceof String) { + // We were given an entity name, not the entity itself. + name = (String) entity; + } else if (annotations.contains("javax.persistence.Entity")) { + // We were given an instance of an entity. + name = entity.getClass().getName(); + } else if (entity instanceof List && !((List) entity).isEmpty()) { + // We have a list of entities. + name = entityName(((List) entity).get(0)); + } + + return name; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.hibernate-common"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/SessionMethodUtils.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/SessionMethodUtils.java new file mode 100644 index 000000000..978f0d491 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/SessionMethodUtils.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate; + +import static io.opentelemetry.javaagent.instrumentation.hibernate.HibernateTracer.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class SessionMethodUtils { + + public static final Set SCOPE_ONLY_METHODS = + new HashSet<>(Arrays.asList("immediateLoad", "internalLoad")); + + public static Context startSpanFrom( + ContextStore contextStore, + TARGET spanKey, + String operationName, + ENTITY entity) { + return startSpanFrom(contextStore, spanKey, () -> operationName, entity); + } + + private static Context startSpanFrom( + ContextStore contextStore, + TARGET spanKey, + Supplier operationNameSupplier, + ENTITY entity) { + + Context sessionContext = contextStore.get(spanKey); + if (sessionContext == null) { + return null; // No state found. We aren't in a Session. + } + + int depth = CallDepthThreadLocalMap.incrementCallDepth(SessionMethodUtils.class); + if (depth > 0) { + return null; // This method call is being traced already. + } + + return tracer().startSpan(sessionContext, operationNameSupplier.get(), entity); + } + + public static Context startSpanFromQuery( + ContextStore contextStore, TARGET spanKey, String query) { + Supplier operationNameSupplier = + () -> { + // set operation to default value that is used when sql sanitizer fails to extract + // operation name + String operation = "Hibernate Query"; + SqlStatementInfo info = SqlStatementSanitizer.sanitize(query); + if (info.getOperation() != null) { + operation = info.getOperation(); + if (info.getTable() != null) { + operation += " " + info.getTable(); + } + } + return operation; + }; + return startSpanFrom(contextStore, spanKey, operationNameSupplier, null); + } + + public static void end( + @Nullable Context context, Throwable throwable, String operationName, Object entity) { + + CallDepthThreadLocalMap.reset(SessionMethodUtils.class); + + if (context == null) { + return; + } + + if (operationName != null && entity != null) { + String entityName = tracer().entityName(entity); + if (entityName != null) { + Span.fromContext(context).updateName(operationName + " " + entityName); + } + } + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + + // Copies a span from the given Session ContextStore into the targetContextStore. Used to + // propagate a Span from a Session to transient Session objects such as Transaction and Query. + public static void attachSpanFromStore( + ContextStore sourceContextStore, + S source, + ContextStore targetContextStore, + T target) { + + Context sessionContext = sourceContextStore.get(source); + if (sessionContext == null) { + return; + } + + targetContextStore.putIfAbsent(target, sessionContext); + } + + private SessionMethodUtils() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/hibernate-procedure-call-4.3-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/hibernate-procedure-call-4.3-javaagent.gradle new file mode 100644 index 000000000..fcfd02dee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/hibernate-procedure-call-4.3-javaagent.gradle @@ -0,0 +1,31 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.hibernate" + module = "hibernate-core" + versions = "[4.3.0.Final,)" + assertInverse = true + } +} + +dependencies { + library "org.hibernate:hibernate-core:4.3.0.Final" + + implementation project(':instrumentation:hibernate:hibernate-common:javaagent') + + testInstrumentation project(':instrumentation:jdbc:javaagent') + // Added to ensure cross compatibility: + testInstrumentation project(':instrumentation:hibernate:hibernate-3.3:javaagent') + testInstrumentation project(':instrumentation:hibernate:hibernate-4.0:javaagent') + + testLibrary "org.hibernate:hibernate-entitymanager:4.3.0.Final" + + testImplementation "org.hsqldb:hsqldb:2.0.0" + testImplementation "javax.xml.bind:jaxb-api:2.3.1" + testImplementation "org.glassfish.jaxb:jaxb-runtime:2.3.3" + + // hibernate 6 is alpha so use 5 as latest version + latestDepTestLibrary "org.hibernate:hibernate-core:5.+" + latestDepTestLibrary "org.hibernate:hibernate-entitymanager:5.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/HibernateInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/HibernateInstrumentationModule.java new file mode 100644 index 000000000..8d26e778b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/HibernateInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_3; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class HibernateInstrumentationModule extends InstrumentationModule { + public HibernateInstrumentationModule() { + super("hibernate", "hibernate-4.3"); + } + + @Override + public List typeInstrumentations() { + return asList(new ProcedureCallInstrumentation(), new SessionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallInstrumentation.java new file mode 100644 index 000000000..57bcdb5f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallInstrumentation.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.procedure.ProcedureCall; + +public class ProcedureCallInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.procedure.ProcedureCall"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.procedure.ProcedureCall")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("getOutputs")), + ProcedureCallInstrumentation.class.getName() + "$ProcedureCallMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class ProcedureCallMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startMethod( + @Advice.This ProcedureCall call, + @Advice.Origin("#m") String name, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + ContextStore contextStore = + InstrumentationContext.get(ProcedureCall.class, Context.class); + + context = + SessionMethodUtils.startSpanFrom( + contextStore, call, "ProcedureCall." + name, call.getProcedureName()); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endMethod( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope != null) { + scope.close(); + SessionMethodUtils.end(context, throwable, null, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/SessionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/SessionInstrumentation.java new file mode 100644 index 000000000..53f6ee309 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/SessionInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.hibernate.SessionMethodUtils; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.hibernate.SharedSessionContract; +import org.hibernate.procedure.ProcedureCall; + +public class SessionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.hibernate.SharedSessionContract"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.hibernate.SharedSessionContract")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(returns(implementsInterface(named("org.hibernate.procedure.ProcedureCall")))), + SessionInstrumentation.class.getName() + "$GetProcedureCallAdvice"); + } + + @SuppressWarnings("unused") + public static class GetProcedureCallAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void getProcedureCall( + @Advice.This SharedSessionContract session, @Advice.Return ProcedureCall returned) { + + ContextStore sessionContextStore = + InstrumentationContext.get(SharedSessionContract.class, Context.class); + ContextStore returnedContextStore = + InstrumentationContext.get(ProcedureCall.class, Context.class); + + SessionMethodUtils.attachSpanFromStore( + sessionContextStore, session, returnedContextStore, returned); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/groovy/ProcedureCallTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/groovy/ProcedureCallTest.groovy new file mode 100644 index 000000000..3ced5e988 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/groovy/ProcedureCallTest.groovy @@ -0,0 +1,168 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.sql.Connection +import java.sql.DriverManager +import java.sql.Statement +import javax.persistence.ParameterMode +import org.hibernate.Session +import org.hibernate.SessionFactory +import org.hibernate.cfg.Configuration +import org.hibernate.exception.SQLGrammarException +import org.hibernate.procedure.ProcedureCall +import org.junit.Assume +import spock.lang.Shared + +class ProcedureCallTest extends AgentInstrumentationSpecification { + + @Shared + protected SessionFactory sessionFactory + + @Shared + protected List prepopulated + + def setupSpec() { + sessionFactory = new Configuration().configure().buildSessionFactory() + // Pre-populate the DB, so delete/update can be tested. + Session writer = sessionFactory.openSession() + writer.beginTransaction() + prepopulated = new ArrayList<>() + for (int i = 0; i < 2; i++) { + prepopulated.add(new Value("Hello :) " + i)) + writer.save(prepopulated.get(i)) + } + writer.getTransaction().commit() + writer.close() + + // Create a stored procedure. + Connection conn = DriverManager.getConnection("jdbc:hsqldb:mem:test", "sa", "1") + Statement stmt = conn.createStatement() + stmt.execute("CREATE PROCEDURE TEST_PROC() MODIFIES SQL DATA BEGIN ATOMIC INSERT INTO Value VALUES (420, 'fred'); END") + stmt.close() + conn.close() + } + + def cleanupSpec() { + if (sessionFactory != null) { + sessionFactory.close() + } + } + + def callProcedure(ProcedureCall call) { + try { + call.getOutputs() + } catch (Exception exception) { + // ignore failures on hibernate 6 where this functionality has not been implemented yet + Assume.assumeFalse("org.hibernate.NotYetImplementedFor6Exception" == exception.getClass().getName()) + throw exception + } + } + + def "test ProcedureCall"() { + setup: + + Session session = sessionFactory.openSession() + session.beginTransaction() + + ProcedureCall call = session.createStoredProcedureCall("TEST_PROC") + callProcedure(call) + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "ProcedureCall.getOutputs TEST_PROC" + kind INTERNAL + childOf span(0) + attributes { + } + } + span(2) { + name "test" + kind CLIENT + childOf span(1) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_STATEMENT.key}" "{call TEST_PROC()}" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + } + } + span(3) { + kind INTERNAL + name "Transaction.commit" + childOf span(0) + attributes { + } + } + } + } + } + + def "test failing ProcedureCall"() { + setup: + + Session session = sessionFactory.openSession() + session.beginTransaction() + + ProcedureCall call = session.createStoredProcedureCall("TEST_PROC") + def parameterRegistration = call.registerParameter("nonexistent", Long, ParameterMode.IN) + Assume.assumeTrue(parameterRegistration.metaClass.getMetaMethod("bindValue", Object) != null) + parameterRegistration.bindValue(420L) + try { + callProcedure(call) + } catch (Exception e) { + // We expected this. + } + + session.getTransaction().commit() + session.close() + + expect: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "Session" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "ProcedureCall.getOutputs TEST_PROC" + kind INTERNAL + childOf span(0) + status ERROR + errorEvent(SQLGrammarException, "could not prepare statement") + } + span(2) { + name "Transaction.commit" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + } +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/Value.java b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/Value.java new file mode 100644 index 000000000..a9de87173 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/Value.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NamedQuery; + +@Entity +@Table +@NamedQuery(name = "TestNamedQuery", query = "from Value") +public class Value { + + private Long id; + private String name; + + public Value() {} + + public Value(String name) { + this.name = name; + } + + @Id + @GeneratedValue(generator = "increment") + @GenericGenerator(name = "increment", strategy = "increment") + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String title) { + name = title; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/resources/hibernate.cfg.xml b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/resources/hibernate.cfg.xml new file mode 100644 index 000000000..3fb8f505c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/resources/hibernate.cfg.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + class,hbm + org.hibernate.dialect.HSQLDialect + true + org.hsqldb.jdbcDriver + sa + 1 + jdbc:hsqldb:mem:test + create + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/http-url-connection-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/http-url-connection-javaagent.gradle new file mode 100644 index 000000000..a285a4846 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/http-url-connection-javaagent.gradle @@ -0,0 +1,11 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + coreJdk() + } +} + +dependencies { + testImplementation "org.springframework:spring-web:4.3.7.RELEASE" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HeadersInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HeadersInjectAdapter.java new file mode 100644 index 000000000..b67a16b12 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HeadersInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpurlconnection; + +import io.opentelemetry.context.propagation.TextMapSetter; +import java.net.HttpURLConnection; + +public class HeadersInjectAdapter implements TextMapSetter { + + public static final HeadersInjectAdapter SETTER = new HeadersInjectAdapter(); + + @Override + public void set(HttpURLConnection carrier, String key, String value) { + carrier.setRequestProperty(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionInstrumentation.java new file mode 100644 index 000000000..1045a861b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionInstrumentation.java @@ -0,0 +1,161 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpurlconnection; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.httpurlconnection.HttpUrlConnectionTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.HttpStatusConverter; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepth; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.HttpURLConnection; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +public class HttpUrlConnectionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return nameStartsWith("java.net.") + .or(ElementMatchers.nameStartsWith("sun.net")) + // In WebLogic, URL.openConnection() returns its own internal implementation of + // HttpURLConnection, which does not delegate the methods that have to be instrumented to + // the JDK superclass. Therefore it needs to be instrumented directly. + .or(named("weblogic.net.http.HttpURLConnection")) + // This class is a simple delegator. Skip because it does not update its `connected` + // field. + .and(not(named("sun.net.www.protocol.https.HttpsURLConnectionImpl"))) + .and(extendsClass(named("java.net.HttpURLConnection"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(namedOneOf("connect", "getOutputStream", "getInputStream")), + this.getClass().getName() + "$HttpUrlConnectionAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("getResponseCode")), + this.getClass().getName() + "$GetResponseCodeAdvice"); + } + + @SuppressWarnings("unused") + public static class HttpUrlConnectionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.This HttpURLConnection connection, + @Advice.FieldValue("connected") boolean connected, + @Advice.Local("otelHttpUrlState") HttpUrlState httpUrlState, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + + callDepth = CallDepthThreadLocalMap.getCallDepth(HttpURLConnection.class); + if (callDepth.getAndIncrement() > 0) { + // only want the rest of the instrumentation rules (which are complex enough) to apply to + // top-level HttpURLConnection calls + return; + } + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + // using storage for a couple of reasons: + // - to start an operation in connect() and end it in getInputStream() + // - to avoid creating a new operation on multiple subsequent calls to getInputStream() + ContextStore storage = + InstrumentationContext.get(HttpURLConnection.class, HttpUrlState.class); + httpUrlState = storage.get(connection); + + if (httpUrlState != null) { + if (!httpUrlState.finished) { + scope = httpUrlState.context.makeCurrent(); + } + return; + } + + Context context = tracer().startSpan(parentContext, connection); + httpUrlState = new HttpUrlState(context); + storage.put(connection, httpUrlState); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.This HttpURLConnection connection, + @Advice.FieldValue("responseCode") int responseCode, + @Advice.Thrown Throwable throwable, + @Advice.Origin("#m") String methodName, + @Advice.Local("otelHttpUrlState") HttpUrlState httpUrlState, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { + return; + } + if (scope == null) { + return; + } + scope.close(); + + if (throwable != null) { + if (responseCode >= 400) { + // HttpURLConnection unnecessarily throws exception on error response. + // None of the other http clients do this, so not recording the exception on the span + // to be consistent with the telemetry for other http clients. + tracer().end(httpUrlState.context, new HttpUrlResponse(connection, responseCode)); + } else { + tracer().endExceptionally(httpUrlState.context, throwable); + } + httpUrlState.finished = true; + } else if (methodName.equals("getInputStream") && responseCode > 0) { + // responseCode field is sometimes not populated. + // We can't call getResponseCode() due to some unwanted side-effects + // (e.g. breaks getOutputStream). + tracer().end(httpUrlState.context, new HttpUrlResponse(connection, responseCode)); + httpUrlState.finished = true; + } + } + } + + @SuppressWarnings("unused") + public static class GetResponseCodeAdvice { + + @Advice.OnMethodExit + public static void methodExit( + @Advice.This HttpURLConnection connection, @Advice.Return int returnValue) { + + ContextStore storage = + InstrumentationContext.get(HttpURLConnection.class, HttpUrlState.class); + HttpUrlState httpUrlState = storage.get(connection); + if (httpUrlState != null) { + Span span = Java8BytecodeBridge.spanFromContext(httpUrlState.context); + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, returnValue); + StatusCode statusCode = HttpStatusConverter.statusFromHttpStatus(returnValue); + if (statusCode != StatusCode.UNSET) { + span.setStatus(statusCode); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionInstrumentationModule.java new file mode 100644 index 000000000..33d574ae6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpurlconnection; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class HttpUrlConnectionInstrumentationModule extends InstrumentationModule { + + public HttpUrlConnectionInstrumentationModule() { + super("http-url-connection"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpUrlConnectionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTracer.java b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTracer.java new file mode 100644 index 000000000..9c880f4a5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTracer.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpurlconnection; + +import static io.opentelemetry.javaagent.instrumentation.httpurlconnection.HeadersInjectAdapter.SETTER; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; + +public class HttpUrlConnectionTracer + extends HttpClientTracer { + + private static final HttpUrlConnectionTracer TRACER = new HttpUrlConnectionTracer(); + + private HttpUrlConnectionTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static HttpUrlConnectionTracer tracer() { + return TRACER; + } + + public Context startSpan(Context parentContext, HttpURLConnection request) { + return super.startSpan(parentContext, request, request); + } + + @Override + protected String method(HttpURLConnection connection) { + return connection.getRequestMethod(); + } + + @Override + protected URI url(HttpURLConnection connection) throws URISyntaxException { + return connection.getURL().toURI(); + } + + @Override + protected Integer status(HttpUrlResponse response) { + return response.status(); + } + + @Override + protected String requestHeader(HttpURLConnection httpUrlConnection, String name) { + return httpUrlConnection.getRequestProperty(name); + } + + @Override + protected String responseHeader(HttpUrlResponse response, String name) { + return response.header(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.http-url-connection"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlResponse.java b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlResponse.java new file mode 100644 index 000000000..53705d752 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlResponse.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpurlconnection; + +import java.net.HttpURLConnection; + +public class HttpUrlResponse { + private final HttpURLConnection connection; + private final int resolvedResponseCode; + + public HttpUrlResponse(HttpURLConnection connection, int resolvedResponseCode) { + this.connection = connection; + this.resolvedResponseCode = resolvedResponseCode; + } + + int status() { + return resolvedResponseCode; + } + + String header(String name) { + return connection.getHeaderField(name); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlState.java b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlState.java new file mode 100644 index 000000000..c0e095cff --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlState.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpurlconnection; + +import io.opentelemetry.context.Context; + +// everything is public since called directly from advice code +// (which is inlined into other packages) +public class HttpUrlState { + public final Context context; + public boolean finished; + + public HttpUrlState(Context context) { + this.context = context; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionResponseCodeOnlyTest.groovy b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionResponseCodeOnlyTest.groovy new file mode 100644 index 000000000..fe598969e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionResponseCodeOnlyTest.groovy @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest + +class HttpUrlConnectionResponseCodeOnlyTest extends HttpClientTest implements AgentTestTrait { + + @Override + HttpURLConnection buildRequest(String method, URI uri, Map headers) { + return uri.toURL().openConnection() as HttpURLConnection + } + + @Override + int sendRequest(HttpURLConnection connection, String method, URI uri, Map headers) { + try { + connection.setRequestMethod(method) + connection.connectTimeout = CONNECT_TIMEOUT_MS + headers.each { connection.setRequestProperty(it.key, it.value) } + connection.setRequestProperty("Connection", "close") + return connection.getResponseCode() + } finally { + connection.disconnect() + } + } + + @Override + int maxRedirects() { + 20 + } + + @Override + Integer responseCodeOnRedirectError() { + return 302 + } + + @Override + boolean testReusedRequest() { + // HttpURLConnection can't be reused + return false + } + + @Override + boolean testCallback() { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionTest.groovy new file mode 100644 index 000000000..b48bca51a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionTest.groovy @@ -0,0 +1,266 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import spock.lang.Requires +import spock.lang.Unroll +import sun.net.www.protocol.https.HttpsURLConnectionImpl + +class HttpUrlConnectionTest extends HttpClientTest implements AgentTestTrait { + + static final RESPONSE = "Hello." + static final STATUS = 200 + + @Override + HttpURLConnection buildRequest(String method, URI uri, Map headers) { + return uri.toURL().openConnection() as HttpURLConnection + } + + @Override + int sendRequest(HttpURLConnection connection, String method, URI uri, Map headers) { + try { + connection.setRequestMethod(method) + headers.each { connection.setRequestProperty(it.key, it.value) } + connection.setRequestProperty("Connection", "close") + connection.useCaches = true + connection.connectTimeout = CONNECT_TIMEOUT_MS + def parentSpan = Span.current() + def stream = connection.inputStream + assert Span.current() == parentSpan + stream.readLines() + stream.close() + return connection.getResponseCode() + } finally { + connection.disconnect() + } + } + + @Override + int maxRedirects() { + 20 + } + + @Override + Integer responseCodeOnRedirectError() { + return 302 + } + + @Override + boolean testReusedRequest() { + // HttpURLConnection can't be reused + return false + } + + @Override + boolean testCallback() { + return false + } + + @Unroll + def "trace request (useCaches: #useCaches)"() { + setup: + def url = resolveAddress("/success").toURL() + runUnderTrace("someTrace") { + HttpURLConnection connection = url.openConnection() + connection.useCaches = useCaches + assert Span.current().getSpanContext().isValid() + def stream = connection.inputStream + def lines = stream.readLines() + stream.close() + assert connection.getResponseCode() == STATUS + assert lines == [RESPONSE] + + // call again to ensure the cycling is ok + connection = url.openConnection() + connection.useCaches = useCaches + assert Span.current().getSpanContext().isValid() + // call before input stream to test alternate behavior + assert connection.getResponseCode() == STATUS + connection.inputStream + stream = connection.inputStream // one more to ensure state is working + lines = stream.readLines() + stream.close() + assert lines == [RESPONSE] + } + + expect: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "someTrace" + hasNoParent() + attributes { + } + } + span(1) { + name "HTTP GET" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" "$url" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" STATUS + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + } + } + span(2) { + name "test-http-server" + kind SERVER + childOf span(1) + attributes { + } + } + span(3) { + name "HTTP GET" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" "$url" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" STATUS + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + } + } + span(4) { + name "test-http-server" + kind SERVER + childOf span(3) + attributes { + } + } + } + } + + where: + useCaches << [false, true] + } + + def "test broken API usage"() { + setup: + def url = resolveAddress("/success").toURL() + HttpURLConnection connection = runUnderTrace("someTrace") { + HttpURLConnection connection = url.openConnection() + connection.setRequestProperty("Connection", "close") + assert Span.current().getSpanContext().isValid() + assert connection.getResponseCode() == STATUS + return connection + } + + expect: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "someTrace" + hasNoParent() + attributes { + } + } + span(1) { + name "HTTP GET" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.HTTP_URL.key}" "$url" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" STATUS + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + } + } + serverSpan(it, 2, span(1)) + } + } + + cleanup: + connection.disconnect() + + where: + iteration << (1..10) + } + + def "test post request"() { + setup: + def url = resolveAddress("/success").toURL() + runUnderTrace("someTrace") { + HttpURLConnection connection = url.openConnection() + connection.setRequestMethod("POST") + + String urlParameters = "q=ASDF&w=&e=&r=12345&t=" + + // Send post request + connection.setDoOutput(true) + DataOutputStream wr = new DataOutputStream(connection.getOutputStream()) + wr.writeBytes(urlParameters) + wr.flush() + wr.close() + + assert connection.getResponseCode() == STATUS + + def stream = connection.inputStream + def lines = stream.readLines() + stream.close() + assert lines == [RESPONSE] + } + + expect: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "someTrace" + hasNoParent() + attributes { + } + } + span(1) { + name "HTTP POST" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" server.httpPort() + "${SemanticAttributes.HTTP_URL.key}" "$url" + "${SemanticAttributes.HTTP_METHOD.key}" "POST" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" STATUS + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + } + } + span(2) { + name "test-http-server" + kind SERVER + childOf span(1) + attributes { + } + } + } + } + } + + // This test makes no sense on IBM JVM because there is no HttpsURLConnectionImpl class there + @Requires({ !System.getProperty("java.vm.name").contains("IBM J9 VM") }) + def "Make sure we can load HttpsURLConnectionImpl"() { + when: + def instance = new HttpsURLConnectionImpl(null, null, null) + + then: + instance != null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionUseCachesFalseTest.groovy b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionUseCachesFalseTest.groovy new file mode 100644 index 000000000..6c3d2360b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionUseCachesFalseTest.groovy @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest + +class HttpUrlConnectionUseCachesFalseTest extends HttpClientTest implements AgentTestTrait { + + @Override + HttpURLConnection buildRequest(String method, URI uri, Map headers) { + return uri.toURL().openConnection() as HttpURLConnection + } + + @Override + int sendRequest(HttpURLConnection connection, String method, URI uri, Map headers) { + try { + connection.setRequestMethod(method) + headers.each { connection.setRequestProperty(it.key, it.value) } + connection.setRequestProperty("Connection", "close") + connection.useCaches = false + connection.connectTimeout = CONNECT_TIMEOUT_MS + def parentSpan = Span.current() + def stream = connection.inputStream + assert Span.current() == parentSpan + stream.readLines() + stream.close() + return connection.getResponseCode() + } finally { + connection.disconnect() + } + } + + @Override + int maxRedirects() { + 20 + } + + @Override + Integer responseCodeOnRedirectError() { + return 302 + } + + @Override + boolean testReusedRequest() { + // HttpURLConnection can't be reused + return false + } + + @Override + boolean testCallback() { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/SpringRestTemplateTest.groovy b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/SpringRestTemplateTest.groovy new file mode 100644 index 000000000..cba4b37eb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/SpringRestTemplateTest.groovy @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.client.ClientHttpRequestFactory +import org.springframework.http.client.SimpleClientHttpRequestFactory +import org.springframework.web.client.ResourceAccessException +import org.springframework.web.client.RestTemplate +import spock.lang.Shared + +class SpringRestTemplateTest extends HttpClientTest> implements AgentTestTrait { + + @Shared + ClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory() + @Shared + RestTemplate restTemplate = new RestTemplate(factory) + + def setupSpec() { + factory.connectTimeout = CONNECT_TIMEOUT_MS + } + + @Override + HttpEntity buildRequest(String method, URI uri, Map headers) { + def httpHeaders = new HttpHeaders() + headers.each { httpHeaders.put(it.key, [it.value]) } + return new HttpEntity(httpHeaders) + } + + @Override + int sendRequest(HttpEntity request, String method, URI uri, Map headers) { + try { + return restTemplate.exchange(uri, HttpMethod.valueOf(method), request, String) + .statusCode + .value() + } catch (ResourceAccessException exception) { + throw exception.getCause() + } + } + + @Override + void sendRequestWithCallback(HttpEntity request, String method, URI uri, Map headers = [:], RequestResult requestResult) { + try { + restTemplate.execute(uri, HttpMethod.valueOf(method), { req -> + req.getHeaders().putAll(request.getHeaders()) + }, { response -> + requestResult.complete(response.statusCode.value()) + }) + } catch (ResourceAccessException exception) { + requestResult.complete(exception.getCause()) + } + } + + @Override + int maxRedirects() { + 20 + } + + @Override + Integer responseCodeOnRedirectError() { + return 302 + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/UrlConnectionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/UrlConnectionTest.groovy new file mode 100644 index 000000000..2225f0be9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/http-url-connection/javaagent/src/test/groovy/UrlConnectionTest.groovy @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class UrlConnectionTest extends AgentInstrumentationSpecification { + + def "trace request with connection failure #scheme"() { + when: + runUnderTrace("someTrace") { + URLConnection connection = url.openConnection() + connection.setConnectTimeout(10000) + connection.setReadTimeout(10000) + assert Span.current() != null + connection.inputStream + } + + then: + thrown ConnectException + + expect: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "someTrace" + hasNoParent() + status ERROR + errorEvent ConnectException, String + } + span(1) { + name "HTTP GET" + kind CLIENT + childOf span(0) + status ERROR + errorEvent ConnectException, String + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" UNUSABLE_PORT + "${SemanticAttributes.HTTP_URL.key}" "$url" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + } + } + } + } + + where: + scheme << ["http", "https"] + + url = new URI("$scheme://localhost:$UNUSABLE_PORT").toURL() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/hystrix-1.4-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/hystrix-1.4-javaagent.gradle new file mode 100644 index 000000000..e1fb82e7b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/hystrix-1.4-javaagent.gradle @@ -0,0 +1,27 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.netflix.hystrix" + module = 'hystrix-core' + versions = "[1.4.0,)" + } +} + +dependencies { + implementation project(':instrumentation:rxjava:rxjava-1.0:library') + + library "com.netflix.hystrix:hystrix-core:1.4.0" + library "io.reactivex:rxjava:1.0.7" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.hystrix.experimental-span-attributes=true" + // Disable so failure testing below doesn't inadvertently change the behavior. + jvmArgs "-Dhystrix.command.default.circuitBreaker.enabled=false" + jvmArgs "-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=false" + + // Uncomment for debugging: + // jvmArgs "-Dhystrix.command.default.execution.timeout.enabled=false" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixCommandInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixCommandInstrumentation.java new file mode 100644 index 000000000..98b6b15f2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixCommandInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hystrix; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.netflix.hystrix.HystrixInvokableInfo; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import rx.Observable; + +public class HystrixCommandInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed( + "com.netflix.hystrix.HystrixCommand", "com.netflix.hystrix.HystrixObservableCommand"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass( + namedOneOf( + "com.netflix.hystrix.HystrixCommand", "com.netflix.hystrix.HystrixObservableCommand")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("getExecutionObservable").and(returns(named("rx.Observable"))), + this.getClass().getName() + "$ExecuteAdvice"); + transformer.applyAdviceToMethod( + named("getFallbackObservable").and(returns(named("rx.Observable"))), + this.getClass().getName() + "$FallbackAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.This HystrixInvokableInfo command, + @Advice.Return(readOnly = false) Observable result, + @Advice.Thrown Throwable throwable) { + + result = Observable.create(new HystrixOnSubscribe<>(result, command, "execute")); + } + } + + @SuppressWarnings("unused") + public static class FallbackAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.This HystrixInvokableInfo command, + @Advice.Return(readOnly = false) Observable result, + @Advice.Thrown Throwable throwable) { + + result = Observable.create(new HystrixOnSubscribe<>(result, command, "fallback")); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixInstrumentationModule.java new file mode 100644 index 000000000..e60c6cc0e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hystrix; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class HystrixInstrumentationModule extends InstrumentationModule { + + public HystrixInstrumentationModule() { + super("hystrix", "hystrix-1.4"); + } + + @Override + public boolean isHelperClass(String className) { + return className.equals("rx.__OpenTelemetryTracingUtil"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HystrixCommandInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixOnSubscribe.java b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixOnSubscribe.java new file mode 100644 index 000000000..27f20b166 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixOnSubscribe.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hystrix; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.javaagent.instrumentation.hystrix.HystrixTracer.tracer; + +import com.netflix.hystrix.HystrixInvokableInfo; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.rxjava.TracedOnSubscribe; +import rx.Observable; + +public class HystrixOnSubscribe extends TracedOnSubscribe { + private static final String OPERATION_NAME = "hystrix.cmd"; + + private final HystrixInvokableInfo command; + private final String methodName; + + public HystrixOnSubscribe( + Observable originalObservable, HystrixInvokableInfo command, String methodName) { + super(originalObservable, OPERATION_NAME, tracer(), INTERNAL); + + this.command = command; + this.methodName = methodName; + } + + @Override + protected void decorateSpan(Span span) { + tracer().onCommand(span, command, methodName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixTracer.java b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixTracer.java new file mode 100644 index 000000000..94ea990c9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixTracer.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hystrix; + +import com.netflix.hystrix.HystrixInvokableInfo; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; + +public class HystrixTracer extends BaseTracer { + private static final HystrixTracer TRACER = new HystrixTracer(); + + public static HystrixTracer tracer() { + return TRACER; + } + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.hystrix.experimental-span-attributes", false); + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.hystrix-1.4"; + } + + public void onCommand(Span span, HystrixInvokableInfo command, String methodName) { + if (command != null) { + String commandName = command.getCommandKey().name(); + String groupName = command.getCommandGroup().name(); + boolean circuitOpen = command.isCircuitBreakerOpen(); + + String spanName = groupName + "." + commandName + "." + methodName; + + span.updateName(spanName); + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("hystrix.command", commandName); + span.setAttribute("hystrix.group", groupName); + span.setAttribute("hystrix.circuit-open", circuitOpen); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableChainTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableChainTest.groovy new file mode 100644 index 000000000..e3f23813b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableChainTest.groovy @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static com.netflix.hystrix.HystrixCommandGroupKey.Factory.asKey +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.netflix.hystrix.HystrixObservableCommand +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import rx.Observable +import rx.schedulers.Schedulers + +class HystrixObservableChainTest extends AgentInstrumentationSpecification { + + def "test command #action"() { + setup: + + def result = runUnderTrace("parent") { + def val = new HystrixObservableCommand(asKey("ExampleGroup")) { + private String tracedMethod() { + runInternalSpan("tracedMethod") + return "Hello" + } + + @Override + protected Observable construct() { + Observable.defer { + Observable.just(tracedMethod()) + } + .subscribeOn(Schedulers.immediate()) + } + }.toObservable() + .subscribeOn(Schedulers.io()) + .map { + it.toUpperCase() + }.flatMap { str -> + new HystrixObservableCommand(asKey("OtherGroup")) { + private String anotherTracedMethod() { + runInternalSpan("anotherTracedMethod") + return "$str!" + } + + @Override + protected Observable construct() { + Observable.defer { + Observable.just(anotherTracedMethod()) + } + .subscribeOn(Schedulers.computation()) + } + }.toObservable() + .subscribeOn(Schedulers.trampoline()) + }.toBlocking().first() + return val + } + + expect: + result == "HELLO!" + + assertTraces(1) { + trace(0, 5) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "ExampleGroup.HystrixObservableChainTest\$1.execute" + childOf span(0) + attributes { + "hystrix.command" "HystrixObservableChainTest\$1" + "hystrix.group" "ExampleGroup" + "hystrix.circuit-open" false + } + } + span(2) { + name "tracedMethod" + childOf span(1) + attributes { + } + } + span(3) { + name "OtherGroup.HystrixObservableChainTest\$2.execute" + childOf span(1) + attributes { + "hystrix.command" "HystrixObservableChainTest\$2" + "hystrix.group" "OtherGroup" + "hystrix.circuit-open" false + } + } + span(4) { + name "anotherTracedMethod" + childOf span(3) + attributes { + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableTest.groovy new file mode 100644 index 000000000..913f2a64d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableTest.groovy @@ -0,0 +1,308 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static com.netflix.hystrix.HystrixCommandGroupKey.Factory.asKey +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.netflix.hystrix.HystrixObservable +import com.netflix.hystrix.HystrixObservableCommand +import com.netflix.hystrix.exception.HystrixRuntimeException +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import rx.Observable +import rx.schedulers.Schedulers + +class HystrixObservableTest extends AgentInstrumentationSpecification { + + def "test command #action"() { + setup: + def observeOnFn = observeOn + def subscribeOnFn = subscribeOn + def result = runUnderTrace("parent") { + def val = operation new HystrixObservableCommand(asKey("ExampleGroup")) { + private String tracedMethod() { + runInternalSpan("tracedMethod") + return "Hello!" + } + + @Override + protected Observable construct() { + def obs = Observable.defer { + Observable.just(tracedMethod()).repeat(1) + } + if (observeOnFn) { + obs = obs.observeOn(observeOnFn) + } + if (subscribeOnFn) { + obs = obs.subscribeOn(subscribeOnFn) + } + return obs + } + } + return val + } + + expect: + result == "Hello!" + + assertTraces(1) { + trace(0, 3) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "ExampleGroup.HystrixObservableTest\$1.execute" + childOf span(0) + attributes { + "hystrix.command" "HystrixObservableTest\$1" + "hystrix.group" "ExampleGroup" + "hystrix.circuit-open" false + } + } + span(2) { + name "tracedMethod" + childOf span(1) + attributes { + } + } + } + } + + where: + action | observeOn | subscribeOn | operation + "toObservable" | null | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-I" | Schedulers.immediate() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-T" | Schedulers.trampoline() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-C" | Schedulers.computation() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-IO" | Schedulers.io() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-NT" | Schedulers.newThread() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+I" | null | Schedulers.immediate() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+T" | null | Schedulers.trampoline() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+C" | null | Schedulers.computation() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+IO" | null | Schedulers.io() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+NT" | null | Schedulers.newThread() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "observe" | null | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-I" | Schedulers.immediate() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-T" | Schedulers.trampoline() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-C" | Schedulers.computation() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-IO" | Schedulers.io() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-NT" | Schedulers.newThread() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+I" | null | Schedulers.immediate() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+T" | null | Schedulers.trampoline() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+C" | null | Schedulers.computation() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+IO" | null | Schedulers.io() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+NT" | null | Schedulers.newThread() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "toObservable block" | Schedulers.computation() | Schedulers.newThread() | { HystrixObservable cmd -> + BlockingQueue queue = new LinkedBlockingQueue() + def subscription = cmd.toObservable().subscribe { next -> + queue.put(next) + } + def val = queue.take() + subscription.unsubscribe() + return val + } + } + + def "test command #action fallback"() { + setup: + def observeOnFn = observeOn + def subscribeOnFn = subscribeOn + def result = runUnderTrace("parent") { + def val = operation new HystrixObservableCommand(asKey("ExampleGroup")) { + @Override + protected Observable construct() { + def err = Observable.defer { + Observable.error(new IllegalArgumentException()).repeat(1) + } + if (observeOnFn) { + err = err.observeOn(observeOnFn) + } + if (subscribeOnFn) { + err = err.subscribeOn(subscribeOnFn) + } + return err + } + + protected Observable resumeWithFallback() { + return Observable.just("Fallback!").repeat(1) + } + } + return val + } + + expect: + result == "Fallback!" + + assertTraces(1) { + trace(0, 3) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "ExampleGroup.HystrixObservableTest\$2.execute" + childOf span(0) + status ERROR + errorEvent(IllegalArgumentException) + attributes { + "hystrix.command" "HystrixObservableTest\$2" + "hystrix.group" "ExampleGroup" + "hystrix.circuit-open" false + } + } + span(2) { + name "ExampleGroup.HystrixObservableTest\$2.fallback" + childOf span(1) + attributes { + "hystrix.command" "HystrixObservableTest\$2" + "hystrix.group" "ExampleGroup" + "hystrix.circuit-open" false + } + } + } + } + + where: + action | observeOn | subscribeOn | operation + "toObservable" | null | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-I" | Schedulers.immediate() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-T" | Schedulers.trampoline() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-C" | Schedulers.computation() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-IO" | Schedulers.io() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-NT" | Schedulers.newThread() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+I" | null | Schedulers.immediate() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+T" | null | Schedulers.trampoline() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+C" | null | Schedulers.computation() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+IO" | null | Schedulers.io() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+NT" | null | Schedulers.newThread() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "observe" | null | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-I" | Schedulers.immediate() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-T" | Schedulers.trampoline() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-C" | Schedulers.computation() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-IO" | Schedulers.io() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-NT" | Schedulers.newThread() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+I" | null | Schedulers.immediate() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+T" | null | Schedulers.trampoline() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+C" | null | Schedulers.computation() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+IO" | null | Schedulers.io() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+NT" | null | Schedulers.newThread() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "toObservable block" | null | null | { HystrixObservable cmd -> + BlockingQueue queue = new LinkedBlockingQueue() + def subscription = cmd.toObservable().subscribe { next -> + queue.put(next) + } + def val = queue.take() + subscription.unsubscribe() + return val + } + } + + def "test no fallback results in error for #action"() { + setup: + def observeOnFn = observeOn + def subscribeOnFn = subscribeOn + + when: + runUnderTrace("parent") { + operation new HystrixObservableCommand(asKey("FailingGroup")) { + + @Override + protected Observable construct() { + def err = Observable.defer { + Observable.error(new IllegalArgumentException()) + } + if (observeOnFn) { + err = err.observeOn(observeOnFn) + } + if (subscribeOnFn) { + err = err.subscribeOn(subscribeOnFn) + } + return err + } + } + } + + then: + def err = thrown HystrixRuntimeException + err.cause instanceof IllegalArgumentException + + assertTraces(1) { + trace(0, 3) { + span(0) { + name "parent" + hasNoParent() + status ERROR + errorEvent(HystrixRuntimeException, "HystrixObservableTest\$3 failed and no fallback available.") + } + span(1) { + name "FailingGroup.HystrixObservableTest\$3.execute" + childOf span(0) + status ERROR + errorEvent(IllegalArgumentException) + attributes { + "hystrix.command" "HystrixObservableTest\$3" + "hystrix.group" "FailingGroup" + "hystrix.circuit-open" false + } + } + span(2) { + name "FailingGroup.HystrixObservableTest\$3.fallback" + childOf span(1) + status ERROR + errorEvent(UnsupportedOperationException, "No fallback available.") + attributes { + "hystrix.command" "HystrixObservableTest\$3" + "hystrix.group" "FailingGroup" + "hystrix.circuit-open" false + } + } + } + } + + where: + action | observeOn | subscribeOn | operation + "toObservable" | null | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-I" | Schedulers.immediate() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-T" | Schedulers.trampoline() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-C" | Schedulers.computation() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-IO" | Schedulers.io() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable-NT" | Schedulers.newThread() | null | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+I" | null | Schedulers.immediate() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+T" | null | Schedulers.trampoline() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+C" | null | Schedulers.computation() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+IO" | null | Schedulers.io() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "toObservable+NT" | null | Schedulers.newThread() | { HystrixObservable cmd -> cmd.toObservable().toBlocking().first() } + "observe" | null | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-I" | Schedulers.immediate() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-T" | Schedulers.trampoline() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-C" | Schedulers.computation() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-IO" | Schedulers.io() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe-NT" | Schedulers.newThread() | null | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+I" | null | Schedulers.immediate() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+T" | null | Schedulers.trampoline() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+C" | null | Schedulers.computation() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+IO" | null | Schedulers.io() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "observe+NT" | null | Schedulers.newThread() | { HystrixObservable cmd -> cmd.observe().toBlocking().first() } + "toObservable block" | Schedulers.computation() | Schedulers.newThread() | { HystrixObservable cmd -> + def queue = new LinkedBlockingQueue() + def subscription = cmd.toObservable().subscribe({ next -> + queue.put(new Exception("Unexpectedly got a next")) + }, { next -> + queue.put(next) + }) + Throwable ex = queue.take() + subscription.unsubscribe() + throw ex + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixTest.groovy b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixTest.groovy new file mode 100644 index 000000000..2c887a45a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixTest.groovy @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static com.netflix.hystrix.HystrixCommandGroupKey.Factory.asKey +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.netflix.hystrix.HystrixCommand +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue + +class HystrixTest extends AgentInstrumentationSpecification { + + def "test command #action"() { + setup: + def command = new HystrixCommand(asKey("ExampleGroup")) { + @Override + protected String run() throws Exception { + return tracedMethod() + } + + private String tracedMethod() { + runInternalSpan("tracedMethod") + return "Hello!" + } + } + def result = runUnderTrace("parent") { + operation(command) + } + expect: + result == "Hello!" + + assertTraces(1) { + trace(0, 3) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "ExampleGroup.HystrixTest\$1.execute" + childOf span(0) + attributes { + "hystrix.command" "HystrixTest\$1" + "hystrix.group" "ExampleGroup" + "hystrix.circuit-open" false + } + } + span(2) { + name "tracedMethod" + childOf span(1) + attributes { + } + } + } + } + + where: + action | operation + "execute" | { HystrixCommand cmd -> cmd.execute() } + "queue" | { HystrixCommand cmd -> cmd.queue().get() } + "toObservable" | { HystrixCommand cmd -> cmd.toObservable().toBlocking().first() } + "observe" | { HystrixCommand cmd -> cmd.observe().toBlocking().first() } + "observe block" | { HystrixCommand cmd -> + BlockingQueue queue = new LinkedBlockingQueue() + cmd.observe().subscribe { next -> + queue.put(next) + } + queue.take() + } + } + + def "test command #action fallback"() { + setup: + def command = new HystrixCommand(asKey("ExampleGroup")) { + @Override + protected String run() throws Exception { + throw new IllegalArgumentException() + } + + protected String getFallback() { + return "Fallback!" + } + } + def result = runUnderTrace("parent") { + operation(command) + } + expect: + result == "Fallback!" + + assertTraces(1) { + trace(0, 3) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "ExampleGroup.HystrixTest\$2.execute" + childOf span(0) + status ERROR + errorEvent(IllegalArgumentException) + attributes { + "hystrix.command" "HystrixTest\$2" + "hystrix.group" "ExampleGroup" + "hystrix.circuit-open" false + } + } + span(2) { + name "ExampleGroup.HystrixTest\$2.fallback" + childOf span(1) + attributes { + "hystrix.command" "HystrixTest\$2" + "hystrix.group" "ExampleGroup" + "hystrix.circuit-open" false + } + } + } + } + + where: + action | operation + "execute" | { HystrixCommand cmd -> cmd.execute() } + "queue" | { HystrixCommand cmd -> cmd.queue().get() } + "toObservable" | { HystrixCommand cmd -> cmd.toObservable().toBlocking().first() } + "observe" | { HystrixCommand cmd -> cmd.observe().toBlocking().first() } + "observe block" | { HystrixCommand cmd -> + BlockingQueue queue = new LinkedBlockingQueue() + cmd.observe().subscribe { next -> + queue.put(next) + } + queue.take() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/instrumentation.gradle b/opentelemetry-java-instrumentation/instrumentation/instrumentation.gradle new file mode 100644 index 000000000..87406d5e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/instrumentation.gradle @@ -0,0 +1,57 @@ +// this project will run in isolation under the agent's classloader +plugins { + id "otel.shadow-conventions" +} +apply plugin: "otel.java-conventions" + +Project instr_project = project +subprojects { + afterEvaluate { Project subProj -> + if (subProj.getPlugins().hasPlugin('java')) { + // Make it so all instrumentation subproject tests can be run with a single command. + instr_project.tasks.named('test').configure { + dependsOn(subProj.tasks.test) + } + + if (subProj.name == 'javaagent') { + instr_project.dependencies { + implementation(project(subProj.getPath())) + } + } + } + } +} + +dependencies { + compileOnly project(':instrumentation-api') + compileOnly project(':javaagent-api') + implementation project(':javaagent-tooling') + implementation project(':javaagent-extension-api') +} + +configurations { + // exclude bootstrap dependencies from shadowJar + implementation.exclude group: 'org.slf4j' + implementation.exclude group: 'run.mone', module: 'opentelemetry-api' +} +shadowJar { + duplicatesStrategy = DuplicatesStrategy.FAIL + + dependencies { + //These classes are added to bootstrap classloader by javaagent module + exclude(project(':javaagent-bootstrap')) + exclude(project(':instrumentation-api')) + exclude(project(':javaagent-api')) + } +} + +tasks.register("listInstrumentations") { + group = "Help" + description = "List all available instrumentation modules" + doFirst { + subprojects + .findAll { it.plugins.hasPlugin("muzzle") } + .collect { it.path } + .each { println it } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/internal-class-loader-javaagent-integration-tests.gradle b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/internal-class-loader-javaagent-integration-tests.gradle new file mode 100644 index 000000000..ee94ea1d9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/internal-class-loader-javaagent-integration-tests.gradle @@ -0,0 +1,9 @@ +ext.skipPublish = true + +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + testImplementation "org.apache.commons:commons-lang3:3.12.0" + + testInstrumentation project(":instrumentation:internal:internal-class-loader:javaagent") +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java new file mode 100644 index 000000000..5862434d7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package instrumentation; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class TestInstrumentationModule extends InstrumentationModule { + public TestInstrumentationModule() { + super("test-instrumentation"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new TestTypeInstrumentation()); + } + + @Override + public List helperResourceNames() { + return singletonList("test-resources/test-resource.txt"); + } + + public static class TestTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.commons.lang3.SystemUtils"); + } + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.commons.lang3.SystemUtils"); + } + + @Override + public void transform(TypeTransformer transformer) { + // Nothing to transform, this type instrumentation is only used for injecting resources. + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/resources/test-resources/test-resource.txt b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/resources/test-resources/test-resource.txt new file mode 100644 index 000000000..cd0875583 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/resources/test-resources/test-resource.txt @@ -0,0 +1 @@ +Hello world! diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/test/groovy/ResourceInjectionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/test/groovy/ResourceInjectionTest.groovy new file mode 100644 index 000000000..b6ea22f10 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/test/groovy/ResourceInjectionTest.groovy @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.GcUtils.awaitGc + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicReference +import org.apache.commons.lang3.SystemUtils + +class ResourceInjectionTest extends AgentInstrumentationSpecification { + + def "resources injected to non-delegating classloader"() { + setup: + String resourceName = 'test-resources/test-resource.txt' + URL[] urls = [ SystemUtils.getProtectionDomain().getCodeSource().getLocation() ] + AtomicReference emptyLoader = new AtomicReference<>(new URLClassLoader(urls, (ClassLoader) null)) + + when: + def resourceUrls = emptyLoader.get().getResources(resourceName) + then: + !resourceUrls.hasMoreElements() + + when: + URLClassLoader notInjectedLoader = new URLClassLoader(urls, (ClassLoader) null) + + // this triggers resource injection + emptyLoader.get().loadClass(SystemUtils.getName()) + + resourceUrls = emptyLoader.get().getResources(resourceName) + + then: + resourceUrls.hasMoreElements() + resourceUrls.nextElement().openStream().text.trim() == 'Hello world!' + + !notInjectedLoader.getResources(resourceName).hasMoreElements() + + when: "references to emptyLoader are gone" + emptyLoader.get().close() // cleanup + def ref = new WeakReference(emptyLoader.get()) + emptyLoader.set(null) + + awaitGc(ref) + + then: "HelperInjector doesn't prevent it from being collected" + null == ref.get() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/internal-class-loader-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/internal-class-loader-javaagent.gradle new file mode 100644 index 000000000..024c6a83b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/internal-class-loader-javaagent.gradle @@ -0,0 +1,21 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly project(':javaagent-bootstrap') + compileOnly project(':javaagent-tooling') + + testImplementation project(':javaagent-bootstrap') + + // This is the earliest version that has org.apache.catalina.loader.ParallelWebappClassLoader + // which is used in the test + testLibrary "org.apache.tomcat:tomcat-catalina:8.0.14" + + testImplementation "org.jboss.modules:jboss-modules:1.3.10.Final" + + // TODO: we should separate core and Eclipse tests at some point, + // but right now core-specific tests are quite dumb and are run with + // core version provided by Eclipse implementation. + //testImplementation "org.osgi:org.osgi.core:4.0.0" + testImplementation "org.eclipse.platform:org.eclipse.osgi:3.13.200" + testImplementation "org.apache.felix:org.apache.felix.framework:6.0.2" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentation.java new file mode 100644 index 000000000..c2d0e764d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentation.java @@ -0,0 +1,142 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.classloader; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.internal.BootstrapPackagePrefixesHolder; +import io.opentelemetry.javaagent.tooling.Constants; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/* + * Some class loaders do not delegate to their parent, so classes in those class loaders + * will not be able to see classes in the bootstrap class loader. + * + * In particular, instrumentation on classes in those class loaders will not be able to see + * the shaded OpenTelemetry API classes in the bootstrap class loader. + * + * This instrumentation forces all class loaders to delegate to the bootstrap class loader + * for the classes that we have put in the bootstrap class loader. + */ +public class ClassLoaderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + // just an optimization to exclude common class loaders that are known to delegate to the + // bootstrap loader (or happen to _be_ the bootstrap loader) + return not(namedOneOf( + "java.lang.ClassLoader", + "com.ibm.oti.vm.BootstrapClassLoader", + "io.opentelemetry.javaagent.instrumentation.api.AgentClassLoader")) + .and(extendsClass(named("java.lang.ClassLoader"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("loadClass")) + .and( + takesArguments(1) + .and(takesArgument(0, String.class)) + .or( + takesArguments(2) + .and(takesArgument(0, String.class)) + .and(takesArgument(1, boolean.class)))) + .and(isPublic().or(isProtected())) + .and(not(isStatic())), + ClassLoaderInstrumentation.class.getName() + "$LoadClassAdvice"); + } + + public static class Holder { + public static final List bootstrapPackagesPrefixes = findBootstrapPackagePrefixes(); + + /** + * We have to make sure that {@link BootstrapPackagePrefixesHolder} is loaded from bootstrap + * classloader. After that we can use in {@link LoadClassAdvice}. + */ + private static List findBootstrapPackagePrefixes() { + try { + Class holderClass = + Class.forName( + "io.opentelemetry.javaagent.instrumentation.api.internal.BootstrapPackagePrefixesHolder", + true, + null); + MethodHandle methodHandle = + MethodHandles.publicLookup() + .findStatic( + holderClass, "getBoostrapPackagePrefixes", MethodType.methodType(List.class)); + //noinspection unchecked + return (List) methodHandle.invokeExact(); + } catch (Throwable e) { + return Constants.BOOTSTRAP_PACKAGE_PREFIXES; + } + } + } + + @SuppressWarnings("unused") + public static class LoadClassAdvice { + + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + public static Class onEnter(@Advice.Argument(0) String name) { + // need to use call depth here to prevent re-entry from call to Class.forName() below + // because on some JVMs (e.g. IBM's, though IBM bootstrap loader is explicitly excluded above) + // Class.forName() ends up calling loadClass() on the bootstrap loader which would then come + // back to this instrumentation over and over, causing a StackOverflowError + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(ClassLoader.class); + if (callDepth > 0) { + return null; + } + + try { + for (String prefix : Holder.bootstrapPackagesPrefixes) { + if (name.startsWith(prefix)) { + try { + return Class.forName(name, false, null); + } catch (ClassNotFoundException ignored) { + // Ignore + } + } + } + } finally { + // need to reset it right away, not waiting until onExit() + // otherwise it will prevent this instrumentation from being applied when loadClass() + // ends up calling a ClassFileTransformer which ends up calling loadClass() further down the + // stack on one of our bootstrap packages (since the call depth check would then suppress + // the nested loadClass instrumentation) + CallDepthThreadLocalMap.reset(ClassLoader.class); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void onExit( + @Advice.Return(readOnly = false) Class result, + @Advice.Enter Class resultFromBootstrapLoader) { + if (resultFromBootstrapLoader != null) { + result = resultFromBootstrapLoader; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentationModule.java new file mode 100644 index 000000000..b544abcef --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentationModule.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.classloader; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ClassLoaderInstrumentationModule extends InstrumentationModule { + public ClassLoaderInstrumentationModule() { + super("internal-class-loader"); + } + + @Override + public boolean defaultEnabled() { + // internal instrumentations are always enabled by default + return true; + } + + @Override + public boolean isHelperClass(String className) { + return className.equals("io.opentelemetry.javaagent.tooling.Constants"); + } + + @Override + public List typeInstrumentations() { + return asList(new ClassLoaderInstrumentation(), new ResourceInjectionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ResourceInjectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ResourceInjectionInstrumentation.java new file mode 100644 index 000000000..964325864 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ResourceInjectionInstrumentation.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.classloader; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.bootstrap.HelperResources; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Instruments {@link ClassLoader} to have calls to get resources intercepted and check our map of + * helper resources that is filled by instrumentation when they need helpers. + * + *

We currently only intercept {@link ClassLoader#getResources(String)} because this is the case + * we are currently always interested in, where it's used for service loading. + */ +public class ResourceInjectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("java.lang.ClassLoader")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("getResources")).and(takesArguments(String.class)), + ResourceInjectionInstrumentation.class.getName() + "$GetResourcesAdvice"); + } + + @SuppressWarnings("unused") + public static class GetResourcesAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This ClassLoader classLoader, + @Advice.Argument(0) String name, + @Advice.Return(readOnly = false) Enumeration resources) { + URL helper = HelperResources.load(classLoader, name); + if (helper == null) { + return; + } + + if (!resources.hasMoreElements()) { + resources = Collections.enumeration(Collections.singleton(helper)); + return; + } + + List result = Collections.list(resources); + boolean duplicate = false; + for (URL loadedUrl : result) { + if (helper.sameFile(loadedUrl)) { + duplicate = true; + break; + } + } + if (!duplicate) { + result.add(helper); + } + + resources = Collections.enumeration(result); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/ClassLoadingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/ClassLoadingTest.groovy new file mode 100644 index 000000000..42aa64c83 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/ClassLoadingTest.groovy @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +class ClassLoadingTest extends AgentInstrumentationSpecification { + def "delegates to bootstrap class loader for agent classes"() { + setup: + def classLoader = new NonDelegatingURLClassLoader() + + when: + Class clazz + try { + clazz = Class.forName("io.opentelemetry.javaagent.instrumentation.api.concurrent.State", false, classLoader) + } catch (ClassNotFoundException e) { + } + + then: + assert clazz != null + assert clazz.getClassLoader() == null + } + + static class NonDelegatingURLClassLoader extends URLClassLoader { + + NonDelegatingURLClassLoader() { + super(new URL[0]) + } + + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class clazz = findLoadedClass(name) + if (clazz == null) { + clazz = findClass(name) + } + if (resolve) { + resolveClass(clazz) + } + return clazz + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/JBossClassloadingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/JBossClassloadingTest.groovy new file mode 100644 index 000000000..f73db89fd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/JBossClassloadingTest.groovy @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.jboss.modules.ModuleFinder +import org.jboss.modules.ModuleIdentifier +import org.jboss.modules.ModuleLoadException +import org.jboss.modules.ModuleLoader +import org.jboss.modules.ModuleSpec + +class JBossClassloadingTest extends AgentInstrumentationSpecification { + def "delegates to bootstrap class loader for agent classes"() { + setup: + def moduleFinders = new ModuleFinder[1] + moduleFinders[0] = new ModuleFinder() { + @Override + ModuleSpec findModule(ModuleIdentifier identifier, ModuleLoader delegateLoader) throws ModuleLoadException { + return ModuleSpec.build(identifier).create() + } + } + def moduleLoader = new ModuleLoader(moduleFinders) + def moduleId = ModuleIdentifier.fromString("test") + def testModule = moduleLoader.loadModule(moduleId) + def classLoader = testModule.getClassLoader() + + when: + Class clazz + try { + clazz = Class.forName("io.opentelemetry.javaagent.instrumentation.api.concurrent.State", false, classLoader) + } catch (ClassNotFoundException e) { + } + + then: + assert clazz != null + assert clazz.getClassLoader() == null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/OSGIClassloadingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/OSGIClassloadingTest.groovy new file mode 100644 index 000000000..3802dadc7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/OSGIClassloadingTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.apache.felix.framework.BundleWiringImpl +import org.eclipse.osgi.internal.debug.Debug +import org.eclipse.osgi.internal.framework.EquinoxConfiguration +import org.eclipse.osgi.internal.loader.BundleLoader +import org.eclipse.osgi.internal.loader.ModuleClassLoader +import org.eclipse.osgi.internal.loader.classpath.ClasspathManager +import org.eclipse.osgi.storage.BundleInfo + +class OSGIClassloadingTest extends AgentInstrumentationSpecification { + def "OSGI delegates to bootstrap class loader for agent classes"() { + when: + def clazz + if (args == 1) { + clazz = loader.loadClass("io.opentelemetry.javaagent.instrumentation.api.concurrent.State") + } else { + clazz = loader.loadClass("io.opentelemetry.javaagent.instrumentation.api.concurrent.State", false) + } + + then: + assert clazz != null + assert clazz.getClassLoader() == null + + where: + loader | args + new TestClassLoader() | 1 + new TestClassLoader() | 2 + new BundleWiringImpl.BundleClassLoader(null, null, null) | 1 + new BundleWiringImpl.BundleClassLoader(null, null, null) | 2 + } + + static class TestClassLoader extends ModuleClassLoader { + + TestClassLoader() { + super(null) + } + + @Override + protected BundleInfo.Generation getGeneration() { + return null + } + + @Override + protected Debug getDebug() { + return null + } + + @Override + ClasspathManager getClasspathManager() { + return null + } + + @Override + protected EquinoxConfiguration getConfiguration() { + return null + } + + @Override + BundleLoader getBundleLoader() { + return null + } + + @Override + boolean isRegisteredAsParallel() { + return false + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/TomcatClassloadingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/TomcatClassloadingTest.groovy new file mode 100644 index 000000000..59ae76a5b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/TomcatClassloadingTest.groovy @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.apache.catalina.WebResource +import org.apache.catalina.WebResourceRoot +import org.apache.catalina.loader.ParallelWebappClassLoader + +class TomcatClassloadingTest extends AgentInstrumentationSpecification { + + WebResourceRoot resources = Mock(WebResourceRoot) { + getResource(_) >> Mock(WebResource) + listResources(_) >> [] + // Looks like 9.x.x needs this one: + getResources(_) >> [] + } + ParallelWebappClassLoader classloader = new ParallelWebappClassLoader(null) + + def "tomcat class loading delegates to parent for agent classes"() { + setup: + classloader.setResources(resources) + classloader.init() + classloader.start() + + expect: + // If instrumentation didn't work this would blow up with NPE due to incomplete resources mocking + classloader.loadClass("io.opentelemetry.javaagent.instrumentation.api.concurrent.State") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/internal-eclipse-osgi-3.6-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/internal-eclipse-osgi-3.6-javaagent.gradle new file mode 100644 index 000000000..c1b86a5e4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/internal-eclipse-osgi-3.6-javaagent.gradle @@ -0,0 +1,22 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +// this instrumentation applies to the class 'org.eclipse.osgi.internal.loader.BundleLoader' +// which is present in the following artifacts dating back to version 3.6 (2010): +// +// * 'org.eclipse.platform:org.eclipse.osgi' +// * 'org.eclipse.tycho:org.eclipse.osgi' +// * 'org.eclipse.osgi:org.eclipse.osgi' + +// TODO write a smoke test that does the following: +// +// docker run --mount 'type=bind,src=$AGENT_PATH,dst=/opentelemetry-javaagent-all.jar' +// -e JAVA_TOOL_OPTIONS=-javaagent:/opentelemetry-javaagent-all.jar +// wso2/wso2ei-business-process:6.5.0 +// +// without this instrumentation, the following error will appear in the docker logs: +// java.lang.ClassNotFoundException: org.wso2.carbon.humantask.ui.fileupload.HumanTaskUploadExecutor +// cannot be found by org.wso2.carbon.ui_4.4.36 +// +// ... or even better, write a standalone OSGi application that exhibits similar issue, +// so we can run against arbitrary (e.g. latest) Eclipse OSGi release, especially since +// this instrumentation patches a private method which could be renamed at any time diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentation.java new file mode 100644 index 000000000..fcb8ef1a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentation.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.osgi; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.internal.InClassLoaderMatcher; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * The ClassLoaderMatcher's call to ClassLoader.getResource() causes the Eclipse OSGi class loader + * to "dynamically import" a bundle for the package if such a bundle is not found, which can lead to + * application failure later on due to the bundle hierarchy no longer being "consistent". + * + *

Any side-effect of the ClassLoaderMatcher's call to ClassLoader.getResource() is generally + * undesirable, and so this instrumentation patches the behavior and suppresses the "dynamic import" + * of the missing package/bundle when the call is originating from ClassLoaderMatcher.. + */ +class EclipseOsgiInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.eclipse.osgi.internal.loader.BundleLoader"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("isDynamicallyImported")).and(returns(boolean.class)), + this.getClass().getName() + "$IsDynamicallyImportedAdvice"); + } + + @SuppressWarnings("unused") + public static class IsDynamicallyImportedAdvice { + + // "skipOn" is used to skip execution of the instrumented method when a ClassLoaderMatcher is + // currently executing, since we will be returning false regardless in onExit below + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) + public static boolean onEnter() { + return InClassLoaderMatcher.get(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Return(readOnly = false) boolean result, + @Advice.Enter boolean inClassLoaderMatcher) { + if (inClassLoaderMatcher) { + result = false; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentationModule.java new file mode 100644 index 000000000..f5d7e8cd2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.osgi; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class EclipseOsgiInstrumentationModule extends InstrumentationModule { + public EclipseOsgiInstrumentationModule() { + super("internal-eclipse-osgi"); + } + + @Override + public boolean defaultEnabled() { + // internal instrumentations are always enabled by default + return true; + } + + @Override + public List typeInstrumentations() { + return singletonList(new EclipseOsgiInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent-unit-tests/internal-proxy-javaagent-unit-tests.gradle b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent-unit-tests/internal-proxy-javaagent-unit-tests.gradle new file mode 100644 index 000000000..83c781ec6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent-unit-tests/internal-proxy-javaagent-unit-tests.gradle @@ -0,0 +1,6 @@ +apply plugin: "otel.java-conventions" + +dependencies { + testImplementation project(':instrumentation:internal:internal-proxy:javaagent') + testImplementation project(':javaagent-bootstrap') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent-unit-tests/src/test/groovy/ProxyHelperTest.groovy b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent-unit-tests/src/test/groovy/ProxyHelperTest.groovy new file mode 100644 index 000000000..9a2be5ae0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent-unit-tests/src/test/groovy/ProxyHelperTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker +import io.opentelemetry.javaagent.instrumentation.internal.proxy.ProxyHelper +import java.util.concurrent.Callable +import spock.lang.Specification + +class ProxyHelperTest extends Specification { + + def "should filter #interfaces"() { + expect: + ProxyHelper.filtered(interfaces.toArray() as Class[]) == filtered.toArray() + + where: + interfaces | filtered + [] | [] + [Runnable] | [Runnable] + [Runnable, Callable] | [Runnable, Callable] + [Runnable, FieldBackedContextStoreAppliedMarker] | [Runnable, FieldBackedContextStoreAppliedMarker] + [Runnable, FieldBackedContextStoreAppliedMarker, FieldBackedContextStoreAppliedMarker] | [Runnable, FieldBackedContextStoreAppliedMarker] + [FieldBackedContextStoreAppliedMarker, Runnable] | [FieldBackedContextStoreAppliedMarker, Runnable] + [FieldBackedContextStoreAppliedMarker, Runnable, FieldBackedContextStoreAppliedMarker] | [FieldBackedContextStoreAppliedMarker, Runnable] + [FieldBackedContextStoreAppliedMarker, FieldBackedContextStoreAppliedMarker, Runnable] | [FieldBackedContextStoreAppliedMarker, Runnable] + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/internal-proxy-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/internal-proxy-javaagent.gradle new file mode 100644 index 000000000..a8663baf0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/internal-proxy-javaagent.gradle @@ -0,0 +1,7 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly project(':javaagent-bootstrap') + + testImplementation project(':javaagent-bootstrap') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyHelper.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyHelper.java new file mode 100644 index 000000000..d0794a12a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyHelper.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.proxy; + +import io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker; +import java.util.Arrays; +import java.util.LinkedHashSet; + +public class ProxyHelper { + + private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + public static Class[] filtered(Class[] interfaces) { + int numMarkers = 0; + for (Class iface : interfaces) { + if (iface == FieldBackedContextStoreAppliedMarker.class) { + numMarkers++; + } + } + if (numMarkers <= 1) { + // no duplicates, safe to use original interfaces + return interfaces; + } + + // it's probably ok to remove them all(?) + // but just doing the minimum here and only removing the duplicates to prevent + // Proxy.newProxyInstance() from throwing IllegalArgumentException: repeated interface + return new LinkedHashSet<>(Arrays.asList(interfaces)).toArray(EMPTY_CLASS_ARRAY); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyInstrumentation.java new file mode 100644 index 000000000..99b86e83c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyInstrumentation.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.proxy; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.InvocationHandler; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ProxyInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("java.lang.reflect.Proxy"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("newProxyInstance")) + .and(takesArguments(3)) + .and(takesArgument(0, ClassLoader.class)) + .and(takesArgument(1, Class[].class)) + .and(takesArgument(2, InvocationHandler.class)) + .and(isPublic()) + .and(isStatic()), + ProxyInstrumentation.class.getName() + "$FilterDuplicateMarkerInterfaces"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("getProxyClass")) + .and(takesArguments(2)) + .and(takesArgument(0, ClassLoader.class)) + .and(takesArgument(1, Class[].class)) + .and(isPublic()) + .and(isStatic()), + ProxyInstrumentation.class.getName() + "$FilterDuplicateMarkerInterfaces"); + } + + public static class FilterDuplicateMarkerInterfaces { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 1, readOnly = false) Class[] interfaces) { + interfaces = ProxyHelper.filtered(interfaces); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyInstrumentationModule.java new file mode 100644 index 000000000..347496a0a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/proxy/ProxyInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.proxy; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ProxyInstrumentationModule extends InstrumentationModule { + public ProxyInstrumentationModule() { + super("internal-proxy"); + } + + @Override + public boolean defaultEnabled() { + // internal instrumentations are always enabled by default + return true; + } + + @Override + public List typeInstrumentations() { + return singletonList(new ProxyInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/test/groovy/NewProxyInstanceTest.groovy b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/test/groovy/NewProxyInstanceTest.groovy new file mode 100644 index 000000000..ddce9331c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-proxy/javaagent/src/test/groovy/NewProxyInstanceTest.groovy @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy + +class NewProxyInstanceTest extends AgentInstrumentationSpecification { + def "should filter out duplicate FieldBackedContextStoreAppliedMarker interfaces from newProxyInstance"() { + setup: + Class[] interfaces = new Class[3] + interfaces[0] = Runnable + interfaces[1] = FieldBackedContextStoreAppliedMarker + interfaces[2] = FieldBackedContextStoreAppliedMarker + + expect: + Runnable proxy = Proxy.newProxyInstance(NewProxyInstanceTest.getClassLoader(), interfaces, new MyHandler()) as Runnable + proxy.run() + + // should not throw IllegalArgumentException: + // repeated interface: io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker + } + + def "should filter out duplicate FieldBackedContextStoreAppliedMarker interfaces from getProxyClass"() { + setup: + Class[] interfaces = new Class[3] + interfaces[0] = Runnable + interfaces[1] = FieldBackedContextStoreAppliedMarker + interfaces[2] = FieldBackedContextStoreAppliedMarker + + expect: + Class proxyClass = Proxy.getProxyClass(NewProxyInstanceTest.getClassLoader(), interfaces) + def proxy = proxyClass.newInstance(new MyHandler()) as Runnable + proxy.run() + + // should not throw IllegalArgumentException: + // repeated interface: io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker + } + + static class MyHandler implements InvocationHandler { + + @Override + Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return null + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/internal-url-class-loader-javaagent-integration-tests.gradle b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/internal-url-class-loader-javaagent-integration-tests.gradle new file mode 100644 index 000000000..f34888e86 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/internal-url-class-loader-javaagent-integration-tests.gradle @@ -0,0 +1,10 @@ +ext.skipPublish = true + +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + testImplementation "org.apache.commons:commons-lang3:3.12.0" + testImplementation "commons-io:commons-io:2.8.0" + + testInstrumentation project(":instrumentation:internal:internal-url-class-loader:javaagent") +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java new file mode 100644 index 000000000..d9e406633 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package instrumentation; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class TestInstrumentationModule extends InstrumentationModule { + public TestInstrumentationModule() { + super("test-instrumentation"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new TestTypeInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestTypeInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestTypeInstrumentation.java new file mode 100644 index 000000000..d6bf0b037 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestTypeInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package instrumentation; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class TestTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.commons.lang3.SystemUtils"); + } + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.commons.lang3.SystemUtils"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("getHostName"), TestTypeInstrumentation.class.getName() + "$GetHostNameAdvice"); + } + + @SuppressWarnings("unused") + public static class GetHostNameAdvice { + + @Advice.OnMethodExit + public static void methodExit(@Advice.Return(readOnly = false) String hostName) { + hostName = "not-the-host-name"; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/groovy/AddUrlTest.groovy b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/groovy/AddUrlTest.groovy new file mode 100644 index 000000000..057fb2c7f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/groovy/AddUrlTest.groovy @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.apache.commons.io.IOUtils +import org.apache.commons.lang3.SystemUtils + +class AddUrlTest extends AgentInstrumentationSpecification { + + def "should instrument class after it is loaded via addURL"() { + given: + TestURLClassLoader loader = new TestURLClassLoader() + + when: + // this is just to verify the assumption that TestURLClassLoader is not finding SystemUtils via + // the test class path (in which case the verification below would not be very meaningful) + loader.loadClass(SystemUtils.getName()) + + then: + thrown ClassNotFoundException + + when: + // loading a class in the URLClassLoader in order to trigger + // a negative cache hit on org.apache.commons.lang3.SystemUtils + loader.addURL(IOUtils.getProtectionDomain().getCodeSource().getLocation()) + loader.loadClass(IOUtils.getName()) + + loader.addURL(SystemUtils.getProtectionDomain().getCodeSource().getLocation()) + def clazz = loader.loadClass(SystemUtils.getName()) + + then: + clazz.getClassLoader() == loader + clazz.getMethod("getHostName").invoke(null) == "not-the-host-name" + } + + static class TestURLClassLoader extends URLClassLoader { + + TestURLClassLoader() { + super(new URL[0], (ClassLoader) null) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/internal-url-class-loader-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/internal-url-class-loader-javaagent.gradle new file mode 100644 index 000000000..80b3cc1b1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/internal-url-class-loader-javaagent.gradle @@ -0,0 +1 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/urlclassloader/UrlClassLoaderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/urlclassloader/UrlClassLoaderInstrumentation.java new file mode 100644 index 000000000..e48f2fabc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/urlclassloader/UrlClassLoaderInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.urlclassloader; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.bootstrap.ClassLoaderMatcherCacheHolder; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.net.URL; +import java.net.URLClassLoader; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class UrlClassLoaderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("java.net.URLClassLoader"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("addURL")) + .and(takesArguments(1)) + .and(takesArgument(0, URL.class)) + .and(isProtected()) + .and(not(isStatic())), + UrlClassLoaderInstrumentation.class.getName() + "$InvalidateClassLoaderMatcher"); + } + + public static class InvalidateClassLoaderMatcher { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This URLClassLoader loader) { + ClassLoaderMatcherCacheHolder.invalidateAllCachesForClassLoader(loader); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/urlclassloader/UrlClassLoaderInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/urlclassloader/UrlClassLoaderInstrumentationModule.java new file mode 100644 index 000000000..03f92be72 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/internal/internal-url-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/urlclassloader/UrlClassLoaderInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.urlclassloader; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class UrlClassLoaderInstrumentationModule extends InstrumentationModule { + public UrlClassLoaderInstrumentationModule() { + super("internal-url-class-loader"); + } + + @Override + public boolean defaultEnabled() { + // internal instrumentations are always enabled by default + return true; + } + + @Override + public List typeInstrumentations() { + return singletonList(new UrlClassLoaderInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/java-http-client-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/java-http-client-javaagent.gradle new file mode 100644 index 000000000..30fe15087 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/java-http-client-javaagent.gradle @@ -0,0 +1,11 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + coreJdk() + } +} + +otelJava { + minJavaVersionSupported = JavaVersion.VERSION_11 +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/BodyHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/BodyHandlerWrapper.java new file mode 100644 index 000000000..9249f2846 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/BodyHandlerWrapper.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; + +public class BodyHandlerWrapper implements BodyHandler { + private final BodyHandler delegate; + private final Context context; + + public BodyHandlerWrapper(BodyHandler delegate, Context context) { + this.delegate = delegate; + this.context = context; + } + + @Override + public BodySubscriber apply(ResponseInfo responseInfo) { + BodySubscriber subscriber = delegate.apply(responseInfo); + if (subscriber instanceof BodySubscriberWrapper) { + return subscriber; + } + return new BodySubscriberWrapper<>(delegate.apply(responseInfo), context); + } + + public static class BodySubscriberWrapper implements BodySubscriber { + private final BodySubscriber delegate; + private final Context context; + + public BodySubscriberWrapper(BodySubscriber delegate, Context context) { + this.delegate = delegate; + this.context = context; + } + + public BodySubscriber getDelegate() { + return delegate; + } + + @Override + public CompletionStage getBody() { + return delegate.getBody(); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + delegate.onSubscribe(subscription); + } + + @Override + public void onNext(List item) { + try (Scope ignore = context.makeCurrent()) { + delegate.onNext(item); + } + } + + @Override + public void onError(Throwable throwable) { + try (Scope ignore = context.makeCurrent()) { + delegate.onError(throwable); + } + } + + @Override + public void onComplete() { + try (Scope ignore = context.makeCurrent()) { + delegate.onComplete(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java new file mode 100644 index 000000000..6616168af --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentation.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HttpClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("java.net.http.HttpClient"); + } + + @Override + public ElementMatcher typeMatcher() { + return nameStartsWith("java.net.") + .or(nameStartsWith("jdk.internal.")) + .and(not(named("jdk.internal.net.http.HttpClientFacade"))) + .and(extendsClass(named("java.net.http.HttpClient"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("send")) + .and(isPublic()) + .and(takesArguments(2)) + .and(takesArgument(0, named("java.net.http.HttpRequest"))), + HttpClientInstrumentation.class.getName() + "$SendAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("sendAsync")) + .and(isPublic()) + .and(takesArgument(0, named("java.net.http.HttpRequest"))) + .and(takesArgument(1, named("java.net.http.HttpResponse$BodyHandler"))), + HttpClientInstrumentation.class.getName() + "$SendAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class SendAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(value = 0) HttpRequest httpRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, httpRequest, httpRequest); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return HttpResponse result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable == null) { + tracer().end(context, result); + } else { + tracer().endExceptionally(context, result, throwable); + } + } + } + + @SuppressWarnings("unused") + public static class SendAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(value = 0) HttpRequest httpRequest, + @Advice.Argument(value = 1, readOnly = false) HttpResponse.BodyHandler bodyHandler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (bodyHandler != null) { + bodyHandler = new BodyHandlerWrapper(bodyHandler, parentContext); + } + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, httpRequest, httpRequest); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return(readOnly = false) CompletableFuture> future, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, null, throwable); + } else { + future = future.whenComplete(new ResponseConsumer(context)); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentationModule.java new file mode 100644 index 000000000..8506b9bba --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpClientInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class HttpClientInstrumentationModule extends InstrumentationModule { + public HttpClientInstrumentationModule() { + super("java-http-client"); + } + + @Override + public List typeInstrumentations() { + return asList( + new HttpClientInstrumentation(), + new HttpHeadersInstrumentation(), + new TrustedSubscriberInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInjectAdapter.java new file mode 100644 index 000000000..801f02ad5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import io.opentelemetry.context.propagation.TextMapSetter; +import java.net.http.HttpRequest; + +/** Context propagation is implemented via {@link HttpHeadersInstrumentation}. */ +public class HttpHeadersInjectAdapter implements TextMapSetter { + public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); + + @Override + public void set(HttpRequest carrier, String key, String value) { + // Don't do anything because headers are immutable + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java new file mode 100644 index 000000000..08eeccd6b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/HttpHeadersInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import java.net.http.HttpHeaders; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HttpHeadersInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return nameStartsWith("java.net.") + .or(nameStartsWith("jdk.internal.")) + .and(extendsClass(named("java.net.http.HttpRequest"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("headers")), + HttpHeadersInstrumentation.class.getName() + "$HeadersAdvice"); + } + + @SuppressWarnings("unused") + public static class HeadersAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit(@Advice.Return(readOnly = false) HttpHeaders headers) { + if (Java8BytecodeBridge.currentSpan().isRecording()) { + headers = tracer().inject(headers); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientTracer.java new file mode 100644 index 000000000..525960061 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JdkHttpClientTracer.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JdkHttpClientTracer + extends HttpClientTracer> { + private static final JdkHttpClientTracer TRACER = new JdkHttpClientTracer(); + + private JdkHttpClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static JdkHttpClientTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.java-http-client"; + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.method(); + } + + @Override + protected URI url(HttpRequest httpRequest) { + return httpRequest.uri(); + } + + @Override + protected Integer status(HttpResponse httpResponse) { + return httpResponse.statusCode(); + } + + @Override + protected String requestHeader(HttpRequest httpRequest, String name) { + return httpRequest.headers().firstValue(name).orElse(null); + } + + @Override + protected String responseHeader(HttpResponse httpResponse, String name) { + return httpResponse.headers().firstValue(name).orElse(null); + } + + @Override + protected void onResponse(Span span, HttpResponse httpResponse) { + super.onResponse(span, httpResponse); + + if (httpResponse != null) { + span.setAttribute( + SemanticAttributes.HTTP_FLAVOR, + httpResponse.version() == Version.HTTP_1_1 ? "1.1" : "2.0"); + } + } + + @Override + protected TextMapSetter getSetter() { + return HttpHeadersInjectAdapter.SETTER; + } + + public HttpHeaders inject(HttpHeaders original) { + Map> headerMap = new HashMap<>(original.map()); + + inject( + Context.current(), + headerMap, + (carrier, key, value) -> carrier.put(key, Collections.singletonList(value))); + + return HttpHeaders.of(headerMap, (s, s2) -> true); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java new file mode 100644 index 000000000..fce963a6d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/ResponseConsumer.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import static io.opentelemetry.javaagent.instrumentation.httpclient.JdkHttpClientTracer.tracer; + +import io.opentelemetry.context.Context; +import java.net.http.HttpResponse; +import java.util.function.BiConsumer; + +public class ResponseConsumer implements BiConsumer, Throwable> { + private final Context context; + + public ResponseConsumer(Context context) { + this.context = context; + } + + @Override + public void accept(HttpResponse httpResponse, Throwable throwable) { + if (throwable == null) { + tracer().end(context, httpResponse); + } else { + tracer().endExceptionally(context, httpResponse, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/TrustedSubscriberInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/TrustedSubscriberInstrumentation.java new file mode 100644 index 000000000..9f96db6a3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/TrustedSubscriberInstrumentation.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.httpclient; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.httpclient.BodyHandlerWrapper.BodySubscriberWrapper; +import java.net.http.HttpResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class TrustedSubscriberInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("jdk.internal.net.http.ResponseSubscribers$TrustedSubscriber"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("needsExecutor") + .and(takesArgument(0, named("java.net.http.HttpResponse$BodySubscriber"))), + TrustedSubscriberInstrumentation.class.getName() + "$NeedsExecutorAdvice"); + } + + @SuppressWarnings("unused") + public static class NeedsExecutorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(value = 0, readOnly = false) HttpResponse.BodySubscriber bodySubscriber) { + if (bodySubscriber instanceof BodySubscriberWrapper) { + bodySubscriber = ((BodySubscriberWrapper) bodySubscriber).getDelegate(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/test/groovy/JdkHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/test/groovy/JdkHttpClientTest.groovy new file mode 100644 index 000000000..9ecf6989b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/java-http-client/javaagent/src/test/groovy/JdkHttpClientTest.groovy @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.time.Duration +import java.time.temporal.ChronoUnit +import spock.lang.Shared + +class JdkHttpClientTest extends HttpClientTest implements AgentTestTrait { + + @Shared + def client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.of(CONNECT_TIMEOUT_MS, ChronoUnit.MILLIS)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build() + + @Override + HttpRequest buildRequest(String method, URI uri, Map headers) { + def requestBuilder = HttpRequest.newBuilder() + .uri(uri) + .method(method, HttpRequest.BodyPublishers.noBody()) + headers.entrySet().each { + requestBuilder.header(it.key, it.value) + } + return requestBuilder.build() + } + + @Override + int sendRequest(HttpRequest request, String method, URI uri, Map headers) { + return client.send(request, HttpResponse.BodyHandlers.ofString()).statusCode() + } + + @Override + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .whenComplete { response, throwable -> + requestResult.complete({ response.statusCode() }, throwable?.getCause()) + } + } + + @Override + boolean testCircularRedirects() { + return false + } + + // TODO nested client span is not created, but context is still injected + // which is not what the test expects + @Override + boolean testWithClientParent() { + false + } + + // TODO: context not propagated to callback + @Override + boolean testErrorWithCallback() { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/jaxrs-client-1.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/jaxrs-client-1.1-javaagent.gradle new file mode 100644 index 000000000..aac7acf30 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/jaxrs-client-1.1-javaagent.gradle @@ -0,0 +1,14 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.sun.jersey" + module = "jersey-client" + versions = "[1.1,]" + assertInverse = true + } +} + +dependencies { + library "com.sun.jersey:jersey-client:1.1.4" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientHandlerInstrumentation.java new file mode 100644 index 000000000..5a27caf5b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientHandlerInstrumentation.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1; + +import static io.opentelemetry.instrumentation.api.tracer.HttpServerTracer.CONTEXT_ATTRIBUTE; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1.JaxRsClientV1Tracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.ClientResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ClientHandlerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.sun.jersey.api.client.ClientHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("com.sun.jersey.api.client.ClientHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handle") + .and(takesArgument(0, extendsClass(named("com.sun.jersey.api.client.ClientRequest")))) + .and(returns(extendsClass(named("com.sun.jersey.api.client.ClientResponse")))), + this.getClass().getName() + "$HandleAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleAdvice { + + @Advice.OnMethodEnter + public static void onEnter( + @Advice.Argument(0) ClientRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + // WARNING: this might be a chain...so we only have to trace the first in the chain. + boolean isRootClientHandler = null == request.getProperties().get(CONTEXT_ATTRIBUTE); + Context parentContext = currentContext(); + if (isRootClientHandler && tracer().shouldStartSpan(parentContext)) { + context = tracer().startSpan(parentContext, request, request); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Return ClientResponse response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context, response); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/InjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/InjectAdapter.java new file mode 100644 index 000000000..b54e2517f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/InjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1; + +import com.sun.jersey.api.client.ClientRequest; +import io.opentelemetry.context.propagation.TextMapSetter; + +public final class InjectAdapter implements TextMapSetter { + + public static final InjectAdapter SETTER = new InjectAdapter(); + + @Override + public void set(ClientRequest carrier, String key, String value) { + carrier.getHeaders().putSingle(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientInstrumentationModule.java new file mode 100644 index 000000000..86c731c99 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JaxRsClientInstrumentationModule extends InstrumentationModule { + + public JaxRsClientInstrumentationModule() { + super("jaxrs-client", "jaxrs-client-1.1"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ClientHandlerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientV1Tracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientV1Tracer.java new file mode 100644 index 000000000..6c5b93b0c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientV1Tracer.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1; + +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1.InjectAdapter.SETTER; + +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.ClientResponse; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; + +public class JaxRsClientV1Tracer + extends HttpClientTracer { + private static final JaxRsClientV1Tracer TRACER = new JaxRsClientV1Tracer(); + + private JaxRsClientV1Tracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static JaxRsClientV1Tracer tracer() { + return TRACER; + } + + @Override + protected String method(ClientRequest httpRequest) { + return httpRequest.getMethod(); + } + + @Override + protected URI url(ClientRequest httpRequest) { + return httpRequest.getURI(); + } + + @Override + protected Integer status(ClientResponse clientResponse) { + return clientResponse.getStatus(); + } + + @Override + protected String requestHeader(ClientRequest clientRequest, String name) { + Object header = clientRequest.getHeaders().getFirst(name); + return header != null ? header.toString() : null; + } + + @Override + protected String responseHeader(ClientResponse clientResponse, String name) { + return clientResponse.getHeaders().getFirst(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxrs-client-1.1"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/test/groovy/JaxRsClientV1Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/test/groovy/JaxRsClientV1Test.groovy new file mode 100644 index 000000000..6c69522c6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/test/groovy/JaxRsClientV1Test.groovy @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.ClientResponse +import com.sun.jersey.api.client.WebResource +import com.sun.jersey.api.client.filter.GZIPContentEncodingFilter +import com.sun.jersey.api.client.filter.LoggingFilter +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import spock.lang.Shared + +class JaxRsClientV1Test extends HttpClientTest implements AgentTestTrait { + + @Shared + Client client = Client.create() + + def setupSpec() { + client.setConnectTimeout(CONNECT_TIMEOUT_MS) + // Add filters to ensure spans aren't duplicated. + client.addFilter(new LoggingFilter()) + client.addFilter(new GZIPContentEncodingFilter()) + } + + @Override + WebResource.Builder buildRequest(String method, URI uri, Map headers) { + def resource = client.resource(uri).requestBuilder + headers.each { resource.header(it.key, it.value) } + return resource + } + + @Override + int sendRequest(WebResource.Builder resource, String method, URI uri, Map headers) { + def body = BODY_METHODS.contains(method) ? "" : null + return resource.method(method, ClientResponse, body).status + } + + @Override + boolean testCircularRedirects() { + false + } + + @Override + boolean testCallback() { + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/jaxrs-client-2.0-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/jaxrs-client-2.0-common-javaagent.gradle new file mode 100644 index 000000000..9180b54d3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/jaxrs-client-2.0-common-javaagent.gradle @@ -0,0 +1,45 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "javax.ws.rs" + module = "javax.ws.rs-api" + versions = "[2.0,)" + } + pass { + // We want to support the dropwizard clients too. + group = 'io.dropwizard' + module = 'dropwizard-client' + versions = "[0.8.0,)" + assertInverse = true + } +} + +dependencies { + compileOnly "javax.ws.rs:javax.ws.rs-api:2.0.1" + compileOnly "javax.annotation:javax.annotation-api:1.3.2" + + testInstrumentation project(':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-cxf-3.0:javaagent') + testInstrumentation project(':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-jersey-2.0:javaagent') + testInstrumentation project(':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-resteasy-3.0:javaagent') + + testImplementation "javax.ws.rs:javax.ws.rs-api:2.0.1" + + testLibrary "org.glassfish.jersey.core:jersey-client:2.0" + testLibrary "org.jboss.resteasy:resteasy-client:3.0.5.Final" + // ^ This version has timeouts https://issues.redhat.com/browse/RESTEASY-975 + testLibrary "org.apache.cxf:cxf-rt-rs-client:3.1.0" + // Doesn't work with CXF 3.0.x because their context is wrong: + // https://github.com/apache/cxf/commit/335c7bad2436f08d6d54180212df5a52157c9f21 + + testImplementation "javax.xml.bind:jaxb-api:2.2.3" + + testInstrumentation project(':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent') + + latestDepTestLibrary "org.glassfish.jersey.inject:jersey-hk2:2.+" + latestDepTestLibrary "org.glassfish.jersey.core:jersey-client:2.+" + latestDepTestLibrary "org.jboss.resteasy:resteasy-client:3.0.26.Final" +} + +// Requires old Guava. Can't use enforcedPlatform since predates BOM +configurations.testRuntimeClasspath.resolutionStrategy.force "com.google.guava:guava:19.0" diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientBuilderInstrumentation.java new file mode 100644 index 000000000..f98ad6ded --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientBuilderInstrumentation.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import javax.ws.rs.client.Client; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; + +public class ClientBuilderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.ws.rs.client.ClientBuilder"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("javax.ws.rs.client.ClientBuilder")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("build").and(returns(implementsInterface(named("javax.ws.rs.client.Client")))), + this.getClass().getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodExit + public static void registerFeature( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Client client) { + // Register on the generated client instead of the builder + // The build() can be called multiple times and is not thread safe + // A client is only created once + client.register(ClientTracingFeature.class); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientTracingFeature.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientTracingFeature.java new file mode 100644 index 000000000..2408ac92a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientTracingFeature.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ClientTracingFeature implements Feature { + + private static final Logger log = LoggerFactory.getLogger(ClientTracingFeature.class); + + @Override + public boolean configure(FeatureContext context) { + context.register(new ClientTracingFilter()); + log.debug("ClientTracingFilter registered"); + return true; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientTracingFilter.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientTracingFilter.java new file mode 100644 index 000000000..4103f9a6f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ClientTracingFilter.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.JaxRsClientTracer.tracer; + +import io.opentelemetry.context.Context; +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; + +@Priority(Priorities.HEADER_DECORATOR) +public class ClientTracingFilter implements ClientRequestFilter, ClientResponseFilter { + public static final String CONTEXT_PROPERTY_NAME = "io.opentelemetry.javaagent.context"; + + @Override + public void filter(ClientRequestContext requestContext) { + Context parentContext = Context.current(); + if (tracer().shouldStartSpan(parentContext)) { + Context context = tracer().startSpan(parentContext, requestContext, requestContext); + requestContext.setProperty(CONTEXT_PROPERTY_NAME, context); + } + } + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) { + Object contextObj = requestContext.getProperty(CONTEXT_PROPERTY_NAME); + if (contextObj instanceof Context) { + Context context = (Context) contextObj; + tracer().end(context, responseContext); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/InjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/InjectAdapter.java new file mode 100644 index 000000000..7ec0094bf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/InjectAdapter.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import io.opentelemetry.context.propagation.TextMapSetter; +import javax.ws.rs.client.ClientRequestContext; + +public final class InjectAdapter implements TextMapSetter { + + public static final InjectAdapter SETTER = new InjectAdapter(); + + @Override + public void set(ClientRequestContext carrier, String key, String value) { + // Don't allow duplicates. + carrier.getHeaders().putSingle(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JaxRsClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JaxRsClientInstrumentationModule.java new file mode 100644 index 000000000..c51dd6163 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JaxRsClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JaxRsClientInstrumentationModule extends InstrumentationModule { + + public JaxRsClientInstrumentationModule() { + super("jaxrs-client", "jaxrs-client-2.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ClientBuilderInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JaxRsClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JaxRsClientTracer.java new file mode 100644 index 000000000..ce9001683 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JaxRsClientTracer.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.InjectAdapter.SETTER; + +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; + +public class JaxRsClientTracer + extends HttpClientTracer { + private static final JaxRsClientTracer TRACER = new JaxRsClientTracer(); + + private JaxRsClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static JaxRsClientTracer tracer() { + return TRACER; + } + + @Override + protected String method(ClientRequestContext httpRequest) { + return httpRequest.getMethod(); + } + + @Override + protected URI url(ClientRequestContext httpRequest) { + return httpRequest.getUri(); + } + + @Override + protected Integer status(ClientResponseContext httpResponse) { + return httpResponse.getStatus(); + } + + @Override + protected String requestHeader(ClientRequestContext clientRequestContext, String name) { + return clientRequestContext.getHeaderString(name); + } + + @Override + protected String responseHeader(ClientResponseContext clientResponseContext, String name) { + return clientResponseContext.getHeaderString(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected Throwable unwrapThrowable(Throwable throwable) { + if (throwable instanceof ProcessingException) { + throwable = throwable.getCause(); + } + return super.unwrapThrowable(throwable); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxrs-client-2.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxMultithreadedClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxMultithreadedClientTest.groovy new file mode 100644 index 000000000..40f749f43 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxMultithreadedClientTest.groovy @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.testing.internal.armeria.common.HttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpStatus +import io.opentelemetry.testing.internal.armeria.common.MediaType +import io.opentelemetry.testing.internal.armeria.server.ServerBuilder +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.ServerExtension +import javax.ws.rs.client.Client +import org.glassfish.jersey.client.JerseyClientBuilder +import spock.lang.Shared +import spock.util.concurrent.AsyncConditions + +class JaxMultithreadedClientTest extends AgentInstrumentationSpecification { + + @Shared + def server = new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) throws Exception { + sb.service("/success") {ctx, req -> + HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT, "Hello.") + } + } + } + + def setupSpec() { + server.start() + } + + def cleanupSpec() { + server.stop() + } + + def "multiple threads using the same builder works"() { + given: + def conds = new AsyncConditions(10) + def uri = server.httpUri().resolve("/success") + def builder = new JerseyClientBuilder() + + // Start 10 threads and do 50 requests each + when: + (1..10).each { + Thread.start { + boolean hadErrors = (1..50).any { + try { + Client client = builder.build() + client.target(uri).request().get() + } catch (Exception e) { + e.printStackTrace() + return true + } + return false + } + + conds.evaluate { + assert !hadErrors + } + } + } + + then: + conds.await(30) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxRsClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxRsClientTest.groovy new file mode 100644 index 000000000..5f7e75cf5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxRsClientTest.groovy @@ -0,0 +1,181 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.TimeUnit +import javax.ws.rs.ProcessingException +import javax.ws.rs.client.ClientBuilder +import javax.ws.rs.client.Entity +import javax.ws.rs.client.Invocation +import javax.ws.rs.client.InvocationCallback +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import org.apache.cxf.jaxrs.client.spec.ClientBuilderImpl +import org.glassfish.jersey.client.ClientConfig +import org.glassfish.jersey.client.ClientProperties +import org.glassfish.jersey.client.JerseyClientBuilder +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder +import spock.lang.Unroll + +abstract class JaxRsClientTest extends HttpClientTest implements AgentTestTrait { + + @Override + Invocation.Builder buildRequest(String method, URI uri, Map headers) { + return internalBuildRequest(uri, headers) + } + + @Override + int sendRequest(Invocation.Builder request, String method, URI uri, Map headers) { + try { + def body = BODY_METHODS.contains(method) ? Entity.text("") : null + def response = request.build(method, body).invoke() + response.close() + return response.status + } catch (ProcessingException exception) { + throw exception.getCause() + } + } + + @Override + void sendRequestWithCallback(Invocation.Builder request, String method, URI uri, Map headers, RequestResult requestResult) { + def body = BODY_METHODS.contains(method) ? Entity.text("") : null + + request.async().method(method, (Entity) body, new InvocationCallback() { + @Override + void completed(Response response) { + requestResult.complete(response.status) + } + + @Override + void failed(Throwable throwable) { + if (throwable instanceof ProcessingException) { + throwable = throwable.getCause() + } + requestResult.complete(throwable) + } + }) + } + + private Invocation.Builder internalBuildRequest(URI uri, Map headers) { + def client = builder().build() + def service = client.target(uri) + def requestBuilder = service.request(MediaType.TEXT_PLAIN) + headers.each { requestBuilder.header(it.key, it.value) } + return requestBuilder + } + + abstract ClientBuilder builder() + + @Unroll + def "should properly convert HTTP status #statusCode to span error status"() { + given: + def method = "GET" + def uri = resolveAddress(path) + + when: + def actualStatusCode = doRequest(method, uri) + + then: + assert actualStatusCode == statusCode + + assertTraces(1) { + trace(0, 2) { + span(0) { + hasNoParent() + name "HTTP $method" + kind CLIENT + status ERROR + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_NAME.key}" uri.host + "${SemanticAttributes.NET_PEER_IP.key}" { it == null || it == "127.0.0.1" } + "${SemanticAttributes.NET_PEER_PORT.key}" uri.port > 0 ? uri.port : { it == null || it == 443 } + "${SemanticAttributes.HTTP_URL.key}" "${uri}" + "${SemanticAttributes.HTTP_METHOD.key}" method + "${SemanticAttributes.HTTP_STATUS_CODE.key}" statusCode + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + } + } + serverSpan(it, 1, span(0)) + } + } + + where: + path | statusCode + "/client-error" | 400 + "/error" | 500 + } +} + +class JerseyClientTest extends JaxRsClientTest { + + @Override + ClientBuilder builder() { + ClientConfig config = new ClientConfig() + config.property(ClientProperties.CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS) + return new JerseyClientBuilder().withConfig(config) + } + + @Override + int maxRedirects() { + 20 + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + // Jersey JAX-RS client uses HttpURLConnection internally, which does not support pipelining nor + // waiting for a connection in the pool to become available. Therefore a high concurrency test + // would require manually doing requests one after another which is not meaningful for a high + // concurrency test. + return null + } +} + +class ResteasyClientTest extends JaxRsClientTest { + + @Override + ClientBuilder builder() { + return new ResteasyClientBuilder() + .establishConnectionTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + } + + boolean testRedirects() { + false + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + return new ResteasySingleConnection(host, port) + } +} + +class CxfClientTest extends JaxRsClientTest { + + @Override + ClientBuilder builder() { + return new ClientBuilderImpl() + .property("http.connection.timeout", (long) CONNECT_TIMEOUT_MS) + } + + boolean testRedirects() { + false + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + // CXF JAX-RS client uses HttpURLConnection internally, which does not support pipelining nor + // waiting for a connection in the pool to become available. Therefore a high concurrency test + // would require manually doing requests one after another which is not meaningful for a high + // concurrency test. + return null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasyProxyClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasyProxyClientTest.groovy new file mode 100644 index 000000000..103a2e80e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasyProxyClientTest.groovy @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.nio.charset.StandardCharsets +import javax.ws.rs.GET +import javax.ws.rs.HeaderParam +import javax.ws.rs.POST +import javax.ws.rs.PUT +import javax.ws.rs.Path +import javax.ws.rs.QueryParam +import javax.ws.rs.core.Response +import org.apache.http.client.utils.URLEncodedUtils +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder +import org.jboss.resteasy.specimpl.ResteasyUriBuilder + +class ResteasyProxyClientTest extends HttpClientTest implements AgentTestTrait { + + @Override + ResteasyProxyResource buildRequest(String method, URI uri, Map headers) { + return new ResteasyClientBuilder() + .build() + .target(new ResteasyUriBuilder().uri(resolveAddress(""))) + .proxy(ResteasyProxyResource) + } + + @Override + int sendRequest(ResteasyProxyResource proxy, String method, URI uri, Map headers) { + def proxyMethodName = "${method}_${uri.path}".toLowerCase() + .replace("/", "") + .replace('-', '_') + + def param = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8.name()) + .stream().findFirst() + .map({ it.value }) + .orElse(null) + + def isTestServer = headers.get("is-test-server") + + Response response = proxy."$proxyMethodName"(param, isTestServer) + response.close() + + return response.status + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testConnectionFailure() { + false + } + + @Override + boolean testRemoteConnection() { + false + } + + @Override + boolean testCausality() { + false + } + + @Override + boolean testCallback() { + false + } + +} + +@Path("") +interface ResteasyProxyResource { + @GET + @Path("success") + Response get_success(@QueryParam("with") String param, + @HeaderParam("is-test-server") String isTestServer) + + @POST + @Path("success") + Response post_success(@QueryParam("with") String param, + @HeaderParam("is-test-server") String isTestServer) + + @PUT + @Path("success") + Response put_success(@QueryParam("with") String param, + @HeaderParam("is-test-server") String isTestServer) + + @GET + @Path("error") + Response get_error(@QueryParam("with") String param, + @HeaderParam("is-test-server") String isTestServer) +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasySingleConnection.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasySingleConnection.groovy new file mode 100644 index 000000000..ab220cc9d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasySingleConnection.groovy @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.base.SingleConnection +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import javax.ws.rs.core.MediaType +import org.jboss.resteasy.client.jaxrs.ResteasyClient +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder + +class ResteasySingleConnection implements SingleConnection { + private final ResteasyClient client + private final String host + private final int port + + ResteasySingleConnection(String host, int port) { + this.host = host + this.port = port + this.client = new ResteasyClientBuilder() + .establishConnectionTimeout(5000, TimeUnit.MILLISECONDS) + .connectionPoolSize(1) + .build() + } + + @Override + int doRequest(String path, Map headers) throws ExecutionException, InterruptedException, TimeoutException { + String requestId = Objects.requireNonNull(headers.get(REQUEST_ID_HEADER)) + + URI uri + try { + uri = new URL("http", host, port, path).toURI() + } catch (MalformedURLException e) { + throw new ExecutionException(e) + } + + def requestBuilder = client.target(uri).request(MediaType.TEXT_PLAIN) + headers.each { requestBuilder.header(it.key, it.value) } + + def response = requestBuilder.buildGet().invoke() + response.close() + + String responseId = response.getHeaderString(REQUEST_ID_HEADER) + if (requestId != responseId) { + throw new IllegalStateException( + String.format("Received response with id %s, expected %s", responseId, requestId)) + } + + return response.getStatus() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/jaxrs-client-2.0-cxf-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/jaxrs-client-2.0-cxf-3.0-javaagent.gradle new file mode 100644 index 000000000..75a69df6b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/jaxrs-client-2.0-cxf-3.0-javaagent.gradle @@ -0,0 +1,15 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.cxf" + module = "cxf-rt-rs-client" + versions = "[3.0.0,)" + } +} + +dependencies { + library "org.apache.cxf:cxf-rt-rs-client:3.0.0" + + implementation project(':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-common:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfAsyncClientConnectionErrorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfAsyncClientConnectionErrorInstrumentation.java new file mode 100644 index 000000000..1ca89f168 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfAsyncClientConnectionErrorInstrumentation.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.cxf.message.Message; + +public class CxfAsyncClientConnectionErrorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.cxf.jaxrs.client.JaxrsClientCallback"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handleException") + .and( + takesArgument(0, named(Map.class.getName())) + .and(takesArgument(1, named(Throwable.class.getName())))), + this.getClass().getName() + "$HandleExceptionAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleExceptionAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void handleError( + @Advice.Argument(0) Map map, @Advice.Argument(1) Throwable throwable) { + if (throwable != null && map instanceof Message) { + CxfClientUtil.handleException((Message) map, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientConnectionErrorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientConnectionErrorInstrumentation.java new file mode 100644 index 000000000..ea7ffeb1c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientConnectionErrorInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.cxf.message.Message; + +public class CxfClientConnectionErrorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.cxf.jaxrs.client.AbstractClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("preProcessResult").and(takesArgument(0, named("org.apache.cxf.message.Message"))), + this.getClass().getName() + "$PreProcessResultAdvice"); + } + + @SuppressWarnings("unused") + public static class PreProcessResultAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void handleError( + @Advice.Argument(0) Message message, @Advice.Thrown Throwable throwable) { + if (throwable != null) { + CxfClientUtil.handleException(message, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientInstrumentationModule.java new file mode 100644 index 000000000..9b36cfc01 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientInstrumentationModule.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +/** + * JAX-RS Client API doesn't define a good point where we can handle connection failures, so we must + * handle these errors at the implementation level. + */ +@AutoService(InstrumentationModule.class) +public class CxfClientInstrumentationModule extends InstrumentationModule { + + public CxfClientInstrumentationModule() { + super("jaxrs-client", "jaxrs-client-2.0", "cxf-client", "cxf-client-3.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new CxfClientConnectionErrorInstrumentation(), + new CxfAsyncClientConnectionErrorInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientUtil.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientUtil.java new file mode 100644 index 000000000..0a203d859 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/CxfClientUtil.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.JaxRsClientTracer.tracer; + +import io.opentelemetry.context.Context; +import javax.ws.rs.client.ClientRequestContext; +import org.apache.cxf.jaxrs.client.spec.ClientRequestContextImpl; +import org.apache.cxf.message.Message; + +public final class CxfClientUtil { + + public static void handleException(Message message, Throwable throwable) { + ClientRequestContext context = + new ClientRequestContextImpl(message, /* responseContext= */ false); + Object prop = context.getProperty(ClientTracingFilter.CONTEXT_PROPERTY_NAME); + if (prop instanceof Context) { + tracer().endExceptionally((Context) prop, throwable); + } + } + + private CxfClientUtil() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/jaxrs-client-2.0-jersey-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/jaxrs-client-2.0-jersey-2.0-javaagent.gradle new file mode 100644 index 000000000..53b45f517 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/jaxrs-client-2.0-jersey-2.0-javaagent.gradle @@ -0,0 +1,15 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.glassfish.jersey.core" + module = "jersey-client" + versions = "[2.0,3.0.0)" + } +} + +dependencies { + library "org.glassfish.jersey.core:jersey-client:2.0" + + implementation project(':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-common:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientConnectionErrorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientConnectionErrorInstrumentation.java new file mode 100644 index 000000000..d68e9ecf6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientConnectionErrorInstrumentation.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.OpenTelemetryResponseCallbackWrapper; + +public class JerseyClientConnectionErrorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.glassfish.jersey.client.ClientRuntime"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("invoke")) + .and(takesArgument(0, named("org.glassfish.jersey.client.ClientRequest"))), + this.getClass().getName() + "$InvokeAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("submit", "createRunnableForAsyncProcessing")) + .and(takesArgument(0, named("org.glassfish.jersey.client.ClientRequest"))) + .and(takesArgument(1, named("org.glassfish.jersey.client.ResponseCallback"))), + this.getClass().getName() + "$SubmitAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void handleError( + @Advice.Argument(0) ClientRequest context, @Advice.Thrown Throwable throwable) { + if (throwable != null) { + JerseyClientUtil.handleException(context, throwable); + } + } + } + + @SuppressWarnings("unused") + public static class SubmitAdvice { + + // using dynamic typing because parameter type is package private + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void handleError( + @Advice.Argument(0) ClientRequest context, + @Advice.Argument(value = 1, readOnly = false, typing = Assigner.Typing.DYNAMIC) + Object callback) { + callback = OpenTelemetryResponseCallbackWrapper.wrap(context, callback); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientInstrumentationModule.java new file mode 100644 index 000000000..30cd39012 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientInstrumentationModule.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +/** + * JAX-RS Client API doesn't define a good point where we can handle connection failures, so we must + * handle these errors at the implementation level. + */ +@AutoService(InstrumentationModule.class) +public class JerseyClientInstrumentationModule extends InstrumentationModule { + + public JerseyClientInstrumentationModule() { + super("jaxrs-client", "jaxrs-client-2.0", "jersey-client", "jersey-client-2.0"); + } + + @Override + public boolean isHelperClass(String className) { + return className.equals("org.glassfish.jersey.client.OpenTelemetryResponseCallbackWrapper"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new JerseyClientConnectionErrorInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientUtil.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientUtil.java new file mode 100644 index 000000000..25df34f52 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/JerseyClientUtil.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.JaxRsClientTracer.tracer; + +import io.opentelemetry.context.Context; +import org.glassfish.jersey.client.ClientRequest; + +public final class JerseyClientUtil { + + public static void handleException(ClientRequest context, Throwable exception) { + Object prop = context.getProperty(ClientTracingFilter.CONTEXT_PROPERTY_NAME); + if (prop instanceof Context) { + tracer().endExceptionally((Context) prop, exception); + } + } + + private JerseyClientUtil() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/org/glassfish/jersey/client/OpenTelemetryResponseCallbackWrapper.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/org/glassfish/jersey/client/OpenTelemetryResponseCallbackWrapper.java new file mode 100644 index 000000000..86a92f6bb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-jersey-2.0/javaagent/src/main/java/org/glassfish/jersey/client/OpenTelemetryResponseCallbackWrapper.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.glassfish.jersey.client; + +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.JerseyClientUtil.handleException; + +import javax.ws.rs.ProcessingException; +import org.glassfish.jersey.process.internal.RequestScope; + +// implemented interface is package private so wrapper needs to be in the same package +public class OpenTelemetryResponseCallbackWrapper implements ResponseCallback { + private final ClientRequest request; + private final ResponseCallback delegate; + + public OpenTelemetryResponseCallbackWrapper(ClientRequest request, ResponseCallback delegate) { + this.request = request; + this.delegate = delegate; + } + + public static Object wrap(ClientRequest request, Object callback) { + if (callback instanceof ResponseCallback) { + return new OpenTelemetryResponseCallbackWrapper(request, (ResponseCallback) callback); + } + return callback; + } + + @Override + public void completed(ClientResponse clientResponse, RequestScope requestScope) { + delegate.completed(clientResponse, requestScope); + } + + @Override + public void failed(ProcessingException exception) { + handleException(request, exception.getCause()); + delegate.failed(exception); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/jaxrs-client-2.0-resteasy-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/jaxrs-client-2.0-resteasy-3.0-javaagent.gradle new file mode 100644 index 000000000..3794f168e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/jaxrs-client-2.0-resteasy-3.0-javaagent.gradle @@ -0,0 +1,31 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.jboss.resteasy" + module = "resteasy-client" + versions = "[3.0.0.Final,)" + } +} + +dependencies { + // compiling against a version prior to 3.0.10.Final will bind the call in ResteasyInjectAdapter: + // carrier.getHeaders().getHeaders().putSingle(key, value) + // to: + // org.jboss.resteasy.util.CaseInsensitiveMap#putSingle(Ljava/lang/String;Ljava/lang/Object;)V + // which will be incompatible with 3.0.10.Final and later, where that API was changed to: + // org.jboss.resteasy.util.CaseInsensitiveMap#putSingle(Ljava/lang/Object;Ljava/lang/Object;)V + // + // conversely, however: + // compiling against 3.0.10.Final will bind the call in ResteasyInjectAdapter: + // carrier.getHeaders().getHeaders().putSingle(key, value) + // to: + // org.jboss.resteasy.util.CaseInsensitiveMap#putSingle(Ljava/lang/Object;Ljava/lang/Object;)V + // which WILL be compatible with versions prior to 3.0.10.Final, because in those versions + // putSingle(String, Object) is a generic implementation for + // javax.ws.rs.core.MultivaluedMap.putSingle(K, V), and so there's also a synthetic bridge method + // putSingle(Object, Object) in those versions + library "org.jboss.resteasy:resteasy-client:3.0.10.Final" + + implementation project(':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-common:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyClientInstrumentationModule.java new file mode 100644 index 000000000..75311a831 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyClientInstrumentationModule.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.ResteasyClientTracer.tracer; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import javax.ws.rs.core.Response; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.resteasy.client.jaxrs.internal.ClientInvocation; + +/** + * Unlike other supported JAX-RS Client implementations, Resteasy's one is very simple and passes + * all requests through single point. Both sync ADN async! This allows for easy instrumentation and + * proper scope handling. + * + *

This specific instrumentation will not conflict with {@link JaxRsClientInstrumentationModule}, + * because {@link JaxRsClientTracer} used by the latter checks against double client spans. + */ +@AutoService(InstrumentationModule.class) +public class ResteasyClientInstrumentationModule extends InstrumentationModule { + + public ResteasyClientInstrumentationModule() { + super("jaxrs-client", "jaxrs-client-2.0", "resteasy-client", "resteasy-client-2.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ResteasyClientConnectionErrorInstrumentation()); + } + + private static final class ResteasyClientConnectionErrorInstrumentation + implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.client.jaxrs.internal.ClientInvocation"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("invoke")).and(takesArguments(0)), + ResteasyClientInstrumentationModule.class.getName() + "$InvokeAdvice"); + } + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.This ClientInvocation invocation, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (tracer().shouldStartSpan(parentContext)) { + context = tracer().startSpan(parentContext, invocation, invocation); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Response response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context, response); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyClientTracer.java new file mode 100644 index 000000000..328b83ce0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyClientTracer.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.ResteasyInjectAdapter.SETTER; + +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Response; +import org.jboss.resteasy.client.jaxrs.internal.ClientInvocation; + +public class ResteasyClientTracer + extends HttpClientTracer { + private static final ResteasyClientTracer TRACER = new ResteasyClientTracer(); + + private ResteasyClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static ResteasyClientTracer tracer() { + return TRACER; + } + + @Override + protected String method(ClientInvocation httpRequest) { + return httpRequest.getMethod(); + } + + @Override + protected URI url(ClientInvocation httpRequest) { + return httpRequest.getUri(); + } + + @Override + protected Integer status(Response httpResponse) { + return httpResponse.getStatus(); + } + + @Override + protected String requestHeader(ClientInvocation clientRequestContext, String name) { + return clientRequestContext.getHeaders().getHeader(name); + } + + @Override + protected String responseHeader(Response httpResponse, String name) { + return httpResponse.getHeaderString(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected Throwable unwrapThrowable(Throwable throwable) { + if (throwable instanceof ProcessingException) { + throwable = throwable.getCause(); + } + return super.unwrapThrowable(throwable); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxrs-client-2.0-resteasy-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyInjectAdapter.java new file mode 100644 index 000000000..b230c606d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v2_0/ResteasyInjectAdapter.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.jboss.resteasy.client.jaxrs.internal.ClientInvocation; + +public final class ResteasyInjectAdapter implements TextMapSetter { + + public static final ResteasyInjectAdapter SETTER = new ResteasyInjectAdapter(); + + @Override + public void set(ClientInvocation carrier, String key, String value) { + // Don't allow duplicates. + carrier.getHeaders().getHeaders().putSingle(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/jaxrs-1.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/jaxrs-1.0-javaagent.gradle new file mode 100644 index 000000000..9f4a93aa8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/jaxrs-1.0-javaagent.gradle @@ -0,0 +1,21 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "javax.ws.rs" + module = "jsr311-api" + versions = "[0.5,)" + } + fail { + group = "javax.ws.rs" + module = "javax.ws.rs-api" + versions = "[,]" + } +} + +dependencies { + compileOnly "javax.ws.rs:jsr311-api:1.1.1" + + testImplementation "io.dropwizard:dropwizard-testing:0.7.1" + testImplementation "javax.xml.bind:jaxb-api:2.2.3" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsAnnotationsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsAnnotationsInstrumentation.java new file mode 100644 index 000000000..0b0e451a8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsAnnotationsInstrumentation.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v1_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperMethod; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v1_0.JaxRsAnnotationsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.lang.reflect.Method; +import javax.ws.rs.Path; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JaxRsAnnotationsInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.ws.rs.Path"); + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType( + isAnnotatedWith(named("javax.ws.rs.Path")) + .or(declaresMethod(isAnnotatedWith(named("javax.ws.rs.Path"))))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and( + hasSuperMethod( + isAnnotatedWith( + namedOneOf( + "javax.ws.rs.Path", + "javax.ws.rs.DELETE", + "javax.ws.rs.GET", + "javax.ws.rs.HEAD", + "javax.ws.rs.OPTIONS", + "javax.ws.rs.POST", + "javax.ws.rs.PUT")))), + JaxRsAnnotationsInstrumentation.class.getName() + "$JaxRsAnnotationsAdvice"); + } + + @SuppressWarnings("unused") + public static class JaxRsAnnotationsAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void nameSpan( + @Advice.This Object target, + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (CallDepthThreadLocalMap.incrementCallDepth(Path.class) > 0) { + return; + } + context = tracer().startSpan(target.getClass(), method); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + CallDepthThreadLocalMap.reset(Path.class); + + scope.close(); + if (throwable == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsAnnotationsTracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsAnnotationsTracer.java new file mode 100644 index 000000000..6db021315 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsAnnotationsTracer.java @@ -0,0 +1,197 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v1_0; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.javaagent.instrumentation.api.ClassHierarchyIterable; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; + +public class JaxRsAnnotationsTracer extends BaseTracer { + + private static final JaxRsAnnotationsTracer TRACER = new JaxRsAnnotationsTracer(); + + public static JaxRsAnnotationsTracer tracer() { + return TRACER; + } + + private final ClassValue> spanNames = + new ClassValue>() { + @Override + protected Map computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + }; + + public Context startSpan(Class target, Method method) { + String pathBasedSpanName = getPathSpanName(target, method); + Context parentContext = Context.current(); + Span serverSpan = ServerSpan.fromContextOrNull(parentContext); + + // When jax-rs is the root, we want to name using the path, otherwise use the class/method. + String spanName; + if (serverSpan == null) { + spanName = pathBasedSpanName; + } else { + spanName = SpanNames.fromMethod(target, method); + updateServerSpanName(parentContext, serverSpan, pathBasedSpanName); + } + + SpanBuilder spanBuilder = spanBuilder(parentContext, spanName, SpanKind.INTERNAL); + setCodeAttributes(spanBuilder, target, method); + Span span = spanBuilder.startSpan(); + return parentContext.with(span); + } + + private static void setCodeAttributes(SpanBuilder spanBuilder, Class target, Method method) { + spanBuilder.setAttribute(SemanticAttributes.CODE_NAMESPACE, target.getName()); + if (method != null) { + spanBuilder.setAttribute(SemanticAttributes.CODE_FUNCTION, method.getName()); + } + } + + private static void updateServerSpanName(Context context, Span span, String spanName) { + if (!spanName.isEmpty()) { + span.updateName(ServletContextPath.prepend(context, spanName)); + } + } + + /** + * Returns the span name given a JaxRS annotated method. Results are cached so this method can be + * called multiple times without significantly impacting performance. + * + * @return The result can be an empty string but will never be {@code null}. + */ + private String getPathSpanName(Class target, Method method) { + Map classMap = spanNames.get(target); + String spanName = classMap.get(method); + if (spanName == null) { + String httpMethod = null; + Path methodPath = null; + Path classPath = findClassPath(target); + for (Class currentClass : new ClassHierarchyIterable(target)) { + Method currentMethod; + if (currentClass.equals(target)) { + currentMethod = method; + } else { + currentMethod = findMatchingMethod(method, currentClass.getDeclaredMethods()); + } + + if (currentMethod != null) { + if (httpMethod == null) { + httpMethod = locateHttpMethod(currentMethod); + } + if (methodPath == null) { + methodPath = findMethodPath(currentMethod); + } + + if (httpMethod != null && methodPath != null) { + break; + } + } + } + spanName = buildSpanName(classPath, methodPath); + classMap.put(method, spanName); + } + + return spanName; + } + + private static String locateHttpMethod(Method method) { + String httpMethod = null; + for (Annotation ann : method.getDeclaredAnnotations()) { + if (ann.annotationType().getAnnotation(HttpMethod.class) != null) { + httpMethod = ann.annotationType().getSimpleName(); + } + } + return httpMethod; + } + + private static Path findMethodPath(Method method) { + return method.getAnnotation(Path.class); + } + + private static Path findClassPath(Class target) { + for (Class currentClass : new ClassHierarchyIterable(target)) { + Path annotation = currentClass.getAnnotation(Path.class); + if (annotation != null) { + // Annotation overridden, no need to continue. + return annotation; + } + } + + return null; + } + + private static Method findMatchingMethod(Method baseMethod, Method[] methods) { + nextMethod: + for (Method method : methods) { + if (!baseMethod.getReturnType().equals(method.getReturnType())) { + continue; + } + + if (!baseMethod.getName().equals(method.getName())) { + continue; + } + + Class[] baseParameterTypes = baseMethod.getParameterTypes(); + Class[] parameterTypes = method.getParameterTypes(); + if (baseParameterTypes.length != parameterTypes.length) { + continue; + } + for (int i = 0; i < baseParameterTypes.length; i++) { + if (!baseParameterTypes[i].equals(parameterTypes[i])) { + continue nextMethod; + } + } + return method; + } + return null; + } + + private static String buildSpanName(Path classPath, Path methodPath) { + StringBuilder spanNameBuilder = new StringBuilder(); + boolean skipSlash = false; + if (classPath != null) { + if (!classPath.value().startsWith("/")) { + spanNameBuilder.append("/"); + } + spanNameBuilder.append(classPath.value()); + skipSlash = classPath.value().endsWith("/"); + } + + if (methodPath != null) { + String path = methodPath.value(); + if (skipSlash) { + if (path.startsWith("/")) { + path = path.length() == 1 ? "" : path.substring(1); + } + } else if (!path.startsWith("/")) { + spanNameBuilder.append("/"); + } + spanNameBuilder.append(path); + } + + return spanNameBuilder.toString().trim(); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxrs-1.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsInstrumentationModule.java new file mode 100644 index 000000000..513c90bd2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxRsInstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v1_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class JaxRsInstrumentationModule extends InstrumentationModule { + public JaxRsInstrumentationModule() { + super("jaxrs", "jaxrs-1.0"); + } + + // this is required to make sure instrumentation won't apply to jax-rs 2 + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return not(hasClassesNamed("javax.ws.rs.container.AsyncResponse")); + } + + @Override + public List typeInstrumentations() { + return singletonList(new JaxRsAnnotationsInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JaxRsAnnotations1InstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JaxRsAnnotations1InstrumentationTest.groovy new file mode 100644 index 000000000..6b9a5ea03 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JaxRsAnnotations1InstrumentationTest.groovy @@ -0,0 +1,185 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.utils.ClassUtils.getClassName +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderServerTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import javax.ws.rs.DELETE +import javax.ws.rs.GET +import javax.ws.rs.HEAD +import javax.ws.rs.OPTIONS +import javax.ws.rs.POST +import javax.ws.rs.PUT +import javax.ws.rs.Path +import spock.lang.Unroll + +class JaxRsAnnotations1InstrumentationTest extends AgentInstrumentationSpecification { + + def "instrumentation can be used as root span and resource is set to METHOD PATH"() { + setup: + def jax = new Jax() { + @POST + @Path("/a") + void call() { + } + } + jax.call() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "/a" + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" jax.getClass().getName() + "${SemanticAttributes.CODE_FUNCTION.key}" "call" + } + } + } + } + } + + @Unroll + def "span named '#paramName' from annotations on class '#className' when is not root span"() { + setup: + runUnderServerTrace("test") { + obj.call() + } + + expect: + assertTraces(1) { + trace(0, 2) { + span(0) { + name paramName + kind SERVER + hasNoParent() + attributes { + } + } + span(1) { + name "${className}.call" + childOf span(0) + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" obj.getClass().getName() + "${SemanticAttributes.CODE_FUNCTION.key}" "call" + } + } + } + } + + when: "multiple calls to the same method" + runUnderServerTrace("test") { + (1..10).each { + obj.call() + } + } + then: "doesn't increase the cache size" + + where: + paramName | obj + "/a" | new Jax() { + @Path("/a") + void call() { + } + } + "/b" | new Jax() { + @GET + @Path("/b") + void call() { + } + } + "/interface/c" | new InterfaceWithPath() { + @POST + @Path("/c") + void call() { + } + } + "/interface" | new InterfaceWithPath() { + @HEAD + void call() { + } + } + "/abstract/d" | new AbstractClassWithPath() { + @POST + @Path("/d") + void call() { + } + } + "/abstract" | new AbstractClassWithPath() { + @PUT + void call() { + } + } + "/child/e" | new ChildClassWithPath() { + @OPTIONS + @Path("/e") + void call() { + } + } + "/child/call" | new ChildClassWithPath() { + @DELETE + void call() { + } + } + "/child/call" | new ChildClassWithPath() + "/child/call" | new JavaInterfaces.ChildClassOnInterface() + "/child/call" | new JavaInterfaces.DefaultChildClassOnInterface() + + className = getClassName(obj.class) + } + + def "no annotations has no effect"() { + setup: + runUnderServerTrace("test") { + obj.call() + } + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + kind SERVER + attributes { + } + } + } + } + + where: + obj | _ + new Jax() { + void call() { + } + } | _ + } + + interface Jax { + void call() + } + + @Path("/interface") + interface InterfaceWithPath extends Jax { + @GET + void call() + } + + @Path("/abstract") + static abstract class AbstractClassWithPath implements Jax { + @PUT + abstract void call() + } + + @Path("child") + static class ChildClassWithPath extends AbstractClassWithPath { + @Path("call") + @POST + void call() { + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JerseyTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JerseyTest.groovy new file mode 100644 index 000000000..c73c3fe87 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JerseyTest.groovy @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderServerTrace + +import io.dropwizard.testing.junit.ResourceTestRule +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.junit.ClassRule +import spock.lang.Shared +import spock.lang.Unroll + +class JerseyTest extends AgentInstrumentationSpecification { + + @Shared + @ClassRule + ResourceTestRule resources = ResourceTestRule.builder() + .addResource(new Resource.Test1()) + .addResource(new Resource.Test2()) + .addResource(new Resource.Test3()) + .build() + + @Unroll + def "test #resource"() { + when: + // start a trace because the test doesn't go through any servlet or other instrumentation. + def response = runUnderServerTrace("test.span") { + resources.client().resource(resource).post(String) + } + + then: + response == expectedResponse + + assertTraces(1) { + trace(0, 2) { + span(0) { + name expectedSpanName + kind SERVER + attributes { + } + } + + span(1) { + childOf span(0) + name controllerName + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" ~/Resource[$]Test*/ + "${SemanticAttributes.CODE_FUNCTION.key}" "hello" + } + } + } + } + + where: + resource | expectedSpanName | controllerName | expectedResponse + "/test/hello/bob" | "/test/hello/{name}" | "Test1.hello" | "Test1 bob!" + "/test2/hello/bob" | "/test2/hello/{name}" | "Test2.hello" | "Test2 bob!" + "/test3/hi/bob" | "/test3/hi/{name}" | "Test3.hello" | "Test3 bob!" + } + + def "test nested call"() { + + when: + // start a trace because the test doesn't go through any servlet or other instrumentation. + def response = runUnderServerTrace("test.span") { + resources.client().resource(resource).post(String) + } + + then: + response == expectedResponse + + assertTraces(1) { + trace(0, 2) { + span(0) { + name expectedSpanName + kind SERVER + attributes { + } + } + span(1) { + childOf span(0) + name controller1Name + kind INTERNAL + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" ~/Resource[$]Test*/ + "${SemanticAttributes.CODE_FUNCTION.key}" "nested" + } + } + } + } + + where: + resource | expectedSpanName | controller1Name | expectedResponse + "/test3/nested" | "/test3/nested" | "Test3.nested" | "Test3 nested!" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/java/JavaInterfaces.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/java/JavaInterfaces.java new file mode 100644 index 000000000..ed0e594b9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/java/JavaInterfaces.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +public class JavaInterfaces { + + interface Jax { + + void call(); + } + + @Path("interface") + interface InterfaceWithClassMethodPath extends Jax { + + @Override + @GET + @Path("invoke") + void call(); + } + + @Path("abstract") + abstract static class AbstractClassOnInterfaceWithClassPath + implements InterfaceWithClassMethodPath { + + @GET + @Path("call") + @Override + public void call() { + // do nothing + } + + abstract void actual(); + } + + @Path("child") + static class ChildClassOnInterface extends AbstractClassOnInterfaceWithClassPath { + + @Override + void actual() { + // do nothing + } + } + + @Path("interface") + interface DefaultInterfaceWithClassMethodPath extends Jax { + + @Override + @GET + @Path("call") + default void call() { + actual(); + } + + void actual(); + } + + @Path("child") + static class DefaultChildClassOnInterface implements DefaultInterfaceWithClassMethodPath { + + @Override + public void actual() { + // do nothing + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/java/Resource.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/java/Resource.java new file mode 100644 index 000000000..d962f172c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/java/Resource.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +// Originally had this as a groovy class but was getting some weird errors. +@Path("/ignored") +public interface Resource { + @Path("ignored") + String hello(String name); + + @Path("/test") + interface SubResource extends Cloneable, Resource { + @Override + @POST + @Path("/hello/{name}") + String hello(@PathParam("name") String name); + } + + class Test1 implements SubResource { + @Override + public String hello(String name) { + return "Test1 " + name + "!"; + } + } + + @Path("/test2") + class Test2 implements SubResource { + @Override + public String hello(String name) { + return "Test2 " + name + "!"; + } + } + + @Path("/test3") + class Test3 implements SubResource { + @Override + @POST + @Path("/hi/{name}") + public String hello(@PathParam("name") String name) { + return "Test3 " + name + "!"; + } + + @POST + @Path("/nested") + public String nested() { + return hello("nested"); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/jaxrs-2.0-arquillian-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/jaxrs-2.0-arquillian-testing.gradle new file mode 100644 index 000000000..aa8bfbf6e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/jaxrs-2.0-arquillian-testing.gradle @@ -0,0 +1,24 @@ +ext { + skipPublish = true +} +apply plugin: "otel.java-conventions" + +// add repo for org.gradle:gradle-tooling-api which org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-gradle-depchain depends on +repositories { + mavenCentral() + maven { url 'https://repo.gradle.org/artifactory/libs-releases-local' } + mavenLocal() +} + +dependencies { + compileOnly "javax:javaee-api:7.0" + + api project(':testing-common') + implementation "io.opentelemetry:opentelemetry-api" + + def arquillianVersion = '1.4.0.Final' + implementation "org.jboss.arquillian.junit:arquillian-junit-container:${arquillianVersion}" + implementation "org.jboss.arquillian.protocol:arquillian-protocol-servlet:${arquillianVersion}" + implementation 'org.jboss.arquillian.spock:arquillian-spock-container:1.0.0.CR1' + api "org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-gradle-depchain:3.1.3" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/groovy/ArquillianRestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/groovy/ArquillianRestTest.groovy new file mode 100644 index 000000000..5986cb058 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/groovy/ArquillianRestTest.groovy @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import org.jboss.arquillian.container.test.api.Deployment +import org.jboss.arquillian.container.test.api.RunAsClient +import org.jboss.arquillian.spock.ArquillianSputnik +import org.jboss.arquillian.test.api.ArquillianResource +import org.jboss.shrinkwrap.api.ShrinkWrap +import org.jboss.shrinkwrap.api.asset.EmptyAsset +import org.jboss.shrinkwrap.api.spec.WebArchive +import org.junit.runner.RunWith +import spock.lang.Unroll +import test.CdiRestResource +import test.EjbRestResource +import test.RestApplication + +@RunWith(ArquillianSputnik) +@RunAsClient +abstract class ArquillianRestTest extends AgentInstrumentationSpecification { + + static WebClient client = WebClient.of() + + @ArquillianResource + static URI url + + @Deployment + static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive) + .addClass(RestApplication) + .addClass(CdiRestResource) + .addClass(EjbRestResource) + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml") + } + + def getContextRoot() { + return url.getPath() + } + + @Unroll + def "test #path"() { + when: + AggregatedHttpResponse response = client.get(url.resolve(path).toString()).aggregate().join() + + then: + response.status().code() == 200 + response.contentUtf8() == "hello" + + and: + assertTraces(1) { + trace(0, 2) { + span(0) { + name getContextRoot() + path + kind SERVER + hasNoParent() + } + span(1) { + name className + ".hello" + childOf span(0) + } + } + } + + where: + path | className + "rest-app/cdiHello" | "CdiRestResource" + "rest-app/ejbHello" | "EjbRestResource" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/CdiRestResource.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/CdiRestResource.java new file mode 100644 index 000000000..43c9306c9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/CdiRestResource.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test; + +import javax.inject.Named; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/cdiHello") +@Named("cdiHello") +public class CdiRestResource { + + @GET + public String hello() { + return "hello"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/EjbRestResource.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/EjbRestResource.java new file mode 100644 index 000000000..edfcccbba --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/EjbRestResource.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test; + +import javax.ejb.Stateless; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/ejbHello") +@Stateless +public class EjbRestResource { + + @GET + public String hello() { + return "hello"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/RestApplication.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/RestApplication.java new file mode 100644 index 000000000..19d5f3a91 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/main/java/test/RestApplication.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationPath("/rest-app/") +public class RestApplication extends Application { + + @Override + public Set> getClasses() { + return new HashSet<>(Arrays.asList(CdiRestResource.class, EjbRestResource.class)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/test/resources/arquillian.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/test/resources/arquillian.xml new file mode 100644 index 000000000..02e0432fa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-arquillian-testing/src/test/resources/arquillian.xml @@ -0,0 +1,15 @@ + + + + + + + build/server/wildfly-18.0.0.Final + build/server/wildfly-18.0.0.Final/modules + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/jaxrs-2.0-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/jaxrs-2.0-common-javaagent.gradle new file mode 100644 index 000000000..24ae9c352 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/jaxrs-2.0-common-javaagent.gradle @@ -0,0 +1,18 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + fail { + group = "javax.ws.rs" + module = "jsr311-api" + versions = "[,]" + } + pass { + group = "javax.ws.rs" + module = "javax.ws.rs-api" + versions = "[,]" + } +} + +dependencies { + compileOnly "javax.ws.rs:javax.ws.rs-api:2.0" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/AbstractRequestContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/AbstractRequestContextInstrumentation.java new file mode 100644 index 000000000..62fcc0236 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/AbstractRequestContextInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public abstract class AbstractRequestContextInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.ws.rs.container.ContainerRequestContext"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.ws.rs.container.ContainerRequestContext")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("abortWith")) + .and(takesArguments(1)) + .and(takesArgument(0, named("javax.ws.rs.core.Response"))), + abortAdviceName()); + } + + protected abstract String abortAdviceName(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CompletionStageFinishCallback.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CompletionStageFinishCallback.java new file mode 100644 index 000000000..26a172805 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CompletionStageFinishCallback.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxRsAnnotationsTracer.tracer; + +import io.opentelemetry.context.Context; +import java.util.function.BiFunction; + +public class CompletionStageFinishCallback implements BiFunction { + private final Context context; + + public CompletionStageFinishCallback(Context context) { + this.context = context; + } + + @Override + public T apply(T result, Throwable throwable) { + if (throwable == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, throwable); + } + return result; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ContainerRequestFilterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ContainerRequestFilterInstrumentation.java new file mode 100644 index 000000000..62de8b337 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ContainerRequestFilterInstrumentation.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This adds the filter class name to the request properties. The class name is used by + * DefaultRequestContextInstrumentation + */ +public class ContainerRequestFilterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.ws.rs.container.ContainerRequestFilter"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.ws.rs.container.ContainerRequestFilter")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("filter")) + .and(takesArguments(1)) + .and(takesArgument(0, named("javax.ws.rs.container.ContainerRequestContext"))), + ContainerRequestFilterInstrumentation.class.getName() + "$RequestFilterAdvice"); + } + + @SuppressWarnings("unused") + public static class RequestFilterAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void setFilterClass( + @Advice.This ContainerRequestFilter filter, + @Advice.Argument(0) ContainerRequestContext context) { + context.setProperty(JaxRsAnnotationsTracer.ABORT_FILTER_CLASS, filter.getClass()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java new file mode 100644 index 000000000..34abb2cb3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxRsAnnotationsTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Local; + +/** + * Default context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

This default instrumentation uses the class name of the filter to create the span. More + * specific instrumentations may override this value. + */ +public class DefaultRequestContextInstrumentation extends AbstractRequestContextInstrumentation { + @Override + protected String abortAdviceName() { + return getClass().getName() + "$ContainerRequestContextAdvice"; + } + + @SuppressWarnings("unused") + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void createGenericSpan( + @Advice.This ContainerRequestContext requestContext, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope) { + if (requestContext.getProperty(JaxRsAnnotationsTracer.ABORT_HANDLED) == null) { + Class filterClass = + (Class) requestContext.getProperty(JaxRsAnnotationsTracer.ABORT_FILTER_CLASS); + Method method = null; + try { + method = filterClass.getMethod("filter", ContainerRequestContext.class); + } catch (NoSuchMethodException e) { + // Unable to find the filter method. This should not be reachable because the context + // can only be aborted inside the filter method + } + + context = tracer().startSpan(filterClass, method); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Local("otelContext") Context context, + @Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + RequestContextHelper.closeSpanAndScope(context, scope, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAnnotationsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAnnotationsInstrumentation.java new file mode 100644 index 000000000..0eea983d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAnnotationsInstrumentation.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperMethod; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxRsAnnotationsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import java.lang.reflect.Method; +import java.util.concurrent.CompletionStage; +import javax.ws.rs.Path; +import javax.ws.rs.container.AsyncResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner.Typing; +import net.bytebuddy.matcher.ElementMatcher; + +public class JaxRsAnnotationsInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.ws.rs.Path"); + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType( + isAnnotatedWith(named("javax.ws.rs.Path")) + .or(declaresMethod(isAnnotatedWith(named("javax.ws.rs.Path"))))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and( + hasSuperMethod( + isAnnotatedWith( + namedOneOf( + "javax.ws.rs.Path", + "javax.ws.rs.DELETE", + "javax.ws.rs.GET", + "javax.ws.rs.HEAD", + "javax.ws.rs.OPTIONS", + "javax.ws.rs.PATCH", + "javax.ws.rs.POST", + "javax.ws.rs.PUT")))), + JaxRsAnnotationsInstrumentation.class.getName() + "$JaxRsAnnotationsAdvice"); + } + + @SuppressWarnings("unused") + public static class JaxRsAnnotationsAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void nameSpan( + @Advice.This Object target, + @Advice.Origin Method method, + @Advice.AllArguments Object[] args, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelAsyncResponse") AsyncResponse asyncResponse) { + ContextStore contextStore = null; + for (Object arg : args) { + if (arg instanceof AsyncResponse) { + asyncResponse = (AsyncResponse) arg; + contextStore = InstrumentationContext.get(AsyncResponse.class, Context.class); + if (contextStore.get(asyncResponse) != null) { + /* + * We are probably in a recursive call and don't want to start a new span because it + * would replace the existing span in the asyncResponse and cause it to never finish. We + * could work around this by using a list instead, but we likely don't want the extra + * span anyway. + */ + return; + } + break; + } + } + + if (CallDepthThreadLocalMap.incrementCallDepth(Path.class) > 0) { + return; + } + + context = tracer().startSpan(target.getClass(), method); + + if (contextStore != null && asyncResponse != null) { + contextStore.put(asyncResponse, context); + } + + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(readOnly = false, typing = Typing.DYNAMIC) Object returnValue, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelAsyncResponse") AsyncResponse asyncResponse) { + if (context == null || scope == null) { + return; + } + CallDepthThreadLocalMap.reset(Path.class); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + scope.close(); + return; + } + + CompletionStage asyncReturnValue = + returnValue instanceof CompletionStage ? (CompletionStage) returnValue : null; + + if (asyncResponse != null && !asyncResponse.isSuspended()) { + // Clear span from the asyncResponse. Logically this should never happen. Added to be safe. + InstrumentationContext.get(AsyncResponse.class, Context.class).put(asyncResponse, null); + } + if (asyncReturnValue != null) { + // span finished by CompletionStageFinishCallback + asyncReturnValue = asyncReturnValue.handle(new CompletionStageFinishCallback<>(context)); + } + if ((asyncResponse == null || !asyncResponse.isSuspended()) && asyncReturnValue == null) { + tracer().end(context); + } + // else span finished by AsyncResponseAdvice + + scope.close(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAnnotationsTracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAnnotationsTracer.java new file mode 100644 index 000000000..57cc6cb14 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAnnotationsTracer.java @@ -0,0 +1,230 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.javaagent.instrumentation.api.ClassHierarchyIterable; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; + +public class JaxRsAnnotationsTracer extends BaseTracer { + public static final String ABORT_FILTER_CLASS = + "io.opentelemetry.javaagent.instrumentation.jaxrs2.filter.abort.class"; + public static final String ABORT_HANDLED = + "io.opentelemetry.javaagent.instrumentation.jaxrs2.filter.abort.handled"; + + private static final JaxRsAnnotationsTracer TRACER = new JaxRsAnnotationsTracer(); + + public static JaxRsAnnotationsTracer tracer() { + return TRACER; + } + + private final ClassValue> spanNames = + new ClassValue>() { + @Override + protected Map computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + }; + + public Context startSpan(Class target, Method method) { + return startSpan(Context.current(), target, method); + } + + public Context startSpan(Context parentContext, Class target, Method method) { + // We create span and immediately update its name + // We do that in order to reuse logic inside updateSpanNames method, which is used externally as + // well. + SpanBuilder spanBuilder = spanBuilder(parentContext, "jax-rs.request", INTERNAL); + setCodeAttributes(spanBuilder, target, method); + Span span = spanBuilder.startSpan(); + updateSpanNames( + parentContext, span, ServerSpan.fromContextOrNull(parentContext), target, method); + return parentContext.with(span); + } + + public void updateSpanNames( + Context context, Span span, Span serverSpan, Class target, Method method) { + Supplier spanNameSupplier = getPathSpanNameSupplier(context, target, method); + if (serverSpan == null) { + updateSpanName(span, spanNameSupplier.get()); + } else { + ServerSpanNaming.updateServerSpanName( + context, ServerSpanNaming.Source.CONTROLLER, spanNameSupplier); + updateSpanName(span, SpanNames.fromMethod(target, method)); + } + } + + private static void updateSpanName(Span span, String spanName) { + if (!spanName.isEmpty()) { + span.updateName(spanName); + } + } + + private static void setCodeAttributes(SpanBuilder spanBuilder, Class target, Method method) { + spanBuilder.setAttribute(SemanticAttributes.CODE_NAMESPACE, target.getName()); + if (method != null) { + spanBuilder.setAttribute(SemanticAttributes.CODE_FUNCTION, method.getName()); + } + } + + private Supplier getPathSpanNameSupplier( + Context context, Class target, Method method) { + return () -> { + String pathBasedSpanName = getPathSpanName(target, method); + // If path based name is empty skip prepending context path so that path based name would + // remain as an empty string for which we skip updating span name. Path base span name is + // empty when method and class don't have a jax-rs path annotation, this can happen when + // creating an "abort" span, see RequestContextHelper. + if (!pathBasedSpanName.isEmpty()) { + pathBasedSpanName = JaxrsContextPath.prepend(context, pathBasedSpanName); + pathBasedSpanName = ServletContextPath.prepend(context, pathBasedSpanName); + } + return pathBasedSpanName; + }; + } + + /** + * Returns the span name given a JaxRS annotated method. Results are cached so this method can be + * called multiple times without significantly impacting performance. + * + * @return The result can be an empty string but will never be {@code null}. + */ + private String getPathSpanName(Class target, Method method) { + Map classMap = spanNames.get(target); + String spanName = classMap.get(method); + if (spanName == null) { + String httpMethod = null; + Path methodPath = null; + Path classPath = findClassPath(target); + for (Class currentClass : new ClassHierarchyIterable(target)) { + Method currentMethod; + if (currentClass.equals(target)) { + currentMethod = method; + } else { + currentMethod = findMatchingMethod(method, currentClass.getDeclaredMethods()); + } + + if (currentMethod != null) { + if (httpMethod == null) { + httpMethod = locateHttpMethod(currentMethod); + } + if (methodPath == null) { + methodPath = findMethodPath(currentMethod); + } + + if (httpMethod != null && methodPath != null) { + break; + } + } + } + spanName = buildSpanName(classPath, methodPath); + classMap.put(method, spanName); + } + + return spanName; + } + + private static String locateHttpMethod(Method method) { + String httpMethod = null; + for (Annotation ann : method.getDeclaredAnnotations()) { + if (ann.annotationType().getAnnotation(HttpMethod.class) != null) { + httpMethod = ann.annotationType().getSimpleName(); + } + } + return httpMethod; + } + + private static Path findMethodPath(Method method) { + return method.getAnnotation(Path.class); + } + + private static Path findClassPath(Class target) { + for (Class currentClass : new ClassHierarchyIterable(target)) { + Path annotation = currentClass.getAnnotation(Path.class); + if (annotation != null) { + // Annotation overridden, no need to continue. + return annotation; + } + } + + return null; + } + + private static Method findMatchingMethod(Method baseMethod, Method[] methods) { + nextMethod: + for (Method method : methods) { + if (!baseMethod.getReturnType().equals(method.getReturnType())) { + continue; + } + + if (!baseMethod.getName().equals(method.getName())) { + continue; + } + + Class[] baseParameterTypes = baseMethod.getParameterTypes(); + Class[] parameterTypes = method.getParameterTypes(); + if (baseParameterTypes.length != parameterTypes.length) { + continue; + } + for (int i = 0; i < baseParameterTypes.length; i++) { + if (!baseParameterTypes[i].equals(parameterTypes[i])) { + continue nextMethod; + } + } + return method; + } + return null; + } + + private static String buildSpanName(Path classPath, Path methodPath) { + StringBuilder spanNameBuilder = new StringBuilder(); + boolean skipSlash = false; + if (classPath != null) { + String classPathValue = classPath.value(); + if (!classPathValue.startsWith("/")) { + spanNameBuilder.append("/"); + } + spanNameBuilder.append(classPathValue); + skipSlash = classPathValue.endsWith("/") || classPathValue.isEmpty(); + } + + if (methodPath != null) { + String path = methodPath.value(); + if (skipSlash) { + if (path.startsWith("/")) { + path = path.length() == 1 ? "" : path.substring(1); + } + } else if (!path.startsWith("/")) { + spanNameBuilder.append("/"); + } + spanNameBuilder.append(path); + } + + return spanNameBuilder.toString().trim(); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxrs-2.0-common"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAsyncResponseInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAsyncResponseInstrumentation.java new file mode 100644 index 000000000..c4dbcd72f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsAsyncResponseInstrumentation.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxRsAnnotationsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.ws.rs.container.AsyncResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JaxRsAsyncResponseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.ws.rs.container.AsyncResponse"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.ws.rs.container.AsyncResponse")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("resume").and(takesArgument(0, Object.class)).and(isPublic()), + JaxRsAsyncResponseInstrumentation.class.getName() + "$AsyncResponseAdvice"); + transformer.applyAdviceToMethod( + named("resume").and(takesArgument(0, Throwable.class)).and(isPublic()), + JaxRsAsyncResponseInstrumentation.class.getName() + "$AsyncResponseThrowableAdvice"); + transformer.applyAdviceToMethod( + named("cancel"), + JaxRsAsyncResponseInstrumentation.class.getName() + "$AsyncResponseCancelAdvice"); + } + + @SuppressWarnings("unused") + public static class AsyncResponseAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void stopSpan(@Advice.This AsyncResponse asyncResponse) { + + ContextStore contextStore = + InstrumentationContext.get(AsyncResponse.class, Context.class); + + Context context = contextStore.get(asyncResponse); + if (context != null) { + contextStore.put(asyncResponse, null); + tracer().end(context); + } + } + } + + @SuppressWarnings("unused") + public static class AsyncResponseThrowableAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void stopSpan( + @Advice.This AsyncResponse asyncResponse, @Advice.Argument(0) Throwable throwable) { + + ContextStore contextStore = + InstrumentationContext.get(AsyncResponse.class, Context.class); + + Context context = contextStore.get(asyncResponse); + if (context != null) { + contextStore.put(asyncResponse, null); + tracer().endExceptionally(context, throwable); + } + } + } + + @SuppressWarnings("unused") + public static class AsyncResponseCancelAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void stopSpan(@Advice.This AsyncResponse asyncResponse) { + + ContextStore contextStore = + InstrumentationContext.get(AsyncResponse.class, Context.class); + + Context context = contextStore.get(asyncResponse); + if (context != null) { + contextStore.put(asyncResponse, null); + if (JaxrsConfig.CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Java8BytecodeBridge.spanFromContext(context).setAttribute("jaxrs.canceled", true); + } + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsInstrumentationModule.java new file mode 100644 index 000000000..5a32d0a87 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsInstrumentationModule.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class JaxRsInstrumentationModule extends InstrumentationModule { + public JaxRsInstrumentationModule() { + super("jaxrs", "jaxrs-2.0"); + } + + // require jax-rs 2 + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("javax.ws.rs.container.AsyncResponse"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ContainerRequestFilterInstrumentation(), + new DefaultRequestContextInstrumentation(), + new JaxRsAnnotationsInstrumentation(), + new JaxRsAsyncResponseInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsPathUtil.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsPathUtil.java new file mode 100644 index 000000000..83451f29d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxRsPathUtil.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +public final class JaxRsPathUtil { + private JaxRsPathUtil() {} + + public static String normalizePath(String path) { + // ensure that non-empty path starts with / + if (path == null || "/".equals(path)) { + path = ""; + } else if (!path.startsWith("/")) { + path = "/" + path; + } + // remove trailing / + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + return path; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConfig.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConfig.java new file mode 100644 index 000000000..1c0c06c2e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConfig.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import io.opentelemetry.instrumentation.api.config.Config; + +public final class JaxrsConfig { + + public static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.jaxrs.experimental-span-attributes", false); + + private JaxrsConfig() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/RequestContextHelper.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/RequestContextHelper.java new file mode 100644 index 000000000..e3f33fbd9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/RequestContextHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxRsAnnotationsTracer.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; + +public final class RequestContextHelper { + public static Context createOrUpdateAbortSpan( + ContainerRequestContext requestContext, Class resourceClass, Method method) { + + if (method != null && resourceClass != null) { + requestContext.setProperty(JaxRsAnnotationsTracer.ABORT_HANDLED, true); + Context context = Java8BytecodeBridge.currentContext(); + Span serverSpan = ServerSpan.fromContextOrNull(context); + Span currentSpan = Java8BytecodeBridge.spanFromContext(context); + + // if there's no current span or it's the same as the server (servlet) span we need to start + // a JAX-RS one + // in other case, DefaultRequestContextInstrumentation must have already run so it's enough + // to just update the names + if (currentSpan == null || currentSpan == serverSpan) { + return tracer().startSpan(context, resourceClass, method); + } else { + tracer().updateSpanNames(context, currentSpan, serverSpan, resourceClass, method); + } + } + return null; + } + + public static void closeSpanAndScope(Context context, Scope scope, Throwable throwable) { + if (context == null || scope == null) { + return; + } + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + + scope.close(); + } + + private RequestContextHelper() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/jaxrs-2.0-cxf-3.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/jaxrs-2.0-cxf-3.2-javaagent.gradle new file mode 100644 index 000000000..83757b290 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/jaxrs-2.0-cxf-3.2-javaagent.gradle @@ -0,0 +1,44 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + // Cant assert fails because muzzle assumes all instrumentations will fail + // Instrumentations in jaxrs-2.0-common will pass + pass { + group = "org.apache.cxf" + module = "cxf-rt-frontend-jaxrs" + versions = "[3.2,)" + extraDependency "javax.servlet:javax.servlet-api:3.1.0" + } + pass { + group = "org.apache.tomee" + module = "openejb-cxf-rs" + // earlier versions of tomee use cxf older than 3.2 + versions = "(8,)" + extraDependency "javax.servlet:javax.servlet-api:3.1.0" + } +} + +dependencies { + compileOnly "javax.ws.rs:javax.ws.rs-api:2.0" + compileOnly "javax.servlet:javax.servlet-api:3.1.0" + library "org.apache.cxf:cxf-rt-frontend-jaxrs:3.2.0" + + implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') + + testImplementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-testing') + testImplementation "javax.xml.bind:jaxb-api:2.2.3" + testImplementation "org.eclipse.jetty:jetty-webapp:9.4.6.v20170531" + + testLibrary "org.apache.cxf:cxf-rt-transports-http-jetty:3.2.0" + testLibrary "org.apache.cxf:cxf-rt-ws-policy:3.2.0" + + latestDepTestLibrary "org.eclipse.jetty:jetty-webapp:9.+" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.jaxrs.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfInstrumentationModule.java new file mode 100644 index 000000000..59881b50d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class CxfInstrumentationModule extends InstrumentationModule { + public CxfInstrumentationModule() { + super("jaxrs", "jaxrs-2.0", "cxf", "cxf-3.2"); + } + + @Override + public List typeInstrumentations() { + return asList( + new CxfRequestContextInstrumentation(), + new CxfServletControllerInstrumentation(), + new CxfRsHttpListenerInstrumentation(), + new CxfJaxRsInvokerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfJaxRsInvokerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfJaxRsInvokerInstrumentation.java new file mode 100644 index 000000000..6dd713739 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfJaxRsInvokerInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.cxf.message.Exchange; + +public class CxfJaxRsInvokerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.cxf.jaxrs.JAXRSInvoker"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invoke") + .and(takesArgument(0, named("org.apache.cxf.message.Exchange"))) + .and(takesArgument(1, Object.class)) + .and(takesArgument(2, Object.class)), + CxfJaxRsInvokerInstrumentation.class.getName() + "$InvokeAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Exchange exchange, @Advice.Local("otelScope") Scope scope) { + Context context = CxfTracingUtil.updateServerSpanName(exchange); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRequestContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRequestContextInstrumentation.java new file mode 100644 index 000000000..bc00968e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRequestContextInstrumentation.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Local; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.cxf.jaxrs.impl.AbstractRequestContextImpl; +import org.apache.cxf.jaxrs.model.MethodInvocationInfo; +import org.apache.cxf.jaxrs.model.OperationResourceInfoStack; +import org.apache.cxf.message.Message; + +/** + * CXF specific context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the CXF implementation, Message contains OperationResourceInfoStack + * which contains MethodInvocationInfo. The matched resource method can be + * retrieved from that object + */ +public class CxfRequestContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.cxf.jaxrs.impl.AbstractRequestContextImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("abortWith")) + .and(takesArguments(1)) + .and(takesArgument(0, named("javax.ws.rs.core.Response"))), + CxfRequestContextInstrumentation.class.getName() + "$ContainerRequestContextAdvice"); + } + + @SuppressWarnings("unused") + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void decorateAbortSpan( + @Advice.This AbstractRequestContextImpl requestContext, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope) { + + if (requestContext.getProperty(JaxRsAnnotationsTracer.ABORT_HANDLED) == null + && requestContext instanceof ContainerRequestContext) { + Message message = requestContext.getMessage(); + OperationResourceInfoStack resourceInfoStack = + (OperationResourceInfoStack) + message.get("org.apache.cxf.jaxrs.model.OperationResourceInfoStack"); + if (resourceInfoStack == null || resourceInfoStack.isEmpty()) { + return; + } + + MethodInvocationInfo invocationInfo = resourceInfoStack.peek(); + Method method = invocationInfo.getMethodInfo().getMethodToInvoke(); + Class resourceClass = invocationInfo.getRealClass(); + + context = + RequestContextHelper.createOrUpdateAbortSpan( + (ContainerRequestContext) requestContext, resourceClass, method); + if (context != null) { + scope = context.makeCurrent(); + } + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Local("otelContext") Context context, + @Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + RequestContextHelper.closeSpanAndScope(context, scope, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRsHttpListenerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRsHttpListenerInstrumentation.java new file mode 100644 index 000000000..43c7d4eb0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRsHttpListenerInstrumentation.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// TomEE specific instrumentation +public class CxfRsHttpListenerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.openejb.server.cxf.rs.CxfRsHttpListener"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("doInvoke")), + CxfRsHttpListenerInstrumentation.class.getName() + "$InvokeAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue("pattern") String pattern, @Advice.Local("otelScope") Scope scope) { + Context context = JaxrsContextPath.init(Java8BytecodeBridge.currentContext(), pattern); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfServletControllerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfServletControllerInstrumentation.java new file mode 100644 index 000000000..9119f7d73 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfServletControllerInstrumentation.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import javax.servlet.http.HttpServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class CxfServletControllerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.cxf.transport.servlet.ServletController"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("invokeDestination")) + .and(takesArgument(0, named("javax.servlet.http.HttpServletRequest"))), + CxfServletControllerInstrumentation.class.getName() + "$InvokeAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) HttpServletRequest httpServletRequest, + @Advice.Local("otelScope") Scope scope) { + Context context = + JaxrsContextPath.init( + Java8BytecodeBridge.currentContext(), httpServletRequest.getServletPath()); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfTracingUtil.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfTracingUtil.java new file mode 100644 index 000000000..dc9cfb1d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfTracingUtil.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxRsPathUtil.normalizePath; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import org.apache.cxf.jaxrs.model.ClassResourceInfo; +import org.apache.cxf.jaxrs.model.OperationResourceInfo; +import org.apache.cxf.jaxrs.model.URITemplate; +import org.apache.cxf.message.Exchange; + +public final class CxfTracingUtil { + + private CxfTracingUtil() {} + + public static Context updateServerSpanName(Exchange exchange) { + Context context = Context.current(); + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan == null) { + return null; + } + + OperationResourceInfo ori = exchange.get(OperationResourceInfo.class); + ClassResourceInfo cri = ori.getClassResourceInfo(); + String name = getName(cri.getURITemplate(), ori.getURITemplate()); + if (name.isEmpty()) { + return null; + } + + serverSpan.updateName( + ServletContextPath.prepend(context, JaxrsContextPath.prepend(context, name))); + // mark span name as updated from controller to avoid JaxRsAnnotationsTracer updating it + ServerSpanNaming.updateSource(context, ServerSpanNaming.Source.CONTROLLER); + + return JaxrsContextPath.init(context, JaxrsContextPath.prepend(context, name)); + } + + private static String getName(URITemplate classTemplate, URITemplate operationTemplate) { + String classPath = normalize(classTemplate); + String operationPath = normalize(operationTemplate); + + return classPath + operationPath; + } + + private static String normalize(URITemplate uriTemplate) { + if (uriTemplate == null) { + return ""; + } + + return normalizePath(uriTemplate.getValue()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfAnnotationInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfAnnotationInstrumentationTest.groovy new file mode 100644 index 000000000..6e3325d80 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfAnnotationInstrumentationTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class CxfAnnotationInstrumentationTest extends JaxRsAnnotationsInstrumentationTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfFilterTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfFilterTest.groovy new file mode 100644 index 000000000..513a0c8e2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfFilterTest.groovy @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static Resource.Test1 +import static Resource.Test2 +import static Resource.Test3 + +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import org.apache.cxf.endpoint.Server +import org.apache.cxf.jaxrs.JAXRSServerFactoryBean + +class CxfFilterTest extends JaxRsFilterTest implements HttpServerTestTrait { + + @Override + boolean testAbortPrematch() { + false + } + + @Override + boolean runsOnServer() { + true + } + + @Override + Server startServer(int port) { + JAXRSServerFactoryBean serverFactory = new JAXRSServerFactoryBean() + serverFactory.setProviders([simpleRequestFilter, prematchRequestFilter]) + serverFactory.setResourceClasses([Test1, Test2, Test3]) + serverFactory.setAddress(buildAddress().toString()) + + def server = serverFactory.create() + server.start() + + return server + } + + @Override + void stopServer(Server httpServer) { + httpServer.stop() + } + + @Override + def makeRequest(String path) { + AggregatedHttpResponse response = client.post(address.resolve(path).toString(), "").aggregate().join() + + return [response.contentUtf8(), response.status().code()] + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfHttpServerTest.groovy new file mode 100644 index 000000000..f9a88cc33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfHttpServerTest.groovy @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.ext.ExceptionMapper +import org.apache.cxf.jaxrs.JAXRSServerFactoryBean +import org.apache.cxf.endpoint.Server +import test.JaxRsTestApplication + +class CxfHttpServerTest extends JaxRsHttpServerTest { + + @Override + Server startServer(int port) { + JAXRSServerFactoryBean serverFactory = new JAXRSServerFactoryBean() + def application = new JaxRsTestApplication() + serverFactory.setApplication(application) + serverFactory.setResourceClasses(new ArrayList>(application.getClasses())) + serverFactory.setProvider(new ExceptionMapper() { + @Override + Response toResponse(Exception exception) { + return Response.status(500) + .type(MediaType.TEXT_PLAIN_TYPE) + .entity(exception.getMessage()) + .build() + } + }) + serverFactory.setAddress(buildAddress().toString()) + + def server = serverFactory.create() + server.start() + + return server + } + + @Override + void stopServer(Server httpServer) { + httpServer.stop() + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfJettyHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfJettyHttpServerTest.groovy new file mode 100644 index 000000000..39594d3e9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfJettyHttpServerTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class CxfJettyHttpServerTest extends JaxRsJettyHttpServerTest { +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/webapp/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..de8d09a8c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,21 @@ + + + + + CXFNonSpringJaxrsServlet + org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet + + javax.ws.rs.Application + test.JaxRsApplicationPathTestApplication + + true + + + + CXFNonSpringJaxrsServlet + /rest-app/* + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/jaxrs-2.0-jersey-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/jaxrs-2.0-jersey-2.0-javaagent.gradle new file mode 100644 index 000000000..dfb6a55b2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/jaxrs-2.0-jersey-2.0-javaagent.gradle @@ -0,0 +1,52 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + // Cant assert fails because muzzle assumes all instrumentations will fail + // Instrumentations in jaxrs-2.0-common will pass + pass { + group = "org.glassfish.jersey.core" + module = "jersey-server" + versions = "[2.0,3.0.0)" + extraDependency "javax.servlet:javax.servlet-api:3.1.0" + } + pass { + group = "org.glassfish.jersey.containers" + module = "jersey-container-servlet" + versions = "[2.0,3.0.0)" + extraDependency "javax.servlet:javax.servlet-api:3.1.0" + } +} + +dependencies { + compileOnly "javax.ws.rs:javax.ws.rs-api:2.0" + compileOnly "javax.servlet:javax.servlet-api:3.1.0" + library "org.glassfish.jersey.core:jersey-server:2.0" + library "org.glassfish.jersey.containers:jersey-container-servlet:2.0" + + implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + testImplementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-testing') + + // First version with DropwizardTestSupport: + testLibrary "io.dropwizard:dropwizard-testing:0.8.0" + testImplementation "javax.xml.bind:jaxb-api:2.2.3" + testImplementation "com.fasterxml.jackson.module:jackson-module-afterburner" + + latestDepTestLibrary "org.glassfish.jersey.core:jersey-server:2.+" + latestDepTestLibrary "org.glassfish.jersey.containers:jersey-container-servlet:2.+" + // this is needed because dropwizard-testing version 0.8.0 (above) pulls it in transitively, + // but the latest version of dropwizard-testing does not + latestDepTestLibrary "org.eclipse.jetty:jetty-webapp:9.+" +} + +test { + systemProperty 'testLatestDeps', testLatestDeps +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.jaxrs.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyInstrumentationModule.java new file mode 100644 index 000000000..b52ba9c65 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JerseyInstrumentationModule extends InstrumentationModule { + public JerseyInstrumentationModule() { + super("jaxrs", "jaxrs-2.0", "jersey", "jersey-2.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new JerseyRequestContextInstrumentation(), + new JerseyServletContainerInstrumentation(), + new JerseyResourceMethodDispatcherInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyRequestContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyRequestContextInstrumentation.java new file mode 100644 index 000000000..867621498 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyRequestContextInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.UriInfo; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Local; + +/** + * Jersey specific context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the Jersey implementation, UriInfo implements ResourceInfo. The + * matched resource method can be retrieved from that object + */ +public class JerseyRequestContextInstrumentation extends AbstractRequestContextInstrumentation { + @Override + protected String abortAdviceName() { + return getClass().getName() + "$ContainerRequestContextAdvice"; + } + + @SuppressWarnings("unused") + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void decorateAbortSpan( + @Advice.This ContainerRequestContext requestContext, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope) { + UriInfo uriInfo = requestContext.getUriInfo(); + + if (requestContext.getProperty(JaxRsAnnotationsTracer.ABORT_HANDLED) == null + && uriInfo instanceof ResourceInfo) { + + ResourceInfo resourceInfo = (ResourceInfo) uriInfo; + Method method = resourceInfo.getResourceMethod(); + Class resourceClass = resourceInfo.getResourceClass(); + + context = + RequestContextHelper.createOrUpdateAbortSpan(requestContext, resourceClass, method); + if (context != null) { + scope = context.makeCurrent(); + } + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Local("otelContext") Context context, + @Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + RequestContextHelper.closeSpanAndScope(context, scope, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyResourceMethodDispatcherInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyResourceMethodDispatcherInstrumentation.java new file mode 100644 index 000000000..d59f41f7b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyResourceMethodDispatcherInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import javax.ws.rs.core.Request; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JerseyResourceMethodDispatcherInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("dispatch") + .and( + takesArgument( + 1, + namedOneOf( + "javax.ws.rs.core.Request", + "org.glassfish.jersey.server.ContainerRequest"))), + JerseyResourceMethodDispatcherInstrumentation.class.getName() + "$DispatchAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(1) Request request) { + JerseyTracingUtil.updateServerSpanName(request); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyServletContainerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyServletContainerInstrumentation.java new file mode 100644 index 000000000..add4e443c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyServletContainerInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import javax.servlet.http.HttpServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JerseyServletContainerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.glassfish.jersey.servlet.ServletContainer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("service")) + .and(takesArgument(0, named("javax.servlet.http.HttpServletRequest"))) + .and(takesArgument(1, named("javax.servlet.http.HttpServletResponse"))), + JerseyServletContainerInstrumentation.class.getName() + "$ServiceAdvice"); + } + + @SuppressWarnings("unused") + public static class ServiceAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) HttpServletRequest httpServletRequest, + @Advice.Local("otelScope") Scope scope) { + Context context = + JaxrsContextPath.init( + Java8BytecodeBridge.currentContext(), httpServletRequest.getServletPath()); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyTracingUtil.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyTracingUtil.java new file mode 100644 index 000000000..f140d24b0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyTracingUtil.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxRsPathUtil.normalizePath; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import java.util.Optional; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.UriInfo; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; + +public class JerseyTracingUtil { + + public static void updateServerSpanName(Request request) { + Context context = Context.current(); + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + + ContainerRequest containerRequest = (ContainerRequest) request; + UriInfo uriInfo = containerRequest.getUriInfo(); + ExtendedUriInfo extendedUriInfo = (ExtendedUriInfo) uriInfo; + Optional name = + extendedUriInfo.getMatchedTemplates().stream() + .map((uriTemplate) -> normalizePath(uriTemplate.getTemplate())) + .reduce((a, b) -> b + a); + if (!name.isPresent()) { + return; + } + + serverSpan.updateName( + ServletContextPath.prepend(context, JaxrsContextPath.prepend(context, name.get()))); + // mark span name as updated from controller to avoid JaxRsAnnotationsTracer updating it + ServerSpanNaming.updateSource(context, ServerSpanNaming.Source.CONTROLLER); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyAnnotationInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyAnnotationInstrumentationTest.groovy new file mode 100644 index 000000000..a171d38b7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyAnnotationInstrumentationTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class JerseyAnnotationInstrumentationTest extends JaxRsAnnotationsInstrumentationTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyFilterTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyFilterTest.groovy new file mode 100644 index 000000000..11da7ff19 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyFilterTest.groovy @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static Resource.Test1 +import static Resource.Test2 +import static Resource.Test3 + +import io.dropwizard.testing.junit.ResourceTestRule +import javax.ws.rs.client.Entity +import javax.ws.rs.core.Response +import org.junit.ClassRule +import spock.lang.Shared + +class JerseyFilterTest extends JaxRsFilterTest { + @Shared + @ClassRule + ResourceTestRule resources = ResourceTestRule.builder() + .addResource(new Test1()) + .addResource(new Test2()) + .addResource(new Test3()) + .addProvider(simpleRequestFilter) + .addProvider(prematchRequestFilter) + .build() + + @Override + def makeRequest(String url) { + Response response = resources.client().target(url).request().post(Entity.text("")) + + return [response.readEntity(String), response.statusInfo.statusCode] + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy new file mode 100644 index 000000000..ac5907316 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.dropwizard.jetty.NonblockingServletHolder +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.ServletContextHandler +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer +import test.JaxRsTestApplication + +class JerseyHttpServerTest extends JaxRsHttpServerTest { + + @Override + Server startServer(int port) { + def servlet = new ServletContainer(ResourceConfig.forApplicationClass(JaxRsTestApplication)) + + def handler = new ServletContextHandler(ServletContextHandler.SESSIONS) + handler.setContextPath("/") + handler.addServlet(new NonblockingServletHolder(servlet), "/*") + + def server = new Server(port) + server.setHandler(handler) + server.start() + + return server + } + + @Override + void stopServer(Server httpServer) { + httpServer.stop() + } + + @Override + boolean asyncCancelHasSendError() { + true + } + + @Override + boolean testInterfaceMethodWithPath() { + // disables a test that jersey deems invalid + false + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy new file mode 100644 index 000000000..f7529b96c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class JerseyJettyHttpServerTest extends JaxRsJettyHttpServerTest { + + @Override + boolean asyncCancelHasSendError() { + true + } + + @Override + boolean testInterfaceMethodWithPath() { + // disables a test that jersey deems invalid + false + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyStartupListener.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyStartupListener.groovy new file mode 100644 index 000000000..dbd00f66c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyStartupListener.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.servlet.ServletContextEvent +import javax.servlet.ServletContextListener +import org.glassfish.jersey.servlet.init.JerseyServletContainerInitializer +import test.JaxRsApplicationPathTestApplication + +// ServletContainerInitializer isn't automatically called due to the way this test is set up +// so we call it ourself +class JerseyStartupListener implements ServletContextListener { + @Override + void contextInitialized(ServletContextEvent servletContextEvent) { + new JerseyServletContainerInitializer().onStartup(Collections.singleton(JaxRsApplicationPathTestApplication), + servletContextEvent.getServletContext()) + } + + @Override + void contextDestroyed(ServletContextEvent servletContextEvent) { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/webapp/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..5b492283e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,10 @@ + + + + + JerseyStartupListener + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/jaxrs-2.0-payara-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/jaxrs-2.0-payara-testing.gradle new file mode 100644 index 000000000..764f58ff4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/jaxrs-2.0-payara-testing.gradle @@ -0,0 +1,22 @@ +ext { + skipPublish = true +} +apply from: "$rootDir/gradle/instrumentation.gradle" + +// add repo for org.gradle:gradle-tooling-api which org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-gradle-depchain +// which is used by jaxrs-2.0-arquillian-testing depends on +repositories { + mavenCentral() + maven { url 'https://repo.gradle.org/artifactory/libs-releases-local' } + mavenLocal() +} + +dependencies { + testImplementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-arquillian-testing') + testRuntimeOnly "fish.payara.arquillian:arquillian-payara-server-embedded:2.4.1" + testRuntimeOnly 'fish.payara.extras:payara-embedded-web:5.2021.2' + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') + testInstrumentation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-jersey-2.0:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/src/test/groovy/PayaraRestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/src/test/groovy/PayaraRestTest.groovy new file mode 100644 index 000000000..a561d4d21 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/src/test/groovy/PayaraRestTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class PayaraRestTest extends ArquillianRestTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/src/test/resources/arquillian.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/src/test/resources/arquillian.xml new file mode 100644 index 000000000..a769e8b0a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/src/test/resources/arquillian.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/jaxrs-2.0-resteasy-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/jaxrs-2.0-resteasy-3.0-javaagent.gradle new file mode 100644 index 000000000..98adde4a4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/jaxrs-2.0-resteasy-3.0-javaagent.gradle @@ -0,0 +1,55 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + // Cant assert fails because muzzle assumes all instrumentations will fail + // Instrumentations in jaxrs-2.0-common will pass + + // Resteasy changes a class's package in 3.1.0 then moves it back in 3.5.0 and then moves it forward again in 4.0.0 + // so the jaxrs-2.0-resteasy-3.0 module applies to [3.0, 3.1) and [3.5, 4.0) + // and the jaxrs-2.0-resteasy-3.1 module applies to [3.1, 3.5) and [4.0, ) + pass { + group = "org.jboss.resteasy" + module = "resteasy-jaxrs" + versions = "[3.0.0.Final,3.1.0.Final)" + } + + pass { + group = "org.jboss.resteasy" + module = "resteasy-jaxrs" + versions = "[3.5.0.Final,4)" + } +} + +dependencies { + compileOnly "javax.ws.rs:javax.ws.rs-api:2.0" + library "org.jboss.resteasy:resteasy-jaxrs:3.0.0.Final" + + implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') + implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-common:javaagent') + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + testImplementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-testing') + testImplementation "org.eclipse.jetty:jetty-webapp:9.4.6.v20170531" + + testLibrary("org.jboss.resteasy:resteasy-undertow:3.0.4.Final") { + exclude group: 'org.jboss.resteasy', module: 'resteasy-client' + } + testLibrary "io.undertow:undertow-servlet:1.0.0.Final" + testLibrary "org.jboss.resteasy:resteasy-servlet-initializer:3.0.4.Final" + + latestDepTestLibrary "org.jboss.resteasy:resteasy-jaxrs:3.+" + latestDepTestLibrary("org.jboss.resteasy:resteasy-undertow:3.+") { + exclude group: 'org.jboss.resteasy', module: 'resteasy-client' + } +} + +test { + systemProperty 'testLatestDeps', testLatestDeps +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.jaxrs.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30InstrumentationModule.java new file mode 100644 index 000000000..2e32ac67b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30InstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Resteasy30InstrumentationModule extends InstrumentationModule { + public Resteasy30InstrumentationModule() { + super("jaxrs", "jaxrs-2.0", "resteasy", "resteasy-3.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new Resteasy30RequestContextInstrumentation(), + new ResteasyServletContainerDispatcherInstrumentation(), + new ResteasyRootNodeTypeInstrumentation(), + new ResteasyResourceMethodInvokerInstrumentation(), + new ResteasyResourceLocatorInvokerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30RequestContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30RequestContextInstrumentation.java new file mode 100644 index 000000000..94ee2e642 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30RequestContextInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Local; +import org.jboss.resteasy.core.ResourceMethodInvoker; +import org.jboss.resteasy.core.interception.PostMatchContainerRequestContext; + +/** + * RESTEasy specific context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the RESTEasy implementation, ContainerRequestContext is implemented by + * PostMatchContainerRequestContext. This class provides a way to get the matched resource + * method through getResourceMethod(). + */ +public class Resteasy30RequestContextInstrumentation extends AbstractRequestContextInstrumentation { + @Override + protected String abortAdviceName() { + return getClass().getName() + "$ContainerRequestContextAdvice"; + } + + @SuppressWarnings("unused") + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void decorateAbortSpan( + @Advice.This ContainerRequestContext requestContext, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope) { + if (requestContext.getProperty(JaxRsAnnotationsTracer.ABORT_HANDLED) == null + && requestContext instanceof PostMatchContainerRequestContext) { + + ResourceMethodInvoker resourceMethodInvoker = + ((PostMatchContainerRequestContext) requestContext).getResourceMethod(); + Method method = resourceMethodInvoker.getMethod(); + Class resourceClass = resourceMethodInvoker.getResourceClass(); + + context = + RequestContextHelper.createOrUpdateAbortSpan(requestContext, resourceClass, method); + if (context != null) { + scope = context.makeCurrent(); + } + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Local("otelContext") Context context, + @Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + RequestContextHelper.closeSpanAndScope(context, scope, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30ServletContainerDispatcherInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30ServletContainerDispatcherInstrumentation.java new file mode 100644 index 000000000..66dc0c405 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30ServletContainerDispatcherInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class Resteasy30ServletContainerDispatcherInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("service")), + Resteasy30ServletContainerDispatcherInstrumentation.class.getName() + "$ServiceAdvice"); + } + + @SuppressWarnings("unused") + public static class ServiceAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue("servletMappingPrefix") String servletMappingPrefix, + @Advice.Local("otelScope") Scope scope) { + Context context = + JaxrsContextPath.init(Java8BytecodeBridge.currentContext(), servletMappingPrefix); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyAnnotationInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyAnnotationInstrumentationTest.groovy new file mode 100644 index 000000000..f86d3473d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyAnnotationInstrumentationTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class ResteasyAnnotationInstrumentationTest extends JaxRsAnnotationsInstrumentationTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyFilterTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyFilterTest.groovy new file mode 100644 index 000000000..9b00879b2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyFilterTest.groovy @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static Resource.Test1 +import static Resource.Test2 +import static Resource.Test3 + +import javax.ws.rs.core.MediaType +import org.jboss.resteasy.mock.MockDispatcherFactory +import org.jboss.resteasy.mock.MockHttpRequest +import org.jboss.resteasy.mock.MockHttpResponse +import spock.lang.Shared + +class ResteasyFilterTest extends JaxRsFilterTest { + @Shared + def dispatcher + + def setupSpec() { + dispatcher = MockDispatcherFactory.createDispatcher() + def registry = dispatcher.getRegistry() + registry.addSingletonResource(new Test1()) + registry.addSingletonResource(new Test2()) + registry.addSingletonResource(new Test3()) + + dispatcher.getProviderFactory().register(simpleRequestFilter) + dispatcher.getProviderFactory().register(prematchRequestFilter) + } + + @Override + def makeRequest(String url) { + MockHttpRequest request = MockHttpRequest.post(url) + request.contentType(MediaType.TEXT_PLAIN_TYPE) + request.content(new byte[0]) + + MockHttpResponse response = new MockHttpResponse() + dispatcher.invoke(request, response) + + return [response.contentAsString, response.status] + } + +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy new file mode 100644 index 000000000..c16d3b030 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.undertow.Undertow +import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer +import test.JaxRsTestApplication + +class ResteasyHttpServerTest extends JaxRsHttpServerTest { + + @Override + String getContextPath() { + "/resteasy-context" + } + + @Override + UndertowJaxrsServer startServer(int port) { + def server = new UndertowJaxrsServer() + server.deploy(JaxRsTestApplication, getContextPath()) + server.start(Undertow.builder() + .addHttpListener(port, "localhost")) + return server + } + + @Override + void stopServer(UndertowJaxrsServer server) { + server.stop() + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy new file mode 100644 index 000000000..f856fdbc7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class ResteasyJettyHttpServerTest extends JaxRsJettyHttpServerTest { + +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyStartupListener.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyStartupListener.groovy new file mode 100644 index 000000000..a10dccfd8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyStartupListener.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.servlet.ServletContextEvent +import javax.servlet.ServletContextListener +import org.jboss.resteasy.plugins.servlet.ResteasyServletInitializer +import test.JaxRsApplicationPathTestApplication + +// ServletContainerInitializer isn't automatically called due to the way this test is set up +// so we call it ourself +class ResteasyStartupListener implements ServletContextListener { + @Override + void contextInitialized(ServletContextEvent servletContextEvent) { + new ResteasyServletInitializer().onStartup(Collections.singleton(JaxRsApplicationPathTestApplication), + servletContextEvent.getServletContext()) + } + + @Override + void contextDestroyed(ServletContextEvent servletContextEvent) { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/webapp/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..1da566433 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,10 @@ + + + + + ResteasyStartupListener + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/jaxrs-2.0-resteasy-3.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/jaxrs-2.0-resteasy-3.1-javaagent.gradle new file mode 100644 index 000000000..ab5d2ef91 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/jaxrs-2.0-resteasy-3.1-javaagent.gradle @@ -0,0 +1,60 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + // Cant assert fails because muzzle assumes all instrumentations will fail + // Instrumentations in jaxrs-2.0-common will pass + + // Resteasy changes a class's package in 3.1.0 then moves it back in 3.5.0 and then moves it forward again in 4.0.0 + // so the jaxrs-2.0-resteasy-3.0 module applies to [3.0, 3.1) and [3.5, 4.0) + // and the jaxrs-2.0-resteasy-3.1 module applies to [3.1, 3.5) and [4.0, ) + pass { + group = "org.jboss.resteasy" + module = "resteasy-jaxrs" + versions = "[3.1.0.Final,3.5.0.Final)" + } + + pass { + group = "org.jboss.resteasy" + module = "resteasy-core" + versions = "[4.0.0.Final,)" + } +} + +dependencies { + compileOnly "javax.ws.rs:javax.ws.rs-api:2.0" + library "org.jboss.resteasy:resteasy-jaxrs:3.1.0.Final" + + implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') + implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-common:javaagent') + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + testImplementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-testing') + testImplementation "org.eclipse.jetty:jetty-webapp:9.4.6.v20170531" + + testLibrary("org.jboss.resteasy:resteasy-undertow:3.1.0.Final") { + exclude group: 'org.jboss.resteasy', module: 'resteasy-client' + } + testLibrary "org.jboss.resteasy:resteasy-servlet-initializer:3.1.0.Final" + + // artifact name changed from 'resteasy-jaxrs' to 'resteasy-core' starting from version 4.0.0 + // TODO (trask) 4.6.1.Beta3 appears broken, revisit after next release + latestDepTestLibrary "org.jboss.resteasy:resteasy-core:4.6.1.Beta2" +} + +test { + systemProperty 'testLatestDeps', testLatestDeps +} + +if (findProperty('testLatestDeps')) { + configurations { + // artifact name changed from 'resteasy-jaxrs' to 'resteasy-core' starting from version 4.0.0 + testImplementation.exclude group: 'org.jboss.resteasy', module: 'resteasy-jaxrs' + } +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.jaxrs.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31InstrumentationModule.java new file mode 100644 index 000000000..3b9b07d1a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31InstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Resteasy31InstrumentationModule extends InstrumentationModule { + public Resteasy31InstrumentationModule() { + super("jaxrs", "jaxrs-2.0", "resteasy", "resteasy-3.1"); + } + + @Override + public List typeInstrumentations() { + return asList( + new Resteasy31RequestContextInstrumentation(), + new ResteasyServletContainerDispatcherInstrumentation(), + new ResteasyRootNodeTypeInstrumentation(), + new ResteasyResourceMethodInvokerInstrumentation(), + new ResteasyResourceLocatorInvokerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31RequestContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31RequestContextInstrumentation.java new file mode 100644 index 000000000..762cfd855 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31RequestContextInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Local; +import org.jboss.resteasy.core.ResourceMethodInvoker; +import org.jboss.resteasy.core.interception.jaxrs.PostMatchContainerRequestContext; + +/** + * RESTEasy specific context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the RESTEasy implementation, ContainerRequestContext is implemented by + * PostMatchContainerRequestContext. This class provides a way to get the matched resource + * method through getResourceMethod(). + */ +public class Resteasy31RequestContextInstrumentation extends AbstractRequestContextInstrumentation { + @Override + protected String abortAdviceName() { + return getClass().getName() + "$ContainerRequestContextAdvice"; + } + + @SuppressWarnings("unused") + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void decorateAbortSpan( + @Advice.This ContainerRequestContext requestContext, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope) { + if (requestContext.getProperty(JaxRsAnnotationsTracer.ABORT_HANDLED) == null + && requestContext instanceof PostMatchContainerRequestContext) { + + ResourceMethodInvoker resourceMethodInvoker = + ((PostMatchContainerRequestContext) requestContext).getResourceMethod(); + Method method = resourceMethodInvoker.getMethod(); + Class resourceClass = resourceMethodInvoker.getResourceClass(); + + context = + RequestContextHelper.createOrUpdateAbortSpan(requestContext, resourceClass, method); + if (context != null) { + scope = context.makeCurrent(); + } + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Local("otelContext") Context context, + @Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + RequestContextHelper.closeSpanAndScope(context, scope, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyAnnotationInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyAnnotationInstrumentationTest.groovy new file mode 100644 index 000000000..f86d3473d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyAnnotationInstrumentationTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class ResteasyAnnotationInstrumentationTest extends JaxRsAnnotationsInstrumentationTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyFilterTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyFilterTest.groovy new file mode 100644 index 000000000..9b00879b2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyFilterTest.groovy @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static Resource.Test1 +import static Resource.Test2 +import static Resource.Test3 + +import javax.ws.rs.core.MediaType +import org.jboss.resteasy.mock.MockDispatcherFactory +import org.jboss.resteasy.mock.MockHttpRequest +import org.jboss.resteasy.mock.MockHttpResponse +import spock.lang.Shared + +class ResteasyFilterTest extends JaxRsFilterTest { + @Shared + def dispatcher + + def setupSpec() { + dispatcher = MockDispatcherFactory.createDispatcher() + def registry = dispatcher.getRegistry() + registry.addSingletonResource(new Test1()) + registry.addSingletonResource(new Test2()) + registry.addSingletonResource(new Test3()) + + dispatcher.getProviderFactory().register(simpleRequestFilter) + dispatcher.getProviderFactory().register(prematchRequestFilter) + } + + @Override + def makeRequest(String url) { + MockHttpRequest request = MockHttpRequest.post(url) + request.contentType(MediaType.TEXT_PLAIN_TYPE) + request.content(new byte[0]) + + MockHttpResponse response = new MockHttpResponse() + dispatcher.invoke(request, response) + + return [response.contentAsString, response.status] + } + +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy new file mode 100644 index 000000000..c16d3b030 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.undertow.Undertow +import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer +import test.JaxRsTestApplication + +class ResteasyHttpServerTest extends JaxRsHttpServerTest { + + @Override + String getContextPath() { + "/resteasy-context" + } + + @Override + UndertowJaxrsServer startServer(int port) { + def server = new UndertowJaxrsServer() + server.deploy(JaxRsTestApplication, getContextPath()) + server.start(Undertow.builder() + .addHttpListener(port, "localhost")) + return server + } + + @Override + void stopServer(UndertowJaxrsServer server) { + server.stop() + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy new file mode 100644 index 000000000..f856fdbc7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class ResteasyJettyHttpServerTest extends JaxRsJettyHttpServerTest { + +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyStartupListener.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyStartupListener.groovy new file mode 100644 index 000000000..a10dccfd8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyStartupListener.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.servlet.ServletContextEvent +import javax.servlet.ServletContextListener +import org.jboss.resteasy.plugins.servlet.ResteasyServletInitializer +import test.JaxRsApplicationPathTestApplication + +// ServletContainerInitializer isn't automatically called due to the way this test is set up +// so we call it ourself +class ResteasyStartupListener implements ServletContextListener { + @Override + void contextInitialized(ServletContextEvent servletContextEvent) { + new ResteasyServletInitializer().onStartup(Collections.singleton(JaxRsApplicationPathTestApplication), + servletContextEvent.getServletContext()) + } + + @Override + void contextDestroyed(ServletContextEvent servletContextEvent) { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/webapp/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..1da566433 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,10 @@ + + + + + ResteasyStartupListener + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/jaxrs-2.0-resteasy-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/jaxrs-2.0-resteasy-common-javaagent.gradle new file mode 100644 index 000000000..b3057f51b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/jaxrs-2.0-resteasy-common-javaagent.gradle @@ -0,0 +1,8 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly "javax.ws.rs:javax.ws.rs-api:2.0" + compileOnly "org.jboss.resteasy:resteasy-jaxrs:3.1.0.Final" + + implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyResourceLocatorInvokerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyResourceLocatorInvokerInstrumentation.java new file mode 100644 index 000000000..fe6bcdf33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyResourceLocatorInvokerInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.resteasy.core.ResourceLocatorInvoker; + +public class ResteasyResourceLocatorInvokerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.core.ResourceLocatorInvoker"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invokeOnTargetObject") + .and(takesArgument(0, named("org.jboss.resteasy.spi.HttpRequest"))) + .and(takesArgument(1, named("org.jboss.resteasy.spi.HttpResponse"))) + .and(takesArgument(2, Object.class)), + ResteasyResourceLocatorInvokerInstrumentation.class.getName() + + "$InvokeOnTargetObjectAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeOnTargetObjectAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This ResourceLocatorInvoker resourceInvoker, + @Advice.Local("otelScope") Scope scope) { + + Context currentContext = Java8BytecodeBridge.currentContext(); + + String name = + InstrumentationContext.get(ResourceLocatorInvoker.class, String.class) + .get(resourceInvoker); + ResteasyTracingUtil.updateServerSpanName(currentContext, name); + + // subresource locator returns a resources class that may have @Path annotations + // append current path to jax-rs context path so that it would be present in the final path + Context context = + JaxrsContextPath.init(currentContext, JaxrsContextPath.prepend(currentContext, name)); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyResourceMethodInvokerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyResourceMethodInvokerInstrumentation.java new file mode 100644 index 000000000..7c7b73d38 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyResourceMethodInvokerInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.resteasy.core.ResourceMethodInvoker; + +public class ResteasyResourceMethodInvokerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.core.ResourceMethodInvoker"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invokeOnTarget") + .and(takesArgument(0, named("org.jboss.resteasy.spi.HttpRequest"))) + .and(takesArgument(1, named("org.jboss.resteasy.spi.HttpResponse"))) + .and(takesArgument(2, Object.class)), + ResteasyResourceMethodInvokerInstrumentation.class.getName() + "$InvokeOnTargetAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeOnTargetAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This ResourceMethodInvoker resourceInvoker) { + + String name = + InstrumentationContext.get(ResourceMethodInvoker.class, String.class) + .get(resourceInvoker); + ResteasyTracingUtil.updateServerSpanName(Java8BytecodeBridge.currentContext(), name); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyRootNodeTypeInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyRootNodeTypeInstrumentation.java new file mode 100644 index 000000000..f70b6cefc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyRootNodeTypeInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.resteasy.core.ResourceLocatorInvoker; +import org.jboss.resteasy.core.ResourceMethodInvoker; + +public class ResteasyRootNodeTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.core.registry.RootNode"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("addInvoker") + .and(takesArgument(0, String.class)) + // package of ResourceInvoker was changed in reasteasy 4 + .and( + takesArgument( + 1, + namedOneOf( + "org.jboss.resteasy.core.ResourceInvoker", + "org.jboss.resteasy.spi.ResourceInvoker"))), + ResteasyRootNodeTypeInstrumentation.class.getName() + "$AddInvokerAdvice"); + } + + @SuppressWarnings("unused") + public static class AddInvokerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void addInvoker( + @Advice.Argument(0) String path, + @Advice.Argument(value = 1, typing = Assigner.Typing.DYNAMIC) Object invoker) { + String normalizedPath = JaxRsPathUtil.normalizePath(path); + if (invoker instanceof ResourceLocatorInvoker) { + ResourceLocatorInvoker resourceLocatorInvoker = (ResourceLocatorInvoker) invoker; + InstrumentationContext.get(ResourceLocatorInvoker.class, String.class) + .put(resourceLocatorInvoker, normalizedPath); + } else if (invoker instanceof ResourceMethodInvoker) { + ResourceMethodInvoker resourceMethodInvoker = (ResourceMethodInvoker) invoker; + InstrumentationContext.get(ResourceMethodInvoker.class, String.class) + .put(resourceMethodInvoker, normalizedPath); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyServletContainerDispatcherInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyServletContainerDispatcherInstrumentation.java new file mode 100644 index 000000000..7333759b6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyServletContainerDispatcherInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ResteasyServletContainerDispatcherInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("service")), + ResteasyServletContainerDispatcherInstrumentation.class.getName() + "$ServiceAdvice"); + } + + @SuppressWarnings("unused") + public static class ServiceAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue("servletMappingPrefix") String servletMappingPrefix, + @Advice.Local("otelScope") Scope scope) { + Context context = + JaxrsContextPath.init(Java8BytecodeBridge.currentContext(), servletMappingPrefix); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyTracingUtil.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyTracingUtil.java new file mode 100644 index 000000000..453f8d1e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyTracingUtil.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath; + +public final class ResteasyTracingUtil { + + private ResteasyTracingUtil() {} + + public static void updateServerSpanName(Context context, String name) { + if (name == null || name.isEmpty()) { + return; + } + + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + + serverSpan.updateName( + ServletContextPath.prepend(context, JaxrsContextPath.prepend(context, name))); + // mark span name as updated from controller to avoid JaxRsAnnotationsTracer updating it + ServerSpanNaming.updateSource(context, ServerSpanNaming.Source.CONTROLLER); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/jaxrs-2.0-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/jaxrs-2.0-testing.gradle new file mode 100644 index 000000000..e76139ec9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/jaxrs-2.0-testing.gradle @@ -0,0 +1,21 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + api "javax.ws.rs:javax.ws.rs-api:2.0" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" + implementation "org.slf4j:slf4j-api" + implementation "ch.qos.logback:logback-classic" + implementation "org.slf4j:log4j-over-slf4j" + implementation "org.slf4j:jcl-over-slf4j" + implementation "org.slf4j:jul-to-slf4j" + + implementation project(':javaagent-api') + implementation project(':instrumentation-api') + implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') + + compileOnly "org.eclipse.jetty:jetty-webapp:8.0.0.v20110901" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsAnnotationsInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsAnnotationsInstrumentationTest.groovy new file mode 100644 index 000000000..8f2dd2c40 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsAnnotationsInstrumentationTest.groovy @@ -0,0 +1,185 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.utils.ClassUtils.getClassName +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderServerTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import javax.ws.rs.DELETE +import javax.ws.rs.GET +import javax.ws.rs.HEAD +import javax.ws.rs.OPTIONS +import javax.ws.rs.POST +import javax.ws.rs.PUT +import javax.ws.rs.Path +import spock.lang.Unroll + +abstract class JaxRsAnnotationsInstrumentationTest extends AgentInstrumentationSpecification { + + def "instrumentation can be used as root span and resource is set to METHOD PATH"() { + setup: + def jax = new Jax() { + @POST + @Path("/a") + void call() { + } + } + jax.call() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "/a" + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" jax.getClass().getName() + "${SemanticAttributes.CODE_FUNCTION.key}" "call" + } + } + } + } + } + + @Unroll + def "span named '#paramName' from annotations on class '#className' when is not root span"() { + setup: + runUnderServerTrace("test") { + obj.call() + } + + expect: + assertTraces(1) { + trace(0, 2) { + span(0) { + name paramName + kind SERVER + hasNoParent() + attributes { + } + } + span(1) { + name "${className}.call" + childOf span(0) + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" obj.getClass().getName() + "${SemanticAttributes.CODE_FUNCTION.key}" "call" + } + } + } + } + + when: "multiple calls to the same method" + runUnderServerTrace("test") { + (1..10).each { + obj.call() + } + } + then: "doesn't increase the cache size" + + where: + paramName | obj + "/a" | new Jax() { + @Path("/a") + void call() { + } + } + "/b" | new Jax() { + @GET + @Path("/b") + void call() { + } + } + "/interface/c" | new InterfaceWithPath() { + @POST + @Path("/c") + void call() { + } + } + "/interface" | new InterfaceWithPath() { + @HEAD + void call() { + } + } + "/abstract/d" | new AbstractClassWithPath() { + @POST + @Path("/d") + void call() { + } + } + "/abstract" | new AbstractClassWithPath() { + @PUT + void call() { + } + } + "/child/e" | new ChildClassWithPath() { + @OPTIONS + @Path("/e") + void call() { + } + } + "/child/call" | new ChildClassWithPath() { + @DELETE + void call() { + } + } + "/child/call" | new ChildClassWithPath() + "/child/call" | new JavaInterfaces.ChildClassOnInterface() + "/child/call" | new JavaInterfaces.DefaultChildClassOnInterface() + + className = getClassName(obj.class) + } + + def "no annotations has no effect"() { + setup: + runUnderServerTrace("test") { + obj.call() + } + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + kind SERVER + attributes { + } + } + } + } + + where: + obj | _ + new Jax() { + void call() { + } + } | _ + } + + interface Jax { + void call() + } + + @Path("/interface") + interface InterfaceWithPath extends Jax { + @GET + void call() + } + + @Path("/abstract") + static abstract class AbstractClassWithPath implements Jax { + @PUT + abstract void call() + } + + @Path("child") + static class ChildClassWithPath extends AbstractClassWithPath { + @Path("call") + @POST + void call() { + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsFilterTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsFilterTest.groovy new file mode 100644 index 000000000..4dcde6c47 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsFilterTest.groovy @@ -0,0 +1,189 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderServerTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.container.ContainerRequestFilter +import javax.ws.rs.container.PreMatching +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.ext.Provider +import org.junit.Assume +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class JaxRsFilterTest extends AgentInstrumentationSpecification { + + @Shared + SimpleRequestFilter simpleRequestFilter = new SimpleRequestFilter() + + @Shared + PrematchRequestFilter prematchRequestFilter = new PrematchRequestFilter() + + abstract makeRequest(String url) + + Tuple2 runRequest(String resource) { + if (runsOnServer()) { + return makeRequest(resource) + } + // start a trace because the test doesn't go through any servlet or other instrumentation. + return runUnderServerTrace("test.span") { + makeRequest(resource) + } + } + + boolean testAbortPrematch() { + true + } + + boolean runsOnServer() { + false + } + + def "test #resource, #abortNormal, #abortPrematch"() { + Assume.assumeTrue(!abortPrematch || testAbortPrematch()) + + given: + simpleRequestFilter.abort = abortNormal + prematchRequestFilter.abort = abortPrematch + def abort = abortNormal || abortPrematch + + when: + + def (responseText, responseStatus) = runRequest(resource) + + then: + responseText == expectedResponse + + if (abort) { + responseStatus == Response.Status.UNAUTHORIZED.statusCode + } else { + responseStatus == Response.Status.OK.statusCode + } + + assertTraces(1) { + trace(0, 2) { + span(0) { + name parentSpanName != null ? parentSpanName : "test.span" + kind SERVER + if (runsOnServer() && abortNormal) { + status ERROR + } + } + span(1) { + childOf span(0) + name controllerName + if (abortPrematch) { + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "JaxRsFilterTest\$PrematchRequestFilter" + "${SemanticAttributes.CODE_FUNCTION.key}" "filter" + } + } else { + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" ~/Resource[$]Test*/ + "${SemanticAttributes.CODE_FUNCTION.key}" "hello" + } + } + } + } + } + + where: + resource | abortNormal | abortPrematch | parentSpanName | controllerName | expectedResponse + "/test/hello/bob" | false | false | "/test/hello/{name}" | "Test1.hello" | "Test1 bob!" + "/test2/hello/bob" | false | false | "/test2/hello/{name}" | "Test2.hello" | "Test2 bob!" + "/test3/hi/bob" | false | false | "/test3/hi/{name}" | "Test3.hello" | "Test3 bob!" + + // Resteasy and Jersey give different resource class names for just the below case + // Resteasy returns "SubResource.class" + // Jersey returns "Test1.class + // "/test/hello/bob" | true | false | "/test/hello/{name}" | "Test1.hello" | "Aborted" + + "/test2/hello/bob" | true | false | "/test2/hello/{name}" | "Test2.hello" | "Aborted" + "/test3/hi/bob" | true | false | "/test3/hi/{name}" | "Test3.hello" | "Aborted" + "/test/hello/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" + "/test2/hello/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" + "/test3/hi/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" + } + + def "test nested call"() { + given: + simpleRequestFilter.abort = false + prematchRequestFilter.abort = false + + when: + def (responseText, responseStatus) = runRequest(resource) + + then: + responseStatus == Response.Status.OK.statusCode + responseText == expectedResponse + + assertTraces(1) { + trace(0, 2) { + span(0) { + name parentResourceName + kind SERVER + if (!runsOnServer()) { + attributes { + } + } + } + span(1) { + childOf span(0) + name controller1Name + kind INTERNAL + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" ~/Resource[$]Test*/ + "${SemanticAttributes.CODE_FUNCTION.key}" "nested" + } + } + } + } + + where: + resource | parentResourceName | controller1Name | expectedResponse + "/test3/nested" | "/test3/nested" | "Test3.nested" | "Test3 nested!" + } + + @Provider + static class SimpleRequestFilter implements ContainerRequestFilter { + boolean abort = false + + @Override + void filter(ContainerRequestContext requestContext) throws IOException { + if (abort) { + requestContext.abortWith( + Response.status(Response.Status.UNAUTHORIZED) + .entity("Aborted") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()) + } + } + } + + @Provider + @PreMatching + static class PrematchRequestFilter implements ContainerRequestFilter { + boolean abort = false + + @Override + void filter(ContainerRequestContext requestContext) throws IOException { + if (abort) { + requestContext.abortWith( + Response.status(Response.Status.UNAUTHORIZED) + .entity("Aborted Prematch") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsHttpServerTest.groovy new file mode 100644 index 000000000..daa32f29f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsHttpServerTest.groovy @@ -0,0 +1,300 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static java.util.concurrent.TimeUnit.SECONDS +import static org.junit.Assume.assumeTrue + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import spock.lang.Unroll +import test.JaxRsTestResource + +abstract class JaxRsHttpServerTest extends HttpServerTest implements AgentTestTrait { + + def "test super method without @Path"() { + given: + def response = client.get(address.resolve("test-resource-super").toString()).aggregate().join() + + expect: + response.status().code() == SUCCESS.status + response.contentUtf8() == SUCCESS.body + + assertTraces(1) { + trace(0, 2) { + span(0) { + hasNoParent() + kind SERVER + name getContextPath() + "/test-resource-super" + } + basicSpan(it, 1, "controller", span(0)) + } + } + } + + def "test interface method with @Path"() { + assumeTrue(testInterfaceMethodWithPath()) + + given: + def response = client.get(address.resolve("test-resource-interface/call").toString()).aggregate().join() + + expect: + response.status().code() == SUCCESS.status + response.contentUtf8() == SUCCESS.body + + assertTraces(1) { + trace(0, 2) { + span(0) { + hasNoParent() + kind SERVER + name getContextPath() + "/test-resource-interface/call" + } + basicSpan(it, 1, "controller", span(0)) + } + } + } + + def "test sub resource locator"() { + given: + def response = client.get(address.resolve("test-sub-resource-locator/call/sub").toString()).aggregate().join() + + expect: + response.status().code() == SUCCESS.status + response.contentUtf8() == SUCCESS.body + + assertTraces(1) { + trace(0, 5) { + span(0) { + hasNoParent() + kind SERVER + name getContextPath() + "/test-sub-resource-locator/call/sub" + } + basicSpan(it, 1,"JaxRsSubResourceLocatorTestResource.call", span(0)) + basicSpan(it, 2, "controller", span(1)) + basicSpan(it, 3, "SubResource.call", span(0)) + basicSpan(it, 4, "controller", span(3)) + } + } + } + + @Unroll + def "should handle #desc AsyncResponse"() { + given: + def url = address.resolve("async?action=${action}").toString() + + when: "async call is started" + def futureResponse = client.get(url).aggregate() + + then: "there are no traces yet" + assertTraces(0) { + } + + when: "barrier is released and resource class sends response" + JaxRsTestResource.BARRIER.await(1, SECONDS) + def response = futureResponse.join() + + then: + response.status().code() == statusCode + bodyPredicate(response.contentUtf8()) + + def spanCount = 2 + def hasSendError = asyncCancelHasSendError() && action == "cancel" + if (hasSendError) { + spanCount++ + } + assertTraces(1) { + trace(0, spanCount) { + asyncServerSpan(it, 0, url, statusCode) + handlerSpan(it, 1, span(0), "asyncOp", isCancelled, isError, errorMessage) + if (hasSendError) { + sendErrorSpan(it, 2, span(1)) + } + } + } + + where: + desc | action | statusCode | bodyPredicate | isCancelled | isError | errorMessage + "successful" | "succeed" | 200 | { it == "success" } | false | false | null + "failing" | "throw" | 500 | { it == "failure" } | false | true | "failure" + "canceled" | "cancel" | 503 | { it instanceof String } | true | false | null + } + + @Unroll + def "should handle #desc CompletionStage (JAX-RS 2.1+ only)"() { + assumeTrue(shouldTestCompletableStageAsync()) + given: + def url = address.resolve("async-completion-stage?action=${action}").toString() + + when: "async call is started" + def futureResponse = client.get(url).aggregate() + + then: "there are no traces yet" + assertTraces(0) { + } + + when: "barrier is released and resource class sends response" + JaxRsTestResource.BARRIER.await(1, SECONDS) + def response = futureResponse.join() + + then: + response.status().code() == statusCode + bodyPredicate(response.contentUtf8()) + + assertTraces(1) { + trace(0, 2) { + asyncServerSpan(it, 0, url, statusCode) + handlerSpan(it, 1, span(0), "jaxRs21Async", false, isError, errorMessage) + } + } + + where: + desc | action | statusCode | bodyPredicate | isError | errorMessage + "successful" | "succeed" | 200 | { it == "success" } | false | null + "failing" | "throw" | 500 | { it == "failure" } | true | "failure" + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + true + } + + @Override + boolean testNotFound() { + false + } + + @Override + boolean testPathParam() { + true + } + + @Override + boolean testConcurrency() { + true + } + + boolean testInterfaceMethodWithPath() { + true + } + + boolean asyncCancelHasSendError() { + false + } + + private static boolean shouldTestCompletableStageAsync() { + Boolean.getBoolean("testLatestDeps") + } + + @Override + void serverSpan(TraceAssert trace, + int index, + String traceID = null, + String parentID = null, + String method = "GET", + Long responseContentLength = null, + ServerEndpoint endpoint = SUCCESS) { + serverSpan(trace, index, traceID, parentID, method, + endpoint == PATH_PARAM ? getContextPath() + "/path/{id}/param" : endpoint.resolvePath(address).path, + endpoint.resolve(address), + endpoint.errored, + endpoint.status, + endpoint.query) + } + + void asyncServerSpan(TraceAssert trace, + int index, + String url, + int statusCode) { + def rawUrl = URI.create(url).toURL() + serverSpan(trace, index, null, null, "GET", + rawUrl.path, + rawUrl.toURI(), + statusCode >= 500, + statusCode, + null) + } + + void serverSpan(TraceAssert trace, + int index, + String traceID, + String parentID, + String method, + String path, + URI fullUrl, + boolean isError, + int statusCode, + String query) { + trace.span(index) { + name path + kind SERVER + if (isError) { + status ERROR + } + if (parentID != null) { + traceId traceID + parentSpanId parentID + } else { + hasNoParent() + } + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" { it == null || it == "127.0.0.1" } // Optional + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" fullUrl.toString() + "${SemanticAttributes.HTTP_METHOD.key}" method + "${SemanticAttributes.HTTP_STATUS_CODE.key}" statusCode + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT + "${SemanticAttributes.HTTP_CLIENT_IP.key}" TEST_CLIENT_IP + } + } + } + + @Override + void handlerSpan(TraceAssert trace, + int index, + Object parent, + String method = "GET", + ServerEndpoint endpoint = SUCCESS) { + handlerSpan(trace, index, parent, + endpoint.name().toLowerCase(), + false, + endpoint == EXCEPTION, + EXCEPTION.body) + } + + void handlerSpan(TraceAssert trace, + int index, + Object parent, + String methodName, + boolean isCancelled, + boolean isError, + String exceptionMessage = null) { + trace.span(index) { + name "JaxRsTestResource.${methodName}" + kind INTERNAL + if (isError) { + status ERROR + errorEvent(Exception, exceptionMessage) + } + childOf((SpanData) parent) + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "test.JaxRsTestResource" + "${SemanticAttributes.CODE_FUNCTION.key}" methodName + if (isCancelled) { + "jaxrs.canceled" true + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy new file mode 100644 index 000000000..bd193152c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static org.eclipse.jetty.util.resource.Resource.newResource + +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.webapp.WebAppContext + +class JaxRsJettyHttpServerTest extends JaxRsHttpServerTest { + + @Override + Server startServer(int port) { + WebAppContext webAppContext = new WebAppContext() + webAppContext.setContextPath("/") + // set up test application + webAppContext.setBaseResource(newResource("src/test/webapp")) + + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + jettyServer.setHandler(webAppContext) + jettyServer.start() + + return jettyServer + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + "/rest-app" + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/test/JaxRsTestResource.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/test/JaxRsTestResource.groovy new file mode 100644 index 000000000..0601ff486 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/groovy/test/JaxRsTestResource.groovy @@ -0,0 +1,227 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static java.util.concurrent.TimeUnit.SECONDS + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage +import java.util.concurrent.CyclicBarrier +import javax.ws.rs.ApplicationPath +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.PathParam +import javax.ws.rs.QueryParam +import javax.ws.rs.container.AsyncResponse +import javax.ws.rs.container.Suspended +import javax.ws.rs.core.Application +import javax.ws.rs.core.Context +import javax.ws.rs.core.Response +import javax.ws.rs.core.UriInfo +import javax.ws.rs.ext.ExceptionMapper + +@Path("") +class JaxRsTestResource { + @Path("/success") + @GET + String success() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } + + @Path("query") + @GET + String query_param(@QueryParam("some") String param) { + HttpServerTest.controller(QUERY_PARAM) { + "some=$param" + } + } + + @Path("redirect") + @GET + Response redirect(@Context UriInfo uriInfo) { + HttpServerTest.controller(REDIRECT) { + Response.status(Response.Status.FOUND) + .location(uriInfo.relativize(new URI(REDIRECT.body))) + .build() + } + } + + @Path("error-status") + @GET + Response error() { + HttpServerTest.controller(ERROR) { + Response.status(ERROR.status) + .entity(ERROR.body) + .build() + } + } + + @Path("exception") + @GET + Object exception() { + HttpServerTest.controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + + @Path("path/{id}/param") + @GET + String path_param(@PathParam("id") int id) { + HttpServerTest.controller(PATH_PARAM) { + id + } + } + + @Path("/child") + @GET + void indexed_child(@Context UriInfo uriInfo, @Suspended AsyncResponse response) { + def parameters = uriInfo.queryParameters + + CompletableFuture.runAsync({ + HttpServerTest.controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { parameters.getFirst(it) } + response.resume("") + } + }) + } + + static final BARRIER = new CyclicBarrier(2) + + @Path("async") + @GET + void asyncOp(@Suspended AsyncResponse response, @QueryParam("action") String action) { + CompletableFuture.runAsync({ + // await for the test method to verify that there are no spans yet + BARRIER.await(1, SECONDS) + + switch (action) { + case "succeed": + response.resume("success") + break + case "throw": + response.resume(new Exception("failure")) + break + case "cancel": + response.cancel() + break + default: + response.resume(new AssertionError((Object) ("invalid action value: " + action))) + break + } + }) + } + + @Path("async-completion-stage") + @GET + CompletionStage jaxRs21Async(@QueryParam("action") String action) { + def result = new CompletableFuture() + CompletableFuture.runAsync({ + // await for the test method to verify that there are no spans yet + BARRIER.await(1, SECONDS) + + switch (action) { + case "succeed": + result.complete("success") + break + case "throw": + result.completeExceptionally(new Exception("failure")) + break + default: + result.completeExceptionally(new AssertionError((Object) ("invalid action value: " + action))) + break + } + }) + result + } +} + +@Path("test-resource-super") +class JaxRsSuperClassTestResource extends JaxRsSuperClassTestResourceSuper { +} + +class JaxRsSuperClassTestResourceSuper { + @GET + Object call() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } +} + +class JaxRsInterfaceClassTestResource extends JaxRsInterfaceClassTestResourceSuper implements JaxRsInterface { +} + +@Path("test-resource-interface") +interface JaxRsInterface { + @Path("call") + @GET + Object call() +} + +class JaxRsInterfaceClassTestResourceSuper { + Object call() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } +} + +@Path("test-sub-resource-locator") +class JaxRsSubResourceLocatorTestResource { + @Path("call") + Object call() { + HttpServerTest.controller(SUCCESS) { + return new SubResource() + } + } +} + +class SubResource { + @Path("sub") + @GET + String call() { + HttpServerTest.controller(SUCCESS) { + new Exception().printStackTrace() + SUCCESS.body + } + } +} + +class JaxRsTestExceptionMapper implements ExceptionMapper { + @Override + Response toResponse(Exception exception) { + return Response.status(500) + .entity(exception.message) + .build() + } +} + +class JaxRsTestApplication extends Application { + @Override + Set> getClasses() { + def classes = new HashSet() + classes.add(JaxRsTestResource) + classes.add(JaxRsSuperClassTestResource) + classes.add(JaxRsInterfaceClassTestResource) + classes.add(JaxRsSubResourceLocatorTestResource) + classes.add(JaxRsTestExceptionMapper) + return classes + } +} + +@ApplicationPath("/rest-app") +class JaxRsApplicationPathTestApplication extends JaxRsTestApplication { +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/java/JavaInterfaces.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/java/JavaInterfaces.java new file mode 100644 index 000000000..ed0e594b9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/java/JavaInterfaces.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +public class JavaInterfaces { + + interface Jax { + + void call(); + } + + @Path("interface") + interface InterfaceWithClassMethodPath extends Jax { + + @Override + @GET + @Path("invoke") + void call(); + } + + @Path("abstract") + abstract static class AbstractClassOnInterfaceWithClassPath + implements InterfaceWithClassMethodPath { + + @GET + @Path("call") + @Override + public void call() { + // do nothing + } + + abstract void actual(); + } + + @Path("child") + static class ChildClassOnInterface extends AbstractClassOnInterfaceWithClassPath { + + @Override + void actual() { + // do nothing + } + } + + @Path("interface") + interface DefaultInterfaceWithClassMethodPath extends Jax { + + @Override + @GET + @Path("call") + default void call() { + actual(); + } + + void actual(); + } + + @Path("child") + static class DefaultChildClassOnInterface implements DefaultInterfaceWithClassMethodPath { + + @Override + public void actual() { + // do nothing + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/java/Resource.java b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/java/Resource.java new file mode 100644 index 000000000..d962f172c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-testing/src/main/java/Resource.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +// Originally had this as a groovy class but was getting some weird errors. +@Path("/ignored") +public interface Resource { + @Path("ignored") + String hello(String name); + + @Path("/test") + interface SubResource extends Cloneable, Resource { + @Override + @POST + @Path("/hello/{name}") + String hello(@PathParam("name") String name); + } + + class Test1 implements SubResource { + @Override + public String hello(String name) { + return "Test1 " + name + "!"; + } + } + + @Path("/test2") + class Test2 implements SubResource { + @Override + public String hello(String name) { + return "Test2 " + name + "!"; + } + } + + @Path("/test3") + class Test3 implements SubResource { + @Override + @POST + @Path("/hi/{name}") + public String hello(@PathParam("name") String name) { + return "Test3 " + name + "!"; + } + + @POST + @Path("/nested") + public String nested() { + return hello("nested"); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/jaxrs-2.0-tomee-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/jaxrs-2.0-tomee-testing.gradle new file mode 100644 index 000000000..5fa6e2c0e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/jaxrs-2.0-tomee-testing.gradle @@ -0,0 +1,24 @@ +ext { + skipPublish = true +} +apply from: "$rootDir/gradle/instrumentation.gradle" + +// add repo for org.gradle:gradle-tooling-api which org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-gradle-depchain +// which is used by jaxrs-2.0-arquillian-testing depends on +repositories { + mavenCentral() + maven { url 'https://repo.gradle.org/artifactory/libs-releases-local' } + mavenLocal() +} + +dependencies { + testImplementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-arquillian-testing') + testCompileOnly "jakarta.enterprise:jakarta.enterprise.cdi-api:2.0.2" + testRuntimeOnly "org.apache.tomee:arquillian-tomee-embedded:8.0.6" + testRuntimeOnly "org.apache.tomee:tomee-embedded:8.0.6" + testRuntimeOnly "org.apache.tomee:tomee-jaxrs:8.0.6" + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') + testInstrumentation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-cxf-3.2:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/groovy/TomeeRestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/groovy/TomeeRestTest.groovy new file mode 100644 index 000000000..ca40e9f5a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/groovy/TomeeRestTest.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.enterprise.inject.Vetoed + +// exclude this class from CDI as it causes NullPointerException when tomee is run with jdk8 +@Vetoed +class TomeeRestTest extends ArquillianRestTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/resources/arquillian.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/resources/arquillian.xml new file mode 100644 index 000000000..e0ae2a76e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/resources/arquillian.xml @@ -0,0 +1,15 @@ + + + + + + + -1 + -1 + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/resources/logback.xml new file mode 100644 index 000000000..6875217a6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/src/test/resources/logback.xml @@ -0,0 +1,18 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/jaxrs-2.0-wildfly-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/jaxrs-2.0-wildfly-testing.gradle new file mode 100644 index 000000000..6aa3f7e96 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/jaxrs-2.0-wildfly-testing.gradle @@ -0,0 +1,75 @@ +ext { + skipPublish = true +} +apply from: "$rootDir/gradle/instrumentation.gradle" + +// add repo for org.gradle:gradle-tooling-api which org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-gradle-depchain +// which is used by jaxrs-2.0-arquillian-testing depends on +repositories { + mavenCentral() + maven { url 'https://repo.gradle.org/artifactory/libs-releases-local' } + mavenLocal() +} + +configurations { + testServer +} + +dependencies { + testImplementation "javax:javaee-api:7.0" + + testImplementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-arquillian-testing') + testRuntimeOnly "org.wildfly.arquillian:wildfly-arquillian-container-embedded:2.2.0.Final" + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent') + testInstrumentation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-3.0:javaagent') + + // wildfly version used to run tests + testServer "org.wildfly:wildfly-dist:18.0.0.Final@zip" +} + +// extract wildfly dist, path is used from arquillian.xml +task setupServer(type: Copy) { + from zipTree(configurations.testServer.singleFile) + into file('build/server/') +} + +// logback-classic contains /META-INF/services/javax.servlet.ServletContainerInitializer +// that breaks deploy on embedded wildfly +// create a copy of logback-classic jar that does not have this file +task modifyLogbackJar(type: Jar) { + doFirst { + configurations.configureEach { + if (it.name.toLowerCase().endsWith('testruntimeclasspath')) { + def logbackJar = it.find { it.name.contains('logback-classic') } + from zipTree(logbackJar) + exclude( + "/META-INF/services/javax.servlet.ServletContainerInitializer" + ) + } + } + } + destinationDirectory = file("$buildDir/tmp") + archiveFileName = "logback-classic-modified.jar" +} + +test.dependsOn modifyLogbackJar, setupServer + +test { + doFirst { + // --add-modules is unrecognized on jdk8, ignore it instead of failing + jvmArgs "-XX:+IgnoreUnrecognizedVMOptions" + // needed for java 11 to avoid org.jboss.modules.ModuleNotFoundException: java.se + jvmArgs "--add-modules=java.se" + // add offset to default port values + jvmArgs "-Djboss.socket.binding.port-offset=100" + + // remove logback-classic from classpath + classpath = classpath.filter { + return !it.absolutePath.contains("logback-classic") + } + // add modified copy of logback-classic to classpath + classpath += files("$buildDir/tmp/logback-classic-modified.jar") + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/src/test/groovy/WildflyRestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/src/test/groovy/WildflyRestTest.groovy new file mode 100644 index 000000000..02a0f4afe --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/src/test/groovy/WildflyRestTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class WildflyRestTest extends ArquillianRestTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/src/test/resources/arquillian.xml b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/src/test/resources/arquillian.xml new file mode 100644 index 000000000..02e0432fa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/src/test/resources/arquillian.xml @@ -0,0 +1,15 @@ + + + + + + + build/server/wildfly-18.0.0.Final + build/server/wildfly-18.0.0.Final/modules + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/jaxws-2.0-axis2-1.6-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/jaxws-2.0-axis2-1.6-javaagent.gradle new file mode 100644 index 000000000..706d3778b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/jaxws-2.0-axis2-1.6-javaagent.gradle @@ -0,0 +1,44 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.axis2" + module = "axis2-jaxws" + versions = "[1.6.0,)" + assertInverse = true + // version 1.2 depends on org.apache.axis2:axis2-kernel:1.2 + // which depends on org.apache.neethi:neethi:2.0.1 which does not exist + // version 1.3 depends on org.apache.axis2:axis2-kernel:1.3 + // which depends on org.apache.woden:woden:1.0-incubating-M7b which does not exist + skip('1.2', '1.3') + } +} + +configurations { + // axis has a dependency on servlet api, get rid of it + all*.exclude group: 'javax.servlet', module: 'servlet-api' +} + +dependencies { + implementation project(':instrumentation:jaxws:jaxws-2.0-axis2-1.6:library') + + def axis2Version = '1.6.0' + library "org.apache.axis2:axis2-jaxws:${axis2Version}" + testLibrary "org.apache.axis2:axis2-transport-http:${axis2Version}" + testLibrary "org.apache.axis2:axis2-transport-local:${axis2Version}" + + testImplementation project(":instrumentation:jaxws:jaxws-2.0-testing") + + testInstrumentation project(":instrumentation:jaxws:jaxws-2.0:javaagent") + testInstrumentation project(":instrumentation:jaxws:jws-1.1:javaagent") + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') + + testImplementation "javax.xml.bind:jaxb-api:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-core:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-impl:2.2.11" + + testImplementation "com.sun.xml.ws:jaxws-rt:2.2.8" + testImplementation "com.sun.xml.ws:jaxws-tools:2.2.8" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/axis2/Axis2InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/axis2/Axis2InstrumentationModule.java new file mode 100644 index 000000000..51444c0a3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/axis2/Axis2InstrumentationModule.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.axis2; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class Axis2InstrumentationModule extends InstrumentationModule { + public Axis2InstrumentationModule() { + super("axis2", "axis2-1.6"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // class added in 1.6.0 + return hasClassesNamed("org.apache.axis2.jaxws.api.MessageAccessor"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new InvocationListenerRegistryTypeInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/axis2/InvocationListenerRegistryTypeInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/axis2/InvocationListenerRegistryTypeInstrumentation.java new file mode 100644 index 000000000..ad2dc2fe2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/axis2/InvocationListenerRegistryTypeInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.axis2; + +import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.instrumentation.axis2.TracingInvocationListenerFactory; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.axis2.jaxws.registry.InvocationListenerRegistry; + +public class InvocationListenerRegistryTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.axis2.jaxws.registry.InvocationListenerRegistry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isTypeInitializer(), + InvocationListenerRegistryTypeInstrumentation.class.getName() + "$ClassInitializerAdvice"); + } + + @SuppressWarnings("unused") + public static class ClassInitializerAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit() { + InvocationListenerRegistry.addFactory(new TracingInvocationListenerFactory()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/groovy/Axis2JaxWsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/groovy/Axis2JaxWsTest.groovy new file mode 100644 index 000000000..29ae9a5fe --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/groovy/Axis2JaxWsTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Axis2JaxWsTest extends AbstractJaxWsTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/java/test/CustomJaxWsDeployer.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/java/test/CustomJaxWsDeployer.java new file mode 100644 index 000000000..0efa09ae0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/java/test/CustomJaxWsDeployer.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import org.apache.axis2.jaxws.framework.JAXWSDeployer; + +// used in axis2.xml +public class CustomJaxWsDeployer extends JAXWSDeployer { + + @Override + protected ArrayList getClassesInWebInfDirectory(File file) { + // help axis find our webservice classes + return new ArrayList<>(Arrays.asList("hello.HelloService", "hello.HelloServiceImpl")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/axis2.xml b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/axis2.xml new file mode 100644 index 000000000..5c83d737d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/axis2.xml @@ -0,0 +1,530 @@ + + + + + + + true + false + false + false + + + + + false + + + true + + + + + + + + + + + + + + 30000 + + + + false + + + + + + false + + admin + axis2 + + + + + + + + + + + + + + + + + + + + + ws + + + + false + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8080 + + + + + + + + + + + + + + + + + + + + + HTTP/1.1 + chunked + + + + + + + HTTP/1.1 + chunked + + + + + + + + + + + + + + + + + + + + + + + + true + + + multicast + + + wso2.carbon.domain + + + true + + + 10 + + + 228.0.0.4 + + + 45564 + + + 500 + + + 3000 + + + 127.0.0.1 + + + 127.0.0.1 + + + 4000 + + + true + + + true + + + + + + + + + + + 127.0.0.1 + 4000 + + + 127.0.0.1 + 4001 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/classes/placeholder.txt b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/classes/placeholder.txt new file mode 100644 index 000000000..4941f2866 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/classes/placeholder.txt @@ -0,0 +1 @@ +axis requires WEB-INF/classes to exist \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/lib/placeholder.txt b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/lib/placeholder.txt new file mode 100644 index 000000000..a1572ca70 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/lib/placeholder.txt @@ -0,0 +1 @@ +axis requires WEB-INF/lib to exist \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/web.xml new file mode 100644 index 000000000..a7db7797a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/resources/test-app/WEB-INF/web.xml @@ -0,0 +1,18 @@ + + + + + AxisServlet + org.apache.axis2.transport.http.AxisServlet + 1 + + + + AxisServlet + /ws/* + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/jaxws-2.0-axis2-1.6-library.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/jaxws-2.0-axis2-1.6-library.gradle new file mode 100644 index 000000000..a19aa9d2d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/jaxws-2.0-axis2-1.6-library.gradle @@ -0,0 +1,5 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "org.apache.axis2:axis2-jaxws:1.6.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/src/main/java/io/opentelemetry/instrumentation/axis2/Axis2JaxWsTracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/src/main/java/io/opentelemetry/instrumentation/axis2/Axis2JaxWsTracer.java new file mode 100644 index 000000000..76acfb5dc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/src/main/java/io/opentelemetry/instrumentation/axis2/Axis2JaxWsTracer.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.axis2; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import javax.servlet.http.HttpServletRequest; +import org.apache.axis2.jaxws.core.MessageContext; + +public class Axis2JaxWsTracer extends BaseTracer { + private static final String CONTEXT_KEY = Axis2JaxWsTracer.class.getName() + ".Context"; + private static final String SCOPE_KEY = Axis2JaxWsTracer.class.getName() + ".Scope"; + private static final Axis2JaxWsTracer TRACER = new Axis2JaxWsTracer(); + + public static Axis2JaxWsTracer tracer() { + return TRACER; + } + + public void startSpan(MessageContext message) { + org.apache.axis2.context.MessageContext axisMessageContext = message.getAxisMessageContext(); + String serviceName = axisMessageContext.getOperationContext().getServiceName(); + String operationName = axisMessageContext.getOperationContext().getOperationName(); + String spanName = serviceName + "/" + operationName; + Context context = startSpan(spanName, INTERNAL); + Scope scope = context.makeCurrent(); + + message.setProperty(CONTEXT_KEY, context); + message.setProperty(SCOPE_KEY, scope); + + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan != null) { + String serverSpanName = spanName; + HttpServletRequest request = + (HttpServletRequest) message.getMEPContext().get("transport.http.servletRequest"); + if (request != null) { + String servletPath = request.getServletPath(); + if (!servletPath.isEmpty()) { + serverSpanName = servletPath + "/" + spanName; + } + } + serverSpan.updateName(ServletContextPath.prepend(context, serverSpanName)); + } + } + + public void end(MessageContext message) { + end(message, null); + } + + public void end(MessageContext message, Throwable throwable) { + Scope scope = (Scope) message.getProperty(SCOPE_KEY); + if (scope == null) { + return; + } + + scope.close(); + Context context = (Context) message.getProperty(CONTEXT_KEY); + + message.setProperty(CONTEXT_KEY, null); + message.setProperty(SCOPE_KEY, null); + + if (throwable != null) { + endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxws-2.0-axis2-1.6"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/src/main/java/io/opentelemetry/instrumentation/axis2/TracingInvocationListenerFactory.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/src/main/java/io/opentelemetry/instrumentation/axis2/TracingInvocationListenerFactory.java new file mode 100644 index 000000000..700ac140e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-axis2-1.6/library/src/main/java/io/opentelemetry/instrumentation/axis2/TracingInvocationListenerFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.axis2; + +import static io.opentelemetry.instrumentation.axis2.Axis2JaxWsTracer.tracer; + +import org.apache.axis2.jaxws.core.MessageContext; +import org.apache.axis2.jaxws.server.InvocationListener; +import org.apache.axis2.jaxws.server.InvocationListenerBean; +import org.apache.axis2.jaxws.server.InvocationListenerFactory; + +public class TracingInvocationListenerFactory implements InvocationListenerFactory { + @Override + public InvocationListener createInvocationListener(MessageContext messageContext) { + return new TracingInvocationListener(messageContext); + } + + static class TracingInvocationListener implements InvocationListener { + private final MessageContext messageContext; + + TracingInvocationListener(MessageContext messageContext) { + this.messageContext = messageContext; + } + + @Override + public void notify(InvocationListenerBean invocationListenerBean) { + switch (invocationListenerBean.getState()) { + case REQUEST: + tracer().startSpan(messageContext); + break; + case RESPONSE: + tracer().end(messageContext); + break; + default: + } + } + + @Override + public void notifyOnException(InvocationListenerBean invocationListenerBean) { + tracer().end(messageContext, invocationListenerBean.getThrowable()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/jaxws-2.0-cxf-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/jaxws-2.0-cxf-3.0-javaagent.gradle new file mode 100644 index 000000000..9adae1b6b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/jaxws-2.0-cxf-3.0-javaagent.gradle @@ -0,0 +1,34 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.cxf" + module = "cxf-rt-frontend-jaxws" + versions = "[3.0.0,)" + assertInverse = true + extraDependency "javax.servlet:javax.servlet-api:3.0.1" + } +} + +dependencies { + implementation project(':instrumentation:jaxws:jaxws-2.0-cxf-3.0:library') + + library "org.apache.cxf:cxf-rt-frontend-jaxws:3.0.0" + testLibrary "org.apache.cxf:cxf-rt-transports-http:3.0.0" + + testImplementation project(":instrumentation:jaxws:jaxws-2.0-testing") + + testInstrumentation project(":instrumentation:jaxws:jaxws-2.0:javaagent") + testInstrumentation project(":instrumentation:jaxws:jws-1.1:javaagent") + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') + + testImplementation "javax.xml.ws:jaxws-api:2.3.1" + testImplementation "javax.xml.bind:jaxb-api:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-core:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-impl:2.2.11" + testImplementation "javax.activation:javax.activation-api:1.2.0" + testImplementation "javax.annotation:javax.annotation-api:1.2" + testImplementation "com.sun.xml.messaging.saaj:saaj-impl:1.5.2" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfInstrumentationModule.java new file mode 100644 index 000000000..56b158158 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfInstrumentationModule.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cxf; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class CxfInstrumentationModule extends InstrumentationModule { + public CxfInstrumentationModule() { + super("cxf", "cxf-3.0"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new JaxWsServerFactoryBeanInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/JaxWsServerFactoryBeanInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/JaxWsServerFactoryBeanInstrumentation.java new file mode 100644 index 000000000..fb2105d3b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/JaxWsServerFactoryBeanInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cxf; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.cxf.endpoint.Endpoint; +import org.apache.cxf.endpoint.Server; + +public class JaxWsServerFactoryBeanInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + // TODO (trask) add test for Tomee JAX-WS (which is based on OpenEJB/CXF) + // in the meantime: + // it's important to instrument this underlying class, instead of + // org.apache.cxf.jaxws.EndpointImpl, because Tomee JAX-WS (which is based on OpenEJB/CXF) has + // it's own copy/variant of that class (org.apache.openejb.server.cxf.CxfEndpoint) + return named("org.apache.cxf.jaxws.JaxWsServerFactoryBean"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("create") + .and(takesNoArguments().and(returns(named("org.apache.cxf.endpoint.Server")))), + JaxWsServerFactoryBeanInstrumentation.class.getName() + "$CreateAdvice"); + } + + @SuppressWarnings("unused") + public static class CreateAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onEnter(@Advice.Return Server server) { + Endpoint endpoint = server.getEndpoint(); + endpoint.getInInterceptors().add(new TracingStartInInterceptor()); + endpoint.getInInterceptors().add(new TracingEndInInterceptor()); + endpoint.getOutFaultInterceptors().add(new TracingOutFaultInterceptor()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy new file mode 100644 index 000000000..b81fa377c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class CxfJaxWsTest extends AbstractJaxWsTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/TestWsServlet.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/TestWsServlet.groovy new file mode 100644 index 000000000..e86290fe8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/TestWsServlet.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import hello.HelloServiceImpl +import javax.servlet.ServletConfig +import org.apache.cxf.jaxws.EndpointImpl +import org.apache.cxf.transport.servlet.CXFNonSpringServlet + +class TestWsServlet extends CXFNonSpringServlet { + @Override + void loadBus(ServletConfig servletConfig) { + super.loadBus(servletConfig) + + // publish test webservice + Object implementor = new HelloServiceImpl() + EndpointImpl endpoint = new EndpointImpl(bus, implementor) + endpoint.publish("/HelloService") + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/resources/test-app/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/resources/test-app/WEB-INF/web.xml new file mode 100644 index 000000000..9c6ebc21d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/resources/test-app/WEB-INF/web.xml @@ -0,0 +1,17 @@ + + + + + wsServlet + TestWsServlet + 1 + + + + wsServlet + /ws/* + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/jaxws-2.0-cxf-3.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/jaxws-2.0-cxf-3.0-library.gradle new file mode 100644 index 000000000..e2be94692 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/jaxws-2.0-cxf-3.0-library.gradle @@ -0,0 +1,6 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "javax.servlet:javax.servlet-api:3.0.1" + compileOnly "org.apache.cxf:cxf-rt-frontend-jaxws:3.0.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfJaxWsTracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfJaxWsTracer.java new file mode 100644 index 000000000..257db37c6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfJaxWsTracer.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cxf; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import javax.servlet.http.HttpServletRequest; +import org.apache.cxf.interceptor.Fault; +import org.apache.cxf.message.Exchange; +import org.apache.cxf.message.Message; +import org.apache.cxf.service.model.BindingOperationInfo; + +public class CxfJaxWsTracer extends BaseTracer { + private static final String CONTEXT_KEY = CxfJaxWsTracer.class.getName() + ".Context"; + private static final String SCOPE_KEY = CxfJaxWsTracer.class.getName() + ".Scope"; + private static final CxfJaxWsTracer TRACER = new CxfJaxWsTracer(); + + public static CxfJaxWsTracer tracer() { + return TRACER; + } + + public void startSpan(Message message) { + Exchange exchange = message.getExchange(); + BindingOperationInfo bindingOperationInfo = exchange.get(BindingOperationInfo.class); + if (bindingOperationInfo == null) { + return; + } + + String serviceName = bindingOperationInfo.getBinding().getService().getName().getLocalPart(); + String operationName = bindingOperationInfo.getOperationInfo().getName().getLocalPart(); + String spanName = serviceName + "/" + operationName; + Context context = startSpan(spanName, INTERNAL); + Scope scope = context.makeCurrent(); + + exchange.put(CONTEXT_KEY, context); + exchange.put(SCOPE_KEY, scope); + + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan != null) { + String serverSpanName = spanName; + HttpServletRequest request = (HttpServletRequest) message.get("HTTP.REQUEST"); + if (request != null) { + String servletPath = request.getServletPath(); + if (!servletPath.isEmpty()) { + serverSpanName = servletPath + "/" + spanName; + } + } + serverSpan.updateName(ServletContextPath.prepend(context, serverSpanName)); + } + } + + public void stopSpan(Message message) { + Exchange exchange = message.getExchange(); + Scope scope = (Scope) exchange.remove(SCOPE_KEY); + if (scope == null) { + return; + } + scope.close(); + Context context = (Context) exchange.remove(CONTEXT_KEY); + + Throwable throwable = message.getContent(Exception.class); + if (throwable instanceof Fault && throwable.getCause() != null) { + throwable = throwable.getCause(); + } + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxws-2.0-cxf-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingEndInInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingEndInInterceptor.java new file mode 100644 index 000000000..e4ac580fe --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingEndInInterceptor.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cxf; + +import static io.opentelemetry.javaagent.instrumentation.cxf.CxfJaxWsTracer.tracer; + +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase; + +public class TracingEndInInterceptor extends AbstractPhaseInterceptor { + public TracingEndInInterceptor() { + super(Phase.POST_INVOKE); + } + + @Override + public void handleMessage(Message message) { + tracer().stopSpan(message); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingOutFaultInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingOutFaultInterceptor.java new file mode 100644 index 000000000..435e3305a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingOutFaultInterceptor.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cxf; + +import static io.opentelemetry.javaagent.instrumentation.cxf.CxfJaxWsTracer.tracer; + +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase; + +public class TracingOutFaultInterceptor extends AbstractPhaseInterceptor { + public TracingOutFaultInterceptor() { + super(Phase.SETUP); + } + + @Override + public void handleMessage(Message message) { + tracer().stopSpan(message); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptor.java new file mode 100644 index 000000000..7cfab859a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-cxf-3.0/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptor.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cxf; + +import static io.opentelemetry.javaagent.instrumentation.cxf.CxfJaxWsTracer.tracer; + +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase; + +public class TracingStartInInterceptor extends AbstractPhaseInterceptor { + + public TracingStartInInterceptor() { + super(Phase.PRE_INVOKE); + } + + @Override + public void handleMessage(Message message) { + tracer().startSpan(message); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/jaxws-2.0-metro-2.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/jaxws-2.0-metro-2.2-javaagent.gradle new file mode 100644 index 000000000..93e8808ce --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/jaxws-2.0-metro-2.2-javaagent.gradle @@ -0,0 +1,32 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.sun.xml.ws" + module = "jaxws-rt" + versions = "[2.2.0.1,3)" + // version 2.3.4 depends on org.glassfish.gmbal:gmbal-api-only:4.0.3 which does not exist + skip('2.3.4') + assertInverse = true + extraDependency "javax.servlet:javax.servlet-api:3.0.1" + } +} + +dependencies { + library "com.sun.xml.ws:jaxws-rt:2.2.0.1" + + compileOnly "javax.servlet:javax.servlet-api:3.0.1" + + testImplementation project(":instrumentation:jaxws:jaxws-2.0-testing") + + testInstrumentation project(":instrumentation:jaxws:jaxws-2.0:javaagent") + testInstrumentation project(":instrumentation:jaxws:jws-1.1:javaagent") + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') + + // TODO (trask) revisit in a few months hopefully once 2.3.5 is published + // and this can go back version '2.+' + // version 2.3.4 depends on org.glassfish.gmbal:gmbal-api-only:4.0.3 which does not exist + latestDepTestLibrary "com.sun.xml.ws:jaxws-rt:2.3.3" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java new file mode 100644 index 000000000..9e1332b86 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.metro; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Arrays; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class MetroInstrumentationModule extends InstrumentationModule { + public MetroInstrumentationModule() { + super("metro", "metro-2.2"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("javax.jws.WebService"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new ServerTubeAssemblerContextInstrumentation(), new SoapFaultBuilderInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroJaxWsTracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroJaxWsTracer.java new file mode 100644 index 000000000..29e3a7c40 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroJaxWsTracer.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.metro; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; + +import com.sun.xml.ws.api.message.Packet; +import com.sun.xml.ws.api.server.WSEndpoint; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import javax.servlet.http.HttpServletRequest; +import javax.xml.ws.handler.MessageContext; + +public class MetroJaxWsTracer extends BaseTracer { + private static final String CONTEXT_KEY = "TracingPropertySet.Context"; + private static final String SCOPE_KEY = "TracingPropertySet.Scope"; + private static final String THROWABLE_KEY = "TracingPropertySet.Throwable"; + + private static final MetroJaxWsTracer TRACER = new MetroJaxWsTracer(); + + public static MetroJaxWsTracer tracer() { + return TRACER; + } + + public void startSpan(WSEndpoint endpoint, Packet packet) { + String serviceName = endpoint.getServiceName().getLocalPart(); + String operationName = packet.getWSDLOperation().getLocalPart(); + String spanName = serviceName + "/" + operationName; + Context context = startSpan(spanName, INTERNAL); + Scope scope = context.makeCurrent(); + + // store context and scope + packet.invocationProperties.put(CONTEXT_KEY, context); + packet.invocationProperties.put(SCOPE_KEY, scope); + + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan != null) { + String serverSpanName = spanName; + HttpServletRequest request = (HttpServletRequest) packet.get(MessageContext.SERVLET_REQUEST); + if (request != null) { + String servletPath = request.getServletPath(); + if (!servletPath.isEmpty()) { + String pathInfo = request.getPathInfo(); + if (pathInfo != null) { + serverSpanName = servletPath + "/" + spanName; + } else { + // when pathInfo is null then there is a servlet that is mapped to this exact service + // servletPath already contains the service name + serverSpanName = servletPath + "/" + operationName; + } + } + } + serverSpan.updateName(ServletContextPath.prepend(context, serverSpanName)); + } + } + + public void end(Packet packet) { + end(packet, null); + } + + public void end(Packet packet, Throwable throwable) { + Scope scope = (Scope) packet.invocationProperties.remove(SCOPE_KEY); + if (scope != null) { + scope.close(); + + Context context = (Context) packet.invocationProperties.remove(CONTEXT_KEY); + if (throwable == null) { + throwable = (Throwable) packet.invocationProperties.remove(THROWABLE_KEY); + } + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } + + public void storeThrowable(Packet packet, Throwable throwable) { + packet.invocationProperties.put(THROWABLE_KEY, throwable); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxws-2.0-metro-2.2"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/ServerTubeAssemblerContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/ServerTubeAssemblerContextInstrumentation.java new file mode 100644 index 000000000..b898f7a9b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/ServerTubeAssemblerContextInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.metro; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.sun.xml.ws.api.pipe.ServerTubeAssemblerContext; +import com.sun.xml.ws.api.pipe.Tube; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ServerTubeAssemblerContextInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.sun.xml.ws.api.pipe.ServerTubeAssemblerContext"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("createMonitoringTube").and(takesArgument(0, named("com.sun.xml.ws.api.pipe.Tube"))), + ServerTubeAssemblerContextInstrumentation.class.getName() + "$AddTracingAdvice"); + } + + @SuppressWarnings("unused") + public static class AddTracingAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This ServerTubeAssemblerContext context, + @Advice.Return(readOnly = false) Tube tube) { + tube = new TracingTube(context.getEndpoint(), tube); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/SoapFaultBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/SoapFaultBuilderInstrumentation.java new file mode 100644 index 000000000..6b373fb5e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/SoapFaultBuilderInstrumentation.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.metro; + +import static io.opentelemetry.javaagent.instrumentation.metro.MetroJaxWsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.sun.xml.ws.api.message.Packet; +import com.sun.xml.ws.api.pipe.Fiber; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class SoapFaultBuilderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.sun.xml.ws.fault.SOAPFaultBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("createSOAPFaultMessage") + .and(takesArgument(0, named("com.sun.xml.ws.api.SOAPVersion"))) + .and(takesArgument(1, named("com.sun.xml.ws.model.CheckedExceptionImpl"))) + .and(takesArgument(2, named(Throwable.class.getName()))), + SoapFaultBuilderInstrumentation.class.getName() + "$CaptureThrowableAdvice"); + } + + @SuppressWarnings("unused") + public static class CaptureThrowableAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(2) Throwable throwable) { + if (throwable == null) { + return; + } + Packet request = null; + // we expect this to be called with attached fiber + // if fiber is not attached current() throws IllegalStateException + try { + request = Fiber.current().getPacket(); + } catch (IllegalStateException ignore) { + // fiber not available + } + if (request != null) { + tracer().storeThrowable(request, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/TracingTube.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/TracingTube.java new file mode 100644 index 000000000..1c879f9b6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/TracingTube.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.metro; + +import static io.opentelemetry.javaagent.instrumentation.metro.MetroJaxWsTracer.tracer; + +import com.sun.xml.ws.api.message.Packet; +import com.sun.xml.ws.api.pipe.Fiber; +import com.sun.xml.ws.api.pipe.NextAction; +import com.sun.xml.ws.api.pipe.Tube; +import com.sun.xml.ws.api.pipe.TubeCloner; +import com.sun.xml.ws.api.pipe.helper.AbstractFilterTubeImpl; +import com.sun.xml.ws.api.pipe.helper.AbstractTubeImpl; +import com.sun.xml.ws.api.server.WSEndpoint; + +public class TracingTube extends AbstractFilterTubeImpl { + private final WSEndpoint endpoint; + + public TracingTube(WSEndpoint endpoint, Tube next) { + super(next); + this.endpoint = endpoint; + } + + public TracingTube(TracingTube that, TubeCloner tubeCloner) { + super(that, tubeCloner); + this.endpoint = that.endpoint; + } + + @Override + public AbstractTubeImpl copy(TubeCloner tubeCloner) { + return new TracingTube(this, tubeCloner); + } + + @Override + public NextAction processRequest(Packet request) { + tracer().startSpan(endpoint, request); + + return super.processRequest(request); + } + + @Override + public NextAction processResponse(Packet response) { + tracer().end(response); + + return super.processResponse(response); + } + + // this is not used for handling exceptions thrown from webservice invocation + @Override + public NextAction processException(Throwable throwable) { + Packet request = null; + // we expect this to be called with attached fiber + // if fiber is not attached current() throws IllegalStateException + try { + request = Fiber.current().getPacket(); + } catch (IllegalStateException ignore) { + // fiber not available + } + if (request != null) { + tracer().end(request, throwable); + } + + return super.processException(throwable); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/groovy/MetroJaxWsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/groovy/MetroJaxWsTest.groovy new file mode 100644 index 000000000..c20abd445 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/groovy/MetroJaxWsTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class MetroJaxWsTest extends AbstractJaxWsTest { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/sun-jaxws.xml b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/sun-jaxws.xml new file mode 100644 index 000000000..6bf3e05ac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/sun-jaxws.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/web.xml new file mode 100644 index 000000000..037ef81d3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/web.xml @@ -0,0 +1,21 @@ + + + + + com.sun.xml.ws.transport.http.servlet.WSServletContextListener + + + + WSServlet + com.sun.xml.ws.transport.http.servlet.WSServlet + 1 + + + + WSServlet + /ws/* + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/jaxws-2.0-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/jaxws-2.0-testing.gradle new file mode 100644 index 000000000..c0b4d7869 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/jaxws-2.0-testing.gradle @@ -0,0 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" version "2.0.0" +} + +apply plugin: "otel.java-conventions" + +checkstyle { + // exclude generated web service classes + checkstyleMain.exclude "**/hello_web_service/**" +} + +dependencies { + api "javax.xml.ws:jaxws-api:2.0" + api "javax.jws:javax.jws-api:1.1" + + api "ch.qos.logback:logback-classic" + api "org.slf4j:log4j-over-slf4j" + api "org.slf4j:jcl-over-slf4j" + api "org.slf4j:jul-to-slf4j" + api "org.eclipse.jetty:jetty-webapp:9.4.35.v20201120" + api "org.springframework.ws:spring-ws-core:3.0.0.RELEASE" + + implementation(project(':testing-common')) +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/AbstractJaxWsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/AbstractJaxWsTest.groovy new file mode 100644 index 000000000..df400e9eb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/AbstractJaxWsTest.groovy @@ -0,0 +1,204 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.test.hello_web_service.Hello2Request +import io.opentelemetry.test.hello_web_service.HelloRequest +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.util.resource.Resource +import org.eclipse.jetty.webapp.WebAppContext +import org.springframework.oxm.jaxb.Jaxb2Marshaller +import org.springframework.util.ClassUtils +import org.springframework.ws.client.core.WebServiceTemplate +import org.springframework.ws.soap.client.SoapFaultClientException +import spock.lang.Shared +import spock.lang.Unroll + +abstract class AbstractJaxWsTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + @Shared + private Jaxb2Marshaller marshaller = new Jaxb2Marshaller() + + @Shared + protected WebServiceTemplate webServiceTemplate = new WebServiceTemplate(marshaller) + + def setupSpec() { + marshaller.setPackagesToScan(ClassUtils.getPackageName(HelloRequest)) + marshaller.afterPropertiesSet() + } + + @Override + Server startServer(int port) { + List configurationClasses = new ArrayList<>() + Collections.addAll(configurationClasses, WebAppContext.getDefaultConfigurationClasses()) + + WebAppContext webAppContext = new WebAppContext() + webAppContext.setContextPath(getContextPath()) + webAppContext.setConfigurationClasses(configurationClasses) + // set up test application + webAppContext.setBaseResource(Resource.newSystemResource("test-app")) + webAppContext.getMetaData().getWebInfClassesDirs().add(Resource.newClassPathResource("/")) + + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + jettyServer.setHandler(webAppContext) + jettyServer.start() + + return jettyServer + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/jetty-context" + } + + String getServiceAddress(String serviceName) { + return address.resolve("ws/" + serviceName).toString() + } + + def makeRequest(methodName, name) { + Object request = null + if ("hello" == methodName) { + request = new HelloRequest(name: name) + } else if ("hello2" == methodName) { + request = new Hello2Request(name: name) + } else { + throw new IllegalArgumentException(methodName) + } + + return webServiceTemplate.marshalSendAndReceive(getServiceAddress("HelloService"), request) + } + + @Unroll + def "test #methodName"() { + setup: + def response = makeRequest(methodName, "Test") + + expect: + response.getMessage() == "Hello Test" + + and: + def spanCount = 2 + if (hasAnnotationHandlerSpan(methodName)) { + spanCount++ + } + assertTraces(1) { + trace(0, spanCount) { + serverSpan(it, 0, serverSpanName(methodName)) + handlerSpan(it, 1, methodName, span(0)) + if (hasAnnotationHandlerSpan(methodName)) { + annotationHandlerSpan(it, 2, methodName, span(1)) + } + } + } + + where: + methodName << ["hello", "hello2"] + } + + @Unroll + def "test #methodName exception"() { + when: + makeRequest(methodName, "exception") + + then: + def error = thrown(SoapFaultClientException) + error.getMessage() == "hello exception" + + and: + def spanCount = 2 + if (hasAnnotationHandlerSpan(methodName)) { + spanCount++ + } + def expectedException = new Exception("hello exception") + assertTraces(1) { + trace(0, spanCount) { + serverSpan(it, 0, serverSpanName(methodName), expectedException) + handlerSpan(it, 1, methodName, span(0), expectedException) + if (hasAnnotationHandlerSpan(methodName)) { + annotationHandlerSpan(it, 2, methodName, span(1), expectedException) + } + } + } + + where: + methodName << ["hello", "hello2"] + } + + def hasAnnotationHandlerSpan(methodName) { + methodName == "hello" + } + + def serverSpanName(String operation) { + if (operation == "hello") { + return "HelloServiceImpl." + operation + } + return getContextPath() + "/ws/HelloService/" + operation + } + + static serverSpan(TraceAssert trace, int index, String operation, Throwable exception = null) { + trace.span(index) { + hasNoParent() + name operation + kind SERVER + if (exception != null) { + status ERROR + } + } + } + + static handlerSpan(TraceAssert trace, int index, String operation, Object parentSpan = null, Throwable exception = null) { + trace.span(index) { + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + name "HelloService/" + operation + kind INTERNAL + if (exception) { + status ERROR + errorEvent(exception.class, exception.message) + } + } + } + + static annotationHandlerSpan(TraceAssert trace, int index, String methodName, Object parentSpan = null, Throwable exception = null) { + trace.span(index) { + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + name "HelloServiceImpl." + methodName + kind INTERNAL + if (exception) { + status ERROR + errorEvent(exception.class, exception.message) + } + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "hello.HelloServiceImpl" + "${SemanticAttributes.CODE_FUNCTION.key}" methodName + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/BaseHelloService.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/BaseHelloService.groovy new file mode 100644 index 000000000..a2d8da1e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/BaseHelloService.groovy @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +class BaseHelloService { + + String hello2(String name) { + if ("exception" == name) { + throw new Exception("hello exception") + } + return "Hello " + name + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/HelloService.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/HelloService.groovy new file mode 100644 index 000000000..10a03f184 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/HelloService.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import javax.jws.WebParam +import javax.jws.WebResult +import javax.jws.WebService +import javax.xml.ws.RequestWrapper + +@WebService(targetNamespace = "http://opentelemetry.io/test/hello-web-service") +interface HelloService { + + @RequestWrapper(localName = "helloRequest") + @WebResult(name = "message") + String hello(@WebParam(name = "name") String name) + + @RequestWrapper(localName = "hello2Request") + @WebResult(name = "message") + String hello2(@WebParam(name = "name") String name) + +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/HelloServiceImpl.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/HelloServiceImpl.groovy new file mode 100644 index 000000000..781c4d998 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/groovy/hello/HelloServiceImpl.groovy @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import javax.jws.WebService + +@WebService(serviceName = "HelloService", endpointInterface = "hello.HelloService", targetNamespace = "http://opentelemetry.io/test/hello-web-service") +class HelloServiceImpl extends BaseHelloService implements HelloService { + + String hello(String name) { + if ("exception" == name) { + throw new Exception("hello exception") + } + return "Hello " + name + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/schema/hello.xsd b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/schema/hello.xsd new file mode 100644 index 000000000..724692129 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0-testing/src/main/schema/hello.xsd @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/jaxws-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/jaxws-2.0-javaagent.gradle new file mode 100644 index 000000000..420e5973e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/jaxws-2.0-javaagent.gradle @@ -0,0 +1,14 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "javax.xml.ws" + module = "jaxws-api" + versions = "[2.0,]" + } +} + +dependencies { + library "javax.xml.ws:jaxws-api:2.0" + implementation project(":instrumentation:jaxws:jaxws-common:library") +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsInstrumentationModule.java new file mode 100644 index 000000000..f3000d694 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxws.v2_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JaxWsInstrumentationModule extends InstrumentationModule { + + public JaxWsInstrumentationModule() { + super("jaxws", "jaxws-2.0"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new WebServiceProviderInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/WebServiceProviderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/WebServiceProviderInstrumentation.java new file mode 100644 index 000000000..1a9c838b2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/WebServiceProviderInstrumentation.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxws.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jaxws.common.JaxWsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameMatches; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.lang.reflect.Method; +import javax.xml.ws.Provider; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class WebServiceProviderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.xml.ws.Provider"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.xml.ws.Provider")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(nameMatches("invoke")).and(takesArguments(1)), + WebServiceProviderInstrumentation.class.getName() + "$InvokeAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startSpan( + @Advice.This Object target, + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (CallDepthThreadLocalMap.incrementCallDepth(Provider.class) > 0) { + return; + } + context = tracer().startSpan(target.getClass(), method); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + CallDepthThreadLocalMap.reset(Provider.class); + + scope.close(); + if (throwable == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsAnnotationsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsAnnotationsTest.groovy new file mode 100644 index 000000000..1de988c9c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsAnnotationsTest.groovy @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxws.v2_0 + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.test.SoapProvider + +class JaxWsAnnotationsTest extends AgentInstrumentationSpecification { + + def "Web service providers generate spans"() { + when: + new SoapProvider().invoke(null) + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SoapProvider.invoke" + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "io.opentelemetry.test.SoapProvider" + "${SemanticAttributes.CODE_FUNCTION.key}" "invoke" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/java/io/opentelemetry/test/SoapProvider.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/java/io/opentelemetry/test/SoapProvider.java new file mode 100644 index 000000000..6d721bd99 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/java/io/opentelemetry/test/SoapProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test; + +import javax.xml.ws.Provider; + +/** + * Note: this has to stay outside of 'io.opentelemetry.javaagent' package to be considered for + * instrumentation + */ +public class SoapProvider implements Provider { + + @Override + public Message invoke(Message message) { + return message; + } + + public static class Message {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-common/library/jaxws-common-library.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-common/library/jaxws-common-library.gradle new file mode 100644 index 000000000..1cfd24470 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-common/library/jaxws-common-library.gradle @@ -0,0 +1,3 @@ +plugins { + id("otel.library-instrumentation") +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsTracer.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsTracer.java new file mode 100644 index 000000000..b263c85bf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jaxws-common/library/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsTracer.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxws.common; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.reflect.Method; + +public class JaxWsTracer extends BaseTracer { + + private static final JaxWsTracer TRACER = new JaxWsTracer(); + + public static JaxWsTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jaxws-common"; + } + + public Context startSpan(Class target, Method method) { + String spanName = SpanNames.fromMethod(target, method); + + Context parentContext = Context.current(); + Span serverSpan = ServerSpan.fromContextOrNull(parentContext); + if (serverSpan != null) { + serverSpan.updateName(spanName); + } + + Span span = + spanBuilder(parentContext, spanName, INTERNAL) + .setAttribute(SemanticAttributes.CODE_NAMESPACE, method.getDeclaringClass().getName()) + .setAttribute(SemanticAttributes.CODE_FUNCTION, method.getName()) + .startSpan(); + return parentContext.with(span); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/jws-1.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/jws-1.1-javaagent.gradle new file mode 100644 index 000000000..5ceae8616 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/jws-1.1-javaagent.gradle @@ -0,0 +1,14 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "javax.jws" + module = "javax.jws-api" + versions = "[1.1,]" + } +} + +dependencies { + library "javax.jws:javax.jws-api:1.1" + implementation project(":instrumentation:jaxws:jaxws-common:library") +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsInstrumentation.java new file mode 100644 index 000000000..b3b73f99a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsInstrumentation.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperMethod; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.methodIsDeclaredByType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jaxws.common.JaxWsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.inheritsAnnotation; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.lang.reflect.Method; +import javax.jws.WebService; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JwsAnnotationsInstrumentation implements TypeInstrumentation { + + public static final String JWS_WEB_SERVICE_ANNOTATION = "javax.jws.WebService"; + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed(JWS_WEB_SERVICE_ANNOTATION); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(isAnnotatedWith(named(JWS_WEB_SERVICE_ANNOTATION))) + .or(isAnnotatedWith(named(JWS_WEB_SERVICE_ANNOTATION))); + } + + @Override + public void transform(TypeTransformer transformer) { + // JaxWS WebService methods are defined either by implementing an interface annotated + // with @WebService or by any public method from a class annotated with @WebService. + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and( + hasSuperMethod( + methodIsDeclaredByType(inheritsAnnotation(named(JWS_WEB_SERVICE_ANNOTATION))))), + JwsAnnotationsInstrumentation.class.getName() + "$JwsAnnotationsAdvice"); + } + + @SuppressWarnings("unused") + public static class JwsAnnotationsAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startSpan( + @Advice.This Object target, + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (CallDepthThreadLocalMap.incrementCallDepth(WebService.class) > 0) { + return; + } + context = tracer().startSpan(target.getClass(), method); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + CallDepthThreadLocalMap.reset(WebService.class); + + scope.close(); + if (throwable == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsInstrumentationModule.java new file mode 100644 index 000000000..b603410e4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JwsInstrumentationModule extends InstrumentationModule { + + public JwsInstrumentationModule() { + super("jaxws", "jws-1.1"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new JwsAnnotationsInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsTest.groovy new file mode 100644 index 000000000..947fa851e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsTest.groovy @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1 + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.test.WebServiceClass +import io.opentelemetry.test.WebServiceDefinitionInterface +import io.opentelemetry.test.WebServiceFromInterface +import java.lang.reflect.Proxy + +class JwsAnnotationsTest extends AgentInstrumentationSpecification { + + def "WebService on a class generates spans only for public methods"() { + when: + new WebServiceClass().doSomethingPublic() + new WebServiceClass().doSomethingPackagePrivate() + new WebServiceClass().doSomethingProtected() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "WebServiceClass.doSomethingPublic" + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "io.opentelemetry.test.WebServiceClass" + "${SemanticAttributes.CODE_FUNCTION.key}" "doSomethingPublic" + } + } + } + } + } + + def "WebService via interface generates spans only for methods of the interface"() { + when: + new WebServiceFromInterface().partOfPublicInterface() + new WebServiceFromInterface().notPartOfPublicInterface() + new WebServiceFromInterface().notPartOfAnything() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "WebServiceFromInterface.partOfPublicInterface" + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "io.opentelemetry.test.WebServiceFromInterface" + "${SemanticAttributes.CODE_FUNCTION.key}" "partOfPublicInterface" + } + } + } + } + } + + def "WebService via proxy must have span attributes from actual implementation"() { + when: + WebServiceDefinitionInterface proxy = + Proxy.newProxyInstance( + WebServiceFromInterface.getClassLoader(), + [WebServiceDefinitionInterface] as Class[], + new ProxyInvocationHandler(new WebServiceFromInterface())) as WebServiceDefinitionInterface + proxy.partOfPublicInterface() + + then: + proxy.getClass() != WebServiceFromInterface + assertTraces(1) { + trace(0, 1) { + span(0) { + name "WebServiceFromInterface.partOfPublicInterface" + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "io.opentelemetry.test.WebServiceFromInterface" + "${SemanticAttributes.CODE_FUNCTION.key}" "partOfPublicInterface" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/ProxyInvocationHandler.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/ProxyInvocationHandler.java new file mode 100644 index 000000000..a1e20a845 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/ProxyInvocationHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1; + +import io.opentelemetry.test.WebServiceDefinitionInterface; +import io.opentelemetry.test.WebServiceFromInterface; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +public class ProxyInvocationHandler implements InvocationHandler { + + final WebServiceDefinitionInterface target; + + public ProxyInvocationHandler(WebServiceFromInterface webServiceFromInterface) { + target = webServiceFromInterface; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return method.invoke(target, args); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceClass.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceClass.java new file mode 100644 index 000000000..3e167202e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceClass.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test; + +import javax.jws.WebService; + +/** + * Note: this has to stay outside of 'io.opentelemetry.javaagent' package to be considered for + * instrumentation + */ +// This is pure java to not have any groovy generated public method surprises +@WebService +public class WebServiceClass { + public void doSomethingPublic() {} + + protected void doSomethingProtected() {} + + void doSomethingPackagePrivate() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceDefinitionInterface.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceDefinitionInterface.java new file mode 100644 index 000000000..7c2026302 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceDefinitionInterface.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test; + +import javax.jws.WebService; + +/** + * Note: this has to stay outside of 'io.opentelemetry.javaagent' package to be considered for + * instrumentation + */ +@WebService +public interface WebServiceDefinitionInterface { + void partOfPublicInterface(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceFromInterface.java b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceFromInterface.java new file mode 100644 index 000000000..0e650226a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jaxws/jws-1.1/javaagent/src/test/java/io/opentelemetry/test/WebServiceFromInterface.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test; + +/** + * Note: this has to stay outside of 'io.opentelemetry.javaagent' package to be considered for + * instrumentation + */ +public class WebServiceFromInterface implements WebServiceDefinitionInterface { + @Override + public void partOfPublicInterface() {} + + public void notPartOfPublicInterface() {} + + void notPartOfAnything() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent-unit-tests/jdbc-javaagent-unit-tests.gradle b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent-unit-tests/jdbc-javaagent-unit-tests.gradle new file mode 100644 index 000000000..a3436089d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent-unit-tests/jdbc-javaagent-unit-tests.gradle @@ -0,0 +1,5 @@ +apply plugin: "otel.java-conventions" + +dependencies { + testImplementation project(':instrumentation:jdbc:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent-unit-tests/src/test/groovy/JdbcConnectionUrlParserTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent-unit-tests/src/test/groovy/JdbcConnectionUrlParserTest.groovy new file mode 100644 index 000000000..d4c1f29e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent-unit-tests/src/test/groovy/JdbcConnectionUrlParserTest.groovy @@ -0,0 +1,205 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcConnectionUrlParser.parse + +import io.opentelemetry.javaagent.instrumentation.jdbc.DbInfo +import spock.lang.Shared +import spock.lang.Specification + +class JdbcConnectionUrlParserTest extends Specification { + + @Shared + def stdProps = { + def prop = new Properties() + // https://download.oracle.com/otn-pub/jcp/jdbc-4_1-mrel-spec/jdbc4.1-fr-spec.pdf + prop.setProperty("databaseName", "stdDatabaseName") + prop.setProperty("dataSourceName", "stdDatasourceName") + prop.setProperty("description", "Some description") + prop.setProperty("networkProtocol", "stdProto") + prop.setProperty("password", "PASSWORD!") + prop.setProperty("portNumber", "9999") + prop.setProperty("roleName", "stdRoleName") + prop.setProperty("serverName", "stdServerName") + prop.setProperty("user", "stdUserName") + return prop + }() + + def "invalid url returns default"() { + expect: + parse(url, null) == DbInfo.DEFAULT + + where: + url | _ + null | _ + "" | _ + "jdbc:" | _ + "jdbc::" | _ + "bogus:string" | _ + } + + def "verify #system:#subtype parsing of #url"() { + setup: + def info = parse(url, props) + + expect: + info.shortUrl == expected.shortUrl + info.system == expected.system + info.host == expected.host + info.port == expected.port + info.user == expected.user + info.name == expected.name + + info == expected + + where: + url | props | shortUrl | system | subtype | user | host | port | name | db + // https://jdbc.postgresql.org/documentation/94/connect.html + "jdbc:postgresql:///" | null | "postgresql://localhost:5432" | "postgresql" | null | null | "localhost" | 5432 | null | null + "jdbc:postgresql:///" | stdProps | "postgresql://stdServerName:9999" | "postgresql" | null | "stdUserName" | "stdServerName" | 9999 | null | "stdDatabaseName" + "jdbc:postgresql://pg.host" | null | "postgresql://pg.host:5432" | "postgresql" | null | null | "pg.host" | 5432 | null | null + "jdbc:postgresql://pg.host:11/pgdb?user=pguser&password=PW" | null | "postgresql://pg.host:11" | "postgresql" | null | "pguser" | "pg.host" | 11 | null | "pgdb" + + "jdbc:postgresql://pg.host:11/pgdb?user=pguser&password=PW" | stdProps | "postgresql://pg.host:11" | "postgresql" | null | "pguser" | "pg.host" | 11 | null | "pgdb" + + // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html + // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html + "jdbc:mysql:///" | null | "mysql://localhost:3306" | "mysql" | null | null | "localhost" | 3306 | null | null + "jdbc:mysql:///" | stdProps | "mysql://stdServerName:9999" | "mysql" | null | "stdUserName" | "stdServerName" | 9999 | null | "stdDatabaseName" + "jdbc:mysql://my.host" | null | "mysql://my.host:3306" | "mysql" | null | null | "my.host" | 3306 | null | null + "jdbc:mysql://my.host?user=myuser&password=PW" | null | "mysql://my.host:3306" | "mysql" | null | "myuser" | "my.host" | 3306 | null | null + "jdbc:mysql://my.host:22/mydb?user=myuser&password=PW" | null | "mysql://my.host:22" | "mysql" | null | "myuser" | "my.host" | 22 | null | "mydb" + "jdbc:mysql://127.0.0.1:22/mydb?user=myuser&password=PW" | stdProps | "mysql://127.0.0.1:22" | "mysql" | null | "myuser" | "127.0.0.1" | 22 | null | "mydb" + + // https://mariadb.com/kb/en/library/about-mariadb-connector-j/#connection-strings + "jdbc:mariadb:127.0.0.1:33/mdbdb" | null | "mariadb://127.0.0.1:33" | "mariadb" | null | null | "127.0.0.1" | 33 | null | "mdbdb" + "jdbc:mariadb:localhost/mdbdb" | null | "mariadb://localhost:3306" | "mariadb" | null | null | "localhost" | 3306 | null | "mdbdb" + "jdbc:mariadb:localhost/mdbdb?user=mdbuser&password=PW" | stdProps | "mariadb://localhost:9999" | "mariadb" | null | "mdbuser" | "localhost" | 9999 | null | "mdbdb" + "jdbc:mariadb:localhost:33/mdbdb" | stdProps | "mariadb://localhost:33" | "mariadb" | null | "stdUserName" | "localhost" | 33 | null | "mdbdb" + "jdbc:mariadb://mdb.host:33/mdbdb?user=mdbuser&password=PW" | null | "mariadb://mdb.host:33" | "mariadb" | null | "mdbuser" | "mdb.host" | 33 | null | "mdbdb" + "jdbc:mariadb:aurora://mdb.host/mdbdb" | null | "mariadb:aurora://mdb.host:3306" | "mariadb" | "aurora" | null | "mdb.host" | 3306 | null | "mdbdb" + "jdbc:mysql:aurora://mdb.host/mdbdb" | null | "mysql:aurora://mdb.host:3306" | "mysql" | "aurora" | null | "mdb.host" | 3306 | null | "mdbdb" + "jdbc:mysql:failover://localhost/mdbdb?autoReconnect=true" | null | "mysql:failover://localhost:3306" | "mysql" | "failover" | null | "localhost" | 3306 | null | "mdbdb" + "jdbc:mariadb:failover://mdb.host1:33,mdb.host/mdbdb?characterEncoding=utf8" | null | "mariadb:failover://mdb.host1:33" | "mariadb" | "failover" | null | "mdb.host1" | 33 | null | "mdbdb" + "jdbc:mariadb:sequential://mdb.host1,mdb.host2:33/mdbdb" | null | "mariadb:sequential://mdb.host1:3306" | "mariadb" | "sequential" | null | "mdb.host1" | 3306 | null | "mdbdb" + "jdbc:mariadb:loadbalance://127.0.0.1:33,mdb.host/mdbdb" | null | "mariadb:loadbalance://127.0.0.1:33" | "mariadb" | "loadbalance" | null | "127.0.0.1" | 33 | null | "mdbdb" + "jdbc:mariadb:loadbalance://[2001:0660:7401:0200:0000:0000:0edf:bdd7]:33,mdb.host/mdbdb" | null | "mariadb:loadbalance://2001:0660:7401:0200:0000:0000:0edf:bdd7:33" | "mariadb" | "loadbalance" | null | "2001:0660:7401:0200:0000:0000:0edf:bdd7" | 33 | null | "mdbdb" + "jdbc:mysql:loadbalance://127.0.0.1,127.0.0.1:3306/mdbdb?user=mdbuser&password=PW" | null | "mysql:loadbalance://127.0.0.1:3306" | "mysql" | "loadbalance" | "mdbuser" | "127.0.0.1" | 3306 | null | "mdbdb" + "jdbc:mariadb:replication://localhost:33,anotherhost:3306/mdbdb" | null | "mariadb:replication://localhost:33" | "mariadb" | "replication" | null | "localhost" | 33 | null | "mdbdb" + "jdbc:mysql:replication://address=(HOST=127.0.0.1)(port=33)(user=mdbuser)(password=PW)," + + "address=(host=mdb.host)(port=3306)(user=otheruser)(password=PW)/mdbdb?user=wrong&password=PW" | null | "mysql:replication://127.0.0.1:33" | "mysql" | "replication" | "mdbuser" | "127.0.0.1" | 33 | null | "mdbdb" + "jdbc:mysql:replication://address=(HOST=mdb.host)," + + "address=(host=anotherhost)(port=3306)(user=wrong)(password=PW)/mdbdb?user=mdbuser&password=PW" | null | "mysql:replication://mdb.host:3306" | "mysql" | "replication" | "mdbuser" | "mdb.host" | 3306 | null | "mdbdb" + + //https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url + "jdbc:microsoft:sqlserver://;" | null | "microsoft:sqlserver://localhost:1433" | "mssql" | "sqlserver" | null | "localhost" | 1433 | null | null + "jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9" | null | "sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:1433" | "mssql" | null | null | "[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]" | 1433 | null | null + "jdbc:sqlserver://;serverName=2001:0db8:85a3:0000:0000:8a2e:0370:7334" | null | "sqlserver://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:1433" | "mssql" | null | null | "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]" | 1433 | null | null + "jdbc:sqlserver://;serverName=[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43" | null | "sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43" | "mssql" | null | null | "[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]" | 43 | null | null + "jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\ssinstance" | null | "sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:1433" | "mssql" | null | null | "[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]" | 1433 | "ssinstance" | null + "jdbc:sqlserver://;serverName=[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\ssinstance]:43" | null | "sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43" | "mssql" | null | null | "[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]" | 43 | "ssinstance" | null + "jdbc:microsoft:sqlserver://;" | stdProps | "microsoft:sqlserver://stdServerName:9999" | "mssql" | "sqlserver" | "stdUserName" | "stdServerName" | 9999 | null | "stdDatabaseName" + "jdbc:sqlserver://ss.host\\ssinstance:44;databaseName=ssdb;user=ssuser;password=pw" | null | "sqlserver://ss.host:44" | "mssql" | null | "ssuser" | "ss.host" | 44 | "ssinstance" | "ssdb" + "jdbc:sqlserver://;serverName=ss.host\\ssinstance:44;DatabaseName=;" | null | "sqlserver://ss.host:44" | "mssql" | null | null | "ss.host" | 44 | "ssinstance" | null + "jdbc:sqlserver://ss.host;serverName=althost;DatabaseName=ssdb;" | null | "sqlserver://ss.host:1433" | "mssql" | null | null | "ss.host" | 1433 | null | "ssdb" + "jdbc:microsoft:sqlserver://ss.host:44;DatabaseName=ssdb;user=ssuser;password=pw;user=ssuser2;" | null | "microsoft:sqlserver://ss.host:44" | "mssql" | "sqlserver" | "ssuser" | "ss.host" | 44 | null | "ssdb" + + // http://jtds.sourceforge.net/faq.html#urlFormat + "jdbc:jtds:sqlserver://ss.host/ssdb" | null | "jtds:sqlserver://ss.host:1433" | "mssql" | "sqlserver" | null | "ss.host" | 1433 | null | "ssdb" + "jdbc:jtds:sqlserver://ss.host:1433/ssdb" | null | "jtds:sqlserver://ss.host:1433" | "mssql" | "sqlserver" | null | "ss.host" | 1433 | null | "ssdb" + "jdbc:jtds:sqlserver://ss.host:1433/ssdb;user=ssuser" | null | "jtds:sqlserver://ss.host:1433" | "mssql" | "sqlserver" | "ssuser" | "ss.host" | 1433 | null | "ssdb" + "jdbc:jtds:sqlserver://ss.host:1433/ssdb;user=ssuser" | null | "jtds:sqlserver://ss.host:1433" | "mssql" | "sqlserver" | "ssuser" | "ss.host" | 1433 | null | "ssdb" + "jdbc:jtds:sqlserver://ss.host/ssdb;instance=ssinstance" | null | "jtds:sqlserver://ss.host:1433" | "mssql" | "sqlserver" | null | "ss.host" | 1433 | "ssinstance" | "ssdb" + "jdbc:jtds:sqlserver://ss.host:1444/ssdb;instance=ssinstance" | null | "jtds:sqlserver://ss.host:1444" | "mssql" | "sqlserver" | null | "ss.host" | 1444 | "ssinstance" | "ssdb" + "jdbc:jtds:sqlserver://ss.host:1433/ssdb;instance=ssinstance;user=ssuser" | null | "jtds:sqlserver://ss.host:1433" | "mssql" | "sqlserver" | "ssuser" | "ss.host" | 1433 | "ssinstance" | "ssdb" + + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/urls.htm + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/jdbcthin.htm + "jdbc:oracle:thin:orcluser/PW@localhost:55:orclsn" | null | "oracle:thin://localhost:55" | "oracle" | "thin" | "orcluser" | "localhost" | 55 | "orclsn" | null + "jdbc:oracle:thin:orcluser/PW@//orcl.host:55/orclsn" | null | "oracle:thin://orcl.host:55" | "oracle" | "thin" | "orcluser" | "orcl.host" | 55 | "orclsn" | null + "jdbc:oracle:thin:orcluser/PW@127.0.0.1:orclsn" | null | "oracle:thin://127.0.0.1:1521" | "oracle" | "thin" | "orcluser" | "127.0.0.1" | 1521 | "orclsn" | null + "jdbc:oracle:thin:orcluser/PW@//orcl.host/orclsn" | null | "oracle:thin://orcl.host:1521" | "oracle" | "thin" | "orcluser" | "orcl.host" | 1521 | "orclsn" | null + "jdbc:oracle:thin:@//orcl.host:55/orclsn" | null | "oracle:thin://orcl.host:55" | "oracle" | "thin" | null | "orcl.host" | 55 | "orclsn" | null + "jdbc:oracle:thin:@ldap://orcl.host:55/some,cn=OracleContext,dc=com" | null | "oracle:thin://orcl.host:55" | "oracle" | "thin" | null | "orcl.host" | 55 | "some,cn=oraclecontext,dc=com" | null + "jdbc:oracle:thin:127.0.0.1:orclsn" | null | "oracle:thin://127.0.0.1:1521" | "oracle" | "thin" | null | "127.0.0.1" | 1521 | "orclsn" | null + "jdbc:oracle:thin:orcl.host:orclsn" | stdProps | "oracle:thin://orcl.host:9999" | "oracle" | "thin" | "stdUserName" | "orcl.host" | 9999 | "orclsn" | "stdDatabaseName" + "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST= 127.0.0.1 )(POR T= 666))" + + "(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orclsn)))" | null | "oracle:thin://127.0.0.1:1521" | "oracle" | "thin" | null | "127.0.0.1" | 1521 | "orclsn" | null + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/instclnt.htm + "jdbc:oracle:drivertype:orcluser/PW@orcl.host:55/orclsn" | null | "oracle:drivertype://orcl.host:55" | "oracle" | "drivertype" | "orcluser" | "orcl.host" | 55 | "orclsn" | null + "jdbc:oracle:oci8:@" | null | "oracle:oci8:" | "oracle" | "oci8" | null | null | 1521 | null | null + "jdbc:oracle:oci8:@" | stdProps | "oracle:oci8://stdServerName:9999" | "oracle" | "oci8" | "stdUserName" | "stdServerName" | 9999 | null | "stdDatabaseName" + "jdbc:oracle:oci8:@orclsn" | null | "oracle:oci8:" | "oracle" | "oci8" | null | null | 1521 | "orclsn" | null + "jdbc:oracle:oci:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)( HOST = orcl.host )" + + "( PORT = 55 ))(CONNECT_DATA=(SERVICE_NAME =orclsn )))" | null | "oracle:oci://orcl.host:55" | "oracle" | "oci" | null | "orcl.host" | 55 | "orclsn" | null + + // https://www.ibm.com/support/knowledgecenter/en/SSEPEK_10.0.0/java/src/tpc/imjcc_tjvjcccn.html + // https://www.ibm.com/support/knowledgecenter/en/SSEPGG_10.5.0/com.ibm.db2.luw.apdv.java.doc/src /tpc/imjcc_r0052342.html + "jdbc:db2://db2.host" | null | "db2://db2.host:50000" | "db2" | null | null | "db2.host" | 50000 | null | null + "jdbc:db2://db2.host" | stdProps | "db2://db2.host:9999" | "db2" | null | "stdUserName" | "db2.host" | 9999 | null | "stdDatabaseName" + "jdbc:db2://db2.host:77/db2db:user=db2user;password=PW;" | null | "db2://db2.host:77" | "db2" | null | "db2user" | "db2.host" | 77 | "db2db" | null + "jdbc:db2://db2.host:77/db2db:user=db2user;password=PW;" | stdProps | "db2://db2.host:77" | "db2" | null | "db2user" | "db2.host" | 77 | "db2db" | "stdDatabaseName" + "jdbc:as400://ashost:66/asdb:user=asuser;password=PW;" | null | "as400://ashost:66" | "db2" | null | "asuser" | "ashost" | 66 | "asdb" | null + + // https://help.sap.com/viewer/0eec0d68141541d1b07893a39944924e/2.0.03/en-US/ff15928cf5594d78b841fbbe649f04b4.html + "jdbc:sap://sap.host" | null | "sap://sap.host" | "sap" | null | null | "sap.host" | null | null | null + "jdbc:sap://sap.host" | stdProps | "sap://sap.host:9999" | "sap" | null | "stdUserName" | "sap.host" | 9999 | null | "stdDatabaseName" + "jdbc:sap://sap.host:88/?databaseName=sapdb&user=sapuser&password=PW" | null | "sap://sap.host:88" | "sap" | null | "sapuser" | "sap.host" | 88 | null | "sapdb" + + // TODO: +// "jdbc:informix-sqli://infxhost:99/infxdb:INFORMIXSERVER=infxsn;user=infxuser;password=PW" | null | "informix-sqli" | null | "infxuser" | "infxhost" | 99 | "infxdb"| null +// "jdbc:informix-direct://infxdb:999;user=infxuser;password=PW" | null | "informix-direct" | null | "infxuser" | "infxhost" | 999 | "infxdb"| null + + // http://www.h2database.com/html/features.html#database_url + "jdbc:h2:mem:" | null | "h2:mem:" | "h2" | "mem" | null | null | null | null | null + "jdbc:h2:mem:" | stdProps | "h2:mem:" | "h2" | "mem" | "stdUserName" | null | null | null | "stdDatabaseName" + "jdbc:h2:mem:h2db" | null | "h2:mem:" | "h2" | "mem" | null | null | null | "h2db" | null + "jdbc:h2:tcp://h2.host:111/path/h2db;user=h2user;password=PW" | null | "h2:tcp://h2.host:111" | "h2" | "tcp" | "h2user" | "h2.host" | 111 | "path/h2db" | null + "jdbc:h2:ssl://h2.host:111/path/h2db;user=h2user;password=PW" | null | "h2:ssl://h2.host:111" | "h2" | "ssl" | "h2user" | "h2.host" | 111 | "path/h2db" | null + "jdbc:h2:/data/h2file" | null | "h2:file:" | "h2" | "file" | null | null | null | "/data/h2file" | null + "jdbc:h2:file:~/h2file;USER=h2user;PASSWORD=PW" | null | "h2:file:" | "h2" | "file" | null | null | null | "~/h2file" | null + "jdbc:h2:file:/data/h2file" | null | "h2:file:" | "h2" | "file" | null | null | null | "/data/h2file" | null + "jdbc:h2:file:C:/data/h2file" | null | "h2:file:" | "h2" | "file" | null | null | null | "c:/data/h2file" | null + "jdbc:h2:zip:~/db.zip!/h2zip" | null | "h2:zip:" | "h2" | "zip" | null | null | null | "~/db.zip!/h2zip" | null + + // http://hsqldb.org/doc/2.0/guide/dbproperties-chapt.html + "jdbc:hsqldb:hsdb" | null | "hsqldb:mem:" | "hsqldb" | "mem" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:hsdb" | stdProps | "hsqldb:mem:" | "hsqldb" | "mem" | "stdUserName" | null | null | "hsdb" | "stdDatabaseName" + "jdbc:hsqldb:mem:hsdb" | null | "hsqldb:mem:" | "hsqldb" | "mem" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:mem:hsdb;shutdown=true" | null | "hsqldb:mem:" | "hsqldb" | "mem" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:mem:hsdb?shutdown=true" | null | "hsqldb:mem:" | "hsqldb" | "mem" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:file:hsdb" | null | "hsqldb:file:" | "hsqldb" | "file" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:file:hsdb;user=aUserName;password=3xLVz" | null | "hsqldb:file:" | "hsqldb" | "file" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:file:hsdb;create=false?user=aUserName&password=3xLVz" | null | "hsqldb:file:" | "hsqldb" | "file" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:file:/loc/hsdb" | null | "hsqldb:file:" | "hsqldb" | "file" | "SA" | null | null | "/loc/hsdb" | null + "jdbc:hsqldb:file:C:/hsdb" | null | "hsqldb:file:" | "hsqldb" | "file" | "SA" | null | null | "c:/hsdb" | null + "jdbc:hsqldb:res:hsdb" | null | "hsqldb:res:" | "hsqldb" | "res" | "SA" | null | null | "hsdb" | null + "jdbc:hsqldb:res:/cp/hsdb" | null | "hsqldb:res:" | "hsqldb" | "res" | "SA" | null | null | "/cp/hsdb" | null + "jdbc:hsqldb:hsql://hs.host:333/hsdb" | null | "hsqldb:hsql://hs.host:333" | "hsqldb" | "hsql" | "SA" | "hs.host" | 333 | "hsdb" | null + "jdbc:hsqldb:hsqls://hs.host/hsdb" | null | "hsqldb:hsqls://hs.host:9001" | "hsqldb" | "hsqls" | "SA" | "hs.host" | 9001 | "hsdb" | null + "jdbc:hsqldb:http://hs.host" | null | "hsqldb:http://hs.host:80" | "hsqldb" | "http" | "SA" | "hs.host" | 80 | null | null + "jdbc:hsqldb:http://hs.host:333/hsdb" | null | "hsqldb:http://hs.host:333" | "hsqldb" | "http" | "SA" | "hs.host" | 333 | "hsdb" | null + "jdbc:hsqldb:https://127.0.0.1/hsdb" | null | "hsqldb:https://127.0.0.1:443" | "hsqldb" | "https" | "SA" | "127.0.0.1" | 443 | "hsdb" | null + + // https://db.apache.org/derby/papers/DerbyClientSpec.html#Connection+URL+Format + // https://db.apache.org/derby/docs/10.8/devguide/cdevdvlp34964.html + "jdbc:derby:derbydb" | null | "derby:directory:" | "derby" | "directory" | "APP" | null | null | "derbydb" | null + "jdbc:derby:derbydb" | stdProps | "derby:directory:" | "derby" | "directory" | "stdUserName" | null | null | "derbydb" | "stdDatabaseName" + "jdbc:derby:derbydb;user=derbyuser;password=pw" | null | "derby:directory:" | "derby" | "directory" | "derbyuser" | null | null | "derbydb" | null + "jdbc:derby:memory:derbydb" | null | "derby:memory:" | "derby" | "memory" | "APP" | null | null | "derbydb" | null + "jdbc:derby:memory:;databaseName=derbydb" | null | "derby:memory:" | "derby" | "memory" | "APP" | null | null | null | "derbydb" + "jdbc:derby:memory:derbydb;databaseName=altdb" | null | "derby:memory:" | "derby" | "memory" | "APP" | null | null | "derbydb" | "altdb" + "jdbc:derby:memory:derbydb;user=derbyuser;password=pw" | null | "derby:memory:" | "derby" | "memory" | "derbyuser" | null | null | "derbydb" | null + "jdbc:derby://derby.host:222/memory:derbydb;create=true" | null | "derby:network://derby.host:222" | "derby" | "network" | "APP" | "derby.host" | 222 | "derbydb" | null + "jdbc:derby://derby.host/memory:derbydb;create=true;user=derbyuser;password=pw" | null | "derby:network://derby.host:1527" | "derby" | "network" | "derbyuser" | "derby.host" | 1527 | "derbydb" | null + "jdbc:derby://127.0.0.1:1527/memory:derbydb;create=true;user=derbyuser;password=pw" | null | "derby:network://127.0.0.1:1527" | "derby" | "network" | "derbyuser" | "127.0.0.1" | 1527 | "derbydb" | null + "jdbc:derby:directory:derbydb;user=derbyuser;password=pw" | null | "derby:directory:" | "derby" | "directory" | "derbyuser" | null | null | "derbydb" | null + "jdbc:derby:classpath:/some/derbydb;user=derbyuser;password=pw" | null | "derby:classpath:" | "derby" | "classpath" | "derbyuser" | null | null | "/some/derbydb" | null + "jdbc:derby:jar:/derbydb;user=derbyuser;password=pw" | null | "derby:jar:" | "derby" | "jar" | "derbyuser" | null | null | "/derbydb" | null + "jdbc:derby:jar:(~/path/to/db.jar)/other/derbydb;user=derbyuser;password=pw" | null | "derby:jar:" | "derby" | "jar" | "derbyuser" | null | null | "(~/path/to/db.jar)/other/derbydb" | null + + expected = DbInfo.builder().system(system).subtype(subtype).user(user).name(name).db(db).host(host).port(port).shortUrl(shortUrl).build() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/jdbc-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/jdbc-javaagent.gradle new file mode 100644 index 000000000..de1eb6f58 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/jdbc-javaagent.gradle @@ -0,0 +1,31 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + coreJdk() + } +} + +dependencies { + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + // jdbc unit testing + testLibrary "com.h2database:h2:1.3.169" + // first version jdk 1.6 compatible + testLibrary "org.apache.derby:derby:10.6.1.0" + testLibrary "org.hsqldb:hsqldb:2.0.0" + + testLibrary "org.apache.tomcat:tomcat-jdbc:7.0.19" + // tomcat needs this to run + testLibrary "org.apache.tomcat:tomcat-juli:7.0.19" + testLibrary "com.zaxxer:HikariCP:2.4.0" + testLibrary "com.mchange:c3p0:0.9.5" + + latestDepTestLibrary "org.apache.derby:derby:10.14.+" +} + +tasks.withType(Test).configureEach { + jvmArgs "-Dotel.instrumentation.jdbc-datasource.enabled=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/ConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/ConnectionInstrumentation.java new file mode 100644 index 000000000..b3a899937 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/ConnectionInstrumentation.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.sql.PreparedStatement; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("java.sql.Connection"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("java.sql.Connection")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + nameStartsWith("prepare") + .and(takesArgument(0, String.class)) + // Also include CallableStatement, which is a sub type of PreparedStatement + .and(returns(implementsInterface(named("java.sql.PreparedStatement")))), + ConnectionInstrumentation.class.getName() + "$PrepareAdvice"); + } + + @SuppressWarnings("unused") + public static class PrepareAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void addDbInfo( + @Advice.Argument(0) String sql, @Advice.Return PreparedStatement statement) { + JdbcMaps.preparedStatements.put(statement, sql); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DbInfo.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DbInfo.java new file mode 100644 index 000000000..a0aa44019 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DbInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; + +@AutoValue +public abstract class DbInfo { + + public static final DbInfo DEFAULT = builder().build(); + + public static DbInfo.Builder builder() { + return new AutoValue_DbInfo.Builder(); + } + + @Nullable + public abstract String getSystem(); + + @Nullable + public abstract String getSubtype(); + + // "type:[subtype:]//host:port" + @Nullable + public abstract String getShortUrl(); + + @Nullable + public abstract String getUser(); + + @Nullable + public abstract String getName(); + + @Nullable + public abstract String getDb(); + + @Nullable + public abstract String getHost(); + + @Nullable + public abstract Integer getPort(); + + public Builder toBuilder() { + return builder() + .system(getSystem()) + .subtype(getSubtype()) + .shortUrl(getShortUrl()) + .user(getUser()) + .name(getName()) + .db(getDb()) + .host(getHost()) + .port(getPort()); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder system(String system); + + public abstract Builder subtype(String subtype); + + public abstract Builder shortUrl(String shortUrl); + + public abstract Builder user(String user); + + public abstract Builder name(String name); + + public abstract Builder db(String db); + + public abstract Builder host(String host); + + public abstract Builder port(Integer port); + + public abstract DbInfo build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DbRequest.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DbRequest.java new file mode 100644 index 000000000..94d24aa1b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DbRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcUtils.connectionFromStatement; +import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcUtils.extractDbInfo; + +import com.google.auto.value.AutoValue; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.Statement; +import org.checkerframework.checker.nullness.qual.Nullable; + +@AutoValue +public abstract class DbRequest { + + @Nullable + public static DbRequest create(PreparedStatement statement) { + return create(statement, JdbcMaps.preparedStatements.get(statement)); + } + + @Nullable + public static DbRequest create(Statement statement, String dbStatementString) { + Connection connection = connectionFromStatement(statement); + if (connection == null) { + return null; + } + + return create(extractDbInfo(connection), dbStatementString); + } + + public static DbRequest create(DbInfo dbInfo, String statement) { + return new AutoValue_DbRequest(dbInfo, statement); + } + + public abstract DbInfo getDbInfo(); + + @Nullable + public abstract String getStatement(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DriverInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DriverInstrumentation.java new file mode 100644 index 000000000..7c27cd925 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DriverInstrumentation.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; + +import java.sql.Connection; +import java.util.Properties; + +import io.opentelemetry.javaagent.instrumentation.jdbc.driver.DriverRequest; +import io.opentelemetry.javaagent.instrumentation.jdbc.driver.DriverSingletons; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DriverInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("java.sql.Driver"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("java.sql.Driver")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + nameStartsWith("connect") + .and(takesArgument(0, String.class)) + .and(takesArgument(1, Properties.class)) + .and(returns(named("java.sql.Connection"))), + DriverInstrumentation.class.getName() + "$DriverAdvice"); + } + + @SuppressWarnings({"unused","SystemOut"}) + public static class DriverAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) String url, + @Advice.Argument(1) Properties props, + @Advice.Local("otelRequest") DriverRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = currentContext(); + request = new DriverRequest(); + request.setType("db"); + if (url != null) { + // jdbc:mysql://localhost:3306/testdb?connectTime=1000&wait_timeout=3600&... + String urlNoProtocol = url.split("//")[1]; + String[] ipAndPortArr = urlNoProtocol.split("/"); + request.setDomainPort(ipAndPortArr[0]); + if(ipAndPortArr[1].contains("?")){ + request.setDataBaseName(ipAndPortArr[1].split("\\?")[0]); + }else{ + request.setDataBaseName(ipAndPortArr[1]); + } + } + if (props != null) { + request.setUserName(props.getProperty("user")); + request.setPassword(props.getProperty("password")); + } + context = DriverSingletons.instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void addDbInfo( + @Advice.Thrown Throwable throwable, + @Advice.Argument(0) String url, + @Advice.Argument(1) Properties props, + @Advice.Local("otelRequest") DriverRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Return Connection connection) { + if (scope != null) { + scope.close(); + DriverSingletons.instrumenter().end(context, request, null, null); + } + + if (connection == null) { + // Exception was probably thrown. + return; + } + DbInfo dbInfo = JdbcConnectionUrlParser.parse(url, props); + JdbcMaps.connectionInfo.put(connection, dbInfo); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcAttributesExtractor.java new file mode 100644 index 000000000..8647eea54 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcAttributesExtractor.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.api.instrumenter.db.SqlAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class JdbcAttributesExtractor extends SqlAttributesExtractor { + @Nullable + @Override + protected String system(DbRequest request) { + return request.getDbInfo().getSystem(); + } + + @Nullable + @Override + protected String user(DbRequest request) { + return request.getDbInfo().getUser(); + } + + @Nullable + @Override + protected String name(DbRequest request) { + DbInfo dbInfo = request.getDbInfo(); + return dbInfo.getName() == null ? dbInfo.getDb() : dbInfo.getName(); + } + + @Nullable + @Override + protected String connectionString(DbRequest request) { + return request.getDbInfo().getShortUrl(); + } + + @Override + protected AttributeKey dbTableAttribute() { + return SemanticAttributes.DB_SQL_TABLE; + } + + @Nullable + @Override + protected String rawStatement(DbRequest request) { + return request.getStatement(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcConnectionUrlParser.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcConnectionUrlParser.java new file mode 100644 index 000000000..1e1a03bf9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcConnectionUrlParser.java @@ -0,0 +1,969 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import static io.opentelemetry.javaagent.instrumentation.jdbc.DbInfo.DEFAULT; +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Structured as an enum instead of a class hierarchy to allow iterating through the parsers + * automatically without having to maintain a separate list of parsers. + */ +public enum JdbcConnectionUrlParser { + GENERIC_URL_LIKE() { + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + try { + // Attempt generic parsing + URI uri = new URI(jdbcUrl); + + populateStandardProperties(builder, splitQuery(uri.getQuery(), "&")); + + String user = uri.getUserInfo(); + if (user != null) { + builder.user(user); + } + + String path = uri.getPath(); + if (path.startsWith("/")) { + path = path.substring(1); + } + if (!path.isEmpty()) { + builder.db(path); + } + + if (uri.getHost() != null) { + builder.host(uri.getHost()); + } + + if (uri.getPort() > 0) { + builder.port(uri.getPort()); + } + + return builder.system(uri.getScheme()); + } catch (Exception e) { + return builder; + } + } + }, + + // see http://jtds.sourceforge.net/faq.html#urlFormat + JTDS_URL_LIKE() { + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + String serverName = ""; + Integer port = null; + + int hostIndex = jdbcUrl.indexOf("jtds:sqlserver://"); + if (hostIndex < 0) { + return builder; + } + + String[] split = jdbcUrl.split(";", 2); + if (split.length > 1) { + Map props = splitQuery(split[1], ";"); + populateStandardProperties(builder, props); + if (props.containsKey("instance")) { + builder.name(props.get("instance")); + } + } + + String urlServerName = split[0].substring(hostIndex + 17); + if (!urlServerName.isEmpty()) { + serverName = urlServerName; + } + + int databaseLoc = serverName.indexOf("/"); + if (databaseLoc > 1) { + builder.db(serverName.substring(databaseLoc + 1)); + serverName = serverName.substring(0, databaseLoc); + } + + int portLoc = serverName.indexOf(":"); + if (portLoc > 1) { + builder.port(Integer.parseInt(serverName.substring(portLoc + 1))); + serverName = serverName.substring(0, portLoc); + } + + if (!serverName.isEmpty()) { + builder.host(serverName); + } + + return builder; + } + }, + + MODIFIED_URL_LIKE() { + // Source: Regular Expressions Cookbook 2nd edition - 8.17. + // Matches Standard, Mixed or Compressed notation in a wider body of text + private final Pattern ipv6 = + Pattern.compile( + // Non Compressed + "(?:(?:(?:[A-F0-9]{1,4}:){6}" + // Compressed with at most 6 colons + + "|(?=(?:[A-F0-9]{0,4}:){0,6}" + // and 4 bytes and anchored + + "(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(?![:.\\w]))" + // and at most 1 double colon + + "(([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:)" + // Compressed with 7 colons and 5 numbers + + "|::(?:[A-F0-9]{1,4}:){5})" + // 255.255.255. + + "(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}" + // 255 + + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + // Standard + + "|(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}" + // Compressed with at most 7 colons and anchored + + "|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}(?![:.\\w]))" + // and at most 1 double colon + + "(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)" + // Compressed with 8 colons + + "|(?:[A-F0-9]{1,4}:){7}:|:(:[A-F0-9]{1,4}){7})(?![:.\\w])", + CASE_INSENSITIVE); + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + String serverName = ""; + Integer port = null; + String name = null; + + int hostIndex = jdbcUrl.indexOf("://"); + + if (hostIndex <= 0) { + return builder; + } + + String type = jdbcUrl.substring(0, hostIndex); + + String[] split; + if (type.equals("db2") || type.equals("as400")) { + if (jdbcUrl.contains("=")) { + int paramLoc = jdbcUrl.lastIndexOf(":"); + split = new String[] {jdbcUrl.substring(0, paramLoc), jdbcUrl.substring(paramLoc + 1)}; + } else { + split = new String[] {jdbcUrl}; + } + } else { + split = jdbcUrl.split(";", 2); + } + + if (split.length > 1) { + Map props = splitQuery(split[1], ";"); + populateStandardProperties(builder, props); + if (props.containsKey("servername")) { + serverName = props.get("servername"); + } + } + + String urlServerName = split[0].substring(hostIndex + 3); + if (!urlServerName.isEmpty()) { + serverName = urlServerName; + } + + int instanceLoc = serverName.indexOf("/"); + if (instanceLoc > 1) { + name = serverName.substring(instanceLoc + 1); + serverName = serverName.substring(0, instanceLoc); + } + + Matcher ipv6Matcher = ipv6.matcher(serverName); + boolean isIpv6 = ipv6Matcher.find(); + + int portLoc = -1; + if (isIpv6) { + if (serverName.startsWith("[")) { + portLoc = serverName.indexOf("]:") + 1; + } else { + serverName = "[" + serverName + "]"; + } + } else { + portLoc = serverName.indexOf(":"); + } + + if (portLoc > 1) { + port = Integer.parseInt(serverName.substring(portLoc + 1)); + serverName = serverName.substring(0, portLoc); + } + + instanceLoc = serverName.indexOf("\\"); + if (instanceLoc > 1) { + if (isIpv6) { + name = serverName.substring(instanceLoc + 1, serverName.lastIndexOf(']')); + serverName = "[" + ipv6Matcher.group(0) + "]"; + } else { + name = serverName.substring(instanceLoc + 1); + serverName = serverName.substring(0, instanceLoc); + } + } + + if (name != null) { + builder.name(name); + } + + if (!serverName.isEmpty()) { + builder.host(serverName); + } + + if (port != null) { + builder.port(port); + } + + return builder; + } + }, + + POSTGRES("postgresql") { + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 5432; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + DbInfo dbInfo = builder.build(); + if (dbInfo.getHost() == null) { + builder.host(DEFAULT_HOST); + } + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); + } + }, + + MYSQL("mysql", "mariadb") { + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 3306; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + DbInfo dbInfo = builder.build(); + if (dbInfo.getHost() == null) { + builder.host(DEFAULT_HOST); + } + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + + int protoLoc = jdbcUrl.indexOf("://"); + int typeEndLoc = jdbcUrl.indexOf(':'); + if (typeEndLoc < protoLoc) { + return MARIA_SUBPROTO + .doParse(jdbcUrl.substring(protoLoc + 3), builder) + .subtype(jdbcUrl.substring(typeEndLoc + 1, protoLoc)); + } + if (protoLoc > 0) { + return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); + } + + int hostEndLoc; + int portLoc = jdbcUrl.indexOf(":", typeEndLoc + 1); + int dbLoc = jdbcUrl.indexOf("/", typeEndLoc); + int paramLoc = jdbcUrl.indexOf("?", dbLoc); + + if (paramLoc > 0) { + populateStandardProperties(builder, splitQuery(jdbcUrl.substring(paramLoc + 1), "&")); + builder.db(jdbcUrl.substring(dbLoc + 1, paramLoc)); + } else { + builder.db(jdbcUrl.substring(dbLoc + 1)); + } + + if (portLoc > 0) { + hostEndLoc = portLoc; + try { + builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, dbLoc))); + } catch (NumberFormatException e) { + log.debug(e.getMessage(), e); + } + } else { + hostEndLoc = dbLoc; + } + + builder.host(jdbcUrl.substring(typeEndLoc + 1, hostEndLoc)); + + return builder; + } + }, + + MARIA_SUBPROTO() { + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + int hostEndLoc; + int clusterSepLoc = jdbcUrl.indexOf(","); + int ipv6End = jdbcUrl.startsWith("[") ? jdbcUrl.indexOf("]") : -1; + int portLoc = jdbcUrl.indexOf(":", Math.max(0, ipv6End)); + portLoc = clusterSepLoc < portLoc ? -1 : portLoc; + int dbLoc = jdbcUrl.indexOf("/", Math.max(portLoc, clusterSepLoc)); + + int paramLoc = jdbcUrl.indexOf("?", dbLoc); + + if (paramLoc > 0) { + populateStandardProperties(builder, splitQuery(jdbcUrl.substring(paramLoc + 1), "&")); + builder.db(jdbcUrl.substring(dbLoc + 1, paramLoc)); + } else { + builder.db(jdbcUrl.substring(dbLoc + 1)); + } + + if (jdbcUrl.startsWith("address=")) { + return MARIA_ADDRESS.doParse(jdbcUrl, builder); + } + + if (portLoc > 0) { + hostEndLoc = portLoc; + int portEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; + try { + builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, portEndLoc))); + } catch (NumberFormatException e) { + log.debug(e.getMessage(), e); + } + } else { + hostEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; + } + + if (ipv6End > 0) { + builder.host(jdbcUrl.substring(1, ipv6End)); + } else { + builder.host(jdbcUrl.substring(0, hostEndLoc)); + } + return builder; + } + }, + + MARIA_ADDRESS() { + private final Pattern hostPattern = Pattern.compile("\\(\\s*host\\s*=\\s*([^ )]+)\\s*\\)"); + private final Pattern portPattern = Pattern.compile("\\(\\s*port\\s*=\\s*([\\d]+)\\s*\\)"); + private final Pattern userPattern = Pattern.compile("\\(\\s*user\\s*=\\s*([^ )]+)\\s*\\)"); + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + int addressEnd = jdbcUrl.indexOf(",address="); + if (addressEnd > 0) { + jdbcUrl = jdbcUrl.substring(0, addressEnd); + } + Matcher hostMatcher = hostPattern.matcher(jdbcUrl); + if (hostMatcher.find()) { + builder.host(hostMatcher.group(1)); + } + + Matcher portMatcher = portPattern.matcher(jdbcUrl); + if (portMatcher.find()) { + builder.port(Integer.parseInt(portMatcher.group(1))); + } + + Matcher userMatcher = userPattern.matcher(jdbcUrl); + if (userMatcher.find()) { + builder.user(userMatcher.group(1)); + } + + return builder; + } + }, + + SAP("sap") { + private static final String DEFAULT_HOST = "localhost"; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + DbInfo dbInfo = builder.build(); + if (dbInfo.getHost() == null) { + builder.host(DEFAULT_HOST); + } + return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); + } + }, + + MSSQLSERVER("jtds", "microsoft", "sqlserver") { + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 1433; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + DbInfo dbInfo = builder.build(); + if (dbInfo.getHost() == null) { + builder.host(DEFAULT_HOST); + } + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + + int protoLoc = jdbcUrl.indexOf("://"); + int typeEndLoc = jdbcUrl.indexOf(':'); + if (protoLoc > typeEndLoc) { + String subtype = jdbcUrl.substring(typeEndLoc + 1, protoLoc); + builder.subtype(subtype); + } + + if (jdbcUrl.startsWith("jtds:")) { + return JTDS_URL_LIKE.doParse(jdbcUrl, builder); + } + + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); + } + }, + + DB2("db2", "as400") { + private static final int DEFAULT_PORT = 50000; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + DbInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); + } + }, + + ORACLE("oracle") { + private static final int DEFAULT_PORT = 1521; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + int typeEndIndex = jdbcUrl.indexOf(":", "oracle:".length()); + String subtype = jdbcUrl.substring("oracle:".length(), typeEndIndex); + jdbcUrl = jdbcUrl.substring(typeEndIndex + 1); + + builder.subtype(subtype); + DbInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + + if (jdbcUrl.contains("@")) { + return ORACLE_AT.doParse(jdbcUrl, builder); + } else { + return ORACLE_CONNECT_INFO.doParse(jdbcUrl, builder); + } + } + }, + + ORACLE_CONNECT_INFO() { + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + + String host; + Integer port; + String instance; + + int hostEnd = jdbcUrl.indexOf(":"); + int instanceLoc = jdbcUrl.indexOf("/"); + if (hostEnd > 0) { + host = jdbcUrl.substring(0, hostEnd); + int afterHostEnd = jdbcUrl.indexOf(":", hostEnd + 1); + if (afterHostEnd > 0) { + port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, afterHostEnd)); + instance = jdbcUrl.substring(afterHostEnd + 1); + } else { + if (instanceLoc > 0) { + instance = jdbcUrl.substring(instanceLoc + 1); + port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, instanceLoc)); + } else { + String portOrInstance = jdbcUrl.substring(hostEnd + 1); + Integer parsedPort = null; + try { + parsedPort = Integer.parseInt(portOrInstance); + } catch (NumberFormatException e) { + log.debug(e.getMessage(), e); + } + if (parsedPort == null) { + port = null; + instance = portOrInstance; + } else { + port = parsedPort; + instance = null; + } + } + } + } else { + if (instanceLoc > 0) { + host = jdbcUrl.substring(0, instanceLoc); + port = null; + instance = jdbcUrl.substring(instanceLoc + 1); + } else { + if (jdbcUrl.isEmpty()) { + return builder; + } else { + host = null; + port = null; + instance = jdbcUrl; + } + } + } + if (host != null) { + builder.host(host); + } + if (port != null) { + builder.port(port); + } + return builder.name(instance); + } + }, + + ORACLE_AT() { + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + if (jdbcUrl.contains("@(description")) { + return ORACLE_AT_DESCRIPTION.doParse(jdbcUrl, builder); + } + String user; + + String[] atSplit = jdbcUrl.split("@", 2); + + int userInfoLoc = atSplit[0].indexOf("/"); + if (userInfoLoc > 0) { + user = atSplit[0].substring(0, userInfoLoc); + } else { + user = null; + } + + String connectInfo = atSplit[1]; + int hostStart; + if (connectInfo.startsWith("//")) { + hostStart = "//".length(); + } else if (connectInfo.startsWith("ldap://")) { + hostStart = "ldap://".length(); + } else { + hostStart = 0; + } + if (user != null) { + builder.user(user); + } + return ORACLE_CONNECT_INFO.doParse(connectInfo.substring(hostStart), builder); + } + }, + + /** + * This parser can locate incorrect data if multiple addresses are defined but not everything is + * defined in the first block. (It would locate data from subsequent address blocks. + */ + ORACLE_AT_DESCRIPTION() { + private final Pattern hostPattern = Pattern.compile("\\(\\s*host\\s*=\\s*([^ )]+)\\s*\\)"); + private final Pattern portPattern = Pattern.compile("\\(\\s*port\\s*=\\s*([\\d]+)\\s*\\)"); + private final Pattern instancePattern = + Pattern.compile("\\(\\s*service_name\\s*=\\s*([^ )]+)\\s*\\)"); + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + String[] atSplit = jdbcUrl.split("@", 2); + + int userInfoLoc = atSplit[0].indexOf("/"); + if (userInfoLoc > 0) { + builder.user(atSplit[0].substring(0, userInfoLoc)); + } + + Matcher hostMatcher = hostPattern.matcher(atSplit[1]); + if (hostMatcher.find()) { + builder.host(hostMatcher.group(1)); + } + + Matcher portMatcher = portPattern.matcher(atSplit[1]); + if (portMatcher.find()) { + builder.port(Integer.parseInt(portMatcher.group(1))); + } + + Matcher instanceMatcher = instancePattern.matcher(atSplit[1]); + if (instanceMatcher.find()) { + builder.name(instanceMatcher.group(1)); + } + + return builder; + } + }, + + H2("h2") { + private static final int DEFAULT_PORT = 8082; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + String instance; + + String h2Url = jdbcUrl.substring("h2:".length()); + if (h2Url.startsWith("mem:")) { + builder.subtype("mem").host(null).port(null); + int propLoc = h2Url.indexOf(";"); + if (propLoc >= 0) { + instance = h2Url.substring("mem:".length(), propLoc); + } else { + instance = h2Url.substring("mem:".length()); + } + } else if (h2Url.startsWith("file:")) { + builder.subtype("file").host(null).port(null); + int propLoc = h2Url.indexOf(";"); + if (propLoc >= 0) { + instance = h2Url.substring("file:".length(), propLoc); + } else { + instance = h2Url.substring("file:".length()); + } + } else if (h2Url.startsWith("zip:")) { + builder.subtype("zip").host(null).port(null); + int propLoc = h2Url.indexOf(";"); + if (propLoc >= 0) { + instance = h2Url.substring("zip:".length(), propLoc); + } else { + instance = h2Url.substring("zip:".length()); + } + } else if (h2Url.startsWith("tcp:")) { + DbInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).system(DbSystemValues.H2).subtype("tcp"); + } else if (h2Url.startsWith("ssl:")) { + DbInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).system(DbSystemValues.H2).subtype("ssl"); + } else { + builder.subtype("file").host(null).port(null); + int propLoc = h2Url.indexOf(";"); + if (propLoc >= 0) { + instance = h2Url.substring(0, propLoc); + } else { + instance = h2Url; + } + } + if (!instance.isEmpty()) { + builder.name(instance); + } + return builder; + } + }, + + HSQL("hsqldb") { + private static final String DEFAULT_USER = "SA"; + private static final int DEFAULT_PORT = 9001; + + // TODO(anuraaga): Replace dbsystem with semantic convention + // https://github.com/open-telemetry/opentelemetry-specification/pull/1321 + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + String instance = null; + DbInfo dbInfo = builder.build(); + if (dbInfo.getUser() == null) { + builder.user(DEFAULT_USER); + } + String hsqlUrl = jdbcUrl.substring("hsqldb:".length()); + int proIndex = hsqlUrl.indexOf(";"); + if (proIndex >= 0) { + hsqlUrl = hsqlUrl.substring(0, proIndex); + } else { + int varIndex = hsqlUrl.indexOf("?"); + if (varIndex >= 0) { + hsqlUrl = hsqlUrl.substring(0, varIndex); + } + } + if (hsqlUrl.startsWith("mem:")) { + builder.subtype("mem").host(null).port(null); + instance = hsqlUrl.substring("mem:".length()); + } else if (hsqlUrl.startsWith("file:")) { + builder.subtype("file").host(null).port(null); + instance = hsqlUrl.substring("file:".length()); + } else if (hsqlUrl.startsWith("res:")) { + builder.subtype("res").host(null).port(null); + instance = hsqlUrl.substring("res:".length()); + } else if (hsqlUrl.startsWith("hsql:")) { + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).system("hsqldb").subtype("hsql"); + } else if (hsqlUrl.startsWith("hsqls:")) { + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).system("hsqldb").subtype("hsqls"); + } else if (hsqlUrl.startsWith("http:")) { + if (dbInfo.getPort() == null) { + builder.port(80); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).system("hsqldb").subtype("http"); + } else if (hsqlUrl.startsWith("https:")) { + if (dbInfo.getPort() == null) { + builder.port(443); + } + return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).system("hsqldb").subtype("https"); + } else { + builder.subtype("mem").host(null).port(null); + instance = hsqlUrl; + } + return builder.name(instance); + } + }, + + DERBY("derby") { + private static final String DEFAULT_USER = "APP"; + private static final int DEFAULT_PORT = 1527; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + String instance = null; + String host = null; + + DbInfo dbInfo = builder.build(); + if (dbInfo.getUser() == null) { + builder.user(DEFAULT_USER); + } + + String derbyUrl = jdbcUrl.substring("derby:".length()); + String[] split = derbyUrl.split(";", 2); + + if (split.length > 1) { + populateStandardProperties(builder, splitQuery(split[1], ";")); + } + + String details = split[0]; + if (details.startsWith("memory:")) { + builder.subtype("memory").host(null).port(null); + String urlInstance = details.substring("memory:".length()); + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } else if (details.startsWith("directory:")) { + builder.subtype("directory").host(null).port(null); + String urlInstance = details.substring("directory:".length()); + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } else if (details.startsWith("classpath:")) { + builder.subtype("classpath").host(null).port(null); + String urlInstance = details.substring("classpath:".length()); + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } else if (details.startsWith("jar:")) { + builder.subtype("jar").host(null).port(null); + String urlInstance = details.substring("jar:".length()); + if (!urlInstance.isEmpty()) { + instance = urlInstance; + } + } else if (details.startsWith("//")) { + builder.subtype("network"); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + String url = details.substring("//".length()); + int instanceLoc = url.indexOf("/"); + if (instanceLoc >= 0) { + instance = url.substring(instanceLoc + 1); + int protoLoc = instance.indexOf(":"); + if (protoLoc >= 0) { + instance = instance.substring(protoLoc + 1); + } + url = url.substring(0, instanceLoc); + } + int portLoc = url.indexOf(":"); + if (portLoc > 0) { + host = url.substring(0, portLoc); + builder.port(Integer.parseInt(url.substring(portLoc + 1))); + } else { + host = url; + } + } else { + builder.subtype("directory").host(null).port(null); + if (!details.isEmpty()) { + instance = details; + } + } + + if (host != null) { + builder.host(host); + } + return builder.name(instance); + } + }; + + private static final Logger log = LoggerFactory.getLogger(JdbcConnectionUrlParser.class); + + private static final Map typeParsers = new HashMap<>(); + + static { + for (JdbcConnectionUrlParser parser : JdbcConnectionUrlParser.values()) { + for (String key : parser.typeKeys) { + typeParsers.put(key, parser); + } + } + } + + // Wrapped in unmodifiableList + @SuppressWarnings("ImmutableEnumChecker") + private final List typeKeys; + + JdbcConnectionUrlParser(String... typeKeys) { + this.typeKeys = Collections.unmodifiableList(Arrays.asList(typeKeys)); + } + + abstract DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder); + + public static DbInfo parse(String connectionUrl, Properties props) { + if (connectionUrl == null) { + return DEFAULT; + } + // Make this easier and ignore case. + connectionUrl = connectionUrl.toLowerCase(Locale.ROOT); + + if (!connectionUrl.startsWith("jdbc:")) { + return DEFAULT; + } + + String jdbcUrl = connectionUrl.substring("jdbc:".length()); + int typeLoc = jdbcUrl.indexOf(':'); + + if (typeLoc < 1) { + // Invalid format: `jdbc:` or `jdbc::` + return DEFAULT; + } + + String type = jdbcUrl.substring(0, typeLoc); + String system = toDbSystem(type); + DbInfo.Builder parsedProps = DEFAULT.toBuilder().system(system); + populateStandardProperties(parsedProps, props); + + try { + if (typeParsers.containsKey(type)) { + // Delegate to specific parser + return withUrl(typeParsers.get(type).doParse(jdbcUrl, parsedProps), type); + } + return withUrl(GENERIC_URL_LIKE.doParse(jdbcUrl, parsedProps), type); + } catch (RuntimeException e) { + log.debug("Error parsing URL", e); + return parsedProps.build(); + } + } + + private static DbInfo withUrl(DbInfo.Builder builder, String type) { + DbInfo info = builder.build(); + StringBuilder url = new StringBuilder(); + url.append(type); + url.append(':'); + String subtype = info.getSubtype(); + if (subtype != null) { + url.append(subtype); + url.append(':'); + } + String host = info.getHost(); + if (host != null) { + url.append("//"); + url.append(host); + Integer port = info.getPort(); + if (port != null) { + url.append(':'); + url.append(port); + } + } + return builder.shortUrl(url.toString()).build(); + } + + // Source: https://stackoverflow.com/a/13592567 + private static Map splitQuery(String query, String separator) { + if (query == null || query.isEmpty()) { + return Collections.emptyMap(); + } + Map queryPairs = new LinkedHashMap<>(); + String[] pairs = query.split(separator); + for (String pair : pairs) { + try { + int idx = pair.indexOf("="); + String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair; + if (!queryPairs.containsKey(key)) { + String value = + idx > 0 && pair.length() > idx + 1 + ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") + : null; + queryPairs.put(key, value); + } + } catch (UnsupportedEncodingException e) { + // Ignore. + } + } + return queryPairs; + } + + private static void populateStandardProperties(DbInfo.Builder builder, Map props) { + if (props != null && !props.isEmpty()) { + if (props.containsKey("user")) { + builder.user((String) props.get("user")); + } + + if (props.containsKey("databasename")) { + builder.db((String) props.get("databasename")); + } + if (props.containsKey("databaseName")) { + builder.db((String) props.get("databaseName")); + } + + if (props.containsKey("servername")) { + builder.host((String) props.get("servername")); + } + if (props.containsKey("serverName")) { + builder.host((String) props.get("serverName")); + } + + if (props.containsKey("portnumber")) { + String portNumber = (String) props.get("portnumber"); + try { + builder.port(Integer.parseInt(portNumber)); + } catch (NumberFormatException e) { + log.debug("Error parsing portnumber property: " + portNumber, e); + } + } + + if (props.containsKey("portNumber")) { + String portNumber = (String) props.get("portNumber"); + try { + builder.port(Integer.parseInt(portNumber)); + } catch (NumberFormatException e) { + log.debug("Error parsing portNumber property: " + portNumber, e); + } + } + } + } + + // see + // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md + private static String toDbSystem(String type) { + switch (type) { + case "as400": // IBM AS400 Database + case "db2": // IBM Db2 + return DbSystemValues.DB2; + case "derby": // Apache Derby + return DbSystemValues.DERBY; + case "h2": // H2 Database + return DbSystemValues.H2; + case "hsqldb": // Hyper SQL Database + return "hsqldb"; + case "mariadb": // MariaDB + return DbSystemValues.MARIADB; + case "mysql": // MySQL + return DbSystemValues.MYSQL; + case "oracle": // Oracle Database + return DbSystemValues.ORACLE; + case "postgresql": // PostgreSQL + return DbSystemValues.POSTGRESQL; + case "jtds": // jTDS - the pure Java JDBC 3.0 driver for Microsoft SQL Server + case "microsoft": + case "sqlserver": // Microsoft SQL Server + return DbSystemValues.MSSQL; + default: + return DbSystemValues.OTHER_SQL; // Unknown DBMS + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcInstrumentationModule.java new file mode 100644 index 000000000..cf5c8be86 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JdbcInstrumentationModule extends InstrumentationModule { + public JdbcInstrumentationModule() { + super("jdbc"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ConnectionInstrumentation(), + new DriverInstrumentation(), + new PreparedStatementInstrumentation(), + new StatementInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcMaps.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcMaps.java new file mode 100644 index 000000000..9e2f472e0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcMaps.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import java.sql.Connection; +import java.sql.PreparedStatement; + +/** + * JDBC instrumentation shares a global map of connection info. + * + *

Should be injected into the bootstrap classpath. + */ +public class JdbcMaps { + public static final Cache connectionInfo = + Cache.newBuilder().setWeakKeys().build(); + public static final Cache preparedStatements = + Cache.newBuilder().setWeakKeys().build(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcNetAttributesExtractor.java new file mode 100644 index 000000000..f4efad618 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcNetAttributesExtractor.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class JdbcNetAttributesExtractor extends NetAttributesExtractor { + + @Nullable + @Override + public String transport(DbRequest request) { + return null; + } + + @Nullable + @Override + public String peerName(DbRequest request, @Nullable Void unused) { + return request.getDbInfo().getHost(); + } + + @Nullable + @Override + public Integer peerPort(DbRequest request, @Nullable Void unused) { + return request.getDbInfo().getPort(); + } + + @Nullable + @Override + public String peerIp(DbRequest request, @Nullable Void unused) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java new file mode 100644 index 000000000..58c3accf7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; + +public final class JdbcSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jdbc"; + + private static final Instrumenter INSTRUMENTER; + + static { + DbAttributesExtractor dbAttributesExtractor = new JdbcAttributesExtractor(); + SpanNameExtractor spanName = DbSpanNameExtractor.create(dbAttributesExtractor); + JdbcNetAttributesExtractor netAttributesExtractor = new JdbcNetAttributesExtractor(); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(dbAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor)) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private JdbcSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcUtils.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcUtils.java new file mode 100644 index 000000000..a4337faff --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcUtils.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import java.lang.reflect.Field; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class JdbcUtils { + + private static final Logger log = LoggerFactory.getLogger(JdbcUtils.class); + + @Nullable private static Field c3poField = null; + + /** Returns the unwrapped connection or null if exception was thrown. */ + public static Connection connectionFromStatement(Statement statement) { + Connection connection; + try { + connection = statement.getConnection(); + + if (c3poField != null) { + if (connection.getClass().getName().equals("com.mchange.v2.c3p0.impl.NewProxyConnection")) { + return (Connection) c3poField.get(connection); + } + } + + try { + // unwrap the connection to cache the underlying actual connection and to not cache proxy + // objects + if (connection.isWrapperFor(Connection.class)) { + connection = connection.unwrap(Connection.class); + } + } catch (Exception | AbstractMethodError e) { + if (connection != null) { + // Attempt to work around c3po delegating to an connection that doesn't support + // unwrapping. + Class connectionClass = connection.getClass(); + if (connectionClass.getName().equals("com.mchange.v2.c3p0.impl.NewProxyConnection")) { + Field inner = connectionClass.getDeclaredField("inner"); + inner.setAccessible(true); + c3poField = inner; + return (Connection) c3poField.get(connection); + } + } + + // perhaps wrapping isn't supported? + // ex: org.h2.jdbc.JdbcConnection v1.3.175 + // or: jdts.jdbc which always throws `AbstractMethodError` (at least up to version 1.3) + // Stick with original connection. + } + } catch (Throwable e) { + // Had some problem getting the connection. + log.debug("Could not get connection for StatementAdvice", e); + return null; + } + return connection; + } + + public static DbInfo extractDbInfo(Connection connection) { + return JdbcMaps.connectionInfo.computeIfAbsent(connection, JdbcUtils::computeDbInfo); + } + + private static DbInfo computeDbInfo(Connection connection) { + /* + * Logic to get the DBInfo from a JDBC Connection, if the connection was not created via + * Driver.connect, or it has never seen before, the connectionInfo map will return null and will + * attempt to extract DBInfo from the connection. If the DBInfo can't be extracted, then the + * connection will be stored with the DEFAULT DBInfo as the value in the connectionInfo map to + * avoid retry overhead. + */ + try { + DatabaseMetaData metaData = connection.getMetaData(); + String url = metaData.getURL(); + if (url != null) { + try { + return JdbcConnectionUrlParser.parse(url, connection.getClientInfo()); + } catch (Throwable ex) { + // getClientInfo is likely not allowed. + return JdbcConnectionUrlParser.parse(url, null); + } + } else { + return DbInfo.DEFAULT; + } + } catch (SQLException se) { + return DbInfo.DEFAULT; + } + } + + private JdbcUtils() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java new file mode 100644 index 000000000..9a704ce0e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.sql.PreparedStatement; +import java.sql.Statement; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class PreparedStatementInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("java.sql.PreparedStatement"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("java.sql.PreparedStatement")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + nameStartsWith("execute").and(takesArguments(0)).and(isPublic()), + PreparedStatementInstrumentation.class.getName() + "$PreparedStatementAdvice"); + } + + @SuppressWarnings("unused") + public static class PreparedStatementAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This PreparedStatement statement, + @Advice.Local("otelRequest") DbRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + // Connection#getMetaData() may execute a Statement or PreparedStatement to retrieve DB info + // this happens before the DB CLIENT span is started (and put in the current context), so this + // instrumentation runs again and the shouldStartSpan() check always returns true - and so on + // until we get a StackOverflowError + // using CallDepth prevents this, because this check happens before Connection#getMetadata() + // is called - the first recursive Statement call is just skipped and we do not create a span + // for it + if (CallDepthThreadLocalMap.getCallDepth(Statement.class).getAndIncrement() > 0) { + return; + } + + Context parentContext = currentContext(); + request = DbRequest.create(statement); + + if (request == null || !instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") DbRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + CallDepthThreadLocalMap.reset(Statement.class); + + scope.close(); + instrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java new file mode 100644 index 000000000..5d9bdf1ad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; + +import java.sql.Statement; + +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class StatementInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("java.sql.Statement"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("java.sql.Statement")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + nameStartsWith("execute").and(takesArgument(0, String.class)).and(isPublic()), + StatementInstrumentation.class.getName() + "$StatementAdvice"); + } + + @SuppressWarnings("unused") + public static class StatementAdvice { + + @SuppressWarnings("SystemOut") + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) String sql, + @Advice.This Statement statement, + @Advice.Local("otelRequest") DbRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + // Connection#getMetaData() may execute a Statement or PreparedStatement to retrieve DB info + // this happens before the DB CLIENT span is started (and put in the current context), so this + // instrumentation runs again and the shouldStartSpan() check always returns true - and so on + // until we get a StackOverflowError + // using CallDepth prevents this, because this check happens before Connection#getMetadata() + // is called - the first recursive Statement call is just skipped and we do not create a span + // for it + + if (!sql.contains("from") && !sql.contains("FROM")) { + return; + } + + + if (CallDepthThreadLocalMap.getCallDepth(Statement.class).getAndIncrement() > 0) { + return; + } + + Context parentContext = currentContext(); + request = DbRequest.create(statement, sql); + + + if (request == null || !instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") DbRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + CallDepthThreadLocalMap.reset(Statement.class); + + scope.close(); + instrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceCodeAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceCodeAttributesExtractor.java new file mode 100644 index 000000000..a4a96fe1f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceCodeAttributesExtractor.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc.datasource; + +import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; +import javax.sql.DataSource; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class DataSourceCodeAttributesExtractor extends CodeAttributesExtractor { + + @Override + protected Class codeClass(DataSource dataSource) { + return dataSource.getClass(); + } + + @Override + protected String methodName(DataSource dataSource) { + return "getConnection"; + } + + @Override + protected @Nullable String filePath(DataSource dataSource) { + return null; + } + + @Override + protected @Nullable Long lineNumber(DataSource dataSource) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentation.java new file mode 100644 index 000000000..e82eb1cb5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc.datasource; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.jdbc.datasource.DataSourceSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.sql.DataSource; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DataSourceInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.sql.DataSource")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("getConnection"), DataSourceInstrumentation.class.getName() + "$GetConnectionAdvice"); + } + + @SuppressWarnings("unused") + public static class GetConnectionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void start( + @Advice.This DataSource ds, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = Java8BytecodeBridge.currentContext(); + if (!Java8BytecodeBridge.spanFromContext(parentContext).getSpanContext().isValid()) { + // this instrumentation is already very noisy, and calls to getConnection outside of an + // existing trace do not tend to be very interesting + return; + } + + context = instrumenter().start(parentContext, ds); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.This DataSource ds, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + scope.close(); + instrumenter().end(context, ds, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentationModule.java new file mode 100644 index 000000000..ba99c87f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc.datasource; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class DataSourceInstrumentationModule extends InstrumentationModule { + public DataSourceInstrumentationModule() { + super("jdbc-datasource"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new DataSourceInstrumentation()); + } + + @Override + public boolean defaultEnabled() { + return false; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceSingletons.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceSingletons.java new file mode 100644 index 000000000..48f751e33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceSingletons.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc.datasource; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; +import javax.sql.DataSource; + +public final class DataSourceSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jdbc"; + + private static final Instrumenter INSTRUMENTER; + + static { + CodeAttributesExtractor attributesExtractor = + new DataSourceCodeAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + CodeSpanNameExtractor.create(attributesExtractor); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor) + .addAttributesExtractor(attributesExtractor) + .newInstrumenter(); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/driver/DriverRequest.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/driver/DriverRequest.java new file mode 100644 index 000000000..cbbbfe8f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/driver/DriverRequest.java @@ -0,0 +1,63 @@ +package io.opentelemetry.javaagent.instrumentation.jdbc.driver; + +public class DriverRequest { + + private String userName; + private String password; + private String domainPort; + private String dataBaseName; + private String type; + + public DriverRequest(){} + + public String getUserName() { + return userName; + } + + public String getPassword() { + return password; + } + + public String getDomainPort() { + return domainPort; + } + + public String getType() { + return type; + } + + public String getDataBaseName() { + return dataBaseName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setDomainPort(String domainPort) { + this.domainPort = domainPort; + } + + public void setDataBaseName(String dataBaseName) { + this.dataBaseName = dataBaseName; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public String toString() { + return "DriverRequest{" + + "userName='" + userName + '\'' + + ", password='" + password + '\'' + + ", domainPort='" + domainPort + '\'' + + ", dataBaseName='" + dataBaseName + '\'' + + ", type='" + type + '\'' + + '}'; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/driver/DriverSingletons.java b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/driver/DriverSingletons.java new file mode 100644 index 000000000..ed00548a9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/driver/DriverSingletons.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc.driver; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +public final class DriverSingletons { + + private static final String INSTRUMENTATION_NAME = "java.sql.Driver"; + + private static final Instrumenter INSTRUMENTER; + + static { + SpanNameExtractor spanName = request -> "dbDriver"; + + AttributesExtractor extractor = new AttributesExtractor() { + @Override + protected void onStart(AttributesBuilder attributes, DriverRequest o) { + attributes.put("db.driver.domainPort", o.getDomainPort()); + attributes.put("db.driver.userName", o.getUserName()); + attributes.put("db.driver.password", o.getPassword()); + attributes.put("db.driver.type", o.getType()); + attributes.put("db.driver.dbName", o.getDataBaseName()); + } + + @Override + protected void onEnd(AttributesBuilder attributes, DriverRequest o, Void o2) { + + } + }; + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(extractor) + .newInstrumenter(SpanKindExtractor.alwaysInternal()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private DriverSingletons() { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy new file mode 100644 index 000000000..2cde83305 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy @@ -0,0 +1,776 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.mchange.v2.c3p0.ComboPooledDataSource +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.sql.CallableStatement +import java.sql.Connection +import java.sql.DatabaseMetaData +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.SQLException +import java.sql.Statement +import javax.sql.DataSource +import org.apache.derby.jdbc.EmbeddedDataSource +import org.apache.derby.jdbc.EmbeddedDriver +import org.h2.Driver +import org.h2.jdbcx.JdbcDataSource +import org.hsqldb.jdbc.JDBCDriver +import spock.lang.Shared +import spock.lang.Unroll +import test.TestConnection +import test.TestDriver + +@Unroll +class JdbcInstrumentationTest extends AgentInstrumentationSpecification { + + @Shared + def dbName = "jdbcUnitTest" + @Shared + def dbNameLower = dbName.toLowerCase() + + @Shared + private Map jdbcUrls = [ + "h2" : "jdbc:h2:mem:$dbName", + "derby" : "jdbc:derby:memory:$dbName", + "hsqldb": "jdbc:hsqldb:mem:$dbName", + ] + + @Shared + private Map jdbcDriverClassNames = [ + "h2" : "org.h2.Driver", + "derby" : "org.apache.derby.jdbc.EmbeddedDriver", + "hsqldb": "org.hsqldb.jdbc.JDBCDriver", + ] + + @Shared + private Map jdbcUserNames = [ + "h2" : null, + "derby" : "APP", + "hsqldb": "SA", + ] + + @Shared + private Properties connectionProps = { + def props = new Properties() +// props.put("user", "someUser") +// props.put("password", "somePassword") + props.put("databaseName", "someDb") + props.put("OPEN_NEW", "true") // So H2 doesn't complain about username/password. + return props + }() + + // JDBC Connection pool name (i.e. HikariCP) -> Map + @Shared + private Map> cpDatasources = new HashMap<>() + + def prepareConnectionPoolDatasources() { + String[] connectionPoolNames = [ + "tomcat", "hikari", "c3p0", + ] + connectionPoolNames.each { + cpName -> + Map dbDSMapping = new HashMap<>() + jdbcUrls.each { + dbType, jdbcUrl -> + dbDSMapping.put(dbType, createDS(cpName, dbType, jdbcUrl)) + } + cpDatasources.put(cpName, dbDSMapping) + } + } + + def createTomcatDS(String dbType, String jdbcUrl) { + DataSource ds = new org.apache.tomcat.jdbc.pool.DataSource() + def jdbcUrlToSet = dbType == "derby" ? jdbcUrl + ";create=true" : jdbcUrl + ds.setUrl(jdbcUrlToSet) + ds.setDriverClassName(jdbcDriverClassNames.get(dbType)) + String username = jdbcUserNames.get(dbType) + if (username != null) { + ds.setUsername(username) + } + ds.setPassword("") + ds.setMaxActive(1) // to test proper caching, having > 1 max active connection will be hard to + // determine whether the connection is properly cached + return ds + } + + def createHikariDS(String dbType, String jdbcUrl) { + HikariConfig config = new HikariConfig() + def jdbcUrlToSet = dbType == "derby" ? jdbcUrl + ";create=true" : jdbcUrl + config.setJdbcUrl(jdbcUrlToSet) + String username = jdbcUserNames.get(dbType) + if (username != null) { + config.setUsername(username) + } + config.setPassword("") + config.addDataSourceProperty("cachePrepStmts", "true") + config.addDataSourceProperty("prepStmtCacheSize", "250") + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048") + config.setMaximumPoolSize(1) + + return new HikariDataSource(config) + } + + def createC3P0DS(String dbType, String jdbcUrl) { + DataSource ds = new ComboPooledDataSource() + ds.setDriverClass(jdbcDriverClassNames.get(dbType)) + def jdbcUrlToSet = dbType == "derby" ? jdbcUrl + ";create=true" : jdbcUrl + ds.setJdbcUrl(jdbcUrlToSet) + String username = jdbcUserNames.get(dbType) + if (username != null) { + ds.setUser(username) + } + ds.setPassword("") + ds.setMaxPoolSize(1) + return ds + } + + def createDS(String connectionPoolName, String dbType, String jdbcUrl) { + DataSource ds = null + if (connectionPoolName == "tomcat") { + ds = createTomcatDS(dbType, jdbcUrl) + } + if (connectionPoolName == "hikari") { + ds = createHikariDS(dbType, jdbcUrl) + } + if (connectionPoolName == "c3p0") { + ds = createC3P0DS(dbType, jdbcUrl) + } + return ds + } + + def setupSpec() { + prepareConnectionPoolDatasources() + } + + def cleanupSpec() { + cpDatasources.values().each { + it.values().each { + datasource -> + if (datasource instanceof Closeable) { + datasource.close() + } + } + } + } + + def "basic statement with #connection.getClass().getCanonicalName() on #system generates spans"() { + setup: + Statement statement = connection.createStatement() + ResultSet resultSet = runUnderTrace("parent") { + return statement.executeQuery(query) + } + + expect: + resultSet.next() + resultSet.getInt(1) == 3 + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name spanName + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" system + "$SemanticAttributes.DB_NAME.key" dbNameLower + if (username != null) { + "$SemanticAttributes.DB_USER.key" username + } + "$SemanticAttributes.DB_CONNECTION_STRING.key" url + "$SemanticAttributes.DB_STATEMENT.key" sanitizedQuery + "$SemanticAttributes.DB_OPERATION.key" "SELECT" + "$SemanticAttributes.DB_SQL_TABLE.key" table + } + } + } + } + + cleanup: + statement.close() + connection.close() + + where: + system | connection | username | query | sanitizedQuery | spanName | url | table + "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "hsqldb" | new JDBCDriver().connect(jdbcUrls.get("hsqldb"), null) | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT INFORMATION_SCHEMA.SYSTEM_USERS" | "hsqldb:mem:" | "INFORMATION_SCHEMA.SYSTEM_USERS" + "h2" | new Driver().connect(jdbcUrls.get("h2"), connectionProps) | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), connectionProps) | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "hsqldb" | new JDBCDriver().connect(jdbcUrls.get("hsqldb"), connectionProps) | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT INFORMATION_SCHEMA.SYSTEM_USERS" | "hsqldb:mem:" | "INFORMATION_SCHEMA.SYSTEM_USERS" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "hsqldb" | cpDatasources.get("tomcat").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT INFORMATION_SCHEMA.SYSTEM_USERS" | "hsqldb:mem:" | "INFORMATION_SCHEMA.SYSTEM_USERS" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "hsqldb" | cpDatasources.get("hikari").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT INFORMATION_SCHEMA.SYSTEM_USERS" | "hsqldb:mem:" | "INFORMATION_SCHEMA.SYSTEM_USERS" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "hsqldb" | cpDatasources.get("c3p0").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT INFORMATION_SCHEMA.SYSTEM_USERS" | "hsqldb:mem:" | "INFORMATION_SCHEMA.SYSTEM_USERS" + } + + def "prepared statement execute on #system with #connection.getClass().getCanonicalName() generates a span"() { + setup: + PreparedStatement statement = connection.prepareStatement(query) + ResultSet resultSet = runUnderTrace("parent") { + assert statement.execute() + return statement.resultSet + } + + expect: + resultSet.next() + resultSet.getInt(1) == 3 + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name spanName + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" system + "$SemanticAttributes.DB_NAME.key" dbNameLower + if (username != null) { + "$SemanticAttributes.DB_USER.key" username + } + "$SemanticAttributes.DB_CONNECTION_STRING.key" url + "$SemanticAttributes.DB_STATEMENT.key" sanitizedQuery + "$SemanticAttributes.DB_OPERATION.key" "SELECT" + "$SemanticAttributes.DB_SQL_TABLE.key" table + } + } + } + } + + cleanup: + statement.close() + connection.close() + + where: + system | connection | username | query | sanitizedQuery | spanName | url | table + "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + } + + def "prepared statement query on #system with #connection.getClass().getCanonicalName() generates a span"() { + setup: + PreparedStatement statement = connection.prepareStatement(query) + ResultSet resultSet = runUnderTrace("parent") { + return statement.executeQuery() + } + + expect: + resultSet.next() + resultSet.getInt(1) == 3 + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name spanName + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" system + "$SemanticAttributes.DB_NAME.key" dbNameLower + if (username != null) { + "$SemanticAttributes.DB_USER.key" username + } + "$SemanticAttributes.DB_CONNECTION_STRING.key" url + "$SemanticAttributes.DB_STATEMENT.key" sanitizedQuery + "$SemanticAttributes.DB_OPERATION.key" "SELECT" + "$SemanticAttributes.DB_SQL_TABLE.key" table + } + } + } + } + + cleanup: + statement.close() + connection.close() + + where: + system | connection | username | query | sanitizedQuery | spanName | url | table + "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + } + + def "prepared call on #system with #connection.getClass().getCanonicalName() generates a span"() { + setup: + CallableStatement statement = connection.prepareCall(query) + ResultSet resultSet = runUnderTrace("parent") { + return statement.executeQuery() + } + + expect: + resultSet.next() + resultSet.getInt(1) == 3 + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name spanName + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" system + "$SemanticAttributes.DB_NAME.key" dbName.toLowerCase() + if (username != null) { + "$SemanticAttributes.DB_USER.key" username + } + "$SemanticAttributes.DB_CONNECTION_STRING.key" url + "$SemanticAttributes.DB_STATEMENT.key" sanitizedQuery + "$SemanticAttributes.DB_OPERATION.key" "SELECT" + "$SemanticAttributes.DB_SQL_TABLE.key" table + } + } + } + } + + cleanup: + statement.close() + connection.close() + + where: + system | connection | username | query | sanitizedQuery | spanName | url | table + "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "SELECT 3" | "SELECT ?" | "SELECT $dbNameLower" | "h2:mem:" | null + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + } + + def "statement update on #system with #connection.getClass().getCanonicalName() generates a span"() { + setup: + Statement statement = connection.createStatement() + def sql = connection.nativeSQL(query) + + expect: + runUnderTrace("parent") { + return !statement.execute(sql) + } + statement.updateCount == 0 + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name dbNameLower + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" system + "$SemanticAttributes.DB_NAME.key" dbNameLower + if (username != null) { + "$SemanticAttributes.DB_USER.key" username + } + "$SemanticAttributes.DB_STATEMENT.key" query + "$SemanticAttributes.DB_CONNECTION_STRING.key" url + } + } + } + } + + cleanup: + statement.close() + connection.close() + + where: + system | connection | username | query | url + "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "CREATE TABLE S_H2 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "h2:mem:" + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "CREATE TABLE S_DERBY (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:" + "hsqldb" | new JDBCDriver().connect(jdbcUrls.get("hsqldb"), null) | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB (id INTEGER not NULL, PRIMARY KEY ( id ))" | "hsqldb:mem:" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "CREATE TABLE S_H2_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "h2:mem:" + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "CREATE TABLE S_DERBY_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:" + "hsqldb" | cpDatasources.get("tomcat").get("hsqldb").getConnection() | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "hsqldb:mem:" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "CREATE TABLE S_H2_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "h2:mem:" + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "CREATE TABLE S_DERBY_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:" + "hsqldb" | cpDatasources.get("hikari").get("hsqldb").getConnection() | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "hsqldb:mem:" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "CREATE TABLE S_H2_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "h2:mem:" + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "CREATE TABLE S_DERBY_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:" + "hsqldb" | cpDatasources.get("c3p0").get("hsqldb").getConnection() | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "hsqldb:mem:" + } + + def "prepared statement update on #system with #connection.getClass().getCanonicalName() generates a span"() { + setup: + def sql = connection.nativeSQL(query) + PreparedStatement statement = connection.prepareStatement(sql) + + expect: + runUnderTrace("parent") { + return statement.executeUpdate() == 0 + } + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name dbNameLower + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" system + "$SemanticAttributes.DB_NAME.key" dbName.toLowerCase() + if (username != null) { + "$SemanticAttributes.DB_USER.key" username + } + "$SemanticAttributes.DB_STATEMENT.key" query + "$SemanticAttributes.DB_CONNECTION_STRING.key" url + } + } + } + } + + cleanup: + statement.close() + connection.close() + + where: + system | connection | username | query | url + "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "CREATE TABLE PS_H2 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "h2:mem:" + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "CREATE TABLE PS_DERBY (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "CREATE TABLE PS_H2_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "h2:mem:" + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "CREATE TABLE PS_H2_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "h2:mem:" + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "CREATE TABLE PS_H2_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "h2:mem:" + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:" + } + + def "connection constructor throwing then generating correct spans after recovery using #driver connection (prepare statement = #prepareStatement)"() { + setup: + Connection connection = null + + when: + try { + connection = new TestConnection(true) + } catch (Exception ignored) { + connection = driver.connect(jdbcUrl, null) + } + + def (Statement statement, ResultSet rs) = runUnderTrace("parent") { + if (prepareStatement) { + def statement = connection.prepareStatement(query) + return new Tuple(statement, statement.executeQuery()) + } + + def statement = connection.createStatement() + return new Tuple(statement, statement.executeQuery(query)) + } + + then: + rs.next() + rs.getInt(1) == 3 + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name spanName + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" system + "$SemanticAttributes.DB_NAME.key" dbNameLower + if (username != null) { + "$SemanticAttributes.DB_USER.key" username + } + "$SemanticAttributes.DB_CONNECTION_STRING.key" url + "$SemanticAttributes.DB_STATEMENT.key" sanitizedQuery + "$SemanticAttributes.DB_OPERATION.key" "SELECT" + "$SemanticAttributes.DB_SQL_TABLE.key" table + } + } + } + } + + cleanup: + statement?.close() + connection?.close() + + where: + prepareStatement | system | driver | jdbcUrl | username | query | sanitizedQuery | spanName | url | table + true | "h2" | new Driver() | "jdbc:h2:mem:" + dbName | null | "SELECT 3;" | "SELECT ?;" | "SELECT $dbNameLower" | "h2:mem:" | null + true | "derby" | new EmbeddedDriver() | "jdbc:derby:memory:" + dbName + ";create=true" | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + false | "h2" | new Driver() | "jdbc:h2:mem:" + dbName | null | "SELECT 3;" | "SELECT ?;" | "SELECT $dbNameLower" | "h2:mem:" | null + false | "derby" | new EmbeddedDriver() | "jdbc:derby:memory:" + dbName + ";create=true" | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1" + } + + def "calling #datasource.class.simpleName getConnection generates a span when under existing trace"() { + setup: + assert datasource instanceof DataSource + init?.call(datasource) + + when: + datasource.getConnection().close() + + then: + !traces.any { it.any { it.name == "database.connection" } } + clearExportedData() + + when: + runUnderTrace("parent") { + datasource.getConnection().close() + } + + then: + assertTraces(1) { + trace(0, recursive ? 3 : 2) { + basicSpan(it, 0, "parent") + + span(1) { + name "${datasource.class.simpleName}.getConnection" + kind INTERNAL + childOf span(0) + attributes { + "$SemanticAttributes.CODE_NAMESPACE.key" datasource.class.name + "$SemanticAttributes.CODE_FUNCTION.key" "getConnection" + } + } + if (recursive) { + span(2) { + name "${datasource.class.simpleName}.getConnection" + kind INTERNAL + childOf span(1) + attributes { + "$SemanticAttributes.CODE_NAMESPACE.key" datasource.class.name + "$SemanticAttributes.CODE_FUNCTION.key" "getConnection" + } + } + } + } + } + + where: + datasource | init + new JdbcDataSource() | { ds -> ds.setURL(jdbcUrls.get("h2")) } + new EmbeddedDataSource() | { ds -> ds.jdbcurl = jdbcUrls.get("derby") } + cpDatasources.get("hikari").get("h2") | null + cpDatasources.get("hikari").get("derby") | null + cpDatasources.get("c3p0").get("h2") | null + cpDatasources.get("c3p0").get("derby") | null + + // Tomcat's pool doesn't work because the getConnection method is + // implemented in a parent class that doesn't implement DataSource + + recursive = datasource instanceof EmbeddedDataSource + } + + def "test getClientInfo exception"() { + setup: + Connection connection = new TestConnection(false) + + when: + Statement statement = null + runUnderTrace("parent") { + statement = connection.createStatement() + return statement.executeQuery(query) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name "DB Query" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "testdb" + "$SemanticAttributes.DB_STATEMENT.key" "testing ?" + "$SemanticAttributes.DB_CONNECTION_STRING.key" "testdb://localhost" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + } + } + } + } + + cleanup: + statement?.close() + connection?.close() + + where: + query = "testing 123" + } + + def "should produce proper span name #spanName"() { + setup: + def driver = new TestDriver() + + when: + def connection = driver.connect(url, null) + runUnderTrace("parent") { + def statement = connection.createStatement() + return statement.executeQuery(query) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name spanName + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "testdb" + "$SemanticAttributes.DB_NAME.key" databaseName + "$SemanticAttributes.DB_CONNECTION_STRING.key" "testdb://localhost" + "$SemanticAttributes.DB_STATEMENT.key" sanitizedQuery + "$SemanticAttributes.DB_OPERATION.key" operation + "$SemanticAttributes.DB_SQL_TABLE.key" table + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + } + } + } + } + + where: + url | query | sanitizedQuery | spanName | databaseName | operation | table + "jdbc:testdb://localhost?databaseName=test" | "SELECT * FROM table" | "SELECT * FROM table" | "SELECT test.table" | "test" | "SELECT" | "table" + "jdbc:testdb://localhost?databaseName=test" | "SELECT 42" | "SELECT ?" | "SELECT test" | "test" | "SELECT" | null + "jdbc:testdb://localhost" | "SELECT * FROM table" | "SELECT * FROM table" | "SELECT table" | null | "SELECT" | "table" + "jdbc:testdb://localhost?databaseName=test" | "CREATE TABLE table" | "CREATE TABLE table" | "test" | "test" | null | null + "jdbc:testdb://localhost" | "CREATE TABLE table" | "CREATE TABLE table" | "DB Query" | null | null | null + } + + def "#connectionPoolName connections should be cached in case of wrapped connections"() { + setup: + String dbType = "hsqldb" + DataSource ds = createDS(connectionPoolName, dbType, jdbcUrls.get(dbType)) + String query = "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" + int numQueries = 5 + Connection connection = null + int[] res = new int[numQueries] + + when: + for (int i = 0; i < numQueries; ++i) { + try { + connection = ds.getConnection() + def statement = connection.prepareStatement(query) + def rs = statement.executeQuery() + if (rs.next()) { + res[i] = rs.getInt(1) + } else { + res[i] = 0 + } + } finally { + connection.close() + } + } + + then: + for (int i = 0; i < numQueries; ++i) { + res[i] == 3 + } + assertTraces(numQueries) { + for (int i = 0; i < numQueries; ++i) { + trace(i, 1) { + span(0) { + name "SELECT INFORMATION_SCHEMA.SYSTEM_USERS" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "hsqldb" + "$SemanticAttributes.DB_NAME.key" dbNameLower + "$SemanticAttributes.DB_USER.key" "SA" + "$SemanticAttributes.DB_CONNECTION_STRING.key" "hsqldb:mem:" + "$SemanticAttributes.DB_STATEMENT.key" "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" + "$SemanticAttributes.DB_OPERATION.key" "SELECT" + "$SemanticAttributes.DB_SQL_TABLE.key" "INFORMATION_SCHEMA.SYSTEM_USERS" + } + } + } + } + } + + cleanup: + if (ds instanceof Closeable) { + ds.close() + } + + where: + connectionPoolName | _ + "hikari" | _ + "tomcat" | _ + "c3p0" | _ + } + + // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2644 + def "should handle recursive Statements inside Connection.getMetaData(): #desc"() { + given: + def connection = new DbCallingConnection(usePreparedStatementInConnection) + + when: + runUnderTrace("parent") { + executeQueryFunction(connection, "SELECT * FROM table") + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + name "SELECT table" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "testdb" + "$SemanticAttributes.DB_CONNECTION_STRING.key" "testdb://localhost" + "$SemanticAttributes.DB_STATEMENT.key" "SELECT * FROM table" + "$SemanticAttributes.DB_OPERATION.key" "SELECT" + "$SemanticAttributes.DB_SQL_TABLE.key" "table" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + } + } + } + } + + where: + desc | usePreparedStatementInConnection | executeQueryFunction + "getMetaData() uses Statement, test Statement" | false | { con, query -> con.createStatement().executeQuery(query) } + "getMetaData() uses PreparedStatement, test Statement" | true | { con, query -> con.createStatement().executeQuery(query) } + "getMetaData() uses Statement, test PreparedStatement" | false | { con, query -> con.prepareStatement(query).executeQuery() } + "getMetaData() uses PreparedStatement, test PreparedStatement" | true | { con, query -> con.prepareStatement(query).executeQuery() } + } + + class DbCallingConnection extends TestConnection { + final boolean usePreparedStatement + + DbCallingConnection(boolean usePreparedStatement) { + super(false) + this.usePreparedStatement = usePreparedStatement + } + + @Override + DatabaseMetaData getMetaData() throws SQLException { + // simulate retrieving DB metadata from the DB itself + if (usePreparedStatement) { + prepareStatement("SELECT * from DB_METADATA").executeQuery() + } else { + createStatement().executeQuery("SELECT * from DB_METADATA") + } + return super.getMetaData() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestConnection.groovy b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestConnection.groovy new file mode 100644 index 000000000..9eca7a13a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestConnection.groovy @@ -0,0 +1,306 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import java.sql.Array +import java.sql.Blob +import java.sql.CallableStatement +import java.sql.Clob +import java.sql.Connection +import java.sql.DatabaseMetaData +import java.sql.NClob +import java.sql.PreparedStatement +import java.sql.SQLClientInfoException +import java.sql.SQLException +import java.sql.SQLWarning +import java.sql.SQLXML +import java.sql.Savepoint +import java.sql.Statement +import java.sql.Struct +import java.util.concurrent.Executor + + +/** + * A JDBC connection class that optionally throws an exception in the constructor, used to test + */ +class TestConnection implements Connection { + TestConnection(boolean throwException) { + if (throwException) { + throw new IllegalStateException("connection exception") + } + } + + + @Override + Statement createStatement() throws SQLException { + return new TestStatement(this) + } + + @Override + PreparedStatement prepareStatement(String sql) throws SQLException { + return new TestPreparedStatement(this) + } + + @Override + CallableStatement prepareCall(String sql) throws SQLException { + return null + } + + @Override + String nativeSQL(String sql) throws SQLException { + return null + } + + @Override + void setAutoCommit(boolean autoCommit) throws SQLException { + + } + + @Override + boolean getAutoCommit() throws SQLException { + return false + } + + @Override + void commit() throws SQLException { + + } + + @Override + void rollback() throws SQLException { + + } + + @Override + void close() throws SQLException { + + } + + @Override + boolean isClosed() throws SQLException { + return false + } + + @Override + DatabaseMetaData getMetaData() throws SQLException { + return new TestDatabaseMetaData() + } + + @Override + void setReadOnly(boolean readOnly) throws SQLException { + + } + + @Override + boolean isReadOnly() throws SQLException { + return false + } + + @Override + void setCatalog(String catalog) throws SQLException { + + } + + @Override + String getCatalog() throws SQLException { + return null + } + + @Override + void setTransactionIsolation(int level) throws SQLException { + + } + + @Override + int getTransactionIsolation() throws SQLException { + return 0 + } + + @Override + SQLWarning getWarnings() throws SQLException { + return null + } + + @Override + void clearWarnings() throws SQLException { + + } + + @Override + Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return null + } + + @Override + PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return null + } + + @Override + CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return null + } + + @Override + Map> getTypeMap() throws SQLException { + return null + } + + @Override + void setTypeMap(Map> map) throws SQLException { + + } + + @Override + void setHoldability(int holdability) throws SQLException { + + } + + @Override + int getHoldability() throws SQLException { + return 0 + } + + @Override + Savepoint setSavepoint() throws SQLException { + return null + } + + @Override + Savepoint setSavepoint(String name) throws SQLException { + return null + } + + @Override + void rollback(Savepoint savepoint) throws SQLException { + + } + + @Override + void releaseSavepoint(Savepoint savepoint) throws SQLException { + + } + + @Override + Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return null + } + + @Override + PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return null + } + + @Override + CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return null + } + + @Override + PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return null + } + + @Override + PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return null + } + + @Override + PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return null + } + + @Override + Clob createClob() throws SQLException { + return null + } + + @Override + Blob createBlob() throws SQLException { + return null + } + + @Override + NClob createNClob() throws SQLException { + return null + } + + @Override + SQLXML createSQLXML() throws SQLException { + return null + } + + @Override + boolean isValid(int timeout) throws SQLException { + return false + } + + @Override + void setClientInfo(String name, String value) throws SQLClientInfoException { + + } + + @Override + void setClientInfo(Properties properties) throws SQLClientInfoException { + + } + + @Override + String getClientInfo(String name) throws SQLException { + throw new UnsupportedOperationException("Test 123") + } + + @Override + Properties getClientInfo() throws SQLException { + throw new Throwable("Test 123") + } + + @Override + Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return null + } + + @Override + Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return null + } + + @Override + void setSchema(String schema) throws SQLException { + + } + + @Override + String getSchema() throws SQLException { + return null + } + + @Override + void abort(Executor executor) throws SQLException { + + } + + @Override + void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + + } + + @Override + int getNetworkTimeout() throws SQLException { + return 0 + } + + @Override + def T unwrap(Class iface) throws SQLException { + return null + } + + @Override + boolean isWrapperFor(Class iface) throws SQLException { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestDatabaseMetaData.groovy b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestDatabaseMetaData.groovy new file mode 100644 index 000000000..757b43a2d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestDatabaseMetaData.groovy @@ -0,0 +1,894 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import java.sql.Connection +import java.sql.DatabaseMetaData +import java.sql.ResultSet +import java.sql.RowIdLifetime +import java.sql.SQLException + +class TestDatabaseMetaData implements DatabaseMetaData { + @Override + boolean allProceduresAreCallable() throws SQLException { + return false + } + + @Override + boolean allTablesAreSelectable() throws SQLException { + return false + } + + @Override + String getURL() throws SQLException { + return "jdbc:testdb://localhost" + } + + @Override + String getUserName() throws SQLException { + return null + } + + @Override + boolean isReadOnly() throws SQLException { + return false + } + + @Override + boolean nullsAreSortedHigh() throws SQLException { + return false + } + + @Override + boolean nullsAreSortedLow() throws SQLException { + return false + } + + @Override + boolean nullsAreSortedAtStart() throws SQLException { + return false + } + + @Override + boolean nullsAreSortedAtEnd() throws SQLException { + return false + } + + @Override + String getDatabaseProductName() throws SQLException { + return null + } + + @Override + String getDatabaseProductVersion() throws SQLException { + return null + } + + @Override + String getDriverName() throws SQLException { + return null + } + + @Override + String getDriverVersion() throws SQLException { + return null + } + + @Override + int getDriverMajorVersion() { + return 0 + } + + @Override + int getDriverMinorVersion() { + return 0 + } + + @Override + boolean usesLocalFiles() throws SQLException { + return false + } + + @Override + boolean usesLocalFilePerTable() throws SQLException { + return false + } + + @Override + boolean supportsMixedCaseIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesUpperCaseIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesLowerCaseIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesMixedCaseIdentifiers() throws SQLException { + return false + } + + @Override + boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + return false + } + + @Override + String getIdentifierQuoteString() throws SQLException { + return null + } + + @Override + String getSQLKeywords() throws SQLException { + return null + } + + @Override + String getNumericFunctions() throws SQLException { + return null + } + + @Override + String getStringFunctions() throws SQLException { + return null + } + + @Override + String getSystemFunctions() throws SQLException { + return null + } + + @Override + String getTimeDateFunctions() throws SQLException { + return null + } + + @Override + String getSearchStringEscape() throws SQLException { + return null + } + + @Override + String getExtraNameCharacters() throws SQLException { + return null + } + + @Override + boolean supportsAlterTableWithAddColumn() throws SQLException { + return false + } + + @Override + boolean supportsAlterTableWithDropColumn() throws SQLException { + return false + } + + @Override + boolean supportsColumnAliasing() throws SQLException { + return false + } + + @Override + boolean nullPlusNonNullIsNull() throws SQLException { + return false + } + + @Override + boolean supportsConvert() throws SQLException { + return false + } + + @Override + boolean supportsConvert(int fromType, int toType) throws SQLException { + return false + } + + @Override + boolean supportsTableCorrelationNames() throws SQLException { + return false + } + + @Override + boolean supportsDifferentTableCorrelationNames() throws SQLException { + return false + } + + @Override + boolean supportsExpressionsInOrderBy() throws SQLException { + return false + } + + @Override + boolean supportsOrderByUnrelated() throws SQLException { + return false + } + + @Override + boolean supportsGroupBy() throws SQLException { + return false + } + + @Override + boolean supportsGroupByUnrelated() throws SQLException { + return false + } + + @Override + boolean supportsGroupByBeyondSelect() throws SQLException { + return false + } + + @Override + boolean supportsLikeEscapeClause() throws SQLException { + return false + } + + @Override + boolean supportsMultipleResultSets() throws SQLException { + return false + } + + @Override + boolean supportsMultipleTransactions() throws SQLException { + return false + } + + @Override + boolean supportsNonNullableColumns() throws SQLException { + return false + } + + @Override + boolean supportsMinimumSQLGrammar() throws SQLException { + return false + } + + @Override + boolean supportsCoreSQLGrammar() throws SQLException { + return false + } + + @Override + boolean supportsExtendedSQLGrammar() throws SQLException { + return false + } + + @Override + boolean supportsANSI92EntryLevelSQL() throws SQLException { + return false + } + + @Override + boolean supportsANSI92IntermediateSQL() throws SQLException { + return false + } + + @Override + boolean supportsANSI92FullSQL() throws SQLException { + return false + } + + @Override + boolean supportsIntegrityEnhancementFacility() throws SQLException { + return false + } + + @Override + boolean supportsOuterJoins() throws SQLException { + return false + } + + @Override + boolean supportsFullOuterJoins() throws SQLException { + return false + } + + @Override + boolean supportsLimitedOuterJoins() throws SQLException { + return false + } + + @Override + String getSchemaTerm() throws SQLException { + return null + } + + @Override + String getProcedureTerm() throws SQLException { + return null + } + + @Override + String getCatalogTerm() throws SQLException { + return null + } + + @Override + boolean isCatalogAtStart() throws SQLException { + return false + } + + @Override + String getCatalogSeparator() throws SQLException { + return null + } + + @Override + boolean supportsSchemasInDataManipulation() throws SQLException { + return false + } + + @Override + boolean supportsSchemasInProcedureCalls() throws SQLException { + return false + } + + @Override + boolean supportsSchemasInTableDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsSchemasInIndexDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInDataManipulation() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInProcedureCalls() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInTableDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInIndexDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsPositionedDelete() throws SQLException { + return false + } + + @Override + boolean supportsPositionedUpdate() throws SQLException { + return false + } + + @Override + boolean supportsSelectForUpdate() throws SQLException { + return false + } + + @Override + boolean supportsStoredProcedures() throws SQLException { + return false + } + + @Override + boolean supportsSubqueriesInComparisons() throws SQLException { + return false + } + + @Override + boolean supportsSubqueriesInExists() throws SQLException { + return false + } + + @Override + boolean supportsSubqueriesInIns() throws SQLException { + return false + } + + @Override + boolean supportsSubqueriesInQuantifieds() throws SQLException { + return false + } + + @Override + boolean supportsCorrelatedSubqueries() throws SQLException { + return false + } + + @Override + boolean supportsUnion() throws SQLException { + return false + } + + @Override + boolean supportsUnionAll() throws SQLException { + return false + } + + @Override + boolean supportsOpenCursorsAcrossCommit() throws SQLException { + return false + } + + @Override + boolean supportsOpenCursorsAcrossRollback() throws SQLException { + return false + } + + @Override + boolean supportsOpenStatementsAcrossCommit() throws SQLException { + return false + } + + @Override + boolean supportsOpenStatementsAcrossRollback() throws SQLException { + return false + } + + @Override + int getMaxBinaryLiteralLength() throws SQLException { + return 0 + } + + @Override + int getMaxCharLiteralLength() throws SQLException { + return 0 + } + + @Override + int getMaxColumnNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInGroupBy() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInIndex() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInOrderBy() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInSelect() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInTable() throws SQLException { + return 0 + } + + @Override + int getMaxConnections() throws SQLException { + return 0 + } + + @Override + int getMaxCursorNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxIndexLength() throws SQLException { + return 0 + } + + @Override + int getMaxSchemaNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxProcedureNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxCatalogNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxRowSize() throws SQLException { + return 0 + } + + @Override + boolean doesMaxRowSizeIncludeBlobs() throws SQLException { + return false + } + + @Override + int getMaxStatementLength() throws SQLException { + return 0 + } + + @Override + int getMaxStatements() throws SQLException { + return 0 + } + + @Override + int getMaxTableNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxTablesInSelect() throws SQLException { + return 0 + } + + @Override + int getMaxUserNameLength() throws SQLException { + return 0 + } + + @Override + int getDefaultTransactionIsolation() throws SQLException { + return 0 + } + + @Override + boolean supportsTransactions() throws SQLException { + return false + } + + @Override + boolean supportsTransactionIsolationLevel(int level) throws SQLException { + return false + } + + @Override + boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + return false + } + + @Override + boolean supportsDataManipulationTransactionsOnly() throws SQLException { + return false + } + + @Override + boolean dataDefinitionCausesTransactionCommit() throws SQLException { + return false + } + + @Override + boolean dataDefinitionIgnoredInTransactions() throws SQLException { + return false + } + + @Override + ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException { + return null + } + + @Override + ResultSet getSchemas() throws SQLException { + return null + } + + @Override + ResultSet getCatalogs() throws SQLException { + return null + } + + @Override + ResultSet getTableTypes() throws SQLException { + return null + } + + @Override + ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { + return null + } + + @Override + ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { + return null + } + + @Override + ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { + return null + } + + @Override + ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { + return null + } + + @Override + ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { + return null + } + + @Override + ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException { + return null + } + + @Override + ResultSet getTypeInfo() throws SQLException { + return null + } + + @Override + ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { + return null + } + + @Override + boolean supportsResultSetType(int type) throws SQLException { + return false + } + + @Override + boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { + return false + } + + @Override + boolean ownUpdatesAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean ownDeletesAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean ownInsertsAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean othersUpdatesAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean othersDeletesAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean othersInsertsAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean updatesAreDetected(int type) throws SQLException { + return false + } + + @Override + boolean deletesAreDetected(int type) throws SQLException { + return false + } + + @Override + boolean insertsAreDetected(int type) throws SQLException { + return false + } + + @Override + boolean supportsBatchUpdates() throws SQLException { + return false + } + + @Override + ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { + return null + } + + @Override + Connection getConnection() throws SQLException { + return null + } + + @Override + boolean supportsSavepoints() throws SQLException { + return false + } + + @Override + boolean supportsNamedParameters() throws SQLException { + return false + } + + @Override + boolean supportsMultipleOpenResults() throws SQLException { + return false + } + + @Override + boolean supportsGetGeneratedKeys() throws SQLException { + return false + } + + @Override + ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException { + return null + } + + @Override + boolean supportsResultSetHoldability(int holdability) throws SQLException { + return false + } + + @Override + int getResultSetHoldability() throws SQLException { + return 0 + } + + @Override + int getDatabaseMajorVersion() throws SQLException { + return 0 + } + + @Override + int getDatabaseMinorVersion() throws SQLException { + return 0 + } + + @Override + int getJDBCMajorVersion() throws SQLException { + return 0 + } + + @Override + int getJDBCMinorVersion() throws SQLException { + return 0 + } + + @Override + int getSQLStateType() throws SQLException { + return 0 + } + + @Override + boolean locatorsUpdateCopy() throws SQLException { + return false + } + + @Override + boolean supportsStatementPooling() throws SQLException { + return false + } + + @Override + RowIdLifetime getRowIdLifetime() throws SQLException { + return null + } + + @Override + ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + return null + } + + @Override + boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + return false + } + + @Override + boolean autoCommitFailureClosesAllResultSets() throws SQLException { + return false + } + + @Override + ResultSet getClientInfoProperties() throws SQLException { + return null + } + + @Override + ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return null + } + + @Override + boolean generatedKeyAlwaysReturned() throws SQLException { + return false + } + + @Override + def T unwrap(Class iface) throws SQLException { + return null + } + + @Override + boolean isWrapperFor(Class iface) throws SQLException { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestDriver.groovy b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestDriver.groovy new file mode 100644 index 000000000..60b2368ba --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestDriver.groovy @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import java.sql.Connection +import java.sql.Driver +import java.sql.DriverPropertyInfo +import java.sql.SQLException +import java.sql.SQLFeatureNotSupportedException +import java.util.logging.Logger + +class TestDriver implements Driver { + @Override + Connection connect(String url, Properties info) throws SQLException { + return new TestConnection("connectException=true" == url) + } + + @Override + boolean acceptsURL(String url) throws SQLException { + return false + } + + @Override + DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return new DriverPropertyInfo[0] + } + + @Override + int getMajorVersion() { + return 0 + } + + @Override + int getMinorVersion() { + return 0 + } + + @Override + boolean jdbcCompliant() { + return false + } + + @Override + Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestPreparedStatement.groovy b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestPreparedStatement.groovy new file mode 100644 index 000000000..5f2b4f1a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestPreparedStatement.groovy @@ -0,0 +1,304 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import java.sql.Array +import java.sql.Blob +import java.sql.Clob +import java.sql.Connection +import java.sql.Date +import java.sql.NClob +import java.sql.ParameterMetaData +import java.sql.PreparedStatement +import java.sql.Ref +import java.sql.ResultSet +import java.sql.ResultSetMetaData +import java.sql.RowId +import java.sql.SQLException +import java.sql.SQLXML +import java.sql.Time +import java.sql.Timestamp + +class TestPreparedStatement extends TestStatement implements PreparedStatement { + TestPreparedStatement(Connection connection) { + super(connection) + } + + @Override + ResultSet executeQuery() throws SQLException { + return null + } + + @Override + int executeUpdate() throws SQLException { + return 0 + } + + @Override + void setNull(int parameterIndex, int sqlType) throws SQLException { + + } + + @Override + void setBoolean(int parameterIndex, boolean x) throws SQLException { + + } + + @Override + void setByte(int parameterIndex, byte x) throws SQLException { + + } + + @Override + void setShort(int parameterIndex, short x) throws SQLException { + + } + + @Override + void setInt(int parameterIndex, int x) throws SQLException { + + } + + @Override + void setLong(int parameterIndex, long x) throws SQLException { + + } + + @Override + void setFloat(int parameterIndex, float x) throws SQLException { + + } + + @Override + void setDouble(int parameterIndex, double x) throws SQLException { + + } + + @Override + void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + + } + + @Override + void setString(int parameterIndex, String x) throws SQLException { + + } + + @Override + void setBytes(int parameterIndex, byte[] x) throws SQLException { + + } + + @Override + void setDate(int parameterIndex, Date x) throws SQLException { + + } + + @Override + void setTime(int parameterIndex, Time x) throws SQLException { + + } + + @Override + void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + + } + + @Override + void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + void clearParameters() throws SQLException { + + } + + @Override + void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + + } + + @Override + void setObject(int parameterIndex, Object x) throws SQLException { + + } + + @Override + boolean execute() throws SQLException { + return false + } + + @Override + void addBatch() throws SQLException { + + } + + @Override + void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + + } + + @Override + void setRef(int parameterIndex, Ref x) throws SQLException { + + } + + @Override + void setBlob(int parameterIndex, Blob x) throws SQLException { + + } + + @Override + void setClob(int parameterIndex, Clob x) throws SQLException { + + } + + @Override + void setArray(int parameterIndex, Array x) throws SQLException { + + } + + @Override + ResultSetMetaData getMetaData() throws SQLException { + return null + } + + @Override + void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + + } + + @Override + void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + + } + + @Override + void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + + } + + @Override + void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + + } + + @Override + void setURL(int parameterIndex, URL x) throws SQLException { + + } + + @Override + ParameterMetaData getParameterMetaData() throws SQLException { + return null + } + + @Override + void setRowId(int parameterIndex, RowId x) throws SQLException { + + } + + @Override + void setNString(int parameterIndex, String value) throws SQLException { + + } + + @Override + void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + + } + + @Override + void setNClob(int parameterIndex, NClob value) throws SQLException { + + } + + @Override + void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + + } + + @Override + void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + + } + + @Override + void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + + } + + @Override + void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + + } + + @Override + void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + + } + + @Override + void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + + } + + @Override + void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + + } + + @Override + void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + + } + + @Override + void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + + } + + @Override + void setClob(int parameterIndex, Reader reader) throws SQLException { + + } + + @Override + void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + + } + + @Override + void setNClob(int parameterIndex, Reader reader) throws SQLException { + + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestStatement.groovy b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestStatement.groovy new file mode 100644 index 000000000..3a2605c35 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jdbc/javaagent/src/test/groovy/test/TestStatement.groovy @@ -0,0 +1,240 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import java.sql.Connection +import java.sql.ResultSet +import java.sql.SQLException +import java.sql.SQLWarning +import java.sql.Statement + +class TestStatement implements Statement { + final Connection connection + + TestStatement(Connection connection) { + this.connection = connection + } + + @Override + ResultSet executeQuery(String sql) throws SQLException { + return null + } + + @Override + int executeUpdate(String sql) throws SQLException { + return 0 + } + + @Override + void close() throws SQLException { + + } + + @Override + int getMaxFieldSize() throws SQLException { + return 0 + } + + @Override + void setMaxFieldSize(int max) throws SQLException { + + } + + @Override + int getMaxRows() throws SQLException { + return 0 + } + + @Override + void setMaxRows(int max) throws SQLException { + + } + + @Override + void setEscapeProcessing(boolean enable) throws SQLException { + + } + + @Override + int getQueryTimeout() throws SQLException { + return 0 + } + + @Override + void setQueryTimeout(int seconds) throws SQLException { + + } + + @Override + void cancel() throws SQLException { + + } + + @Override + SQLWarning getWarnings() throws SQLException { + return null + } + + @Override + void clearWarnings() throws SQLException { + + } + + @Override + void setCursorName(String name) throws SQLException { + + } + + @Override + boolean execute(String sql) throws SQLException { + return false + } + + @Override + ResultSet getResultSet() throws SQLException { + return null + } + + @Override + int getUpdateCount() throws SQLException { + return 0 + } + + @Override + boolean getMoreResults() throws SQLException { + return false + } + + @Override + void setFetchDirection(int direction) throws SQLException { + + } + + @Override + int getFetchDirection() throws SQLException { + return 0 + } + + @Override + void setFetchSize(int rows) throws SQLException { + + } + + @Override + int getFetchSize() throws SQLException { + return 0 + } + + @Override + int getResultSetConcurrency() throws SQLException { + return 0 + } + + @Override + int getResultSetType() throws SQLException { + return 0 + } + + @Override + void addBatch(String sql) throws SQLException { + + } + + @Override + void clearBatch() throws SQLException { + + } + + @Override + int[] executeBatch() throws SQLException { + return new int[0] + } + + @Override + Connection getConnection() throws SQLException { + return connection + } + + @Override + boolean getMoreResults(int current) throws SQLException { + return false + } + + @Override + ResultSet getGeneratedKeys() throws SQLException { + return null + } + + @Override + int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + return 0 + } + + @Override + int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + return 0 + } + + @Override + int executeUpdate(String sql, String[] columnNames) throws SQLException { + return 0 + } + + @Override + boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + return false + } + + @Override + boolean execute(String sql, int[] columnIndexes) throws SQLException { + return false + } + + @Override + boolean execute(String sql, String[] columnNames) throws SQLException { + return false + } + + @Override + int getResultSetHoldability() throws SQLException { + return 0 + } + + @Override + boolean isClosed() throws SQLException { + return false + } + + @Override + void setPoolable(boolean poolable) throws SQLException { + + } + + @Override + boolean isPoolable() throws SQLException { + return false + } + + @Override + void closeOnCompletion() throws SQLException { + + } + + @Override + boolean isCloseOnCompletion() throws SQLException { + return false + } + + @Override + def T unwrap(Class iface) throws SQLException { + return null + } + + @Override + boolean isWrapperFor(Class iface) throws SQLException { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/jedis-factory-2.5-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/jedis-factory-2.5-javaagent.gradle new file mode 100644 index 000000000..14397a6fb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/jedis-factory-2.5-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "redis.clients" + module = "jedis" + versions = "(,2.5.0]" + assertInverse = true + } +} + +dependencies { + api(project(':instrumentation:jdbc:javaagent')) + compileOnly "redis.clients:jedis:2.5.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + // ensures jedis-1.4 instrumentation does not load with jedis 3.0+ by failing + // the tests in the event it does. The tests will end up with double spans + testInstrumentation project(':instrumentation:jedis-factory:jedis-factory-2.5:javaagent') + + testLibrary "redis.clients:jedis:2.5.0" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory25Instrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory25Instrumentation.java new file mode 100644 index 000000000..d35c1b612 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory25Instrumentation.java @@ -0,0 +1,73 @@ +package io.opentelemetry.javaagent.instrumentation.jedisfactory.v3_5; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jdbc.driver.DriverRequest; +import io.opentelemetry.javaagent.instrumentation.jdbc.driver.DriverSingletons; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +@SuppressWarnings({"SystemOut","CatchAndPrintStackTrace","CatchingUnchecked"}) +public class JedisFactory25Instrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + ElementMatcher.Junction matcher = + named("redis.clients.jedis.JedisFactory").and( + new ElementMatcher() { + @Override + public boolean matches(TypeDescription target) { + try { + + FieldList declaredFields = target.getDeclaredFields(); + for(FieldDescription.InDefinedShape field : declaredFields){ + if("host".equals(field.getName())){ + return true; + } + } + }catch(Exception e){ + e.printStackTrace(); + } + return false; + } + }); + return matcher; + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isPublic(). + and(named("makeObject")), + this.getClass().getName()+"$MakeObjectAdvice25"); + } + + public static class MakeObjectAdvice25 { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.FieldValue("host") String host, + @Advice.FieldValue("port") int port, + @Advice.FieldValue("password") String password) { + DriverRequest request = new DriverRequest(); + request.setType("redis"); + request.setDomainPort(host+":"+port); + request.setPassword(password); + Context parentContext = currentContext(); + Context context = DriverSingletons.instrumenter().start(parentContext, request); + Scope scope = context.makeCurrent(); + if (scope != null) { + scope.close(); + DriverSingletons.instrumenter().end(context, request, null, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory25InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory25InstrumentationModule.java new file mode 100644 index 000000000..d0b0e5b5a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory25InstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedisfactory.v3_5; + + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.ArrayList; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JedisFactory25InstrumentationModule extends InstrumentationModule { + + public JedisFactory25InstrumentationModule() { + super("jedis-factory", "jedis-factory-2.5"); + } + + @Override + public List typeInstrumentations() { + List list = new ArrayList<>(); + list.add(new JedisFactory25Instrumentation()); + return list; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/jedis-factory-3.5-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/jedis-factory-3.5-javaagent.gradle new file mode 100644 index 000000000..3e8e7a333 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/jedis-factory-3.5-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "redis.clients" + module = "jedis" + versions = "(2.5.0,3.5.0]" + assertInverse = true + } +} + +dependencies { + api(project(':instrumentation:jdbc:javaagent')) + compileOnly "redis.clients:jedis:3.5.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + // ensures jedis-1.4 instrumentation does not load with jedis 3.0+ by failing + // the tests in the event it does. The tests will end up with double spans + testInstrumentation project(':instrumentation:jedis-factory:jedis-factory-3.5:javaagent') + + testLibrary "redis.clients:jedis:3.5.0" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory35Instrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory35Instrumentation.java new file mode 100644 index 000000000..72d2adadc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory35Instrumentation.java @@ -0,0 +1,75 @@ +package io.opentelemetry.javaagent.instrumentation.jedisfactory.v3_5; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jdbc.driver.DriverRequest; +import io.opentelemetry.javaagent.instrumentation.jdbc.driver.DriverSingletons; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.clients.jedis.HostAndPort; + +import java.util.concurrent.atomic.AtomicReference; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +@SuppressWarnings({"SystemOut","CatchAndPrintStackTrace","CatchingUnchecked"}) +public class JedisFactory35Instrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + ElementMatcher.Junction matcher = + named("redis.clients.jedis.JedisFactory").and( + new ElementMatcher() { + @Override + public boolean matches(TypeDescription target) { + try { + FieldList declaredFields = target.getDeclaredFields(); + for(FieldDescription.InDefinedShape field : declaredFields){ + if("hostAndPort".equals(field.getName())){ + return true; + } + } + }catch(Exception e){ + e.printStackTrace(); + } + return false; + } + }); + return matcher; + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isPublic(). + and(named("makeObject")), + this.getClass().getName()+"$MakeObjectAdvice35"); + } + + public static class MakeObjectAdvice35 { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.FieldValue("hostAndPort") AtomicReference hostAndPort, + @Advice.FieldValue("password") String password) { + DriverRequest request = new DriverRequest(); + HostAndPort hp = hostAndPort.get(); + request.setType("redis"); + request.setDomainPort(hp.getHost()+":"+hp.getPort()); + request.setPassword(password); + Context parentContext = currentContext(); + Context context = DriverSingletons.instrumenter().start(parentContext, request); + Scope scope = context.makeCurrent(); + if (scope != null) { + scope.close(); + DriverSingletons.instrumenter().end(context, request, null, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory35InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory35InstrumentationModule.java new file mode 100644 index 000000000..9e17c999c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_5/JedisFactory35InstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedisfactory.v3_5; + + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.ArrayList; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JedisFactory35InstrumentationModule extends InstrumentationModule { + + public JedisFactory35InstrumentationModule() { + super("jedis-factory", "jedis-factory-3.5"); + } + + @Override + public List typeInstrumentations() { + List list = new ArrayList<>(); + list.add(new JedisFactory35Instrumentation()); + return list; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/jedis-factory-3.9-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/jedis-factory-3.9-javaagent.gradle new file mode 100644 index 000000000..ccb86d270 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/jedis-factory-3.9-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "redis.clients" + module = "jedis" + versions = "(3.5.0,3.7.0]" + assertInverse = true + } +} + +dependencies { + api(project(':instrumentation:jdbc:javaagent')) + compileOnly "redis.clients:jedis:3.7.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + // ensures jedis-1.4 instrumentation does not load with jedis 3.0+ by failing + // the tests in the event it does. The tests will end up with double spans + testInstrumentation project(':instrumentation:jedis-factory:jedis-factory-3.9:javaagent') + + testLibrary "redis.clients:jedis:3.7.0" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_9/JedisFactory39Instrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_9/JedisFactory39Instrumentation.java new file mode 100644 index 000000000..7df2ec285 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_9/JedisFactory39Instrumentation.java @@ -0,0 +1,78 @@ +package io.opentelemetry.javaagent.instrumentation.jedisfactory.v3_9; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jdbc.driver.DriverRequest; +import io.opentelemetry.javaagent.instrumentation.jdbc.driver.DriverSingletons; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.clients.jedis.DefaultJedisSocketFactory; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisSocketFactory; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +@SuppressWarnings({"SystemOut","CatchAndPrintStackTrace","CatchingUnchecked"}) +public class JedisFactory39Instrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + ElementMatcher.Junction matcher = + named("redis.clients.jedis.JedisFactory").and( + new ElementMatcher() { + @Override + public boolean matches(TypeDescription target) { + try { + FieldList declaredFields = target.getDeclaredFields(); + for(FieldDescription.InDefinedShape field : declaredFields){ + if("jedisSocketFactory".equals(field.getName())){ + return true; + } + } + }catch(Exception e){ + e.printStackTrace(); + } + return false; + } + }); + return matcher; + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isPublic(). + and(named("makeObject")), + this.getClass().getName()+"$MakeObjectAdvice39"); + } + + public static class MakeObjectAdvice39 { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.FieldValue("jedisSocketFactory") JedisSocketFactory jedisSocketFactory, + @Advice.FieldValue("clientConfig") JedisClientConfig clientConfig) { + DriverRequest request = new DriverRequest(); + if(jedisSocketFactory instanceof DefaultJedisSocketFactory){ + HostAndPort hp = ((DefaultJedisSocketFactory) jedisSocketFactory).getHostAndPort(); + request.setDomainPort(hp.getHost()+":"+hp.getPort()); + } + request.setType("redis"); + request.setPassword(clientConfig.getPassword()); + Context parentContext = currentContext(); + Context context = DriverSingletons.instrumenter().start(parentContext, request); + Scope scope = context.makeCurrent(); + if (scope != null) { + scope.close(); + DriverSingletons.instrumenter().end(context, request, null, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_9/JedisFactory39InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_9/JedisFactory39InstrumentationModule.java new file mode 100644 index 000000000..971e7f80a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis-factory/jedis-factory-3.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedisfactory/v3_9/JedisFactory39InstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedisfactory.v3_9; + + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.ArrayList; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JedisFactory39InstrumentationModule extends InstrumentationModule { + + public JedisFactory39InstrumentationModule() { + super("jedis-factory", "jedis-factory-3.9"); + } + + @Override + public List typeInstrumentations() { + List list = new ArrayList<>(); + list.add(new JedisFactory39Instrumentation()); + return list; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/jedis-1.4-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/jedis-1.4-javaagent.gradle new file mode 100644 index 000000000..0cb31f460 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/jedis-1.4-javaagent.gradle @@ -0,0 +1,20 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "redis.clients" + module = "jedis" + versions = "[1.4.0,3.0.0)" + assertInverse = true + } +} + +dependencies { + library "redis.clients:jedis:1.4.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + // Jedis 3.0 has API changes that prevent instrumentation from applying + latestDepTestLibrary "redis.clients:jedis:2.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisConnectionInstrumentation.java new file mode 100644 index 000000000..e58be3ea2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisConnectionInstrumentation.java @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jedis.v1_4.JedisSingletons.instrumenter; +import static java.util.Arrays.asList; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.db.RedisCommandUtil; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.clients.jedis.Connection; +import redis.clients.jedis.Protocol; + +@SuppressWarnings("SystemOut") +public class JedisConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("redis.clients.jedis.Connection"); + } + + @Override + public void transform(TypeTransformer transformer) { + // FIXME: This instrumentation only incorporates sending the command, not processing the + // result. + transformer.applyAdviceToMethod( + isMethod() + .and(named("sendCommand")) + .and(takesArguments(1)) + .and(takesArgument(0, named("redis.clients.jedis.Protocol$Command"))), + this.getClass().getName() + "$SendCommandNoArgsAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("sendCommand")) + .and(takesArguments(2)) + .and(takesArgument(0, named("redis.clients.jedis.Protocol$Command"))) + .and(takesArgument(1, is(byte[][].class))), + this.getClass().getName() + "$SendCommandWithArgsAdvice"); + } + + @SuppressWarnings("unused") + public static class SendCommandNoArgsAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Connection connection, + @Advice.Argument(0) Protocol.Command command, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("startTime") long startTime) { + startTime = System.currentTimeMillis(); + if(RedisCommandUtil.REDIS_EXCLUDE_COMMAND.contains(command.name())){ + return; + } + Context parentContext = currentContext(); + request = JedisRequest.create(connection, command); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Argument(0) Protocol.Command command, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("startTime") long startTime) { + if (scope == null) { + return; + } + + scope.close(); + + if (RedisCommandUtil.skipEnd(command.name(), throwable, startTime)) { + return; + } + instrumenter().end(context, request, null, throwable); + } + } + + @SuppressWarnings("unused") + public static class SendCommandWithArgsAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Connection connection, + @Advice.Argument(0) Protocol.Command command, + @Advice.Argument(1) byte[][] args, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("startTime") long startTime) { + startTime = System.currentTimeMillis(); + if(RedisCommandUtil.REDIS_EXCLUDE_COMMAND.contains(command.name())){ + return; + } + Context parentContext = currentContext(); + request = JedisRequest.create(connection, command, asList(args)); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Argument(0) Protocol.Command command, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("startTime") long startTime) { + if (scope == null) { + return; + } + + scope.close(); + if (RedisCommandUtil.skipEnd(command.name(), throwable, startTime)) { + return; + } + instrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisDbAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisDbAttributesExtractor.java new file mode 100644 index 000000000..58c7f4055 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisDbAttributesExtractor.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; + +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class JedisDbAttributesExtractor extends DbAttributesExtractor { + @Override + protected String system(JedisRequest request) { + return SemanticAttributes.DbSystemValues.REDIS; + } + + @Override + @Nullable + protected String user(JedisRequest request) { + return null; + } + + @Override + protected String name(JedisRequest request) { + return null; + } + + @Override + protected String connectionString(JedisRequest request) { + return null; + } + + @Override + protected String statement(JedisRequest request) { + return RedisCommandSanitizer.sanitize(request.getCommand().name(), request.getArgs()); + } + + @Override + protected String operation(JedisRequest request) { + return request.getCommand().name(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisInstrumentationModule.java new file mode 100644 index 000000000..2a9b61f4e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisInstrumentationModule.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.ArrayList; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class JedisInstrumentationModule extends InstrumentationModule { + + public JedisInstrumentationModule() { + super("jedis", "jedis-1.4"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // Avoid matching 3.x + return not(hasClassesNamed("redis.clients.jedis.commands.ProtocolCommand")); + } + + @Override + public List typeInstrumentations() { + List list = new ArrayList<>(); + list.add(new JedisConnectionInstrumentation()); + return list; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetAttributesExtractor.java new file mode 100644 index 000000000..0647d9ded --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetAttributesExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class JedisNetAttributesExtractor extends NetAttributesExtractor { + + @Override + @Nullable + public String transport(JedisRequest request) { + return null; + } + + @Override + public String peerName(JedisRequest request, @Nullable Void unused) { + return request.getConnection().getHost(); + } + + @Override + public Integer peerPort(JedisRequest request, @Nullable Void unused) { + return request.getConnection().getPort(); + } + + @Override + @Nullable + public String peerIp(JedisRequest request, @Nullable Void unused) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisRequest.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisRequest.java new file mode 100644 index 000000000..f40d3301e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; + +import static java.util.Collections.emptyList; + +import com.google.auto.value.AutoValue; +import java.util.List; +import redis.clients.jedis.Connection; +import redis.clients.jedis.Protocol; + +@AutoValue +public abstract class JedisRequest { + + public static JedisRequest create(Connection connection, Protocol.Command command) { + return new AutoValue_JedisRequest(connection, command, emptyList()); + } + + public static JedisRequest create( + Connection connection, Protocol.Command command, List args) { + return new AutoValue_JedisRequest(connection, command, args); + } + + public abstract Connection getConnection(); + + public abstract Protocol.Command getCommand(); + + public abstract List getArgs(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisSingletons.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisSingletons.java new file mode 100644 index 000000000..f31dcba7c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisSingletons.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; + +public final class JedisSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jedis-1.4"; + + private static final Instrumenter INSTRUMENTER; + + static { + DbAttributesExtractor attributesExtractor = + new JedisDbAttributesExtractor(); + SpanNameExtractor spanName = DbSpanNameExtractor.create(attributesExtractor); + JedisNetAttributesExtractor netAttributesExtractor = new JedisNetAttributesExtractor(); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(attributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor)) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private JedisSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/piplinecluster/ContextHolder.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/piplinecluster/ContextHolder.java new file mode 100644 index 000000000..1eab89f86 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/piplinecluster/ContextHolder.java @@ -0,0 +1,18 @@ +package io.opentelemetry.javaagent.instrumentation.jedis.v1_4.piplinecluster; + +public class ContextHolder { + + public static ThreadLocal mark = new ThreadLocal<>(); + + public static void set(boolean isTrue){ + mark.set(isTrue); + } + + public static Boolean get(){ + return mark.get(); + } + + public static void clear(){ + mark.remove(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/piplinecluster/PipelineClusterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/piplinecluster/PipelineClusterInstrumentation.java new file mode 100644 index 000000000..31465640f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/piplinecluster/PipelineClusterInstrumentation.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v1_4.piplinecluster; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jedis.v1_4.JedisRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.clients.jedis.Connection; +import redis.clients.jedis.Protocol; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jedis.v1_4.JedisSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public class PipelineClusterInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.xiaomi.data.push.redis.Redis"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("mget")) + .and(takesArguments(1)) + .and(takesArgument(0, named("java.util.List"))), + this.getClass().getName() + "$SendMget"); + } + + @SuppressWarnings({"unused","SystemOut"}) + public static class SendMget { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Local("redisHosts") String redisHosts, + @Advice.Argument(0) List keys, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + System.out.println("获取到的redisHosts:"+redisHosts); + Connection connection = new Connection(redisHosts); + String[] keyArr = (String[]) keys.toArray(new String[keys.size()]); + byte[][] bkeys = new byte[keyArr.length][]; + for (int i = 0; i < bkeys.length; ++i) { + bkeys[i] = keyArr[i].getBytes(StandardCharsets.UTF_8); + } + request = JedisRequest.create(connection, Protocol.Command.MGET, keys); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + ContextHolder.set(true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + ContextHolder.clear(); + scope.close(); + instrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/test/groovy/JedisClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/test/groovy/JedisClientTest.groovy new file mode 100644 index 000000000..18582ded9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-1.4/javaagent/src/test/groovy/JedisClientTest.groovy @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.testcontainers.containers.GenericContainer +import redis.clients.jedis.Jedis +import spock.lang.Shared + +class JedisClientTest extends AgentInstrumentationSpecification { + + private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) + + @Shared + int port + + @Shared + Jedis jedis + + def setupSpec() { + redisServer.start() + port = redisServer.getMappedPort(6379) + jedis = new Jedis("localhost", port) + } + + def cleanupSpec() { + redisServer.stop() +// jedis.close() // not available in the early version we're using. + } + + def setup() { + jedis.flushAll() + clearExportedData() + } + + def "set command"() { + when: + jedis.set("foo", "bar") + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + } + } + + def "get command"() { + when: + jedis.set("foo", "bar") + def value = jedis.get("foo") + + then: + value == "bar" + + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + trace(1, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET foo" + "$SemanticAttributes.DB_OPERATION.key" "GET" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + } + } + + def "command with no arguments"() { + when: + jedis.set("foo", "bar") + def value = jedis.randomKey() + + then: + value == "foo" + + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + trace(1, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "RANDOMKEY" + "$SemanticAttributes.DB_OPERATION.key" "RANDOMKEY" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/jedis-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/jedis-3.0-javaagent.gradle new file mode 100644 index 000000000..a739c8108 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/jedis-3.0-javaagent.gradle @@ -0,0 +1,28 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + fail { + group = "redis.clients" + module = "jedis" + versions = "[,3.0.0)" + } + + pass { + group = "redis.clients" + module = "jedis" + versions = "[3.0.0,)" + } +} + +dependencies { + library "redis.clients:jedis:3.0.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + // ensures jedis-1.4 instrumentation does not load with jedis 3.0+ by failing + // the tests in the event it does. The tests will end up with double spans + testInstrumentation project(':instrumentation:jedis:jedis-1.4:javaagent') + + testLibrary "redis.clients:jedis:3.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisConnectionInstrumentation.java new file mode 100644 index 000000000..0b39ec75c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisConnectionInstrumentation.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jedis.v3_0.JedisSingletons.instrumenter; +import static java.util.Arrays.asList; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.db.RedisCommandUtil; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.clients.jedis.Connection; +import redis.clients.jedis.commands.ProtocolCommand; + +import java.nio.charset.StandardCharsets; + +@SuppressWarnings("SystemOut") +public class JedisConnectionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("redis.clients.jedis.Connection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("sendCommand")) + .and(takesArguments(2)) + .and(takesArgument(0, named("redis.clients.jedis.commands.ProtocolCommand"))) + .and(takesArgument(1, is(byte[][].class))), + this.getClass().getName() + "$SendCommandAdvice"); + // FIXME: This instrumentation only incorporates sending the command, not processing the result. + } + + @SuppressWarnings("unused") + public static class SendCommandAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Connection connection, + @Advice.Argument(0) ProtocolCommand command, + @Advice.Argument(1) byte[][] args, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("startTime") long startTime) { + startTime = System.currentTimeMillis(); + if(RedisCommandUtil.REDIS_EXCLUDE_COMMAND.contains(new String(command.getRaw(), StandardCharsets.UTF_8))){ + return; + } + Context parentContext = currentContext(); + request = JedisRequest.create(connection, command, asList(args)); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("startTime") long startTime) { + if (scope == null) { + return; + } + + scope.close(); + if(request != null){ + if (RedisCommandUtil.skipEnd(new String(request.getCommand().getRaw(), StandardCharsets.UTF_8), throwable, startTime)) { + return; + } + } + instrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisDbAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisDbAttributesExtractor.java new file mode 100644 index 000000000..9d68674e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisDbAttributesExtractor.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; + +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class JedisDbAttributesExtractor extends DbAttributesExtractor { + @Override + protected String system(JedisRequest request) { + return SemanticAttributes.DbSystemValues.REDIS; + } + + @Override + @Nullable + protected String user(JedisRequest request) { + return null; + } + + @Override + protected String name(JedisRequest request) { + return null; + } + + @Override + protected String connectionString(JedisRequest request) { + return null; + } + + @Override + protected String statement(JedisRequest request) { + return request.getStatement(); + } + + @Override + protected String operation(JedisRequest request) { + return request.getOperation(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisInstrumentationModule.java new file mode 100644 index 000000000..d8691875f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; + + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.ArrayList; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JedisInstrumentationModule extends InstrumentationModule { + + public JedisInstrumentationModule() { + super("jedis", "jedis-3.0"); + } + + @Override + public List typeInstrumentations() { + List list = new ArrayList<>(); + list.add(new JedisConnectionInstrumentation()); + return list; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetAttributesExtractor.java new file mode 100644 index 000000000..9cea00077 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetAttributesExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class JedisNetAttributesExtractor extends NetAttributesExtractor { + + @Override + @Nullable + public String transport(JedisRequest request) { + return null; + } + + @Override + public String peerName(JedisRequest request, @Nullable Void unused) { + return request.getConnection().getHost(); + } + + @Override + public Integer peerPort(JedisRequest request, @Nullable Void unused) { + return request.getConnection().getPort(); + } + + @Override + @Nullable + public String peerIp(JedisRequest request, @Nullable Void unused) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisRequest.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisRequest.java new file mode 100644 index 000000000..48bae7c1b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import redis.clients.jedis.Connection; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.commands.ProtocolCommand; + +@AutoValue +public abstract class JedisRequest { + + public static JedisRequest create( + Connection connection, ProtocolCommand command, List args) { + return new AutoValue_JedisRequest(connection, command, args); + } + + public abstract Connection getConnection(); + + public abstract ProtocolCommand getCommand(); + + public abstract List getArgs(); + + public String getOperation() { + ProtocolCommand command = getCommand(); + if (command instanceof Protocol.Command) { + return ((Protocol.Command) command).name(); + } else { + // Protocol.Command is the only implementation in the Jedis lib as of 3.1 but this will save + // us if that changes + return new String(command.getRaw(), StandardCharsets.UTF_8); + } + } + + public String getStatement() { + return RedisCommandSanitizer.sanitize(getOperation(), getArgs()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisSingletons.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisSingletons.java new file mode 100644 index 000000000..b0b75559e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisSingletons.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; + +public final class JedisSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jedis-3.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + DbAttributesExtractor attributesExtractor = + new JedisDbAttributesExtractor(); + SpanNameExtractor spanName = DbSpanNameExtractor.create(attributesExtractor); + JedisNetAttributesExtractor netAttributesExtractor = new JedisNetAttributesExtractor(); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(attributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesExtractor)) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private JedisSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/test/groovy/Jedis30ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/test/groovy/Jedis30ClientTest.groovy new file mode 100644 index 000000000..6c6d51da8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-3.0/javaagent/src/test/groovy/Jedis30ClientTest.groovy @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.testcontainers.containers.GenericContainer +import redis.clients.jedis.Jedis +import spock.lang.Shared + +class Jedis30ClientTest extends AgentInstrumentationSpecification { + + private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) + + @Shared + int port + + @Shared + Jedis jedis + + def setupSpec() { + redisServer.start() + port = redisServer.getMappedPort(6379) + jedis = new Jedis("localhost", port) + } + + def cleanupSpec() { + redisServer.stop() + jedis.close() + } + + def setup() { + jedis.flushAll() + clearExportedData() + } + + def "set command"() { + when: + jedis.set("foo", "bar") + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + } + } + + def "get command"() { + when: + jedis.set("foo", "bar") + def value = jedis.get("foo") + + then: + value == "bar" + + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + trace(1, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET foo" + "$SemanticAttributes.DB_OPERATION.key" "GET" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + } + } + + def "command with no arguments"() { + when: + jedis.set("foo", "bar") + def value = jedis.randomKey() + + then: + value == "foo" + + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + trace(1, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "RANDOMKEY" + "$SemanticAttributes.DB_OPERATION.key" "RANDOMKEY" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/jedis-4.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/jedis-4.0-javaagent.gradle new file mode 100644 index 000000000..7a5c5d8f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/jedis-4.0-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "redis.clients" + module = "jedis" + versions = "[4.0.0-beta1,)" + skip("jedis-3.6.2") + assertInverse = true + } +} + +dependencies { + library "redis.clients:jedis:4.0.0-beta1" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + // ensures jedis-1.4 instrumentation does not load with jedis 3.0+ by failing + // the tests in the event it does. The tests will end up with double spans + testInstrumentation project(':instrumentation:jedis:jedis-1.4:javaagent') + + testLibrary "redis.clients:jedis:3.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisConnectionInstrumentation.java new file mode 100644 index 000000000..25be981b4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisConnectionInstrumentation.java @@ -0,0 +1,152 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jedis.v4_0.connect.JedisConnectionRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.commands.ProtocolCommand; + +import java.net.Socket; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.jedis.v4_0.JedisSingletons.instrumenter; +import static io.opentelemetry.javaagent.instrumentation.jedis.v4_0.connect.JedisConnectSingletons.connectInstrumenter; +import static java.util.Arrays.asList; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public class JedisConnectionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("redis.clients.jedis.Connection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("sendCommand")) + .and(takesArguments(1)) + .and(takesArgument(0, named("redis.clients.jedis.CommandArguments"))), + this.getClass().getName() + "$SendCommand2Advice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("sendCommand")) + .and(takesArguments(2)) + .and(takesArgument(0, named("redis.clients.jedis.commands.ProtocolCommand"))) + .and(takesArgument(1, is(byte[][].class))), + this.getClass().getName() + "$SendCommandAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("connect")), + this.getClass().getName() + "$ConnectAdvice"); + } + + @SuppressWarnings("unused") + public static class SendCommandAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) ProtocolCommand command, + @Advice.Argument(1) byte[][] args, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = JedisRequest.create(command, asList(args)); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.FieldValue("socket") Socket socket, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + request.setSocket(socket); + + scope.close(); + JedisRequestContext.endIfNotAttached(instrumenter(), context, request, throwable); + } + } + + @SuppressWarnings("unused") + public static class SendCommand2Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) CommandArguments command, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = JedisRequest.create(command); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.FieldValue("socket") Socket socket, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelJedisRequest") JedisRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + request.setSocket(socket); + + scope.close(); + JedisRequestContext.endIfNotAttached(instrumenter(), context, request, throwable); + } + } + + public static class ConnectAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.FieldValue("socket") Socket socket, + @Advice.Thrown Throwable throwable) { + // 记录jedis connect异常trace + if (throwable == null) { + return; + } + Context parentContext = currentContext(); + JedisConnectionRequest request = JedisConnectionRequest.create(); + request.setSocket(socket); + Context context = connectInstrumenter().start(parentContext, request); + connectInstrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisDbAttributesGetter.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisDbAttributesGetter.java new file mode 100644 index 000000000..1620d3cb4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisDbAttributesGetter.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; + +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +import javax.annotation.Nullable; + +final class JedisDbAttributesGetter extends DbAttributesExtractor { + + @Override + public String system(JedisRequest request) { + return SemanticAttributes.DbSystemValues.REDIS; + } + + @Override + @Nullable + public String user(JedisRequest request) { + return null; + } + + @Override + public String name(JedisRequest request) { + return null; + } + + @Override + public String connectionString(JedisRequest request) { + return null; + } + + @Override + public String statement(JedisRequest request) { + return request.getStatement(); + } + + @Override + public String operation(JedisRequest request) { + return request.getOperation(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisInstrumentation.java new file mode 100644 index 000000000..94f355e1c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; + +public class JedisInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return namedOneOf("redis.clients.jedis.Jedis", "redis.clients.jedis.UnifiedJedis"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(not(isStatic())) + .and( + not( + namedOneOf( + "close", + "setDataSource", + "getDB", + "isConnected", + "connect", + "resetState", + "getClient", + "disconnect", + "getConnection", + "isConnected", + "isBroken", + "toString"))), + this.getClass().getName() + "$JedisMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class JedisMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static JedisRequestContext onEnter() { + return JedisRequestContext.attach(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter JedisRequestContext requestContext) { + if (requestContext != null) { + requestContext.detachAndEnd(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisInstrumentationModule.java new file mode 100644 index 000000000..a7cb69068 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisInstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import net.bytebuddy.matcher.ElementMatcher; + +import java.util.List; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; + +@AutoService(InstrumentationModule.class) +public class JedisInstrumentationModule extends InstrumentationModule { + + public JedisInstrumentationModule() { + super("jedis", "jedis-4.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("redis.clients.jedis.CommandArguments"); + } + + @Override + public List typeInstrumentations() { + return asList(new JedisConnectionInstrumentation(), new JedisInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetAttributesGetter.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetAttributesGetter.java new file mode 100644 index 000000000..f2fa6c508 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetAttributesGetter.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; + +import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import javax.annotation.Nullable; + +final class JedisNetAttributesGetter + extends InetSocketAddressNetAttributesExtractor { + + @Override + @Nullable + public InetSocketAddress getAddress(JedisRequest jedisRequest, @Nullable Void unused) { + SocketAddress socketAddress = jedisRequest.getRemoteSocketAddress(); + if (socketAddress != null && socketAddress instanceof InetSocketAddress) { + return (InetSocketAddress) socketAddress; + } + return null; + } + + @Override + public String transport(JedisRequest jedisRequest) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequest.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequest.java new file mode 100644 index 000000000..098094259 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequest.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.args.Rawable; +import redis.clients.jedis.commands.ProtocolCommand; + +@AutoValue +public abstract class JedisRequest { + + public static JedisRequest create(ProtocolCommand command, List args) { + return new AutoValue_JedisRequest(command, args); + } + + public static JedisRequest create(CommandArguments commandArguments) { + ProtocolCommand command = commandArguments.getCommand(); + List arguments = new ArrayList<>(); + boolean first = true; + for (Rawable rawable : commandArguments) { + if (first) { + first = false; + continue; + } + arguments.add(rawable.getRaw()); + } + return create(command, arguments); + } + + public abstract ProtocolCommand getCommand(); + + public abstract List getArgs(); + + public String getOperation() { + ProtocolCommand command = getCommand(); + if (command instanceof Protocol.Command) { + return ((Protocol.Command) command).name(); + } else { + // Protocol.Command is the only implementation in the Jedis lib as of 3.1 but this will save + // us if that changes + return new String(command.getRaw(), StandardCharsets.UTF_8); + } + } + + public String getStatement() { + return RedisCommandSanitizer.sanitize(getOperation(), getArgs()); + } + + private SocketAddress remoteSocketAddress; + + public void setSocket(Socket socket) { + if (socket != null) { + remoteSocketAddress = socket.getRemoteSocketAddress(); + } + } + + public SocketAddress getRemoteSocketAddress() { + return remoteSocketAddress; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequestContext.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequestContext.java new file mode 100644 index 000000000..a8c01bcda --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequestContext.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +public final class JedisRequestContext { + private static final ThreadLocal> contextThreadLocal = new ThreadLocal<>(); + + private Instrumenter instrumenter; + private T request; + private Context context; + private Throwable throwable; + + private JedisRequestContext() {} + + public static JedisRequestContext attach() { + JedisRequestContext requestContext = current(); + // if there already is an active request context don't start a new one + if (requestContext != null) { + return null; + } + requestContext = new JedisRequestContext<>(); + contextThreadLocal.set(requestContext); + return requestContext; + } + + public void detachAndEnd() { + contextThreadLocal.remove(); + if (request != null) { + endSpan(instrumenter, context, request, throwable); + } + } + + /** + * Schedule ending of instrumented operation when current {@link JedisRequestContext} is closed. + */ + public static void endIfNotAttached( + Instrumenter instrumenter, Context context, T request, Throwable throwable) { + JedisRequestContext requestContext = current(); + if (requestContext == null || requestContext.request != null) { + // end the span immediately if we are already tracking a request + endSpan(instrumenter, context, request, throwable); + } else { + requestContext.instrumenter = instrumenter; + requestContext.context = context; + requestContext.request = request; + requestContext.throwable = throwable; + } + } + + @SuppressWarnings("unchecked") + private static JedisRequestContext current() { + return (JedisRequestContext) contextThreadLocal.get(); + } + + private static void endSpan( + Instrumenter instrumenter, Context context, T request, Throwable throwable) { + instrumenter.end(context, request, null, throwable); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisSingletons.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisSingletons.java new file mode 100644 index 000000000..456f969f6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisSingletons.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; + +public final class JedisSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jedis-4.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + DbAttributesExtractor dbAttributesGetter = + new JedisDbAttributesGetter(); + JedisNetAttributesGetter netAttributesGetter = new JedisNetAttributesGetter(); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), + INSTRUMENTATION_NAME, + DbSpanNameExtractor.create(dbAttributesGetter)) + .addAttributesExtractor(dbAttributesGetter) + .addAttributesExtractor(netAttributesGetter) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(netAttributesGetter)) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private JedisSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectDbAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectDbAttributesExtractor.java new file mode 100644 index 000000000..036336c95 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectDbAttributesExtractor.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0.connect; + +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class JedisConnectDbAttributesExtractor extends DbAttributesExtractor { + @Override + protected String system(JedisConnectionRequest request) { + return SemanticAttributes.DbSystemValues.REDIS; + } + + @Override + @Nullable + protected String user(JedisConnectionRequest request) { + return null; + } + + @Override + protected String name(JedisConnectionRequest request) { + return null; + } + + @Override + protected String connectionString(JedisConnectionRequest request) { + return null; + } + + @Override + protected String statement(JedisConnectionRequest request) { + return "connect"; + } + + @Override + protected String operation(JedisConnectionRequest request) { + return "connect"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectSingletons.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectSingletons.java new file mode 100644 index 000000000..5a40e0e67 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectSingletons.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0.connect; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; + +public final class JedisConnectSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jedis-4.0"; + + private static final Instrumenter CONNECT_INSTRUMENTER; + + static { + DbAttributesExtractor connectAttributesExtractor = + new JedisConnectDbAttributesExtractor(); + SpanNameExtractor connectSpanName = request -> "connect"; + JedisConnectionNetAttributesExtractor connectNetAttributesExtractor = new JedisConnectionNetAttributesExtractor(); + + CONNECT_INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, connectSpanName) + .addAttributesExtractor(connectAttributesExtractor) + .addAttributesExtractor(connectNetAttributesExtractor) + .addAttributesExtractor(PeerServiceAttributesExtractor.create(connectNetAttributesExtractor)) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter connectInstrumenter() { + return CONNECT_INSTRUMENTER; + } + + private JedisConnectSingletons() { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectionNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectionNetAttributesExtractor.java new file mode 100644 index 000000000..2277121f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectionNetAttributesExtractor.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0.connect; + +import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +import javax.annotation.Nullable; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public final class JedisConnectionNetAttributesExtractor extends InetSocketAddressNetAttributesExtractor { + + @Override + @javax.annotation.Nullable + public InetSocketAddress getAddress(JedisConnectionRequest jedisRequest, @Nullable Void unused) { + SocketAddress socketAddress = jedisRequest.getRemoteSocketAddress(); + if (socketAddress != null && socketAddress instanceof InetSocketAddress) { + return (InetSocketAddress) socketAddress; + } + return null; + } + + @Override + public String transport(JedisConnectionRequest jedisRequest) { + return SemanticAttributes.NetTransportValues.IP_TCP; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectionRequest.java b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectionRequest.java new file mode 100644 index 000000000..8c7fe0c70 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/connect/JedisConnectionRequest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v4_0.connect; + +import com.google.auto.value.AutoValue; + +import java.net.Socket; +import java.net.SocketAddress; + +@AutoValue +public abstract class JedisConnectionRequest { + + public static JedisConnectionRequest create() { + return new AutoValue_JedisConnectionRequest(); + } + + private SocketAddress remoteSocketAddress; + + public void setSocket(Socket socket) { + if (socket != null) { + remoteSocketAddress = socket.getRemoteSocketAddress(); + } + } + + public SocketAddress getRemoteSocketAddress() { + return remoteSocketAddress; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/README.md b/opentelemetry-java-instrumentation/instrumentation/jetty/README.md new file mode 100644 index 000000000..afbb8679d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/README.md @@ -0,0 +1,11 @@ +# Instrumentation for Jetty request handlers + +Jetty support is divided into the following sub-modules: +- `jetty-common:javaagent` contains common type instrumentation and advice helper classes used by + the `javaagent` modules of all supported Jetty versions +- `jetty-8.0:javaagent` applies Jetty request handler instrumentation for versions `[8, 11)` +- `jetty-11.0:javaagent` applies Jetty request handler instrumentation for versions `[11,)` + +Instrumentations in `jetty-8.0` and `jetty-11.0` are mutually exclusive, this is guaranteed by the +instrumentation requiring parameters with types from packages `javax.servlet` or `jakarta.servlet` +respectively. diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/jetty-11.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/jetty-11.0-javaagent.gradle new file mode 100644 index 000000000..bde3d2401 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/jetty-11.0-javaagent.gradle @@ -0,0 +1,26 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.eclipse.jetty" + module = 'jetty-server' + versions = "[11,)" + } +} + +dependencies { + library "org.eclipse.jetty:jetty-server:11.0.0" + implementation project(':instrumentation:servlet:servlet-5.0:javaagent') + implementation project(':instrumentation:jetty:jetty-common:javaagent') + + // Don't want to conflict with jetty from the test server. + testImplementation(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + + testLibrary "org.eclipse.jetty:jetty-servlet:11.0.0" +} + +otelJava { + minJavaVersionSupported = JavaVersion.VERSION_11 +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11HandlerAdvice.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11HandlerAdvice.java new file mode 100644 index 000000000..ef619485f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11HandlerAdvice.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v11_0; + +import static io.opentelemetry.javaagent.instrumentation.jetty.v11_0.Jetty11HttpServerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.jetty.common.JettyHandlerAdviceHelper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class Jetty11HandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Object source, + @Advice.Argument(2) HttpServletRequest request, + @Advice.Argument(3) HttpServletResponse response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context attachedContext = tracer().getServerContext(request); + if (attachedContext != null) { + // We are inside nested handler, don't create new span + return; + } + + context = tracer().startServerSpan(request); + scope = context.makeCurrent(); + + // Must be set here since Jetty handlers can use startAsync outside of servlet scope. + tracer().setAsyncListenerResponse(request, response); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(2) HttpServletRequest request, + @Advice.Argument(3) HttpServletResponse response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + JettyHandlerAdviceHelper.stopSpan(tracer(), request, response, throwable, context, scope); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11HttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11HttpServerTracer.java new file mode 100644 index 000000000..678d0fc87 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11HttpServerTracer.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v11_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.servlet.jakarta.v5_0.JakartaServletHttpServerTracer; +import jakarta.servlet.http.HttpServletRequest; + +public class Jetty11HttpServerTracer extends JakartaServletHttpServerTracer { + private static final Jetty11HttpServerTracer TRACER = new Jetty11HttpServerTracer(); + + public static Jetty11HttpServerTracer tracer() { + return TRACER; + } + + public Context startServerSpan(HttpServletRequest request) { + return startSpan(request, "HTTP " + request.getMethod(), /* servlet= */ false); + } + + @Override + protected Context customizeContext(Context context, HttpServletRequest request) { + context = super.customizeContext(context, request); + return AppServerBridge.init(context, /* shouldRecordException= */ false); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jetty-11.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11InstrumentationModule.java new file mode 100644 index 000000000..676cde85c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11InstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v11_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.jetty.common.JettyHandlerInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Jetty11InstrumentationModule extends InstrumentationModule { + + public Jetty11InstrumentationModule() { + super("jetty", "jetty-11.0"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList( + new JettyHandlerInstrumentation( + "jakarta.servlet", + Jetty11InstrumentationModule.class.getPackage().getName() + ".Jetty11HandlerAdvice")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/test/groovy/JettyHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/test/groovy/JettyHandlerTest.groovy new file mode 100644 index 000000000..242fb1b5f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-11.0/javaagent/src/test/groovy/JettyHandlerTest.groovy @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import jakarta.servlet.DispatcherType +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.eclipse.jetty.server.Request +import org.eclipse.jetty.server.Response +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.handler.AbstractHandler +import org.eclipse.jetty.server.handler.ErrorHandler +import spock.lang.Shared + +class JettyHandlerTest extends HttpServerTest implements AgentTestTrait { + + @Shared + ErrorHandler errorHandler = new ErrorHandler() { + @Override + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { + Throwable th = (Throwable) request.getAttribute("jakarta.servlet.error.exception") + message = th ? th.message : message + if (message) { + writer.write(message) + } + } + } + + @Shared + TestHandler testHandler = new TestHandler() + + @Override + Server startServer(int port) { + def server = new Server(port) + server.setHandler(handler()) + server.addBean(errorHandler) + server.start() + return server + } + + AbstractHandler handler() { + testHandler + } + + @Override + void stopServer(Server server) { + server.stop() + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == ERROR + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case REDIRECT: + redirectSpan(trace, index, parent) + break + case ERROR: + sendErrorSpan(trace, index, parent) + break + } + } + + static void handleRequest(Request request, HttpServletResponse response) { + ServerEndpoint endpoint = ServerEndpoint.forPath(request.requestURI) + controller(endpoint) { + response.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + response.status = endpoint.status + response.writer.print(endpoint.body) + break + case QUERY_PARAM: + response.status = endpoint.status + response.writer.print(request.queryString) + break + case REDIRECT: + response.sendRedirect(endpoint.body) + break + case ERROR: + response.sendError(endpoint.status, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + default: + response.status = NOT_FOUND.status + response.writer.print(NOT_FOUND.body) + break + } + } + } + + static class TestHandler extends AbstractHandler { + @Override + void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + //This line here is to verify that we don't break Jetty if it wants to cast to implementation class + //See https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1096 + Response jettyResponse = response as Response + if (baseRequest.dispatcherType != DispatcherType.ERROR) { + handleRequest(baseRequest, jettyResponse) + baseRequest.handled = true + } else { + errorHandler.handle(target, baseRequest, request, response) + } + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + "HTTP GET" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/jetty-8.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/jetty-8.0-javaagent.gradle new file mode 100644 index 000000000..7ed4d5ecb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/jetty-8.0-javaagent.gradle @@ -0,0 +1,32 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.eclipse.jetty" + module = 'jetty-server' + // Jetty 11+ is covered by jetty-11.0 module + versions = "[8.0.0.v20110901,11)" + assertInverse = true + } +} + +dependencies { + library "org.eclipse.jetty:jetty-server:8.0.0.v20110901" + implementation project(':instrumentation:servlet:servlet-3.0:javaagent') + implementation project(':instrumentation:jetty:jetty-common:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + // Don't want to conflict with jetty from the test server. + testImplementation(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + + testLibrary "org.eclipse.jetty:jetty-servlet:8.0.0.v20110901" + testLibrary "org.eclipse.jetty:jetty-continuation:8.0.0.v20110901" + + // Jetty 10 seems to refuse to run on java8. + // TODO: we need to setup separate test for Jetty 10 when that is released. + latestDepTestLibrary "org.eclipse.jetty:jetty-server:9.+" + latestDepTestLibrary "org.eclipse.jetty:jetty-servlet:9.+" + latestDepTestLibrary "org.eclipse.jetty:jetty-continuation:9.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8HandlerAdvice.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8HandlerAdvice.java new file mode 100644 index 000000000..42d60cf08 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8HandlerAdvice.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; + +import static io.opentelemetry.javaagent.instrumentation.jetty.v8_0.Jetty8HttpServerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.jetty.common.JettyHandlerAdviceHelper; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class Jetty8HandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Object source, + @Advice.Argument(2) HttpServletRequest request, + @Advice.Argument(3) HttpServletResponse response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context attachedContext = tracer().getServerContext(request); + if (attachedContext != null) { + // We are inside nested handler, don't create new span + return; + } + + context = tracer().startServerSpan(request); + scope = context.makeCurrent(); + + // Must be set here since Jetty handlers can use startAsync outside of servlet scope. + tracer().setAsyncListenerResponse(request, response); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(2) HttpServletRequest request, + @Advice.Argument(3) HttpServletResponse response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + JettyHandlerAdviceHelper.stopSpan(tracer(), request, response, throwable, context, scope); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8HttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8HttpServerTracer.java new file mode 100644 index 000000000..8595d0484 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8HttpServerTracer.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.servlet.v3_0.Servlet3HttpServerTracer; +import javax.servlet.http.HttpServletRequest; + +public class Jetty8HttpServerTracer extends Servlet3HttpServerTracer { + private static final Jetty8HttpServerTracer TRACER = new Jetty8HttpServerTracer(); + + public static Jetty8HttpServerTracer tracer() { + return TRACER; + } + + public Context startServerSpan(HttpServletRequest request) { + return startSpan(request, "HTTP " + request.getMethod(), /* servlet= */ false); + } + + @Override + protected Context customizeContext(Context context, HttpServletRequest request) { + context = super.customizeContext(context, request); + return AppServerBridge.init(context, /* shouldRecordException= */ false); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jetty-8.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8InstrumentationModule.java new file mode 100644 index 000000000..88b9106a9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8InstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.jetty.common.JettyHandlerInstrumentation; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Jetty8InstrumentationModule extends InstrumentationModule { + + public Jetty8InstrumentationModule() { + super("jetty", "jetty-8.0"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new JettyHandlerInstrumentation( + "javax.servlet", + Jetty8InstrumentationModule.class.getPackage().getName() + ".Jetty8HandlerAdvice"), + new JettyQueuedThreadPoolInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyQueuedThreadPoolInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyQueuedThreadPoolInstrumentation.java new file mode 100644 index 000000000..2e83ff812 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyQueuedThreadPoolInstrumentation.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.RunnableWrapper; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JettyQueuedThreadPoolInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.eclipse.jetty.util.thread.QueuedThreadPool"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("dispatch").and(takesArguments(1)).and(takesArgument(0, Runnable.class)), + this.getClass().getName() + "$DispatchAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterJobSubmit( + @Advice.Argument(value = 0, readOnly = false) Runnable task) { + Runnable newTask = RunnableWrapper.wrapIfNeeded(task); + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(newTask)) { + task = newTask; + ContextStore contextStore = + InstrumentationContext.get(Runnable.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, newTask, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitJobSubmit( + @Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/package-info.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/package-info.java new file mode 100644 index 000000000..edd7e09db --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/package-info.java @@ -0,0 +1,16 @@ +/** + * This module provides support for creating server spans for Jetty Handlers. + * + *

It is possible to write web application running on Eclipse Jetty without actually writing any servlets. + * Instead one can use Jetty + * Handlers. + * + *

As instrumentation points differ between servlet instrumentations and this one, this module + * has its own {@code JettyHandlerInstrumentation} and {@code JettyHandlerAdvice}. But this is the + * only difference between two instrumentations, thus {@link + * io.opentelemetry.javaagent.instrumentation.jetty.v8_0.Jetty8HttpServerTracer} is a very thin + * subclass of {@link io.opentelemetry.instrumentation.servlet.v3_0.Servlet3HttpServerTracer}. + */ +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JavaAsyncChild.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JavaAsyncChild.java new file mode 100644 index 000000000..1eedd3454 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JavaAsyncChild.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.atomic.AtomicBoolean; + +public class JavaAsyncChild extends ForkJoinTask implements Runnable, Callable { + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + private final AtomicBoolean blockThread; + private final boolean doTraceableWork; + private final CountDownLatch latch = new CountDownLatch(1); + + public JavaAsyncChild() { + this(/* doTraceableWork= */ true, /* blockThread= */ false); + } + + public JavaAsyncChild(boolean doTraceableWork, boolean blockThread) { + this.doTraceableWork = doTraceableWork; + this.blockThread = new AtomicBoolean(blockThread); + } + + @Override + public Object getRawResult() { + return null; + } + + @Override + protected void setRawResult(Object value) {} + + @Override + protected boolean exec() { + runImpl(); + return true; + } + + public void unblock() { + blockThread.set(false); + } + + @Override + public void run() { + runImpl(); + } + + @Override + public Object call() { + runImpl(); + return null; + } + + public void waitForCompletion() throws InterruptedException { + latch.await(); + } + + private void runImpl() { + while (blockThread.get()) { + // busy-wait to block thread + } + if (doTraceableWork) { + asyncChild(); + } + latch.countDown(); + } + + private static void asyncChild() { + tracer.spanBuilder("asyncChild").startSpan().end(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyContinuationHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyContinuationHandlerTest.groovy new file mode 100644 index 000000000..25ecb917f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyContinuationHandlerTest.groovy @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import org.eclipse.jetty.continuation.Continuation +import org.eclipse.jetty.continuation.ContinuationSupport +import org.eclipse.jetty.server.Request +import org.eclipse.jetty.server.handler.AbstractHandler + +// FIXME: We don't currently handle jetty continuations properly (at all). +abstract class JettyContinuationHandlerTest extends JettyHandlerTest { + + @Override + AbstractHandler handler() { + ContinuationTestHandler.INSTANCE + } + + static class ContinuationTestHandler extends AbstractHandler { + static final ContinuationTestHandler INSTANCE = new ContinuationTestHandler() + final ExecutorService executorService = Executors.newSingleThreadExecutor() + + @Override + void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + final Continuation continuation = ContinuationSupport.getContinuation(request) + if (continuation.initial) { + continuation.suspend() + executorService.execute { + continuation.resume() + } + } else { + handleRequest(baseRequest, response) + } + baseRequest.handled = true + } + } + +// // This server seems to generate a TEST_SPAN twice... once for the initial request, and once for the continuation. +// void cleanAndAssertTraces( +// final int size, +// @ClosureParams(value = SimpleType, options = "io.opentelemetry.instrumentation.test.asserts.ListWriterAssert") +// @DelegatesTo(value = ListWriterAssert, strategy = Closure.DELEGATE_FIRST) +// final Closure spec) { +// +// // If this is failing, make sure HttpServerTestAdvice is applied correctly. +// testWriter.waitForTraces(size * 3) +// // testWriter is a CopyOnWriteArrayList, which doesn't support remove() +// def toRemove = testWriter.findAll { +// it.size() == 1 && it.get(0).name == "TEST_SPAN" +// } +// toRemove.each { +// assertTrace(it, 1) { +// basicSpan(it, 0, "TEST_SPAN") +// } +// } +// assert toRemove.size() == size * 2 +// testWriter.removeAll(toRemove) +// +// assertTraces(size, spec) +// } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyHandlerTest.groovy new file mode 100644 index 000000000..a3fc27e8a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyHandlerTest.groovy @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import javax.servlet.DispatcherType +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import org.eclipse.jetty.server.Request +import org.eclipse.jetty.server.Response +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.handler.AbstractHandler +import org.eclipse.jetty.server.handler.ErrorHandler +import spock.lang.Shared + +class JettyHandlerTest extends HttpServerTest implements AgentTestTrait { + + @Shared + ErrorHandler errorHandler = new ErrorHandler() { + @Override + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { + Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception") + message = th ? th.message : message + if (message) { + writer.write(message) + } + } + } + + @Shared + TestHandler testHandler = new TestHandler() + + @Override + Server startServer(int port) { + def server = new Server(port) + server.setHandler(handler()) + server.addBean(errorHandler) + server.start() + return server + } + + AbstractHandler handler() { + testHandler + } + + @Override + void stopServer(Server server) { + server.stop() + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == ERROR + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case REDIRECT: + redirectSpan(trace, index, parent) + break + case ERROR: + sendErrorSpan(trace, index, parent) + break + } + } + + static void handleRequest(Request request, HttpServletResponse response) { + ServerEndpoint endpoint = ServerEndpoint.forPath(request.requestURI) + controller(endpoint) { + response.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + response.status = endpoint.status + response.writer.print(endpoint.body) + break + case QUERY_PARAM: + response.status = endpoint.status + response.writer.print(request.queryString) + break + case REDIRECT: + response.sendRedirect(endpoint.body) + break + case ERROR: + response.sendError(endpoint.status, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + default: + response.status = NOT_FOUND.status + response.writer.print(NOT_FOUND.body) + break + } + } + } + + static class TestHandler extends AbstractHandler { + @Override + void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + //This line here is to verify that we don't break Jetty if it wants to cast to implementation class + //See https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1096 + Response jettyResponse = response as Response + if (baseRequest.dispatcherType != DispatcherType.ERROR) { + handleRequest(baseRequest, jettyResponse) + baseRequest.handled = true + } else { + errorHandler.handle(target, baseRequest, baseRequest, response) + } + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + "HTTP GET" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/QueuedThreadPoolTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/QueuedThreadPoolTest.groovy new file mode 100644 index 000000000..331e51319 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/QueuedThreadPoolTest.groovy @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.junit.Assume.assumeTrue + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.javaagent.instrumentation.jetty.JavaLambdaMaker +import org.eclipse.jetty.util.thread.QueuedThreadPool + +class QueuedThreadPoolTest extends AgentInstrumentationSpecification { + + def "QueueThreadPool 'dispatch' propagates"() { + setup: + def pool = new QueuedThreadPool() + // run test only if QueuedThreadPool has dispatch method + // dispatch method was removed in jetty 9.1 + assumeTrue(pool.metaClass.getMetaMethod("dispatch", Runnable) != null) + pool.start() + + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + // this child will have a span + def child1 = new JavaAsyncChild() + // this child won't + def child2 = new JavaAsyncChild(false, false) + pool.dispatch(child1) + pool.dispatch(child2) + child1.waitForCompletion() + child2.waitForCompletion() + } + } + }.run() + + expect: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "asyncChild", span(0)) + } + } + + cleanup: + pool.stop() + } + + def "QueueThreadPool 'dispatch' propagates lambda"() { + setup: + def pool = new QueuedThreadPool() + // run test only if QueuedThreadPool has dispatch method + // dispatch method was removed in jetty 9.1 + assumeTrue(pool.metaClass.getMetaMethod("dispatch", Runnable) != null) + pool.start() + + JavaAsyncChild child = new JavaAsyncChild(true, true) + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + pool.dispatch(JavaLambdaMaker.lambda(child)) + } + } + }.run() + // We block in child to make sure spans close in predictable order + child.unblock() + child.waitForCompletion() + + expect: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "asyncChild", span(0)) + } + } + + cleanup: + pool.stop() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/JavaLambdaMaker.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/JavaLambdaMaker.java new file mode 100644 index 000000000..6ef578e21 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/JavaLambdaMaker.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty; + +public class JavaLambdaMaker { + + @SuppressWarnings("FunctionalExpressionCanBeFolded") + public static Runnable lambda(Runnable runnable) { + return runnable::run; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/jetty-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/jetty-common-javaagent.gradle new file mode 100644 index 000000000..bfc2052cd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/jetty-common-javaagent.gradle @@ -0,0 +1,6 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + api(project(':instrumentation:servlet:servlet-common:library')) + implementation(project(':instrumentation:servlet:servlet-common:javaagent')) +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/common/JettyHandlerAdviceHelper.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/common/JettyHandlerAdviceHelper.java new file mode 100644 index 000000000..0af191a78 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/common/JettyHandlerAdviceHelper.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.common; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.servlet.ServletHttpServerTracer; +import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterAdviceHelper; + +public class JettyHandlerAdviceHelper { + /** Shared method exit implementation for Jetty handler advices. */ + public static void stopSpan( + ServletHttpServerTracer tracer, + REQUEST request, + RESPONSE response, + Throwable throwable, + Context context, + Scope scope) { + if (scope == null) { + return; + } + scope.close(); + + if (context == null) { + // an existing span was found + return; + } + + tracer.setPrincipal(context, request); + + // throwable is read-only, copy it to a new local that can be modified + Throwable exception = throwable; + if (exception == null) { + // on jetty versions before 9.4 exceptions from servlet don't propagate to this method + // check from request whether a throwable has been stored there + exception = tracer.errorException(request); + } + if (exception != null) { + tracer.endExceptionally(context, exception, response); + return; + } + + if (ServletAndFilterAdviceHelper.mustEndOnHandlerMethodExit(tracer, request)) { + tracer.end(context, response); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/common/JettyHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/common/JettyHandlerInstrumentation.java new file mode 100644 index 000000000..b5a0e19d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jetty/jetty-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/common/JettyHandlerInstrumentation.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.common; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JettyHandlerInstrumentation implements TypeInstrumentation { + private final String servletBasePackage; + private final String adviceClassName; + + public JettyHandlerInstrumentation(String servletBasePackage, String adviceClassName) { + this.servletBasePackage = servletBasePackage; + this.adviceClassName = adviceClassName; + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.eclipse.jetty.server.Handler"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.eclipse.jetty.server.Handler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handle") + .and(takesArgument(0, String.class)) + .and(takesArgument(1, named("org.eclipse.jetty.server.Request"))) + .and(takesArgument(2, named(servletBasePackage + ".http.HttpServletRequest"))) + .and(takesArgument(3, named(servletBasePackage + ".http.HttpServletResponse"))) + .and(isPublic()), + adviceClassName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent-unit-tests/jms-1.1-javaagent-unit-tests.gradle b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent-unit-tests/jms-1.1-javaagent-unit-tests.gradle new file mode 100644 index 000000000..4ced74b6c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent-unit-tests/jms-1.1-javaagent-unit-tests.gradle @@ -0,0 +1,10 @@ +apply plugin: "otel.java-conventions" + +dependencies { + testImplementation "javax.jms:jms-api:1.1-rev-1" + testImplementation project(':instrumentation:jms-1.1:javaagent') + testImplementation project(':instrumentation-api') + + testImplementation "org.mockito:mockito-core" + testImplementation "org.mockito:mockito-junit-jupiter" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestinationTest.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestinationTest.java new file mode 100644 index 000000000..c0bd6c22d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestinationTest.java @@ -0,0 +1,167 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import static io.opentelemetry.javaagent.instrumentation.jms.MessageWithDestination.TIBCO_TMP_PREFIX; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.BDDMockito.given; + +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; +import java.time.Instant; +import java.util.stream.Stream; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Queue; +import javax.jms.TemporaryQueue; +import javax.jms.TemporaryTopic; +import javax.jms.Topic; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MessageWithDestinationTest { + private static final Instant START_TIME = Instant.ofEpochSecond(42); + + @Mock Message message; + @Mock Topic topic; + @Mock TemporaryTopic temporaryTopic; + @Mock Queue queue; + @Mock TemporaryQueue temporaryQueue; + @Mock Destination destination; + + @Test + void shouldCreateMessageWithUnknownDestination() throws JMSException { + // given + given(message.getJMSDestination()).willReturn(destination); + + // when + MessageWithDestination result = + MessageWithDestination.create(message, MessageOperation.SEND, null, START_TIME); + + // then + assertMessage( + MessageOperation.SEND, + "unknown", + "unknown", + /* expectedTemporary= */ false, + START_TIME, + result); + } + + @Test + void shouldUseFallbackDestinationToCreateMessage() throws JMSException { + // given + given(message.getJMSDestination()).willThrow(JMSException.class); + + // when + MessageWithDestination result = + MessageWithDestination.create(message, MessageOperation.SEND, destination, START_TIME); + + // then + assertMessage( + MessageOperation.SEND, + "unknown", + "unknown", + /* expectedTemporary= */ false, + START_TIME, + result); + } + + @ParameterizedTest + @MethodSource("destinations") + void shouldCreateMessageWithQueue( + String queueName, + boolean useTemporaryDestination, + String expectedDestinationName, + boolean expectedTemporary) + throws JMSException { + // given + Queue queue = useTemporaryDestination ? this.temporaryQueue : this.queue; + + given(message.getJMSDestination()).willReturn(queue); + if (queueName == null) { + given(queue.getQueueName()).willThrow(JMSException.class); + } else { + given(queue.getQueueName()).willReturn(queueName); + } + + // when + MessageWithDestination result = + MessageWithDestination.create(message, MessageOperation.RECEIVE, null); + + // then + assertMessage( + MessageOperation.RECEIVE, + "queue", + expectedDestinationName, + expectedTemporary, + null, + result); + } + + @ParameterizedTest + @MethodSource("destinations") + void shouldCreateMessageWithTopic( + String topicName, + boolean useTemporaryDestination, + String expectedDestinationName, + boolean expectedTemporary) + throws JMSException { + // given + Topic topic = useTemporaryDestination ? this.temporaryTopic : this.topic; + + given(message.getJMSDestination()).willReturn(topic); + if (topicName == null) { + given(topic.getTopicName()).willThrow(JMSException.class); + } else { + given(topic.getTopicName()).willReturn(topicName); + } + + // when + MessageWithDestination result = + MessageWithDestination.create(message, MessageOperation.RECEIVE, null); + + // then + assertMessage( + MessageOperation.RECEIVE, + "topic", + expectedDestinationName, + expectedTemporary, + null, + result); + } + + static Stream destinations() { + return Stream.of( + Arguments.of("destination", false, "destination", false), + Arguments.of(null, false, "unknown", false), + Arguments.of(TIBCO_TMP_PREFIX + "dest", false, TIBCO_TMP_PREFIX + "dest", true), + Arguments.of("destination", true, "destination", true)); + } + + private void assertMessage( + MessageOperation expectedMessageOperation, + String expectedDestinationKind, + String expectedDestinationName, + boolean expectedTemporary, + Instant expectedStartTime, + MessageWithDestination actual) { + + assertSame(message, actual.getMessage()); + assertSame(expectedMessageOperation, actual.getMessageOperation()); + assertEquals(expectedDestinationKind, actual.getDestinationKind()); + assertEquals(expectedDestinationName, actual.getDestinationName()); + assertEquals(expectedTemporary, actual.isTemporaryDestination()); + assertEquals(expectedStartTime, actual.getStartTime()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/jms-1.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/jms-1.1-javaagent.gradle new file mode 100644 index 000000000..cb6d5f6c1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/jms-1.1-javaagent.gradle @@ -0,0 +1,42 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +muzzle { + pass { + group = "javax.jms" + module = "jms-api" + versions = "(,)" + } + pass { + group = "javax.jms" + module = "javax.jms-api" + versions = "(,)" + } +} + +testSets { + jms2Test { + filter { + // this is needed because "test.dependsOn jms2Test", and so without this, + // running a single test in the default test set will fail + setFailOnNoMatchingTests(false) + } + } +} + +test.dependsOn jms2Test +dependencies { + compileOnly "javax.jms:jms-api:1.1-rev-1" + + testImplementation "javax.annotation:javax.annotation-api:1.3.2" + testImplementation("org.springframework.boot:spring-boot-starter-activemq:${versions["org.springframework.boot"]}") + testImplementation("org.springframework.boot:spring-boot-starter-test:${versions["org.springframework.boot"]}") { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + + jms2TestImplementation "org.hornetq:hornetq-jms-client:2.4.7.Final" + jms2TestImplementation("org.hornetq:hornetq-jms-server:2.4.7.Final") { + // this doesn't exist in maven central, and doesn't seem to be needed anyways + exclude group: 'org.jboss.naming', module: 'jnpserver' + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/Jms2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/Jms2Test.groovy new file mode 100644 index 000000000..7c2e29883 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/Jms2Test.groovy @@ -0,0 +1,282 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +import com.google.common.io.Files +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicReference +import javax.jms.Message +import javax.jms.MessageListener +import javax.jms.Session +import javax.jms.TextMessage +import org.hornetq.api.core.TransportConfiguration +import org.hornetq.api.core.client.HornetQClient +import org.hornetq.api.jms.HornetQJMSClient +import org.hornetq.api.jms.JMSFactoryType +import org.hornetq.core.config.Configuration +import org.hornetq.core.config.CoreQueueConfiguration +import org.hornetq.core.config.impl.ConfigurationImpl +import org.hornetq.core.remoting.impl.invm.InVMAcceptorFactory +import org.hornetq.core.remoting.impl.invm.InVMConnectorFactory +import org.hornetq.core.server.HornetQServer +import org.hornetq.core.server.HornetQServers +import org.hornetq.jms.client.HornetQTextMessage +import spock.lang.Shared + +class Jms2Test extends AgentInstrumentationSpecification { + @Shared + HornetQServer server + @Shared + String messageText = "a message" + @Shared + Session session + + HornetQTextMessage message = session.createTextMessage(messageText) + + def setupSpec() { + def tempDir = Files.createTempDir() + tempDir.deleteOnExit() + + Configuration config = new ConfigurationImpl() + config.bindingsDirectory = tempDir.path + config.journalDirectory = tempDir.path + config.createBindingsDir = false + config.createJournalDir = false + config.securityEnabled = false + config.persistenceEnabled = false + config.setQueueConfigurations([new CoreQueueConfiguration("someQueue", "someQueue", null, true)]) + config.setAcceptorConfigurations([new TransportConfiguration(InVMAcceptorFactory.name)].toSet()) + + server = HornetQServers.newHornetQServer(config) + server.start() + + def serverLocator = HornetQClient.createServerLocatorWithoutHA(new TransportConfiguration(InVMConnectorFactory.name)) + def sf = serverLocator.createSessionFactory() + def clientSession = sf.createSession(false, false, false) + clientSession.createQueue("jms.queue.someQueue", "jms.queue.someQueue", true) + clientSession.createQueue("jms.topic.someTopic", "jms.topic.someTopic", true) + clientSession.close() + sf.close() + serverLocator.close() + + def connectionFactory = HornetQJMSClient.createConnectionFactoryWithoutHA(JMSFactoryType.CF, + new TransportConfiguration(InVMConnectorFactory.name)) + + def connection = connectionFactory.createConnection() + connection.start() + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE) + session.run() + } + + def cleanupSpec() { + server.stop() + } + + def "sending a message to #destinationName #destinationType generates spans"() { + setup: + def producer = session.createProducer(destination) + def consumer = session.createConsumer(destination) + + producer.send(message) + + TextMessage receivedMessage = consumer.receive() + String messageId = receivedMessage.getJMSMessageID() + + expect: + receivedMessage.text == messageText + assertTraces(2) { + trace(0, 1) { + producerSpan(it, 0, destinationType, destinationName) + } + trace(1, 1) { + consumerSpan(it, 0, destinationType, destinationName, messageId, null, "receive") + } + } + + cleanup: + producer.close() + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + session.createTemporaryQueue() | "queue" | "(temporary)" + session.createTemporaryTopic() | "topic" | "(temporary)" + } + + def "sending to a MessageListener on #destinationName #destinationType generates a span"() { + setup: + def lock = new CountDownLatch(1) + def messageRef = new AtomicReference() + def producer = session.createProducer(destination) + def consumer = session.createConsumer(destination) + consumer.setMessageListener new MessageListener() { + @Override + void onMessage(Message message) { + lock.await() // ensure the producer trace is reported first. + messageRef.set(message) + } + } + + producer.send(message) + lock.countDown() + + expect: + assertTraces(1) { + trace(0, 2) { + producerSpan(it, 0, destinationType, destinationName) + consumerSpan(it, 1, destinationType, destinationName, messageRef.get().getJMSMessageID(), span(0), "process") + } + } + // This check needs to go after all traces have been accounted for + messageRef.get().text == messageText + + cleanup: + producer.close() + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + session.createTemporaryQueue() | "queue" | "(temporary)" + session.createTemporaryTopic() | "topic" | "(temporary)" + } + + def "failing to receive message with receiveNoWait on #destinationName #destinationType works"() { + setup: + def consumer = session.createConsumer(destination) + + // Receive with timeout + TextMessage receivedMessage = consumer.receiveNoWait() + + expect: + receivedMessage == null + // span is not created if no message is received + assertTraces(0) {} + + cleanup: + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + } + + def "failing to receive message with wait(timeout) on #destinationName #destinationType works"() { + setup: + def consumer = session.createConsumer(destination) + + // Receive with timeout + TextMessage receivedMessage = consumer.receive(100) + + expect: + receivedMessage == null + // span is not created if no message is received + assertTraces(0) {} + + cleanup: + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + } + + def "sending a message to #destinationName #destinationType with explicit destination propagates context"() { + given: + def producer = session.createProducer(null) + def consumer = session.createConsumer(destination) + + def lock = new CountDownLatch(1) + def messageRef = new AtomicReference() + consumer.setMessageListener new MessageListener() { + @Override + void onMessage(Message message) { + lock.await() // ensure the producer trace is reported first. + messageRef.set(message) + } + } + + when: + producer.send(destination, message) + lock.countDown() + + then: + assertTraces(1) { + trace(0, 2) { + producerSpan(it, 0, destinationType, destinationName) + consumerSpan(it, 1, destinationType, destinationName, messageRef.get().getJMSMessageID(), span(0), "process") + } + } + // This check needs to go after all traces have been accounted for + messageRef.get().text == messageText + + cleanup: + producer.close() + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + session.createTemporaryQueue() | "queue" | "(temporary)" + session.createTemporaryTopic() | "topic" | "(temporary)" + } + + static producerSpan(TraceAssert trace, int index, String destinationType, String destinationName) { + trace.span(index) { + name destinationName + " send" + kind PRODUCER + hasNoParent() + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "jms" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" destinationName + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" destinationType + if (destinationName == "(temporary)") { + "${SemanticAttributes.MESSAGING_TEMP_DESTINATION.key}" true + } + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + } + } + } + + // passing messageId = null will verify message.id is not captured, + // passing messageId = "" will verify message.id is captured (but won't verify anything about the value), + // any other value for messageId will verify that message.id is captured and has that same value + static consumerSpan(TraceAssert trace, int index, String destinationType, String destinationName, String messageId, Object parentOrLinkedSpan, String operation) { + trace.span(index) { + name destinationName + " " + operation + kind CONSUMER + if (parentOrLinkedSpan != null) { + childOf((SpanData) parentOrLinkedSpan) + } else { + hasNoParent() + } + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "jms" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" destinationName + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" destinationType + "${SemanticAttributes.MESSAGING_OPERATION.key}" operation + if (messageId != null) { + //In some tests we don't know exact messageId, so we pass "" and verify just the existence of the attribute + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" { it == messageId || messageId == "" } + } + if (destinationName == "(temporary)") { + "${SemanticAttributes.MESSAGING_TEMP_DESTINATION.key}" true + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/SpringListenerJms2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/SpringListenerJms2Test.groovy new file mode 100644 index 000000000..47d031a2e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/SpringListenerJms2Test.groovy @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static Jms2Test.consumerSpan +import static Jms2Test.producerSpan +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import javax.jms.ConnectionFactory +import listener.Config +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.jms.core.JmsTemplate + +class SpringListenerJms2Test extends AgentInstrumentationSpecification { + def "receiving message in spring listener generates spans"() { + setup: + def context = new AnnotationConfigApplicationContext(Config) + def factory = context.getBean(ConnectionFactory) + def template = new JmsTemplate(factory) + + template.convertAndSend("SpringListenerJms2", "a message") + + expect: + assertTraces(2) { + traces.sort(orderByRootSpanKind(CONSUMER, PRODUCER)) + + trace(0, 1) { + consumerSpan(it, 0, "queue", "SpringListenerJms2", "", null, "receive") + } + trace(1, 2) { + producerSpan(it, 0, "queue", "SpringListenerJms2") + consumerSpan(it, 1, "queue", "SpringListenerJms2", "", span(0), "process") + } + } + + cleanup: + context.close() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/SpringTemplateJms2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/SpringTemplateJms2Test.groovy new file mode 100644 index 000000000..f6ecbe156 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/SpringTemplateJms2Test.groovy @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static Jms2Test.consumerSpan +import static Jms2Test.producerSpan + +import com.google.common.io.Files +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import javax.jms.Session +import javax.jms.TextMessage +import org.hornetq.api.core.TransportConfiguration +import org.hornetq.api.core.client.HornetQClient +import org.hornetq.api.jms.HornetQJMSClient +import org.hornetq.api.jms.JMSFactoryType +import org.hornetq.core.config.Configuration +import org.hornetq.core.config.CoreQueueConfiguration +import org.hornetq.core.config.impl.ConfigurationImpl +import org.hornetq.core.remoting.impl.invm.InVMAcceptorFactory +import org.hornetq.core.remoting.impl.invm.InVMConnectorFactory +import org.hornetq.core.server.HornetQServer +import org.hornetq.core.server.HornetQServers +import org.springframework.jms.core.JmsTemplate +import spock.lang.Shared + +class SpringTemplateJms2Test extends AgentInstrumentationSpecification { + @Shared + HornetQServer server + @Shared + String messageText = "a message" + @Shared + JmsTemplate template + @Shared + Session session + + def setupSpec() { + def tempDir = Files.createTempDir() + tempDir.deleteOnExit() + + Configuration config = new ConfigurationImpl() + config.bindingsDirectory = tempDir.path + config.journalDirectory = tempDir.path + config.createBindingsDir = false + config.createJournalDir = false + config.securityEnabled = false + config.persistenceEnabled = false + config.setQueueConfigurations([new CoreQueueConfiguration("someQueue", "someQueue", null, true)]) + config.setAcceptorConfigurations([new TransportConfiguration(InVMAcceptorFactory.name)].toSet()) + + server = HornetQServers.newHornetQServer(config) + server.start() + + def serverLocator = HornetQClient.createServerLocatorWithoutHA(new TransportConfiguration(InVMConnectorFactory.name)) + def sf = serverLocator.createSessionFactory() + def clientSession = sf.createSession(false, false, false) + clientSession.createQueue("jms.queue.SpringTemplateJms2", "jms.queue.SpringTemplateJms2", true) + clientSession.close() + sf.close() + serverLocator.close() + + def connectionFactory = HornetQJMSClient.createConnectionFactoryWithoutHA(JMSFactoryType.CF, + new TransportConfiguration(InVMConnectorFactory.name)) + + def connection = connectionFactory.createConnection() + connection.start() + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE) + session.run() + + template = new JmsTemplate(connectionFactory) + template.receiveTimeout = TimeUnit.SECONDS.toMillis(10) + } + + def cleanupSpec() { + server.stop() + } + + def "sending a message to #destinationName generates spans"() { + setup: + template.convertAndSend(destination, messageText) + TextMessage receivedMessage = template.receive(destination) + + expect: + receivedMessage.text == messageText + assertTraces(2) { + trace(0, 1) { + producerSpan(it, 0, destinationType, destinationName) + } + trace(1, 1) { + consumerSpan(it, 0, destinationType, destinationName, receivedMessage.getJMSMessageID(), null, "receive") + } + } + + where: + destination | destinationType | destinationName + session.createQueue("SpringTemplateJms2") | "queue" | "SpringTemplateJms2" + } + + def "send and receive message generates spans"() { + setup: + AtomicReference msgId = new AtomicReference<>() + Thread.start { + TextMessage msg = template.receive(destination) + assert msg.text == messageText + msgId.set(msg.getJMSMessageID()) + + // There's a chance this might be reported last, messing up the assertion. + template.send(msg.getJMSReplyTo()) { + session -> template.getMessageConverter().toMessage("responded!", session) + } + } + // wait for thread to start, we expect the first span to be from receive + TextMessage receivedMessage = template.sendAndReceive(destination) { + session -> template.getMessageConverter().toMessage(messageText, session) + } + + expect: + receivedMessage.text == "responded!" + assertTraces(4) { + traces.sort(orderByRootSpanName( + "$destinationName receive", + "$destinationName send", + "(temporary) receive", + "(temporary) send")) + + trace(0, 1) { + consumerSpan(it, 0, destinationType, destinationName, msgId.get(), null, "receive") + } + trace(1, 1) { + producerSpan(it, 0, destinationType, destinationName) + } + trace(2, 1) { + consumerSpan(it, 0, "queue", "(temporary)", receivedMessage.getJMSMessageID(), null, "receive") + } + trace(3, 1) { + producerSpan(it, 0, "queue", "(temporary)") + } + } + + where: + destination | destinationType | destinationName + session.createQueue("SpringTemplateJms2") | "queue" | "SpringTemplateJms2" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/listener/Config.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/listener/Config.groovy new file mode 100644 index 000000000..1f4f95ede --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/listener/Config.groovy @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package listener + +import com.google.common.io.Files +import javax.annotation.PreDestroy +import javax.jms.ConnectionFactory +import org.hornetq.api.core.TransportConfiguration +import org.hornetq.api.core.client.HornetQClient +import org.hornetq.api.jms.HornetQJMSClient +import org.hornetq.api.jms.JMSFactoryType +import org.hornetq.core.config.CoreQueueConfiguration +import org.hornetq.core.config.impl.ConfigurationImpl +import org.hornetq.core.remoting.impl.invm.InVMAcceptorFactory +import org.hornetq.core.remoting.impl.invm.InVMConnectorFactory +import org.hornetq.core.server.HornetQServer +import org.hornetq.core.server.HornetQServers +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.jms.annotation.EnableJms +import org.springframework.jms.config.DefaultJmsListenerContainerFactory +import org.springframework.jms.config.JmsListenerContainerFactory + +@Configuration +@ComponentScan +@EnableJms +class Config { + + private HornetQServer server + + @Bean + ConnectionFactory connectionFactory() { + def tempDir = Files.createTempDir() + tempDir.deleteOnExit() + + org.hornetq.core.config.Configuration config = new ConfigurationImpl() + config.bindingsDirectory = tempDir.path + config.journalDirectory = tempDir.path + config.createBindingsDir = false + config.createJournalDir = false + config.securityEnabled = false + config.persistenceEnabled = false + config.setQueueConfigurations([new CoreQueueConfiguration("someQueue", "someQueue", null, true)]) + config.setAcceptorConfigurations([new TransportConfiguration(InVMAcceptorFactory.name)].toSet()) + + server = HornetQServers.newHornetQServer(config) + server.start() + + def serverLocator = HornetQClient.createServerLocatorWithoutHA(new TransportConfiguration(InVMConnectorFactory.name)) + def sf = serverLocator.createSessionFactory() + def clientSession = sf.createSession(false, false, false) + clientSession.createQueue("jms.queue.SpringListenerJms2", "jms.queue.SpringListenerJms2", true) + clientSession.close() + sf.close() + serverLocator.close() + + return HornetQJMSClient.createConnectionFactoryWithoutHA(JMSFactoryType.CF, + new TransportConfiguration(InVMConnectorFactory.name)) + } + + @Bean + JmsListenerContainerFactory containerFactory(ConnectionFactory connectionFactory) { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory() + factory.setConnectionFactory(connectionFactory) + return factory + } + + @PreDestroy + void destroy() { + server.stop() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/listener/TestListener.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/listener/TestListener.groovy new file mode 100644 index 000000000..229dc0b90 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/jms2Test/groovy/listener/TestListener.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package listener + +import org.springframework.jms.annotation.JmsListener +import org.springframework.stereotype.Component + +@Component +class TestListener { + + @JmsListener(destination = "SpringListenerJms2", containerFactory = "containerFactory") + void receiveMessage(String message) { + println "received: " + message + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsInstrumentationModule.java new file mode 100644 index 000000000..156cfe1f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JmsInstrumentationModule extends InstrumentationModule { + public JmsInstrumentationModule() { + super("jms", "jms-1.1"); + } + + @Override + public List typeInstrumentations() { + return asList( + new JmsMessageConsumerInstrumentation(), + new JmsMessageListenerInstrumentation(), + new JmsMessageProducerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageAttributesExtractor.java new file mode 100644 index 000000000..d708edf95 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageAttributesExtractor.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; +import javax.jms.JMSException; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JmsMessageAttributesExtractor + extends MessagingAttributesExtractor { + private static final Logger log = LoggerFactory.getLogger(JmsMessageAttributesExtractor.class); + + @Nullable + @Override + protected String system(MessageWithDestination messageWithDestination) { + return "jms"; + } + + @Nullable + @Override + protected String destinationKind(MessageWithDestination messageWithDestination) { + return messageWithDestination.getDestinationKind(); + } + + @Nullable + @Override + protected String destination(MessageWithDestination messageWithDestination) { + return messageWithDestination.getDestinationName(); + } + + @Override + protected boolean temporaryDestination(MessageWithDestination messageWithDestination) { + return messageWithDestination.isTemporaryDestination(); + } + + @Nullable + @Override + protected String protocol(MessageWithDestination messageWithDestination) { + return null; + } + + @Nullable + @Override + protected String protocolVersion(MessageWithDestination messageWithDestination) { + return null; + } + + @Nullable + @Override + protected String url(MessageWithDestination messageWithDestination) { + return null; + } + + @Nullable + @Override + protected String conversationId(MessageWithDestination messageWithDestination) { + try { + return messageWithDestination.getMessage().getJMSCorrelationID(); + } catch (JMSException e) { + log.debug("Failure getting JMS correlation id", e); + return null; + } + } + + @Nullable + @Override + protected Long messagePayloadSize(MessageWithDestination messageWithDestination) { + return null; + } + + @Nullable + @Override + protected Long messagePayloadCompressedSize(MessageWithDestination messageWithDestination) { + return null; + } + + @Override + protected MessageOperation operation(MessageWithDestination messageWithDestination) { + return messageWithDestination.getMessageOperation(); + } + + @Nullable + @Override + protected String messageId(MessageWithDestination messageWithDestination, Void unused) { + try { + return messageWithDestination.getMessage().getJMSMessageID(); + } catch (JMSException e) { + log.debug("Failure getting JMS message id", e); + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageConsumerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageConsumerInstrumentation.java new file mode 100644 index 000000000..21478a637 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageConsumerInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jms.JmsSingletons.consumerInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import java.time.Instant; +import javax.jms.Message; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JmsMessageConsumerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.jms.MessageConsumer"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.jms.MessageConsumer")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("receive").and(takesArguments(0).or(takesArguments(1))).and(isPublic()), + JmsMessageConsumerInstrumentation.class.getName() + "$ConsumerAdvice"); + transformer.applyAdviceToMethod( + named("receiveNoWait").and(takesArguments(0)).and(isPublic()), + JmsMessageConsumerInstrumentation.class.getName() + "$ConsumerAdvice"); + } + + @SuppressWarnings("unused") + public static class ConsumerAdvice { + + @Advice.OnMethodEnter + public static Instant onEnter() { + return Instant.now(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter Instant startTime, + @Advice.Return Message message, + @Advice.Thrown Throwable throwable) { + if (message == null) { + // Do not create span when no message is received + return; + } + + Context parentContext = Java8BytecodeBridge.currentContext(); + MessageWithDestination request = + MessageWithDestination.create(message, MessageOperation.RECEIVE, null, startTime); + + if (consumerInstrumenter().shouldStart(parentContext, request)) { + Context context = consumerInstrumenter().start(parentContext, request); + consumerInstrumenter().end(context, request, null, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageListenerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageListenerInstrumentation.java new file mode 100644 index 000000000..6a51bd71b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageListenerInstrumentation.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jms.JmsSingletons.listenerInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.jms.Message; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JmsMessageListenerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.jms.MessageListener"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.jms.MessageListener")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("onMessage").and(takesArgument(0, named("javax.jms.Message"))).and(isPublic()), + JmsMessageListenerInstrumentation.class.getName() + "$MessageListenerAdvice"); + } + + @SuppressWarnings("unused") + public static class MessageListenerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Message message, + @Advice.Local("otelRequest") MessageWithDestination request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = Java8BytecodeBridge.currentContext(); + request = MessageWithDestination.create(message, MessageOperation.PROCESS, null); + + if (!listenerInstrumenter().shouldStart(parentContext, request)) { + return; + } + + context = listenerInstrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Local("otelRequest") MessageWithDestination request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + scope.close(); + listenerInstrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageProducerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageProducerInstrumentation.java new file mode 100644 index 000000000..93d002b03 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageProducerInstrumentation.java @@ -0,0 +1,143 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jms.JmsSingletons.producerInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageProducer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JmsMessageProducerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.jms.MessageProducer"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.jms.MessageProducer")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("send").and(takesArgument(0, named("javax.jms.Message"))).and(isPublic()), + JmsMessageProducerInstrumentation.class.getName() + "$ProducerAdvice"); + transformer.applyAdviceToMethod( + named("send") + .and(takesArgument(0, named("javax.jms.Destination"))) + .and(takesArgument(1, named("javax.jms.Message"))) + .and(isPublic()), + JmsMessageProducerInstrumentation.class.getName() + "$ProducerWithDestinationAdvice"); + } + + @SuppressWarnings("unused") + public static class ProducerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Message message, + @Advice.This MessageProducer producer, + @Advice.Local("otelRequest") MessageWithDestination request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(MessageProducer.class); + if (callDepth > 0) { + return; + } + + Destination defaultDestination; + try { + defaultDestination = producer.getDestination(); + } catch (JMSException e) { + defaultDestination = null; + } + + Context parentContext = Java8BytecodeBridge.currentContext(); + request = MessageWithDestination.create(message, MessageOperation.SEND, defaultDestination); + if (!producerInstrumenter().shouldStart(parentContext, request)) { + return; + } + + context = producerInstrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Local("otelRequest") MessageWithDestination request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + CallDepthThreadLocalMap.reset(MessageProducer.class); + + scope.close(); + producerInstrumenter().end(context, request, null, throwable); + } + } + + @SuppressWarnings("unused") + public static class ProducerWithDestinationAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Destination destination, + @Advice.Argument(1) Message message, + @Advice.Local("otelRequest") MessageWithDestination request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(MessageProducer.class); + if (callDepth > 0) { + return; + } + + Context parentContext = Java8BytecodeBridge.currentContext(); + request = MessageWithDestination.create(message, MessageOperation.SEND, destination); + if (!producerInstrumenter().shouldStart(parentContext, request)) { + return; + } + + context = producerInstrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Local("otelRequest") MessageWithDestination request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + CallDepthThreadLocalMap.reset(MessageProducer.class); + + scope.close(); + producerInstrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsSingletons.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsSingletons.java new file mode 100644 index 000000000..49e7f6ba6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsSingletons.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor; +import java.time.Instant; + +public final class JmsSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jms-1.1"; + + private static final Instrumenter PRODUCER_INSTRUMENTER; + private static final Instrumenter CONSUMER_INSTRUMENTER; + private static final Instrumenter LISTENER_INSTRUMENTER; + + static { + JmsMessageAttributesExtractor attributesExtractor = new JmsMessageAttributesExtractor(); + SpanNameExtractor spanNameExtractor = + MessagingSpanNameExtractor.create(attributesExtractor); + + OpenTelemetry otel = GlobalOpenTelemetry.get(); + PRODUCER_INSTRUMENTER = + Instrumenter.newBuilder( + otel, INSTRUMENTATION_NAME, spanNameExtractor) + .addAttributesExtractor(attributesExtractor) + .newProducerInstrumenter(new MessagePropertySetter()); + // MessageConsumer does not do context propagation + CONSUMER_INSTRUMENTER = + Instrumenter.newBuilder( + otel, INSTRUMENTATION_NAME, spanNameExtractor) + .addAttributesExtractor(attributesExtractor) + .setTimeExtractors(MessageWithDestination::getStartTime, response -> Instant.now()) + .newInstrumenter(SpanKindExtractor.alwaysConsumer()); + LISTENER_INSTRUMENTER = + Instrumenter.newBuilder( + otel, INSTRUMENTATION_NAME, spanNameExtractor) + .addAttributesExtractor(attributesExtractor) + .newConsumerInstrumenter(new MessagePropertyGetter()); + } + + public static Instrumenter producerInstrumenter() { + return PRODUCER_INSTRUMENTER; + } + + public static Instrumenter consumerInstrumenter() { + return CONSUMER_INSTRUMENTER; + } + + public static Instrumenter listenerInstrumenter() { + return LISTENER_INSTRUMENTER; + } + + private JmsSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessagePropertyGetter.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessagePropertyGetter.java new file mode 100644 index 000000000..0de77cd50 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessagePropertyGetter.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Collections; +import javax.jms.JMSException; + +public final class MessagePropertyGetter implements TextMapGetter { + + @Override + public Iterable keys(MessageWithDestination message) { + try { + return Collections.list(message.getMessage().getPropertyNames()); + } catch (JMSException e) { + return Collections.emptyList(); + } + } + + @Override + public String get(MessageWithDestination carrier, String key) { + String propName = key.replace("-", MessagePropertySetter.DASH); + final Object value; + try { + value = carrier.getMessage().getObjectProperty(propName); + } catch (JMSException e) { + throw new IllegalStateException(e); + } + if (value instanceof String) { + return (String) value; + } else { + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessagePropertySetter.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessagePropertySetter.java new file mode 100644 index 000000000..61f6308b1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessagePropertySetter.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import io.opentelemetry.context.propagation.TextMapSetter; +import javax.jms.JMSException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class MessagePropertySetter implements TextMapSetter { + + private static final Logger log = LoggerFactory.getLogger(MessagePropertySetter.class); + + static final String DASH = "__dash__"; + + @Override + public void set(MessageWithDestination carrier, String key, String value) { + String propName = key.replace("-", DASH); + try { + carrier.getMessage().setStringProperty(propName, value); + } catch (JMSException e) { + if (log.isDebugEnabled()) { + log.debug("Failure setting jms property: {}", propName, e); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestination.java b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestination.java new file mode 100644 index 000000000..20cd3ff8d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestination.java @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; +import java.time.Instant; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Queue; +import javax.jms.TemporaryQueue; +import javax.jms.TemporaryTopic; +import javax.jms.Topic; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class MessageWithDestination { + // visible for tests + static final String TIBCO_TMP_PREFIX = "$TMP$"; + + private final Message message; + private final MessageOperation messageOperation; + private final String destinationName; + private final String destinationKind; + private final boolean temporaryDestination; + private final Instant startTime; + + private MessageWithDestination( + Message message, + MessageOperation messageOperation, + String destinationName, + String destinationKind, + boolean temporary, + Instant startTime) { + this.message = message; + this.messageOperation = messageOperation; + this.destinationName = destinationName; + this.destinationKind = destinationKind; + this.temporaryDestination = temporary; + this.startTime = startTime; + } + + public Message getMessage() { + return message; + } + + public MessageOperation getMessageOperation() { + return messageOperation; + } + + public String getDestinationName() { + return destinationName; + } + + public String getDestinationKind() { + return destinationKind; + } + + public boolean isTemporaryDestination() { + return temporaryDestination; + } + + @Nullable + public Instant getStartTime() { + return startTime; + } + + public static MessageWithDestination create( + Message message, MessageOperation operation, Destination fallbackDestination) { + return create(message, operation, fallbackDestination, null); + } + + public static MessageWithDestination create( + Message message, + MessageOperation operation, + Destination fallbackDestination, + @Nullable Instant startTime) { + Destination jmsDestination = null; + try { + jmsDestination = message.getJMSDestination(); + } catch (Exception ignored) { + // Ignore + } + if (jmsDestination == null) { + jmsDestination = fallbackDestination; + } + + if (jmsDestination instanceof Queue) { + return createMessageWithQueue(message, operation, (Queue) jmsDestination, startTime); + } + if (jmsDestination instanceof Topic) { + return createMessageWithTopic(message, operation, (Topic) jmsDestination, startTime); + } + return new MessageWithDestination( + message, operation, "unknown", "unknown", /* temporary= */ false, startTime); + } + + private static MessageWithDestination createMessageWithQueue( + Message message, MessageOperation operation, Queue destination, @Nullable Instant startTime) { + String queueName; + try { + queueName = destination.getQueueName(); + } catch (JMSException e) { + queueName = "unknown"; + } + + boolean temporary = + destination instanceof TemporaryQueue || queueName.startsWith(TIBCO_TMP_PREFIX); + + return new MessageWithDestination(message, operation, queueName, "queue", temporary, startTime); + } + + private static MessageWithDestination createMessageWithTopic( + Message message, MessageOperation operation, Topic destination, @Nullable Instant startTime) { + String topicName; + try { + topicName = destination.getTopicName(); + } catch (JMSException e) { + topicName = "unknown"; + } + + boolean temporary = + destination instanceof TemporaryTopic || topicName.startsWith(TIBCO_TMP_PREFIX); + + return new MessageWithDestination(message, operation, topicName, "topic", temporary, startTime); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/Jms1Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/Jms1Test.groovy new file mode 100644 index 000000000..b3033698a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/Jms1Test.groovy @@ -0,0 +1,313 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicReference +import javax.jms.Connection +import javax.jms.Message +import javax.jms.MessageListener +import javax.jms.Session +import javax.jms.TextMessage +import org.apache.activemq.ActiveMQConnectionFactory +import org.apache.activemq.command.ActiveMQTextMessage +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +class Jms1Test extends AgentInstrumentationSpecification { + + private static final Logger logger = LoggerFactory.getLogger(Jms1Test) + + private static final GenericContainer broker = new GenericContainer("rmohr/activemq:latest") + .withExposedPorts(61616, 8161) + .withLogConsumer(new Slf4jLogConsumer(logger)) + + @Shared + String messageText = "a message" + @Shared + Session session + + ActiveMQTextMessage message = session.createTextMessage(messageText) + + def setupSpec() { + broker.start() + ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:" + broker.getMappedPort(61616)) + + Connection connection = connectionFactory.createConnection() + connection.start() + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE) + } + + def cleanupSpec() { + broker.stop() + } + + def "sending a message to #destinationName #destinationType generates spans"() { + setup: + def producer = session.createProducer(destination) + def consumer = session.createConsumer(destination) + + producer.send(message) + + TextMessage receivedMessage = consumer.receive() + String messageId = receivedMessage.getJMSMessageID() + + expect: + receivedMessage.text == messageText + assertTraces(2) { + trace(0, 1) { + producerSpan(it, 0, destinationType, destinationName) + } + trace(1, 1) { + consumerSpan(it, 0, destinationType, destinationName, messageId, null, "receive") + } + } + + cleanup: + producer.close() + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + session.createTemporaryQueue() | "queue" | "(temporary)" + session.createTemporaryTopic() | "topic" | "(temporary)" + } + + def "sending to a MessageListener on #destinationName #destinationType generates a span"() { + setup: + def lock = new CountDownLatch(1) + def messageRef = new AtomicReference() + def producer = session.createProducer(destination) + def consumer = session.createConsumer(destination) + consumer.setMessageListener new MessageListener() { + @Override + void onMessage(Message message) { + lock.await() // ensure the producer trace is reported first. + messageRef.set(message) + } + } + + producer.send(message) + lock.countDown() + + expect: + assertTraces(1) { + trace(0, 2) { + producerSpan(it, 0, destinationType, destinationName) + consumerSpan(it, 1, destinationType, destinationName, messageRef.get().getJMSMessageID(), span(0), "process") + } + } + // This check needs to go after all traces have been accounted for + messageRef.get().text == messageText + + cleanup: + producer.close() + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + session.createTemporaryQueue() | "queue" | "(temporary)" + session.createTemporaryTopic() | "topic" | "(temporary)" + } + + def "failing to receive message with receiveNoWait on #destinationName #destinationType works"() { + setup: + def consumer = session.createConsumer(destination) + + // Receive with timeout + TextMessage receivedMessage = consumer.receiveNoWait() + + expect: + receivedMessage == null + // span is not created if no message is received + assertTraces(0) {} + + cleanup: + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + } + + def "failing to receive message with wait(timeout) on #destinationName #destinationType works"() { + setup: + def consumer = session.createConsumer(destination) + + // Receive with timeout + TextMessage receivedMessage = consumer.receive(100) + + expect: + receivedMessage == null + // span is not created if no message is received + assertTraces(0) {} + + cleanup: + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + } + + def "sending a read-only message to #destinationName #destinationType fails"() { + setup: + def producer = session.createProducer(destination) + def consumer = session.createConsumer(destination) + + expect: + !message.isReadOnlyProperties() + + when: + message.setReadOnlyProperties(true) + and: + producer.send(message) + + TextMessage receivedMessage = consumer.receive() + + then: + receivedMessage.text == messageText + + // This will result in a logged failure because we tried to + // write properties in MessagePropertyTextMap when readOnlyProperties = true. + // The consumer span will also not be linked to the parent. + assertTraces(2) { + trace(0, 1) { + producerSpan(it, 0, destinationType, destinationName) + } + trace(1, 1) { + span(0) { + hasNoParent() + name destinationName + " receive" + kind CONSUMER + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "jms" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" destinationName + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" destinationType + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" receivedMessage.getJMSMessageID() + "${SemanticAttributes.MESSAGING_OPERATION.key}" "receive" + if (destinationName == "(temporary)") { + "${SemanticAttributes.MESSAGING_TEMP_DESTINATION.key}" true + } + } + } + } + } + + cleanup: + producer.close() + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + session.createTemporaryQueue() | "queue" | "(temporary)" + session.createTemporaryTopic() | "topic" | "(temporary)" + } + + def "sending a message to #destinationName #destinationType with explicit destination propagates context"() { + given: + def producer = session.createProducer(null) + def consumer = session.createConsumer(destination) + + def lock = new CountDownLatch(1) + def messageRef = new AtomicReference() + consumer.setMessageListener new MessageListener() { + @Override + void onMessage(Message message) { + lock.await() // ensure the producer trace is reported first. + messageRef.set(message) + } + } + + when: + producer.send(destination, message) + lock.countDown() + + then: + assertTraces(1) { + trace(0, 2) { + producerSpan(it, 0, destinationType, destinationName) + consumerSpan(it, 1, destinationType, destinationName, messageRef.get().getJMSMessageID(), span(0), "process") + } + } + // This check needs to go after all traces have been accounted for + messageRef.get().text == messageText + + cleanup: + producer.close() + consumer.close() + + where: + destination | destinationType | destinationName + session.createQueue("someQueue") | "queue" | "someQueue" + session.createTopic("someTopic") | "topic" | "someTopic" + session.createTemporaryQueue() | "queue" | "(temporary)" + session.createTemporaryTopic() | "topic" | "(temporary)" + } + + static producerSpan(TraceAssert trace, int index, String destinationType, String destinationName) { + trace.span(index) { + name destinationName + " send" + kind PRODUCER + hasNoParent() + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "jms" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" destinationName + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" destinationType + if (destinationName == "(temporary)") { + "${SemanticAttributes.MESSAGING_TEMP_DESTINATION.key}" true + } + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + } + } + } + + // passing messageId = null will verify message.id is not captured, + // passing messageId = "" will verify message.id is captured (but won't verify anything about the value), + // any other value for messageId will verify that message.id is captured and has that same value + static consumerSpan(TraceAssert trace, int index, String destinationType, String destinationName, String messageId, Object parentOrLinkedSpan, String operation) { + trace.span(index) { + name destinationName + " " + operation + kind CONSUMER + if (parentOrLinkedSpan != null) { + childOf((SpanData) parentOrLinkedSpan) + } else { + hasNoParent() + } + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "jms" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" destinationName + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" destinationType + "${SemanticAttributes.MESSAGING_OPERATION.key}" operation + if (messageId != null) { + //In some tests we don't know exact messageId, so we pass "" and verify just the existence of the attribute + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" { it == messageId || messageId == "" } + } + if (destinationName == "(temporary)") { + "${SemanticAttributes.MESSAGING_TEMP_DESTINATION.key}" true + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/SpringListenerJms1Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/SpringListenerJms1Test.groovy new file mode 100644 index 000000000..bf1fe0e41 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/SpringListenerJms1Test.groovy @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static Jms1Test.consumerSpan +import static Jms1Test.producerSpan +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import javax.jms.ConnectionFactory +import listener.Config +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.jms.core.JmsTemplate + +class SpringListenerJms1Test extends AgentInstrumentationSpecification { + + def "receiving message in spring listener generates spans"() { + setup: + def context = new AnnotationConfigApplicationContext(Config) + def factory = context.getBean(ConnectionFactory) + def template = new JmsTemplate(factory) + + template.convertAndSend("SpringListenerJms1", "a message") + + expect: + assertTraces(2) { + traces.sort(orderByRootSpanKind(CONSUMER, PRODUCER)) + + trace(0, 1) { + consumerSpan(it, 0, "queue", "SpringListenerJms1", "", null, "receive") + } + trace(1, 2) { + producerSpan(it, 0, "queue", "SpringListenerJms1") + consumerSpan(it, 1, "queue", "SpringListenerJms1", "", span(0), "process") + } + } + + cleanup: + context.stop() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/SpringTemplateJms1Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/SpringTemplateJms1Test.groovy new file mode 100644 index 000000000..84d22ed28 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/SpringTemplateJms1Test.groovy @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static Jms1Test.consumerSpan +import static Jms1Test.producerSpan + +import com.google.common.base.Stopwatch +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import javax.jms.Connection +import javax.jms.Session +import javax.jms.TextMessage +import org.apache.activemq.ActiveMQConnectionFactory +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.jms.core.JmsTemplate +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import spock.lang.Shared + +class SpringTemplateJms1Test extends AgentInstrumentationSpecification { + private static final Logger logger = LoggerFactory.getLogger(SpringTemplateJms1Test) + + private static final GenericContainer broker = new GenericContainer("rmohr/activemq:latest") + .withExposedPorts(61616, 8161) + .withLogConsumer(new Slf4jLogConsumer(logger)) + + @Shared + String messageText = "a message" + @Shared + JmsTemplate template + @Shared + Session session + + def setupSpec() { + broker.start() + ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:" + broker.getMappedPort(61616)) + Connection connection = connectionFactory.createConnection() + connection.start() + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE) + + template = new JmsTemplate(connectionFactory) + // Make this longer than timeout on testWriter.waitForTraces + // Otherwise caller might give up waiting before callee has a chance to respond. + template.receiveTimeout = TimeUnit.SECONDS.toMillis(21) + } + + def cleanupSpec() { + broker.stop() + } + + def "sending a message to #destinationName generates spans"() { + setup: + template.convertAndSend(destination, messageText) + TextMessage receivedMessage = template.receive(destination) + + expect: + receivedMessage.text == messageText + assertTraces(2) { + trace(0, 1) { + producerSpan(it, 0, destinationType, destinationName) + } + trace(1, 1) { + consumerSpan(it, 0, destinationType, destinationName, receivedMessage.getJMSMessageID(), null, "receive") + } + } + + where: + destination | destinationType | destinationName + session.createQueue("SpringTemplateJms1") | "queue" | "SpringTemplateJms1" + } + + def "send and receive message generates spans"() { + setup: + AtomicReference msgId = new AtomicReference<>() + Thread.start { + TextMessage msg = template.receive(destination) + assert msg.text == messageText + msgId.set(msg.getJMSMessageID()) + + template.send(msg.getJMSReplyTo()) { + session -> template.getMessageConverter().toMessage("responded!", session) + } + } + def receivedMessage + def stopwatch = Stopwatch.createStarted() + while (receivedMessage == null && stopwatch.elapsed(TimeUnit.SECONDS) < 10) { + // sendAndReceive() returns null if template.receive() has not been called yet + receivedMessage = template.sendAndReceive(destination) { + session -> template.getMessageConverter().toMessage(messageText, session) + } + } + + expect: + receivedMessage.text == "responded!" + assertTraces(4) { + traces.sort(orderByRootSpanName( + "$destinationName receive", + "$destinationName send", + "(temporary) receive", + "(temporary) send")) + + trace(0, 1) { + consumerSpan(it, 0, destinationType, destinationName, msgId.get(), null, "receive") + } + trace(1, 1) { + producerSpan(it, 0, destinationType, destinationName) + } + trace(2, 1) { + consumerSpan(it, 0, "queue", "(temporary)", receivedMessage.getJMSMessageID(), null, "receive") + } + trace(3, 1) { + // receive doesn't propagate the trace, so this is a root + producerSpan(it, 0, "queue", "(temporary)") + } + } + + where: + destination | destinationType | destinationName + session.createQueue("SpringTemplateJms1") | "queue" | "SpringTemplateJms1" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/listener/Config.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/listener/Config.groovy new file mode 100644 index 000000000..ab05eba5f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/listener/Config.groovy @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package listener + +import java.time.Duration +import javax.annotation.PreDestroy +import javax.jms.ConnectionFactory +import org.apache.activemq.ActiveMQConnectionFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.jms.annotation.EnableJms +import org.springframework.jms.config.DefaultJmsListenerContainerFactory +import org.springframework.jms.config.JmsListenerContainerFactory +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.wait.strategy.Wait + +@Configuration +@ComponentScan +@EnableJms +class Config { + + private static GenericContainer broker = new GenericContainer("rmohr/activemq:latest") + .withExposedPorts(61616, 8161) + .waitingFor(Wait.forLogMessage(".*Apache ActiveMQ .* started.*", 1)) + .withStartupTimeout(Duration.ofMinutes(2)) + + static { + broker.start() + } + + @Bean + ConnectionFactory connectionFactory() { + return new ActiveMQConnectionFactory("tcp://localhost:" + broker.getMappedPort(61616)) + } + + @Bean + JmsListenerContainerFactory containerFactory(ConnectionFactory connectionFactory) { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory() + factory.setConnectionFactory(connectionFactory) + return factory + } + + @PreDestroy + void destroy() { + broker.stop() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/listener/TestListener.groovy b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/listener/TestListener.groovy new file mode 100644 index 000000000..2d7674883 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jms-1.1/javaagent/src/test/groovy/listener/TestListener.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package listener + +import org.springframework.jms.annotation.JmsListener +import org.springframework.stereotype.Component + +@Component +class TestListener { + + @JmsListener(destination = "SpringListenerJms1", containerFactory = "containerFactory") + void receiveMessage(String message) { + println "received: " + message + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-common/library/jsf-common-library.gradle b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-common/library/jsf-common-library.gradle new file mode 100644 index 000000000..65c3fbf36 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-common/library/jsf-common-library.gradle @@ -0,0 +1,6 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "jakarta.faces:jakarta.faces-api:2.3.2" + compileOnly "jakarta.el:jakarta.el-api:3.0.3" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-common/library/src/main/java/io/opentelemetry/instrumentation/jsf/JsfTracer.java b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-common/library/src/main/java/io/opentelemetry/instrumentation/jsf/JsfTracer.java new file mode 100644 index 000000000..179b98cc1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-common/library/src/main/java/io/opentelemetry/instrumentation/jsf/JsfTracer.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsf; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import javax.faces.FacesException; +import javax.faces.component.ActionSource2; +import javax.faces.component.UIViewRoot; +import javax.faces.context.FacesContext; +import javax.faces.event.ActionEvent; + +public abstract class JsfTracer extends BaseTracer { + + public Context startSpan(ActionEvent event) { + // https://jakarta.ee/specifications/faces/2.3/apidocs/index.html?javax/faces/component/ActionSource2.html + // ActionSource2 was added in JSF 1.2 and is implemented by components that have an action + // attribute such as a button or a link + if (event.getComponent() instanceof ActionSource2) { + ActionSource2 actionSource = (ActionSource2) event.getComponent(); + if (actionSource.getActionExpression() != null) { + // either an el expression in the form #{bean.method()} or navigation case name + String expressionString = actionSource.getActionExpression().getExpressionString(); + // start span only if expression string is really an expression + if (expressionString.startsWith("#{") || expressionString.startsWith("${")) { + return startSpan(expressionString); + } + } + } + + return null; + } + + public void updateServerSpanName(Context context, FacesContext facesContext) { + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + + UIViewRoot uiViewRoot = facesContext.getViewRoot(); + if (uiViewRoot == null) { + return; + } + + // JSF spec 7.6.2 + // view id is a context relative path to the web application resource that produces the view, + // such as a JSP page or a Facelets page. + String viewId = uiViewRoot.getViewId(); + serverSpan.updateName(ServletContextPath.prepend(context, viewId)); + } + + @Override + protected Throwable unwrapThrowable(Throwable throwable) { + while (throwable.getCause() != null && throwable instanceof FacesException) { + throwable = throwable.getCause(); + } + return super.unwrapThrowable(throwable); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/jsf-testing-common.gradle b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/jsf-testing-common.gradle new file mode 100644 index 000000000..e05805aab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/jsf-testing-common.gradle @@ -0,0 +1,22 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api "ch.qos.logback:logback-classic" + api "org.slf4j:log4j-over-slf4j" + api "org.slf4j:jcl-over-slf4j" + api "org.slf4j:jul-to-slf4j" + + compileOnly "jakarta.faces:jakarta.faces-api:2.3.2" + compileOnly "jakarta.el:jakarta.el-api:3.0.3" + + implementation(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + implementation "org.jsoup:jsoup:1.13.1" + + def jettyVersion = '9.4.35.v20201120' + api "org.eclipse.jetty:jetty-annotations:${jettyVersion}" + implementation "org.eclipse.jetty:apache-jsp:${jettyVersion}" + implementation "org.glassfish:jakarta.el:3.0.2" + implementation "jakarta.websocket:jakarta.websocket-api:1.1.1" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/BaseJsfTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/BaseJsfTest.groovy new file mode 100644 index 000000000..18c6a5ecb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/BaseJsfTest.groovy @@ -0,0 +1,224 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicServerSpan + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpData +import io.opentelemetry.testing.internal.armeria.common.HttpMethod +import io.opentelemetry.testing.internal.armeria.common.MediaType +import io.opentelemetry.testing.internal.armeria.common.QueryParams +import io.opentelemetry.testing.internal.armeria.common.RequestHeaders +import org.eclipse.jetty.annotations.AnnotationConfiguration +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.util.resource.Resource +import org.eclipse.jetty.webapp.WebAppContext +import org.jsoup.Jsoup +import spock.lang.Unroll + +abstract class BaseJsfTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + @Override + Server startServer(int port) { + String jsfVersion = getJsfVersion() + + List configurationClasses = new ArrayList<>() + Collections.addAll(configurationClasses, WebAppContext.getDefaultConfigurationClasses()) + configurationClasses.add(AnnotationConfiguration.getName()) + + WebAppContext webAppContext = new WebAppContext() + webAppContext.setContextPath(getContextPath()) + webAppContext.setConfigurationClasses(configurationClasses) + // set up test application + webAppContext.setBaseResource(Resource.newSystemResource("test-app-" + jsfVersion)) + // add additional resources for test app + Resource extraResource = Resource.newSystemResource("test-app-" + jsfVersion + "-extra") + if (extraResource != null) { + webAppContext.getMetaData().addWebInfJar(extraResource) + } + webAppContext.getMetaData().getWebInfClassesDirs().add(Resource.newClassPathResource("/")) + + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + jettyServer.setHandler(webAppContext) + jettyServer.start() + + return jettyServer + } + + abstract String getJsfVersion(); + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/jetty-context" + } + + @Unroll + def "test #path"() { + setup: + AggregatedHttpResponse response = client.get(address.resolve("hello.jsf").toString()).aggregate().join() + + expect: + response.status().code() == 200 + response.contentUtf8().trim() == "Hello" + + and: + assertTraces(1) { + trace(0, 1) { + basicServerSpan(it, 0, getContextPath() + "/hello.xhtml", null) + } + } + + where: + path << ['hello.jsf', 'faces/hello.xhtml'] + } + + def "test greeting"() { + // we need to display the page first before posting data to it + setup: + AggregatedHttpResponse response = client.get(address.resolve("greeting.jsf").toString()).aggregate().join() + def doc = Jsoup.parse(response.contentUtf8()) + + expect: + response.status().code() == 200 + doc.selectFirst("title").text() == "Hello, World!" + + and: + assertTraces(1) { + trace(0, 1) { + basicServerSpan(it, 0, getContextPath() + "/greeting.xhtml", null) + } + } + clearExportedData() + + when: + // extract parameters needed to post back form + def viewState = doc.selectFirst("[name=javax.faces.ViewState]")?.val() + def formAction = doc.selectFirst("#app-form").attr("action") + def jsessionid = formAction.substring(formAction.indexOf("jsessionid=") + "jsessionid=".length()) + + then: + viewState != null + jsessionid != null + + when: + // set up form parameter for post + QueryParams formBody = QueryParams.builder() + .add("app-form", "app-form") + // value used for name is returned in app-form:output-message element + .add("app-form:name", "test") + .add("app-form:submit", "Say hello") + .add("app-form_SUBMIT", "1") // MyFaces + .add("javax.faces.ViewState", viewState) + .build() + // use the session created for first request + def request2 = AggregatedHttpRequest.of( + RequestHeaders.builder(HttpMethod.POST, address.resolve("greeting.jsf;jsessionid=" + jsessionid).toString()) + .contentType(MediaType.FORM_DATA) + .build(), + HttpData.ofUtf8(formBody.toQueryString())) + AggregatedHttpResponse response2 = client.execute(request2).aggregate().join() + def responseContent = response2.contentUtf8() + def doc2 = Jsoup.parse(responseContent) + + then: + response2.status().code() == 200 + doc2.getElementById("app-form:output-message").text() == "Hello test" + + and: + assertTraces(1) { + trace(0, 2) { + basicServerSpan(it, 0, getContextPath() + "/greeting.xhtml", null) + handlerSpan(it, 1, span(0), "#{greetingForm.submit()}") + } + } + } + + def "test exception"() { + // we need to display the page first before posting data to it + setup: + AggregatedHttpResponse response = client.get(address.resolve("greeting.jsf").toString()).aggregate().join() + def doc = Jsoup.parse(response.contentUtf8()) + + expect: + response.status().code() == 200 + doc.selectFirst("title").text() == "Hello, World!" + + and: + assertTraces(1) { + trace(0, 1) { + basicServerSpan(it, 0, getContextPath() + "/greeting.xhtml", null) + } + } + clearExportedData() + + when: + // extract parameters needed to post back form + def viewState = doc.selectFirst("[name=javax.faces.ViewState]").val() + def formAction = doc.selectFirst("#app-form").attr("action") + def jsessionid = formAction.substring(formAction.indexOf("jsessionid=") + "jsessionid=".length()) + + then: + viewState != null + jsessionid != null + + when: + // set up form parameter for post + QueryParams formBody = QueryParams.builder() + .add("app-form", "app-form") + // setting name parameter to "exception" triggers throwing exception in GreetingForm + .add("app-form:name", "exception") + .add("app-form:submit", "Say hello") + .add("app-form_SUBMIT", "1") // MyFaces + .add("javax.faces.ViewState", viewState) + .build() + // use the session created for first request + def request2 = AggregatedHttpRequest.of( + RequestHeaders.builder(HttpMethod.POST, address.resolve("greeting.jsf;jsessionid=" + jsessionid).toString()) + .contentType(MediaType.FORM_DATA) + .build(), + HttpData.ofUtf8(formBody.toQueryString())) + AggregatedHttpResponse response2 = client.execute(request2).aggregate().join() + + then: + response2.status().code() == 500 + + and: + assertTraces(1) { + trace(0, 2) { + basicServerSpan(it, 0, getContextPath() + "/greeting.xhtml", null, new Exception("submit exception")) + handlerSpan(it, 1, span(0), "#{greetingForm.submit()}", new Exception("submit exception")) + } + } + } + + void handlerSpan(TraceAssert trace, int index, Object parent, String spanName, Exception expectedException = null) { + trace.span(index) { + name spanName + kind INTERNAL + if (expectedException != null) { + status ERROR + errorEvent(expectedException.getClass(), expectedException.getMessage()) + } + childOf((SpanData) parent) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/ExceptionFilter.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/ExceptionFilter.groovy new file mode 100644 index 000000000..9efcd3acc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/ExceptionFilter.groovy @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.servlet.Filter +import javax.servlet.FilterChain +import javax.servlet.FilterConfig +import javax.servlet.ServletException +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse + +class ExceptionFilter implements Filter { + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + chain.doFilter(request, response) + } catch (Exception exception) { + // to ease testing unwrap our exception to root cause + Exception tmp = exception + while (tmp.getCause() != null) { + tmp = tmp.getCause() + } + if (tmp.getMessage() != null && tmp.getMessage().contains("submit exception")) { + throw tmp + } + throw exception + } + } + + @Override + void destroy() { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/GreetingForm.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/GreetingForm.groovy new file mode 100644 index 000000000..f8d429f4e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/groovy/GreetingForm.groovy @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class GreetingForm { + + String name = "" + String message = "" + + String getName() { + name + } + + void setName(String name) { + this.name = name + } + + String getMessage() { + return message + } + + void submit() { + message = "Hello " + name + if (name == "exception") { + throw new Exception("submit exception") + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/WEB-INF/faces-config.xml b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/WEB-INF/faces-config.xml new file mode 100644 index 000000000..5502993f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/WEB-INF/faces-config.xml @@ -0,0 +1,14 @@ + + + greetingForm + GreetingForm + request + + + + com.sun.facelets.FaceletViewHandler + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/WEB-INF/web.xml new file mode 100644 index 000000000..cb64b6ae8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/WEB-INF/web.xml @@ -0,0 +1,36 @@ + + + + + Faces Servlet + javax.faces.webapp.FacesServlet + 1 + + + Faces Servlet + *.jsf + + + Faces Servlet + /faces/* + + + + ExceptionFilter + ExceptionFilter + + + + ExceptionFilter + /* + + + + javax.faces.DEFAULT_SUFFIX + .xhtml + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/greeting.xhtml b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/greeting.xhtml new file mode 100644 index 000000000..2b53eebba --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/greeting.xhtml @@ -0,0 +1,24 @@ + + + + Hello, World! + + + +

+ + + +

+

+ + +

+

+ +

+ + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/hello.xhtml b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/hello.xhtml new file mode 100644 index 000000000..d3b79206b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-1.2/hello.xhtml @@ -0,0 +1,6 @@ + + + Hello + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/WEB-INF/faces-config.xml b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/WEB-INF/faces-config.xml new file mode 100644 index 000000000..9a0ad7c89 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/WEB-INF/faces-config.xml @@ -0,0 +1,8 @@ + + + + greetingForm + GreetingForm + request + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/WEB-INF/web.xml new file mode 100644 index 000000000..191c55b09 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/WEB-INF/web.xml @@ -0,0 +1,17 @@ + + + + + ExceptionFilter + ExceptionFilter + + + + ExceptionFilter + /* + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/greeting.xhtml b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/greeting.xhtml new file mode 100644 index 000000000..3bc9510ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/greeting.xhtml @@ -0,0 +1,23 @@ + + + + Hello, World! + + + +

+ + + +

+

+ +

+

+ +

+
+
+ \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/hello.xhtml b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/hello.xhtml new file mode 100644 index 000000000..f98ba1c01 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/jsf-testing-common/src/main/resources/test-app-2/hello.xhtml @@ -0,0 +1,8 @@ + + + Hello + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/mojarra-1.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/mojarra-1.2-javaagent.gradle new file mode 100644 index 000000000..b04cc82b9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/mojarra-1.2-javaagent.gradle @@ -0,0 +1,73 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +muzzle { + pass { + group = "org.glassfish" + module = "jakarta.faces" + versions = "[2.3.9,3)" + extraDependency "javax.el:el-api:2.2" + } + pass { + group = "org.glassfish" + module = "javax.faces" + versions = "[2.0.7,3)" + extraDependency "javax.el:el-api:2.2" + } + pass { + group = "com.sun.faces" + module = "jsf-impl" + versions = "[2.1,2.2)" + extraDependency "javax.faces:jsf-api:2.1" + extraDependency "javax.el:el-api:1.0" + } + pass { + group = "com.sun.faces" + module = "jsf-impl" + versions = "[2.0,2.1)" + extraDependency "javax.faces:jsf-api:2.0" + extraDependency "javax.el:el-api:1.0" + } + pass { + group = "javax.faces" + module = "jsf-impl" + versions = "[1.2,2)" + extraDependency "javax.faces:jsf-api:1.2" + extraDependency "javax.el:el-api:1.0" + } + fail { + group = "org.glassfish" + module = "jakarta.faces" + versions = "[3.0,)" + extraDependency "javax.el:el-api:2.2" + } +} + +testSets { + mojarra12Test + mojarra2Test + latestDepTest { + extendsFrom mojarra2Test + dirName = 'mojarra2LatestTest' + } +} + +test.dependsOn mojarra12Test, mojarra2Test + +dependencies { + compileOnly "javax.faces:jsf-api:1.2" + + implementation project(':instrumentation:jsf:jsf-common:library') + + testImplementation project(':instrumentation:jsf:jsf-testing-common') + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + mojarra12TestImplementation "javax.faces:jsf-impl:1.2-20" + mojarra12TestImplementation "javax.faces:jsf-api:1.2" + mojarra12TestImplementation "com.sun.facelets:jsf-facelets:1.1.14" + + mojarra2TestImplementation "org.glassfish:jakarta.faces:2.3.12" + + latestDepTestImplementation "org.glassfish:jakarta.faces:2.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/ActionListenerImplInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/ActionListenerImplInstrumentation.java new file mode 100644 index 000000000..0c7d3962a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/ActionListenerImplInstrumentation.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mojarra; + +import static io.opentelemetry.javaagent.instrumentation.mojarra.MojarraTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import javax.faces.event.ActionEvent; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ActionListenerImplInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.sun.faces.application.ActionListenerImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("processAction"), + ActionListenerImplInstrumentation.class.getName() + "$ProcessActionAdvice"); + } + + @SuppressWarnings("unused") + public static class ProcessActionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) ActionEvent event, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startSpan(event); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/MojarraInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/MojarraInstrumentationModule.java new file mode 100644 index 000000000..1842f3b10 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/MojarraInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mojarra; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class MojarraInstrumentationModule extends InstrumentationModule { + public MojarraInstrumentationModule() { + super("mojarra", "mojarra-1.2"); + } + + @Override + public List typeInstrumentations() { + return asList(new ActionListenerImplInstrumentation(), new RestoreViewPhaseInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/MojarraTracer.java b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/MojarraTracer.java new file mode 100644 index 000000000..42ff5dbec --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/MojarraTracer.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mojarra; + +import io.opentelemetry.instrumentation.jsf.JsfTracer; + +public class MojarraTracer extends JsfTracer { + private static final MojarraTracer TRACER = new MojarraTracer(); + + public static MojarraTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.mojarra-1.2"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/RestoreViewPhaseInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/RestoreViewPhaseInstrumentation.java new file mode 100644 index 000000000..09a65590c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/RestoreViewPhaseInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mojarra; + +import static io.opentelemetry.javaagent.instrumentation.mojarra.MojarraTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.faces.context.FacesContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RestoreViewPhaseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.sun.faces.lifecycle.RestoreViewPhase"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(takesArgument(0, named("javax.faces.context.FacesContext"))), + RestoreViewPhaseInstrumentation.class.getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Argument(0) FacesContext facesContext) { + tracer().updateServerSpanName(Java8BytecodeBridge.currentContext(), facesContext); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra12Test/groovy/Mojarra12Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra12Test/groovy/Mojarra12Test.groovy new file mode 100644 index 000000000..d47c830ed --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra12Test/groovy/Mojarra12Test.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Mojarra12Test extends BaseJsfTest { + @Override + String getJsfVersion() { + "1.2" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra12Test/resources/test-app-1.2-extra/META-INF/web-fragment.xml b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra12Test/resources/test-app-1.2-extra/META-INF/web-fragment.xml new file mode 100644 index 000000000..a5224a421 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra12Test/resources/test-app-1.2-extra/META-INF/web-fragment.xml @@ -0,0 +1,9 @@ + + + + + com.sun.faces.config.ConfigureListener + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra2LatestTest/groovy/Mojarra2LatestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra2LatestTest/groovy/Mojarra2LatestTest.groovy new file mode 100644 index 000000000..8cff7cd58 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra2LatestTest/groovy/Mojarra2LatestTest.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Mojarra2LatestTest extends BaseJsfTest { + @Override + String getJsfVersion() { + "2" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra2Test/groovy/Mojarra2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra2Test/groovy/Mojarra2Test.groovy new file mode 100644 index 000000000..1097bb1e6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/mojarra-1.2/javaagent/src/mojarra2Test/groovy/Mojarra2Test.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Mojarra2Test extends BaseJsfTest { + @Override + String getJsfVersion() { + "2" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/myfaces-1.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/myfaces-1.2-javaagent.gradle new file mode 100644 index 000000000..da21b5553 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/myfaces-1.2-javaagent.gradle @@ -0,0 +1,43 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +muzzle { + pass { + group = "org.apache.myfaces.core" + module = "myfaces-impl" + versions = "[1.2,3)" + extraDependency "jakarta.el:jakarta.el-api:3.0.3" + assertInverse = true + } +} + +testSets { + myfaces12Test + myfaces2Test + latestDepTest { + extendsFrom myfaces2Test + dirName = 'myfaces2LatestTest' + } +} + +test.dependsOn myfaces12Test, myfaces2Test + +dependencies { + compileOnly "org.apache.myfaces.core:myfaces-api:1.2.12" + compileOnly "javax.el:el-api:1.0" + + implementation project(':instrumentation:jsf:jsf-common:library') + + testImplementation project(':instrumentation:jsf:jsf-testing-common') + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + myfaces12TestImplementation "org.apache.myfaces.core:myfaces-impl:1.2.12" + myfaces12TestImplementation "com.sun.facelets:jsf-facelets:1.1.14" + + myfaces2TestImplementation "org.apache.myfaces.core:myfaces-impl:2.3.2" + myfaces2TestImplementation "javax.xml.bind:jaxb-api:2.2.11" + myfaces2TestImplementation "com.sun.xml.bind:jaxb-impl:2.2.11" + + latestDepTestImplementation "org.apache.myfaces.core:myfaces-impl:2.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/ActionListenerImplInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/ActionListenerImplInstrumentation.java new file mode 100644 index 000000000..e0d475674 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/ActionListenerImplInstrumentation.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.myfaces; + +import static io.opentelemetry.javaagent.instrumentation.myfaces.MyFacesTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import javax.faces.event.ActionEvent; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ActionListenerImplInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.myfaces.application.ActionListenerImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("processAction"), + ActionListenerImplInstrumentation.class.getName() + "$ProcessActionAdvice"); + } + + @SuppressWarnings("unused") + public static class ProcessActionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) ActionEvent event, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startSpan(event); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/MyFacesInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/MyFacesInstrumentationModule.java new file mode 100644 index 000000000..075fcbee3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/MyFacesInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.myfaces; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class MyFacesInstrumentationModule extends InstrumentationModule { + public MyFacesInstrumentationModule() { + super("myfaces", "myfaces-1.2"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ActionListenerImplInstrumentation(), new RestoreViewExecutorInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/MyFacesTracer.java b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/MyFacesTracer.java new file mode 100644 index 000000000..3634213f1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/MyFacesTracer.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.myfaces; + +import io.opentelemetry.instrumentation.jsf.JsfTracer; +import javax.el.ELException; + +public class MyFacesTracer extends JsfTracer { + private static final MyFacesTracer TRACER = new MyFacesTracer(); + + public static MyFacesTracer tracer() { + return TRACER; + } + + @Override + protected Throwable unwrapThrowable(Throwable throwable) { + throwable = super.unwrapThrowable(throwable); + while (throwable.getCause() != null && throwable instanceof ELException) { + throwable = throwable.getCause(); + } + return throwable; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.myfaces-1.2"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/RestoreViewExecutorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/RestoreViewExecutorInstrumentation.java new file mode 100644 index 000000000..5ea36213f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/RestoreViewExecutorInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.myfaces; + +import static io.opentelemetry.javaagent.instrumentation.myfaces.MyFacesTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.faces.context.FacesContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RestoreViewExecutorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.myfaces.lifecycle.RestoreViewExecutor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(takesArgument(0, named("javax.faces.context.FacesContext"))), + RestoreViewExecutorInstrumentation.class.getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Argument(0) FacesContext facesContext) { + tracer().updateServerSpanName(Java8BytecodeBridge.currentContext(), facesContext); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces12Test/groovy/Myfaces12Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces12Test/groovy/Myfaces12Test.groovy new file mode 100644 index 000000000..bc532d1a2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces12Test/groovy/Myfaces12Test.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Myfaces12Test extends BaseJsfTest { + @Override + String getJsfVersion() { + "1.2" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces12Test/resources/test-app-1.2-extra/META-INF/web-fragment.xml b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces12Test/resources/test-app-1.2-extra/META-INF/web-fragment.xml new file mode 100644 index 000000000..0e19025d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces12Test/resources/test-app-1.2-extra/META-INF/web-fragment.xml @@ -0,0 +1,15 @@ + + + + + + org.apache.myfaces.ERROR_HANDLING + false + + + + org.apache.myfaces.webapp.StartupServletContextListener + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2LatestTest/groovy/Myfaces2LatestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2LatestTest/groovy/Myfaces2LatestTest.groovy new file mode 100644 index 000000000..e443f4d00 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2LatestTest/groovy/Myfaces2LatestTest.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Myfaces2LatestTest extends BaseJsfTest { + @Override + String getJsfVersion() { + "2" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2LatestTest/resources/test-app-2-extra/META-INF/web-fragment.xml b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2LatestTest/resources/test-app-2-extra/META-INF/web-fragment.xml new file mode 100644 index 000000000..1095e6ad5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2LatestTest/resources/test-app-2-extra/META-INF/web-fragment.xml @@ -0,0 +1,10 @@ + + + + + + org.apache.myfaces.webapp.StartupServletContextListener + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2Test/groovy/Myfaces2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2Test/groovy/Myfaces2Test.groovy new file mode 100644 index 000000000..bd08b6975 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2Test/groovy/Myfaces2Test.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Myfaces2Test extends BaseJsfTest { + @Override + String getJsfVersion() { + "2" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2Test/resources/test-app-2-extra/META-INF/web-fragment.xml b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2Test/resources/test-app-2-extra/META-INF/web-fragment.xml new file mode 100644 index 000000000..1095e6ad5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsf/myfaces-1.2/javaagent/src/myfaces2Test/resources/test-app-2-extra/META-INF/web-fragment.xml @@ -0,0 +1,10 @@ + + + + + + org.apache.myfaces.webapp.StartupServletContextListener + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/jsp-2.3-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/jsp-2.3-javaagent.gradle new file mode 100644 index 000000000..29b3af3e6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/jsp-2.3-javaagent.gradle @@ -0,0 +1,47 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.tomcat" + module = "tomcat-jasper" + // version range [7.0.0,7.0.19) is missing from maven + // tomcat 10 uses JSP 3.0 + versions = "[7.0.19,10)" + // version 8.0.9 depends on org.eclipse.jdt.core.compiler:ecj:4.4RC4 which does not exist + skip('8.0.9') + } +} + +dependencies { + // compiling against tomcat 7.0.20 because there seems to be some issues with Tomcat's dependency < 7.0.20 + compileOnly "org.apache.tomcat:tomcat-jasper:7.0.20" + compileOnly "javax.servlet.jsp:javax.servlet.jsp-api:2.3.0" + compileOnly "javax.servlet:javax.servlet-api:3.1.0" + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + // using tomcat 7.0.37 because there seems to be some issues with Tomcat's jar scanning in versions < 7.0.37 + // https://stackoverflow.com/questions/23484098/org-apache-tomcat-util-bcel-classfile-classformatexception-invalid-byte-tag-in + testLibrary "org.apache.tomcat.embed:tomcat-embed-core:7.0.37" + testLibrary "org.apache.tomcat.embed:tomcat-embed-logging-juli:7.0.37" + testLibrary "org.apache.tomcat.embed:tomcat-embed-jasper:7.0.37" + + latestDepTestLibrary "javax.servlet.jsp:javax.servlet.jsp-api:+" + latestDepTestLibrary "javax.servlet:javax.servlet-api:+" + latestDepTestLibrary "org.apache.tomcat.embed:tomcat-embed-core:9.+" + latestDepTestLibrary "org.apache.tomcat.embed:tomcat-embed-jasper:9.+" + latestDepTestLibrary "org.apache.tomcat.embed:tomcat-embed-logging-juli:9.+" +} + +tasks.withType(Test).configureEach { + // skip jar scanning using environment variables: + // http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html#JAR_Scanning + // having this set allows us to test with old versions of the tomcat api since + // JarScanFilter did not exist in the tomcat 7 api + jvmArgs '-Dorg.apache.catalina.startup.ContextConfig.jarsToSkip=*' + jvmArgs '-Dorg.apache.catalina.startup.TldConfig.jarsToSkip=*' + + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.jsp.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/HttpJspPageInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/HttpJspPageInstrumentation.java new file mode 100644 index 000000000..6a1ddf761 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/HttpJspPageInstrumentation.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jsp.JspTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import javax.servlet.http.HttpServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HttpJspPageInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.servlet.jsp.HttpJspPage"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("javax.servlet.jsp.HttpJspPage")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("_jspService") + .and(takesArgument(0, named("javax.servlet.http.HttpServletRequest"))) + .and(takesArgument(1, named("javax.servlet.http.HttpServletResponse"))) + .and(isPublic()), + HttpJspPageInstrumentation.class.getName() + "$HttpJspPageAdvice"); + } + + @SuppressWarnings("unused") + public static class HttpJspPageAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) HttpServletRequest req, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startSpan(tracer().spanNameOnRender(req), SpanKind.INTERNAL); + tracer().onRender(context, req); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspCompilationContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspCompilationContextInstrumentation.java new file mode 100644 index 000000000..91f91cee6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspCompilationContextInstrumentation.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import static io.opentelemetry.javaagent.instrumentation.jsp.JspTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.jasper.JspCompilationContext; + +public class JspCompilationContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.jasper.JspCompilationContext"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("compile").and(takesArguments(0)).and(isPublic()), + JspCompilationContextInstrumentation.class.getName() + "$JasperJspCompilationContext"); + } + + public static class JasperJspCompilationContext { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This JspCompilationContext jspCompilationContext, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = + tracer().startSpan(tracer().spanNameOnCompile(jspCompilationContext), SpanKind.INTERNAL); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.This JspCompilationContext jspCompilationContext, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + // Decorate on return because additional properties are available + tracer().onCompile(context, jspCompilationContext); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationModule.java new file mode 100644 index 000000000..c4ec781d3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JspInstrumentationModule extends InstrumentationModule { + public JspInstrumentationModule() { + super("jsp"); + } + + @Override + public List typeInstrumentations() { + return asList(new HttpJspPageInstrumentation(), new JspCompilationContextInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspTracer.java b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspTracer.java new file mode 100644 index 000000000..a13bbcc41 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspTracer.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import java.net.URI; +import java.net.URISyntaxException; +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.jsp.HttpJspPage; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.compiler.Compiler; +import org.slf4j.LoggerFactory; + +public class JspTracer extends BaseTracer { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.jsp.experimental-span-attributes", false); + + private static final JspTracer TRACER = new JspTracer(); + + public static JspTracer tracer() { + return TRACER; + } + + public String spanNameOnCompile(JspCompilationContext jspCompilationContext) { + return jspCompilationContext == null + ? "Compile" + : "Compile " + jspCompilationContext.getJspFile(); + } + + public void onCompile(Context context, JspCompilationContext jspCompilationContext) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES && jspCompilationContext != null) { + Span span = Span.fromContext(context); + Compiler compiler = jspCompilationContext.getCompiler(); + if (compiler != null) { + span.setAttribute("jsp.compiler", compiler.getClass().getName()); + } + span.setAttribute("jsp.classFQCN", jspCompilationContext.getFQCN()); + } + } + + public String spanNameOnRender(HttpServletRequest req) { + // get the JSP file name being rendered in an include action + Object includeServletPath = req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + String spanName = req.getServletPath(); + if (includeServletPath instanceof String) { + spanName = includeServletPath.toString(); + } + return "Render " + spanName; + } + + public void onRender(Context context, HttpServletRequest req) { + if (!CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + return; + } + Span span = Span.fromContext(context); + + Object forwardOrigin = req.getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH); + if (forwardOrigin instanceof String) { + span.setAttribute("jsp.forwardOrigin", forwardOrigin.toString()); + } + + // add the request URL as a tag to provide better context when looking at spans produced by + // actions. Tomcat 9 has relative path symbols in the value returned from + // HttpServletRequest#getRequestURL(), + // normalizing the URL should remove those symbols for readability and consistency + try { + span.setAttribute( + "jsp.requestURL", new URI(req.getRequestURL().toString()).normalize().toString()); + } catch (URISyntaxException e) { + LoggerFactory.getLogger(HttpJspPage.class) + .warn("Failed to get and normalize request URL: " + e.getMessage()); + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.jsp-2.3"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationBasicTests.groovy b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationBasicTests.groovy new file mode 100644 index 000000000..7b5e96205 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationBasicTests.groovy @@ -0,0 +1,479 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpMethod +import io.opentelemetry.testing.internal.armeria.common.MediaType +import io.opentelemetry.testing.internal.armeria.common.RequestHeaders +import java.nio.file.Files +import org.apache.catalina.Context +import org.apache.catalina.startup.Tomcat +import org.apache.jasper.JasperException +import spock.lang.Shared +import spock.lang.Unroll + +//TODO should this be HttpServerTest? +class JspInstrumentationBasicTests extends AgentInstrumentationSpecification { + + @Shared + int port + @Shared + Tomcat tomcatServer + @Shared + Context appContext + @Shared + String jspWebappContext = "jsptest-context" + + @Shared + File baseDir + @Shared + String baseUrl + + @Shared + WebClient client + + def setupSpec() { + baseDir = Files.createTempDirectory("jsp").toFile() + baseDir.deleteOnExit() + + port = PortUtils.findOpenPort() + + tomcatServer = new Tomcat() + tomcatServer.setBaseDir(baseDir.getAbsolutePath()) + tomcatServer.setPort(port) + tomcatServer.getConnector() + // comment to debug + tomcatServer.setSilent(true) + // this is needed in tomcat 9, this triggers the creation of a connector, will not + // affect tomcat 7 and 8 + // https://stackoverflow.com/questions/48998387/code-works-with-embedded-apache-tomcat-8-but-not-with-9-whats-changed + tomcatServer.getConnector() + baseUrl = "http://localhost:$port/$jspWebappContext" + client = WebClient.of(baseUrl) + + appContext = tomcatServer.addWebapp("/$jspWebappContext", + JspInstrumentationBasicTests.getResource("/webapps/jsptest").getPath()) + + tomcatServer.start() + System.out.println( + "Tomcat server: http://" + tomcatServer.getHost().getName() + ":" + port + "/") + } + + def cleanupSpec() { + tomcatServer.stop() + tomcatServer.destroy() + } + + @Unroll + def "non-erroneous GET #test test"() { + when: + AggregatedHttpResponse res = client.get("/${jspFileName}").aggregate().join() + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + hasNoParent() + name "/$jspWebappContext/$jspFileName" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/$jspFileName" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /$jspFileName" + attributes { + "jsp.classFQCN" "org.apache.jsp.$jspClassNamePrefix$jspClassName" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /$jspFileName" + attributes { + "jsp.requestURL" "${baseUrl}/${jspFileName}" + } + } + } + } + res.status().code() == 200 + + where: + test | jspFileName | jspClassName | jspClassNamePrefix + "no java jsp" | "nojava.jsp" | "nojava_jsp" | "" + "basic loop jsp" | "common/loop.jsp" | "loop_jsp" | "common." + "invalid HTML markup" | "invalidMarkup.jsp" | "invalidMarkup_jsp" | "" + } + + def "non-erroneous GET with query string"() { + setup: + String queryString = "HELLO" + + when: + AggregatedHttpResponse res = client.get("/getQuery.jsp?${queryString}").aggregate().join() + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + hasNoParent() + name "/$jspWebappContext/getQuery.jsp" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/getQuery.jsp?$queryString" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /getQuery.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.getQuery_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /getQuery.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/getQuery.jsp" + } + } + } + } + res.status().code() == 200 + } + + def "non-erroneous POST"() { + setup: + RequestHeaders headers = RequestHeaders.builder(HttpMethod.POST, "/post.jsp") + .contentType(MediaType.FORM_DATA) + .build() + + when: + AggregatedHttpResponse res = client.execute(headers, "name=world").aggregate().join() + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + hasNoParent() + name "/$jspWebappContext/post.jsp" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/post.jsp" + "${SemanticAttributes.HTTP_METHOD.key}" "POST" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /post.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.post_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /post.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/post.jsp" + } + } + } + } + res.status().code() == 200 + } + + @Unroll + def "erroneous runtime errors GET jsp with #test test"() { + when: + AggregatedHttpResponse res = client.get("/${jspFileName}").aggregate().join() + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + hasNoParent() + name "/$jspWebappContext/$jspFileName" + kind SERVER + status ERROR + event(0) { + eventName(SemanticAttributes.EXCEPTION_EVENT_NAME) + attributes { + "${SemanticAttributes.EXCEPTION_TYPE.key}" { String tagExceptionType -> + return tagExceptionType == exceptionClass.getName() || tagExceptionType.contains(exceptionClass.getSimpleName()) + } + "${SemanticAttributes.EXCEPTION_MESSAGE.key}" { String tagErrorMsg -> + return errorMessageOptional || tagErrorMsg instanceof String + } + "${SemanticAttributes.EXCEPTION_STACKTRACE.key}" String + } + } + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/$jspFileName" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 500 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /$jspFileName" + attributes { + "jsp.classFQCN" "org.apache.jsp.$jspClassName" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /$jspFileName" + status ERROR + event(0) { + eventName(SemanticAttributes.EXCEPTION_EVENT_NAME) + attributes { + "${SemanticAttributes.EXCEPTION_TYPE.key}" { String tagExceptionType -> + return tagExceptionType == exceptionClass.getName() || tagExceptionType.contains(exceptionClass.getSimpleName()) + } + "${SemanticAttributes.EXCEPTION_MESSAGE.key}" { String tagErrorMsg -> + return errorMessageOptional || tagErrorMsg instanceof String + } + "${SemanticAttributes.EXCEPTION_STACKTRACE.key}" String + } + } + attributes { + "jsp.requestURL" "${baseUrl}/${jspFileName}" + } + } + } + } + res.status().code() == 500 + + where: + test | jspFileName | jspClassName | exceptionClass | errorMessageOptional + "java runtime error" | "runtimeError.jsp" | "runtimeError_jsp" | ArithmeticException | false + "invalid write" | "invalidWrite.jsp" | "invalidWrite_jsp" | IndexOutOfBoundsException | true + "missing query gives null" | "getQuery.jsp" | "getQuery_jsp" | NullPointerException | true + } + + def "non-erroneous include plain HTML GET"() { + when: + AggregatedHttpResponse res = client.get("/includes/includeHtml.jsp").aggregate().join() + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + hasNoParent() + name "/$jspWebappContext/includes/includeHtml.jsp" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/includes/includeHtml.jsp" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /includes/includeHtml.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.includes.includeHtml_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /includes/includeHtml.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/includes/includeHtml.jsp" + } + } + } + } + res.status().code() == 200 + } + + def "non-erroneous multi GET"() { + when: + AggregatedHttpResponse res = client.get("/includes/includeMulti.jsp").aggregate().join() + + then: + assertTraces(1) { + trace(0, 7) { + span(0) { + hasNoParent() + name "/$jspWebappContext/includes/includeMulti.jsp" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/includes/includeMulti.jsp" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /includes/includeMulti.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.includes.includeMulti_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /includes/includeMulti.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/includes/includeMulti.jsp" + } + } + span(3) { + childOf span(2) + name "Compile /common/javaLoopH2.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.common.javaLoopH2_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(4) { + childOf span(2) + name "Render /common/javaLoopH2.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/includes/includeMulti.jsp" + } + } + span(5) { + childOf span(2) + name "Compile /common/javaLoopH2.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.common.javaLoopH2_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(6) { + childOf span(2) + name "Render /common/javaLoopH2.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/includes/includeMulti.jsp" + } + } + } + } + res.status().code() == 200 + } + + def "#test compile error should not produce render traces and spans"() { + when: + AggregatedHttpResponse res = client.get("/${jspFileName}").aggregate().join() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + hasNoParent() + name "/$jspWebappContext/$jspFileName" + kind SERVER + status ERROR + errorEvent(JasperException, String) + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/$jspFileName" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 500 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /$jspFileName" + status ERROR + errorEvent(JasperException, String) + attributes { + "jsp.classFQCN" "org.apache.jsp.$jspClassNamePrefix$jspClassName" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + } + } + res.status().code() == 500 + + where: + test | jspFileName | jspClassName | jspClassNamePrefix + "normal" | "compileError.jsp" | "compileError_jsp" | "" + "forward" | "forwards/forwardWithCompileError.jsp" | "forwardWithCompileError_jsp" | "forwards." + } + + def "direct static file reference"() { + when: + AggregatedHttpResponse res = client.get("/${staticFile}").aggregate().join() + + then: + res.status().code() == 200 + assertTraces(1) { + trace(0, 1) { + span(0) { + hasNoParent() + name "/$jspWebappContext/*" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/$staticFile" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + } + } + + where: + staticFile = "common/hello.html" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationForwardTests.groovy b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationForwardTests.groovy new file mode 100644 index 000000000..7f7a2ab42 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationForwardTests.groovy @@ -0,0 +1,445 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import java.nio.file.Files +import org.apache.catalina.Context +import org.apache.catalina.startup.Tomcat +import org.apache.jasper.JasperException +import spock.lang.Shared +import spock.lang.Unroll + +class JspInstrumentationForwardTests extends AgentInstrumentationSpecification { + + @Shared + int port + @Shared + Tomcat tomcatServer + @Shared + Context appContext + @Shared + String jspWebappContext = "jsptest-context" + + @Shared + File baseDir + @Shared + String baseUrl + + @Shared + WebClient client + + def setupSpec() { + baseDir = Files.createTempDirectory("jsp").toFile() + baseDir.deleteOnExit() + + port = PortUtils.findOpenPort() + + tomcatServer = new Tomcat() + tomcatServer.setBaseDir(baseDir.getAbsolutePath()) + tomcatServer.setPort(port) + tomcatServer.getConnector() + // comment to debug + tomcatServer.setSilent(true) + // this is needed in tomcat 9, this triggers the creation of a connector, will not + // affect tomcat 7 and 8 + // https://stackoverflow.com/questions/48998387/code-works-with-embedded-apache-tomcat-8-but-not-with-9-whats-changed + tomcatServer.getConnector() + + baseUrl = "http://localhost:$port/$jspWebappContext" + client = WebClient.of(baseUrl) + + appContext = tomcatServer.addWebapp("/$jspWebappContext", + JspInstrumentationForwardTests.getResource("/webapps/jsptest").getPath()) + + tomcatServer.start() + System.out.println( + "Tomcat server: http://" + tomcatServer.getHost().getName() + ":" + port + "/") + } + + def cleanupSpec() { + tomcatServer.stop() + tomcatServer.destroy() + } + + @Unroll + def "non-erroneous GET forward to #forwardTo"() { + when: + AggregatedHttpResponse res = client.get("/$forwardFromFileName").aggregate().join() + + then: + assertTraces(1) { + trace(0, 5) { + span(0) { + hasNoParent() + name "/$jspWebappContext/$forwardFromFileName" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/$forwardFromFileName" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /$forwardFromFileName" + attributes { + "jsp.classFQCN" "org.apache.jsp.$jspForwardFromClassPrefix$jspForwardFromClassName" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /$forwardFromFileName" + attributes { + "jsp.requestURL" "${baseUrl}/$forwardFromFileName" + } + } + span(3) { + childOf span(2) + name "Compile /$forwardDestFileName" + attributes { + "jsp.classFQCN" "org.apache.jsp.$jspForwardDestClassPrefix$jspForwardDestClassName" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(4) { + childOf span(2) + name "Render /$forwardDestFileName" + attributes { + "jsp.forwardOrigin" "/$forwardFromFileName" + "jsp.requestURL" "${baseUrl}/$forwardDestFileName" + } + } + } + } + res.status().code() == 200 + + where: + forwardTo | forwardFromFileName | forwardDestFileName | jspForwardFromClassName | jspForwardFromClassPrefix | jspForwardDestClassName | jspForwardDestClassPrefix + "no java jsp" | "forwards/forwardToNoJavaJsp.jsp" | "nojava.jsp" | "forwardToNoJavaJsp_jsp" | "forwards." | "nojava_jsp" | "" + "normal java jsp" | "forwards/forwardToSimpleJava.jsp" | "common/loop.jsp" | "forwardToSimpleJava_jsp" | "forwards." | "loop_jsp" | "common." + } + + def "non-erroneous GET forward to plain HTML"() { + when: + AggregatedHttpResponse res = client.get("/forwards/forwardToHtml.jsp").aggregate().join() + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + hasNoParent() + name "/$jspWebappContext/forwards/forwardToHtml.jsp" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/forwards/forwardToHtml.jsp" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /forwards/forwardToHtml.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.forwards.forwardToHtml_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /forwards/forwardToHtml.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/forwards/forwardToHtml.jsp" + } + } + } + } + res.status().code() == 200 + } + + def "non-erroneous GET forwarded to jsp with multiple includes"() { + when: + AggregatedHttpResponse res = client.get("/forwards/forwardToIncludeMulti.jsp").aggregate().join() + + then: + assertTraces(1) { + trace(0, 9) { + span(0) { + hasNoParent() + name "/$jspWebappContext/forwards/forwardToIncludeMulti.jsp" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/forwards/forwardToIncludeMulti.jsp" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /forwards/forwardToIncludeMulti.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.forwards.forwardToIncludeMulti_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /forwards/forwardToIncludeMulti.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/forwards/forwardToIncludeMulti.jsp" + } + } + span(3) { + childOf span(2) + name "Compile /includes/includeMulti.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.includes.includeMulti_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(4) { + childOf span(2) + name "Render /includes/includeMulti.jsp" + attributes { + "jsp.forwardOrigin" "/forwards/forwardToIncludeMulti.jsp" + "jsp.requestURL" "${baseUrl}/includes/includeMulti.jsp" + } + } + span(5) { + childOf span(4) + name "Compile /common/javaLoopH2.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.common.javaLoopH2_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(6) { + childOf span(4) + name "Render /common/javaLoopH2.jsp" + attributes { + "jsp.forwardOrigin" "/forwards/forwardToIncludeMulti.jsp" + "jsp.requestURL" "${baseUrl}/includes/includeMulti.jsp" + } + } + span(7) { + childOf span(4) + name "Compile /common/javaLoopH2.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.common.javaLoopH2_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(8) { + childOf span(4) + name "Render /common/javaLoopH2.jsp" + attributes { + "jsp.forwardOrigin" "/forwards/forwardToIncludeMulti.jsp" + "jsp.requestURL" "${baseUrl}/includes/includeMulti.jsp" + } + } + } + } + res.status().code() == 200 + } + + def "non-erroneous GET forward to another forward (2 forwards)"() { + when: + AggregatedHttpResponse res = client.get("/forwards/forwardToJspForward.jsp").aggregate().join() + + then: + assertTraces(1) { + trace(0, 7) { + span(0) { + hasNoParent() + name "/$jspWebappContext/forwards/forwardToJspForward.jsp" + kind SERVER + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/forwards/forwardToJspForward.jsp" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /forwards/forwardToJspForward.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.forwards.forwardToJspForward_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /forwards/forwardToJspForward.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/forwards/forwardToJspForward.jsp" + } + } + span(3) { + childOf span(2) + name "Compile /forwards/forwardToSimpleJava.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.forwards.forwardToSimpleJava_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(4) { + childOf span(2) + name "Render /forwards/forwardToSimpleJava.jsp" + attributes { + "jsp.forwardOrigin" "/forwards/forwardToJspForward.jsp" + "jsp.requestURL" "${baseUrl}/forwards/forwardToSimpleJava.jsp" + } + } + span(5) { + childOf span(4) + name "Compile /common/loop.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.common.loop_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(6) { + childOf span(4) + name "Render /common/loop.jsp" + attributes { + "jsp.forwardOrigin" "/forwards/forwardToJspForward.jsp" + "jsp.requestURL" "${baseUrl}/common/loop.jsp" + } + } + } + } + res.status().code() == 200 + } + + def "forward to jsp with compile error should not produce a 2nd render span"() { + when: + AggregatedHttpResponse res = client.get("/forwards/forwardToCompileError.jsp").aggregate().join() + + then: + assertTraces(1) { + trace(0, 4) { + span(0) { + hasNoParent() + name "/$jspWebappContext/forwards/forwardToCompileError.jsp" + kind SERVER + status ERROR + errorEvent(JasperException, String) + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/forwards/forwardToCompileError.jsp" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 500 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /forwards/forwardToCompileError.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.forwards.forwardToCompileError_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /forwards/forwardToCompileError.jsp" + status ERROR + errorEvent(JasperException, String) + attributes { + "jsp.requestURL" "${baseUrl}/forwards/forwardToCompileError.jsp" + } + } + span(3) { + childOf span(2) + name "Compile /compileError.jsp" + status ERROR + errorEvent(JasperException, String) + attributes { + "jsp.classFQCN" "org.apache.jsp.compileError_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + } + } + res.status().code() == 500 + } + + def "forward to non existent jsp should be 404"() { + when: + AggregatedHttpResponse res = client.get("/forwards/forwardToNonExistent.jsp").aggregate().join() + + then: + assertTraces(1) { + trace(0, 4) { + span(0) { + hasNoParent() + name "/$jspWebappContext/forwards/forwardToNonExistent.jsp" + kind SERVER + status ERROR + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/$jspWebappContext/forwards/forwardToNonExistent.jsp" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 404 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + childOf span(0) + name "Compile /forwards/forwardToNonExistent.jsp" + attributes { + "jsp.classFQCN" "org.apache.jsp.forwards.forwardToNonExistent_jsp" + "jsp.compiler" "org.apache.jasper.compiler.JDTCompiler" + } + } + span(2) { + childOf span(0) + name "Render /forwards/forwardToNonExistent.jsp" + attributes { + "jsp.requestURL" "${baseUrl}/forwards/forwardToNonExistent.jsp" + } + } + span(3) { + childOf span(2) + name "ResponseFacade.sendError" + } + } + } + res.status().code() == 404 + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/hello.html b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/hello.html new file mode 100644 index 000000000..48dd2eff1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/hello.html @@ -0,0 +1,9 @@ + + + PLAIN HTML + + + +

HELLO!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/javaLoopH2.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/javaLoopH2.jsp new file mode 100644 index 000000000..7e0bc4f98 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/javaLoopH2.jsp @@ -0,0 +1,7 @@ +<% + for (int i = 0; i < 3; ++i) { +%> +

number:<%= i %>

+<% + } +%> diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/loop.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/loop.jsp new file mode 100644 index 000000000..a5e43b7fe --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/loop.jsp @@ -0,0 +1,12 @@ + + BASIC JSP + + <% + for (int i = 0; i < 3; ++i) { + %> +

number:<%= i %>

+ <% + } + %> + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/compileError.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/compileError.jsp new file mode 100644 index 000000000..1929ec35d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/compileError.jsp @@ -0,0 +1,9 @@ + +COMPILE ERROR JSP + + <% + FakeClassThatDontExist thingyWithNoSemiColon = abcd + %> +

This will fail

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToCompileError.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToCompileError.jsp new file mode 100644 index 000000000..bdda3e77e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToCompileError.jsp @@ -0,0 +1,9 @@ + + + + FORWARD TO JSP WITH COMPILE ERROR + + +

BYE!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToHtml.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToHtml.jsp new file mode 100644 index 000000000..41be4e892 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToHtml.jsp @@ -0,0 +1,10 @@ + + + + FORWARD TO PLAIN HTML + + + +

BYE!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToIncludeMulti.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToIncludeMulti.jsp new file mode 100644 index 000000000..8c75e2b13 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToIncludeMulti.jsp @@ -0,0 +1,10 @@ + + + + FORWARD TO JSP WITH MULTIPLE INCLUDES + + + +

BYE!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToJspForward.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToJspForward.jsp new file mode 100644 index 000000000..e0a95a450 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToJspForward.jsp @@ -0,0 +1,10 @@ + + + + FORWARD TO ANOTHER JSP FORWARD THAT FORWARDS TO SIMPLE JAVA + + + +

BYE!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToNoJavaJsp.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToNoJavaJsp.jsp new file mode 100644 index 000000000..ae33b8f76 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToNoJavaJsp.jsp @@ -0,0 +1,9 @@ + + + + FORWARD TO NO JAVA + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToNonExistent.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToNonExistent.jsp new file mode 100644 index 000000000..9e3361ea6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToNonExistent.jsp @@ -0,0 +1,10 @@ + + + + FORWARD TO NON EXISTENT FILE + + + +

BYE!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToSimpleJava.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToSimpleJava.jsp new file mode 100644 index 000000000..624df742b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardToSimpleJava.jsp @@ -0,0 +1,10 @@ + + + + FORWARD TO SIMPLE JAVA + + + +

BYE!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardWithCompileError.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardWithCompileError.jsp new file mode 100644 index 000000000..6af29abab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/forwards/forwardWithCompileError.jsp @@ -0,0 +1,12 @@ + + + + FORWARD WITH COMPILE ERROR + + <% + FakeNonExistentClass fec = new FakeNonExistentClass() + %> + +

BYE!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/getQuery.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/getQuery.jsp new file mode 100644 index 000000000..34fd8ab77 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/getQuery.jsp @@ -0,0 +1,14 @@ + +GET QUERY JSP + + <% + String query = request.getQueryString(); + %> +

<%= query %>

+ <% + if (query.equals("HELLO")) { + out.print("WORLD"); + } + %> + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/includes/includeHtml.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/includes/includeHtml.jsp new file mode 100644 index 000000000..d8899502b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/includes/includeHtml.jsp @@ -0,0 +1,12 @@ + + + INCLUDE HTML JSP + + + +
+

INCLUDE HTML

+ +
+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/includes/includeMulti.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/includes/includeMulti.jsp new file mode 100644 index 000000000..ecd914a29 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/includes/includeMulti.jsp @@ -0,0 +1,13 @@ + + + MULTIPLE INCLUDE ACTION JSP + + + +
+

INCLUDE MULTI

+ + +
+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/invalidMarkup.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/invalidMarkup.jsp new file mode 100644 index 000000000..4df6452f9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/invalidMarkup.jsp @@ -0,0 +1,12 @@ + +INVALID MARKUP JSP +<body> + <% + for (int i = 0; i < 3; ++i) { + %> + <h2>number:<%= i %></h2><p></p> + <% + } + %> +</boody> +</html> diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/invalidWrite.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/invalidWrite.jsp new file mode 100644 index 000000000..d4506c49d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/invalidWrite.jsp @@ -0,0 +1,8 @@ +<html> + <head><title>RUNTIME ERROR JSP: INVALID WRITE + + <% + response.getWriter().write("hello world", 0, 2147483647); + %> + + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/nojava.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/nojava.jsp new file mode 100644 index 000000000..9eff4b79e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/nojava.jsp @@ -0,0 +1,6 @@ + + NO JAVA JSP + +

there's no java code here

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/post.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/post.jsp new file mode 100644 index 000000000..09de84aad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/post.jsp @@ -0,0 +1,6 @@ + + POST JSP + +

Hello <%= request.getParameter("name") %>!

+ + diff --git a/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/runtimeError.jsp b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/runtimeError.jsp new file mode 100644 index 000000000..de8390479 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/runtimeError.jsp @@ -0,0 +1,9 @@ + + RUNTIME ERROR JSP: DIVISION BY ZERO + +

This will fail ...

+ <% + int k = 9 / 0; + %> + + diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/kafka-clients-0.11-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/kafka-clients-0.11-javaagent.gradle new file mode 100644 index 000000000..00277d397 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/kafka-clients-0.11-javaagent.gradle @@ -0,0 +1,52 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.kafka" + module = "kafka-clients" + versions = "[0.11.0.0,)" + assertInverse = true + } +} + +dependencies { + library "org.apache.kafka:kafka-clients:0.11.0.0" + + testLibrary "org.springframework.kafka:spring-kafka:1.3.3.RELEASE" + testLibrary "org.springframework.kafka:spring-kafka-test:1.3.3.RELEASE" + testImplementation "javax.xml.bind:jaxb-api:2.2.3" + testLibrary "org.assertj:assertj-core" + testImplementation "org.mockito:mockito-core" + + // Include latest version of kafka itself along with latest version of client libs. + // This seems to help with jar compatibility hell. + latestDepTestLibrary "org.apache.kafka:kafka_2.11:2.3.+" + // (Pinning to 2.3.x: 2.4.0 introduces an error when executing compileLatestDepTestGroovy) + // Caused by: java.lang.NoClassDefFoundError: org.I0Itec.zkclient.ZkClient + latestDepTestLibrary "org.apache.kafka:kafka-clients:2.3.+" + latestDepTestLibrary "org.springframework.kafka:spring-kafka:2.2.+" + latestDepTestLibrary "org.springframework.kafka:spring-kafka-test:2.2.+" + // assertj-core:3.20.0 is incompatible with spring-kafka-test:2.7.2 + latestDepTestLibrary "org.assertj:assertj-core:3.19.0" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.kafka.experimental-span-attributes=true" +} +test { + filter { + excludeTestsMatching 'KafkaClientPropagationDisabledTest' + } +} +test.finalizedBy(tasks.register("testPropagationDisabled", Test) { + filter { + includeTestsMatching 'KafkaClientPropagationDisabledTest' + } + jvmArgs "-Dotel.instrumentation.kafka.client-propagation.enabled=false" +}) + +// Requires old version of AssertJ for baseline +if (!testLatestDeps) { + configurations.testRuntimeClasspath.resolutionStrategy.force "org.assertj:assertj-core:2.9.1" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaClientsConfig.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaClientsConfig.java new file mode 100644 index 000000000..a1b5afada --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaClientsConfig.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import io.opentelemetry.instrumentation.api.config.Config; + +public final class KafkaClientsConfig { + + private static final boolean CLIENT_PROPAGATION_ENABLED = + Config.get() + .getBooleanProperty("otel.instrumentation.kafka.client-propagation.enabled", true); + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.kafka.experimental-span-attributes", false); + + public static boolean isPropagationEnabled() { + return CLIENT_PROPAGATION_ENABLED; + } + + public static boolean captureExperimentalSpanAttributes() { + return CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES; + } + + private KafkaClientsConfig() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaClientsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaClientsInstrumentationModule.java new file mode 100644 index 000000000..26ab13581 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaClientsInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class KafkaClientsInstrumentationModule extends InstrumentationModule { + public KafkaClientsInstrumentationModule() { + super("kafka-clients", "kafka-clients-0.11", "kafka"); + } + + @Override + public List typeInstrumentations() { + return asList(new KafkaConsumerInstrumentation(), new KafkaProducerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaConsumerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaConsumerInstrumentation.java new file mode 100644 index 000000000..8ece6a7a5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaConsumerInstrumentation.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.KafkaConsumerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Iterator; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.kafka.clients.consumer.ConsumerRecord; + +public class KafkaConsumerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.kafka.clients.consumer.ConsumerRecords"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("records")) + .and(takesArgument(0, String.class)) + .and(returns(Iterable.class)), + KafkaConsumerInstrumentation.class.getName() + "$IterableAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("records")) + .and(takesArgument(0, named("org.apache.kafka.common.TopicPartition"))) + .and(returns(List.class)), + KafkaConsumerInstrumentation.class.getName() + "$ListAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("iterator")) + .and(takesArguments(0)) + .and(returns(Iterator.class)), + KafkaConsumerInstrumentation.class.getName() + "$IteratorAdvice"); + } + + @SuppressWarnings("unused") + public static class IterableAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void wrap( + @Advice.Return(readOnly = false) Iterable> iterable) { + if (iterable != null) { + iterable = new TracingIterable(iterable, tracer()); + } + } + } + + @SuppressWarnings("unused") + public static class ListAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void wrap(@Advice.Return(readOnly = false) List> iterable) { + if (iterable != null) { + iterable = new TracingList(iterable, tracer()); + } + } + } + + @SuppressWarnings("unused") + public static class IteratorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void wrap( + @Advice.Return(readOnly = false) Iterator> iterator) { + if (iterator != null) { + iterator = new TracingIterator(iterator, tracer()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaConsumerTracer.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaConsumerTracer.java new file mode 100644 index 000000000..93a338d84 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaConsumerTracer.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.TextMapExtractAdapter.GETTER; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.concurrent.TimeUnit; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.record.TimestampType; + +public class KafkaConsumerTracer extends BaseTracer { + private static final KafkaConsumerTracer TRACER = new KafkaConsumerTracer(); + + public static KafkaConsumerTracer tracer() { + return TRACER; + } + + public Context startSpan(ConsumerRecord record) { + long now = System.currentTimeMillis(); + + Context parentContext = extractParent(record); + Span span = + spanBuilder(parentContext, spanNameOnConsume(record), CONSUMER) + .setStartTimestamp(now, TimeUnit.MILLISECONDS) + .setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "kafka") + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION, record.topic()) + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic") + .setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process") + .setAttribute( + SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + (long) record.serializedValueSize()) + .startSpan(); + + onConsume(span, now, record); + return withConsumerSpan(parentContext, span); + } + + private Context extractParent(ConsumerRecord record) { + if (KafkaClientsConfig.isPropagationEnabled()) { + return extract(record.headers(), GETTER); + } else { + return Context.current(); + } + } + + public String spanNameOnConsume(ConsumerRecord record) { + return record.topic() + " process"; + } + + public void onConsume(Span span, long startTimeMillis, ConsumerRecord record) { + // TODO should we set topic + offset as messaging.message_id? + span.setAttribute(SemanticAttributes.MESSAGING_KAFKA_PARTITION, record.partition()); + if (record.value() == null) { + span.setAttribute(SemanticAttributes.MESSAGING_KAFKA_TOMBSTONE, true); + } + + if (KafkaClientsConfig.captureExperimentalSpanAttributes()) { + span.setAttribute("kafka.offset", record.offset()); + + // don't record a duration if the message was sent from an old Kafka client + if (record.timestampType() != TimestampType.NO_TIMESTAMP_TYPE) { + long produceTime = record.timestamp(); + // this attribute shows how much time elapsed between the producer and the consumer of this + // message, which can be helpful for identifying queue bottlenecks + span.setAttribute( + "kafka.record.queue_time_ms", Math.max(0L, startTimeMillis - produceTime)); + } + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.kafka-clients-0.11"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaProducerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaProducerInstrumentation.java new file mode 100644 index 000000000..5fb1bef29 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaProducerInstrumentation.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.KafkaProducerTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.TextMapInjectAdapter.SETTER; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.kafka.clients.ApiVersions; +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.ProducerRecord; + +public class KafkaProducerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.kafka.clients.producer.KafkaProducer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("send")) + .and(takesArgument(0, named("org.apache.kafka.clients.producer.ProducerRecord"))) + .and(takesArgument(1, named("org.apache.kafka.clients.producer.Callback"))), + KafkaProducerInstrumentation.class.getName() + "$ProducerAdvice"); + } + + @SuppressWarnings("unused") + public static class ProducerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue("apiVersions") ApiVersions apiVersions, + @Advice.Argument(value = 0, readOnly = false) ProducerRecord record, + @Advice.Argument(value = 1, readOnly = false) Callback callback, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = Java8BytecodeBridge.currentContext(); + + context = tracer().startProducerSpan(parentContext, record); + + callback = new ProducerCallback(callback, parentContext, context); + + if (tracer().shouldPropagate(apiVersions)) { + try { + tracer().inject(context, record.headers(), SETTER); + } catch (IllegalStateException e) { + // headers must be read-only from reused record. try again with new one. + record = + new ProducerRecord<>( + record.topic(), + record.partition(), + record.timestamp(), + record.key(), + record.value(), + record.headers()); + + tracer().inject(context, record.headers(), SETTER); + } + } + + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + // span finished by ProducerCallback + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaProducerTracer.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaProducerTracer.java new file mode 100644 index 000000000..c5e6e9044 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/KafkaProducerTracer.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; + +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.kafka.clients.ApiVersions; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.record.RecordBatch; + +public class KafkaProducerTracer extends BaseTracer { + private static final KafkaProducerTracer TRACER = new KafkaProducerTracer(); + + public static KafkaProducerTracer tracer() { + return TRACER; + } + + public Context startProducerSpan(Context parentContext, ProducerRecord record) { + SpanBuilder span = spanBuilder(parentContext, spanNameOnProduce(record), PRODUCER); + onProduce(span, record); + return parentContext.with(span.startSpan()); + } + + // Do not inject headers for batch versions below 2 + // This is how similar check is being done in Kafka client itself: + // https://github.com/apache/kafka/blob/05fcfde8f69b0349216553f711fdfc3f0259c601/clients/src/main/java/org/apache/kafka/common/record/MemoryRecordsBuilder.java#L411-L412 + // Also, do not inject headers if specified by JVM option or environment variable + // This can help in mixed client environments where clients < 0.11 that do not support + // headers attempt to read messages that were produced by clients > 0.11 and the magic + // value of the broker(s) is >= 2 + public boolean shouldPropagate(ApiVersions apiVersions) { + return apiVersions.maxUsableProduceMagic() >= RecordBatch.MAGIC_VALUE_V2 + && KafkaClientsConfig.isPropagationEnabled(); + } + + public String spanNameOnProduce(ProducerRecord record) { + return record.topic() + " send"; + } + + public void onProduce(SpanBuilder span, ProducerRecord record) { + span.setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "kafka"); + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic"); + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION, record.topic()); + + Integer partition = record.partition(); + if (partition != null) { + span.setAttribute(SemanticAttributes.MESSAGING_KAFKA_PARTITION, partition.longValue()); + } + if (record.value() == null) { + span.setAttribute(SemanticAttributes.MESSAGING_KAFKA_TOMBSTONE, true); + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.kafka-clients-0.11"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/ProducerCallback.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/ProducerCallback.java new file mode 100644 index 000000000..cd46d16ea --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/ProducerCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.KafkaProducerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.RecordMetadata; + +public class ProducerCallback implements Callback { + private final Callback callback; + private final Context parentContext; + private final Context context; + + public ProducerCallback(Callback callback, Context parentContext, Context context) { + this.callback = callback; + this.parentContext = parentContext; + this.context = context; + } + + @Override + public void onCompletion(RecordMetadata metadata, Exception exception) { + if (exception != null) { + tracer().endExceptionally(context, exception); + } else { + tracer().end(context); + } + + if (callback != null) { + try (Scope ignored = parentContext.makeCurrent()) { + callback.onCompletion(metadata, exception); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TextMapExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TextMapExtractAdapter.java new file mode 100644 index 000000000..324550787 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TextMapExtractAdapter.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.kafka.common.header.Header; +import org.apache.kafka.common.header.Headers; + +public class TextMapExtractAdapter implements TextMapGetter { + + public static final TextMapExtractAdapter GETTER = new TextMapExtractAdapter(); + + @Override + public Iterable keys(Headers headers) { + return StreamSupport.stream(headers.spliterator(), false) + .map(Header::key) + .collect(Collectors.toList()); + } + + @Override + public String get(Headers headers, String key) { + Header header = headers.lastHeader(key); + if (header == null) { + return null; + } + byte[] value = header.value(); + if (value == null) { + return null; + } + return new String(value, StandardCharsets.UTF_8); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TextMapInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TextMapInjectAdapter.java new file mode 100644 index 000000000..9664065b8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TextMapInjectAdapter.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import io.opentelemetry.context.propagation.TextMapSetter; +import java.nio.charset.StandardCharsets; +import org.apache.kafka.common.header.Headers; + +public class TextMapInjectAdapter implements TextMapSetter { + + public static final TextMapInjectAdapter SETTER = new TextMapInjectAdapter(); + + @Override + public void set(Headers headers, String key, String value) { + headers.remove(key).add(key, value.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingIterable.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingIterable.java new file mode 100644 index 000000000..e181af7d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingIterable.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import java.util.Iterator; +import org.apache.kafka.clients.consumer.ConsumerRecord; + +public class TracingIterable implements Iterable> { + private final Iterable> delegate; + private final KafkaConsumerTracer tracer; + private boolean firstIterator = true; + + public TracingIterable(Iterable> delegate, KafkaConsumerTracer tracer) { + this.delegate = delegate; + this.tracer = tracer; + } + + @Override + public Iterator> iterator() { + Iterator> it; + // We should only return one iterator with tracing. + // However, this is not thread-safe, but usually the first (hopefully only) traversal of + // ConsumerRecords is performed in the same thread that called poll() + if (firstIterator) { + it = new TracingIterator(delegate.iterator(), tracer); + firstIterator = false; + } else { + it = delegate.iterator(); + } + + return it; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingIterator.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingIterator.java new file mode 100644 index 000000000..0b4fdf9dc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingIterator.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.Iterator; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TracingIterator implements Iterator> { + + private static final Logger log = LoggerFactory.getLogger(TracingIterator.class); + + private final Iterator> delegateIterator; + private final KafkaConsumerTracer tracer; + + /** + * Note: this may potentially create problems if this iterator is used from different threads. But + * at the moment we cannot do much about this. + */ + @Nullable private Context currentContext; + + @Nullable private Scope currentScope; + + public TracingIterator( + Iterator> delegateIterator, KafkaConsumerTracer tracer) { + this.delegateIterator = delegateIterator; + this.tracer = tracer; + } + + @Override + public boolean hasNext() { + closeScopeAndEndSpan(); + return delegateIterator.hasNext(); + } + + @Override + public ConsumerRecord next() { + // in case they didn't call hasNext()... + closeScopeAndEndSpan(); + + ConsumerRecord next = delegateIterator.next(); + + if (next != null) { + currentContext = tracer.startSpan(next); + currentScope = currentContext.makeCurrent(); + } + return next; + } + + private void closeScopeAndEndSpan() { + if (currentScope != null) { + currentScope.close(); + currentScope = null; + tracer.end(currentContext); + currentContext = null; + } + } + + @Override + public void remove() { + delegateIterator.remove(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingList.java b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingList.java new file mode 100644 index 000000000..6b0b4154f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/TracingList.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients; + +import java.util.Collection; +import java.util.List; +import java.util.ListIterator; +import org.apache.kafka.clients.consumer.ConsumerRecord; + +public class TracingList extends TracingIterable implements List> { + private final List> delegate; + + public TracingList(List> delegate, KafkaConsumerTracer tracer) { + super(delegate, tracer); + this.delegate = delegate; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return delegate.contains(o); + } + + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return delegate.toArray(a); + } + + @Override + public boolean add(ConsumerRecord consumerRecord) { + return delegate.add(consumerRecord); + } + + @Override + public void add(int index, ConsumerRecord element) { + delegate.add(index, element); + } + + @Override + public boolean remove(Object o) { + return delegate.remove(o); + } + + @Override + public ConsumerRecord remove(int index) { + return delegate.remove(index); + } + + @Override + public boolean containsAll(Collection c) { + return delegate.containsAll(c); + } + + @Override + public boolean addAll(Collection> c) { + return delegate.addAll(c); + } + + @Override + public boolean addAll(int index, Collection> c) { + return delegate.addAll(index, c); + } + + @Override + public boolean removeAll(Collection c) { + return delegate.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return delegate.retainAll(c); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public ConsumerRecord get(int index) { + // TODO: should this be instrumented as well? + return delegate.get(index); + } + + @Override + public ConsumerRecord set(int index, ConsumerRecord element) { + return delegate.set(index, element); + } + + @Override + public int indexOf(Object o) { + return delegate.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return delegate.lastIndexOf(o); + } + + @Override + public ListIterator> listIterator() { + // TODO: the API for ListIterator is not really good to instrument it in context of Kafka + // Consumer so we will not do that for now + return delegate.listIterator(); + } + + @Override + public ListIterator> listIterator(int index) { + // TODO: the API for ListIterator is not really good to instrument it in context of Kafka + // Consumer so we will not do that for now + return delegate.listIterator(index); + } + + @Override + public List> subList(int fromIndex, int toIndex) { + // TODO: the API for subList is not really good to instrument it in context of Kafka + // Consumer so we will not do that for now + // Kafka is essentially a sequential commit log. We should only enable tracing when traversing + // sequentially with an iterator + return delegate.subList(fromIndex, toIndex); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientBaseTest.groovy b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientBaseTest.groovy new file mode 100644 index 000000000..c52a5832b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientBaseTest.groovy @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.junit.Rule +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.listener.KafkaMessageListenerContainer +import org.springframework.kafka.listener.MessageListener +import org.springframework.kafka.test.rule.KafkaEmbedded +import org.springframework.kafka.test.utils.ContainerTestUtils +import org.springframework.kafka.test.utils.KafkaTestUtils +import spock.lang.Unroll + +abstract class KafkaClientBaseTest extends AgentInstrumentationSpecification { + + protected static final SHARED_TOPIC = "shared.topic" + + private static final boolean propagationEnabled = Boolean.parseBoolean( + System.getProperty("otel.instrumentation.kafka.client-propagation.enabled", "true")) + + @Rule + KafkaEmbedded embeddedKafka = new KafkaEmbedded(1, true, SHARED_TOPIC) + + @Unroll + def "test kafka client header propagation manual config"() { + setup: + def senderProps = KafkaTestUtils.senderProps(embeddedKafka.getBrokersAsString()) + def producerFactory = new DefaultKafkaProducerFactory(senderProps) + def kafkaTemplate = new KafkaTemplate(producerFactory) + + // set up the Kafka consumer properties + def consumerProperties = KafkaTestUtils.consumerProps("sender", "false", embeddedKafka) + + // create a Kafka consumer factory + def consumerFactory = new DefaultKafkaConsumerFactory(consumerProperties) + + // set the topic that needs to be consumed + def containerProperties = containerProperties() + + // create a Kafka MessageListenerContainer + def container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties) + + // create a thread safe queue to store the received message + def records = new LinkedBlockingQueue>() + + // setup a Kafka message listener + container.setupMessageListener(new MessageListener() { + @Override + void onMessage(ConsumerRecord record) { + records.add(record) + } + }) + + // start the container and underlying message listener + container.start() + + // wait until the container has the required number of assigned partitions + ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic()) + + when: + String message = "Testing without headers" + kafkaTemplate.send(SHARED_TOPIC, message) + + then: + // check that the message was received + def received = records.poll(5, TimeUnit.SECONDS) + + received.headers().iterator().hasNext() == propagationEnabled + + cleanup: + producerFactory.stop() + container?.stop() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientPropagationDisabledTest.groovy b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientPropagationDisabledTest.groovy new file mode 100644 index 000000000..b48a4b1db --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientPropagationDisabledTest.groovy @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.listener.KafkaMessageListenerContainer +import org.springframework.kafka.listener.MessageListener +import org.springframework.kafka.test.utils.ContainerTestUtils +import org.springframework.kafka.test.utils.KafkaTestUtils + +class KafkaClientPropagationDisabledTest extends KafkaClientBaseTest { + + def "should not read remote context when consuming messages if propagation is disabled"() { + setup: + def senderProps = KafkaTestUtils.senderProps(embeddedKafka.getBrokersAsString()) + def producerFactory = new DefaultKafkaProducerFactory(senderProps) + def kafkaTemplate = new KafkaTemplate(producerFactory) + + when: "send message" + String message = "Testing without headers" + kafkaTemplate.send(SHARED_TOPIC, message) + + then: "producer span is created" + assertTraces(1) { + trace(0, 1) { + span(0) { + name SHARED_TOPIC + " send" + kind PRODUCER + hasNoParent() + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + } + } + } + } + + when: "read message without context propagation" + // create a thread safe queue to store the received message + def records = new LinkedBlockingQueue>() + KafkaMessageListenerContainer container = startConsumer("consumer-without-propagation", records) + + then: "independent consumer span is created" + // check that the message was received + records.poll(5, TimeUnit.SECONDS) != null + + assertTraces(2) { + trace(0, 1) { + span(0) { + name SHARED_TOPIC + " send" + kind PRODUCER + hasNoParent() + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + } + } + } + trace(1, 1) { + span(0) { + name SHARED_TOPIC + " process" + kind CONSUMER + hasNoParent() + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + "kafka.offset" 0 + "kafka.record.queue_time_ms" { it >= 0 } + } + } + } + + } + + cleanup: + producerFactory.stop() + container?.stop() + } + + protected KafkaMessageListenerContainer startConsumer(String groupId, records) { + // set up the Kafka consumer properties + Map consumerProperties = KafkaTestUtils.consumerProps(groupId, "false", embeddedKafka) + consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + + // create a Kafka consumer factory + def consumerFactory = new DefaultKafkaConsumerFactory(consumerProperties) + + // set the topic that needs to be consumed + def containerProperties = containerProperties() + + // create a Kafka MessageListenerContainer + def container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties) + + // setup a Kafka message listener + container.setupMessageListener(new MessageListener() { + @Override + void onMessage(ConsumerRecord record) { + records.add(record) + } + }) + + // start the container and underlying message listener + container.start() + + // wait until the container has the required number of assigned partitions + ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic()) + container + } + + + def containerProperties() { + try { + // Different class names for test and latestDepTest. + return Class.forName("org.springframework.kafka.listener.config.ContainerProperties").newInstance(SHARED_TOPIC) + } catch (ClassNotFoundException | NoClassDefFoundError e) { + return Class.forName("org.springframework.kafka.listener.ContainerProperties").newInstance(SHARED_TOPIC) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientPropagationEnabledTest.groovy b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientPropagationEnabledTest.groovy new file mode 100644 index 000000000..0a5be69ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-clients-0.11/javaagent/src/test/groovy/KafkaClientPropagationEnabledTest.groovy @@ -0,0 +1,403 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.clients.consumer.KafkaConsumer +import org.apache.kafka.clients.producer.KafkaProducer +import org.apache.kafka.clients.producer.Producer +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.common.TopicPartition +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.listener.KafkaMessageListenerContainer +import org.springframework.kafka.listener.MessageListener +import org.springframework.kafka.test.utils.ContainerTestUtils +import org.springframework.kafka.test.utils.KafkaTestUtils + +class KafkaClientPropagationEnabledTest extends KafkaClientBaseTest { + + def "test kafka produce and consume"() { + setup: + def senderProps = KafkaTestUtils.senderProps(embeddedKafka.getBrokersAsString()) + Producer producer = new KafkaProducer<>(senderProps, new StringSerializer(), new StringSerializer()) + + // set up the Kafka consumer properties + def consumerProperties = KafkaTestUtils.consumerProps("sender", "false", embeddedKafka) + + // create a Kafka consumer factory + def consumerFactory = new DefaultKafkaConsumerFactory(consumerProperties) + + // set the topic that needs to be consumed + def containerProperties = containerProperties() + + // create a Kafka MessageListenerContainer + def container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties) + + // create a thread safe queue to store the received message + def records = new LinkedBlockingQueue>() + + // setup a Kafka message listener + container.setupMessageListener(new MessageListener() { + @Override + void onMessage(ConsumerRecord record) { + waitForTraces(1) // ensure consistent ordering of traces + records.add(record) + } + }) + + // start the container and underlying message listener + container.start() + + // wait until the container has the required number of assigned partitions + ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic()) + + when: + String greeting = "Hello Spring Kafka Sender!" + runUnderTrace("parent") { + producer.send(new ProducerRecord(SHARED_TOPIC, greeting)) { meta, ex -> + if (ex == null) { + runUnderTrace("producer callback") {} + } else { + runUnderTrace("producer exception: " + ex) {} + } + } + } + + then: + // check that the message was received + def received = records.poll(5, TimeUnit.SECONDS) + received.value() == greeting + received.key() == null + + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "parent") + span(1) { + name SHARED_TOPIC + " send" + kind PRODUCER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + } + } + span(2) { + name SHARED_TOPIC + " process" + kind CONSUMER + childOf span(1) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + "kafka.offset" 0 + "kafka.record.queue_time_ms" { it >= 0 } + } + } + basicSpan(it, 3, "producer callback", span(0)) + } + } + + cleanup: + producer.close() + container?.stop() + } + + def "test spring kafka template produce and consume"() { + setup: + def senderProps = KafkaTestUtils.senderProps(embeddedKafka.getBrokersAsString()) + def producerFactory = new DefaultKafkaProducerFactory(senderProps) + def kafkaTemplate = new KafkaTemplate(producerFactory) + + // set up the Kafka consumer properties + def consumerProperties = KafkaTestUtils.consumerProps("sender", "false", embeddedKafka) + + // create a Kafka consumer factory + def consumerFactory = new DefaultKafkaConsumerFactory(consumerProperties) + + // set the topic that needs to be consumed + def containerProperties = containerProperties() + + // create a Kafka MessageListenerContainer + def container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties) + + // create a thread safe queue to store the received message + def records = new LinkedBlockingQueue>() + + // setup a Kafka message listener + container.setupMessageListener(new MessageListener() { + @Override + void onMessage(ConsumerRecord record) { + records.add(record) + } + }) + + // start the container and underlying message listener + container.start() + + // wait until the container has the required number of assigned partitions + ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic()) + + when: + String greeting = "Hello Spring Kafka Sender!" + runUnderTrace("parent") { + kafkaTemplate.send(SHARED_TOPIC, greeting).addCallback({ + runUnderTrace("producer callback") {} + }, { ex -> + runUnderTrace("producer exception: " + ex) {} + }) + } + + then: + // check that the message was received + def received = records.poll(5, TimeUnit.SECONDS) + received.value() == greeting + received.key() == null + + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "parent") + span(1) { + name SHARED_TOPIC + " send" + kind PRODUCER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + } + } + span(2) { + name SHARED_TOPIC + " process" + kind CONSUMER + childOf span(1) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + "kafka.offset" 0 + "kafka.record.queue_time_ms" { it >= 0 } + } + } + basicSpan(it, 3, "producer callback", span(0)) + } + } + + cleanup: + producerFactory.stop() + container?.stop() + } + + def "test pass through tombstone"() { + setup: + def senderProps = KafkaTestUtils.senderProps(embeddedKafka.getBrokersAsString()) + def producerFactory = new DefaultKafkaProducerFactory(senderProps) + def kafkaTemplate = new KafkaTemplate(producerFactory) + + // set up the Kafka consumer properties + def consumerProperties = KafkaTestUtils.consumerProps("sender", "false", embeddedKafka) + + // create a Kafka consumer factory + def consumerFactory = new DefaultKafkaConsumerFactory(consumerProperties) + + // set the topic that needs to be consumed + def containerProperties = containerProperties() + + // create a Kafka MessageListenerContainer + def container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties) + + // create a thread safe queue to store the received message + def records = new LinkedBlockingQueue>() + + // setup a Kafka message listener + container.setupMessageListener(new MessageListener() { + @Override + void onMessage(ConsumerRecord record) { + records.add(record) + } + }) + + // start the container and underlying message listener + container.start() + + // wait until the container has the required number of assigned partitions + ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic()) + + when: + kafkaTemplate.send(SHARED_TOPIC, null) + + then: + // check that the message was received + def received = records.poll(5, TimeUnit.SECONDS) + received.value() == null + received.key() == null + + assertTraces(1) { + trace(0, 2) { + // PRODUCER span 0 + span(0) { + name SHARED_TOPIC + " send" + kind PRODUCER + hasNoParent() + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_KAFKA_TOMBSTONE.key}" true + } + } + // CONSUMER span 0 + span(1) { + name SHARED_TOPIC + " process" + kind CONSUMER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + "${SemanticAttributes.MESSAGING_KAFKA_TOMBSTONE.key}" true + "kafka.offset" 0 + "kafka.record.queue_time_ms" { it >= 0 } + } + } + } + } + + cleanup: + producerFactory.stop() + container?.stop() + } + + def "test records(TopicPartition) kafka consume"() { + setup: + + // set up the Kafka consumer properties + def kafkaPartition = 0 + def consumerProperties = KafkaTestUtils.consumerProps("sender", "false", embeddedKafka) + consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + def consumer = new KafkaConsumer(consumerProperties) + + def senderProps = KafkaTestUtils.senderProps(embeddedKafka.getBrokersAsString()) + def producer = new KafkaProducer(senderProps) + + consumer.assign(Arrays.asList(new TopicPartition(SHARED_TOPIC, kafkaPartition))) + + when: + def greeting = "Hello from MockConsumer!" + producer.send(new ProducerRecord(SHARED_TOPIC, kafkaPartition, null, greeting)) + + then: + waitForTraces(1) + def records = new LinkedBlockingQueue>() + def pollResult = KafkaTestUtils.getRecords(consumer) + + def recs = pollResult.records(new TopicPartition(SHARED_TOPIC, kafkaPartition)).iterator() + + def first = null + if (recs.hasNext()) { + first = recs.next() + } + + then: + recs.hasNext() == false + first.value() == greeting + first.key() == null + + assertTraces(1) { + trace(0, 2) { + span(0) { + name SHARED_TOPIC + " send" + kind PRODUCER + hasNoParent() + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + } + } + span(1) { + name SHARED_TOPIC + " process" + kind CONSUMER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" SHARED_TOPIC + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + "kafka.offset" 0 + "kafka.record.queue_time_ms" { it >= 0 } + } + } + } + } + + cleanup: + consumer.close() + producer.close() + } + + protected KafkaMessageListenerContainer startConsumer(String groupId, records) { + // set up the Kafka consumer properties + Map consumerProperties = KafkaTestUtils.consumerProps(groupId, "false", embeddedKafka) + consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + + // create a Kafka consumer factory + def consumerFactory = new DefaultKafkaConsumerFactory(consumerProperties) + + // set the topic that needs to be consumed + def containerProperties = containerProperties() + + // create a Kafka MessageListenerContainer + def container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties) + + // setup a Kafka message listener + container.setupMessageListener(new MessageListener() { + @Override + void onMessage(ConsumerRecord record) { + records.add(record) + } + }) + + // start the container and underlying message listener + container.start() + + // wait until the container has the required number of assigned partitions + ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic()) + container + } + + + def containerProperties() { + try { + // Different class names for test and latestDepTest. + return Class.forName("org.springframework.kafka.listener.config.ContainerProperties").newInstance(SHARED_TOPIC) + } catch (ClassNotFoundException | NoClassDefFoundError e) { + return Class.forName("org.springframework.kafka.listener.ContainerProperties").newInstance(SHARED_TOPIC) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/kafka-streams-0.11-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/kafka-streams-0.11-javaagent.gradle new file mode 100644 index 000000000..5db4e0340 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/kafka-streams-0.11-javaagent.gradle @@ -0,0 +1,46 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.kafka" + module = "kafka-streams" + versions = "[0.11.0.0,)" + } +} + +dependencies { + library "org.apache.kafka:kafka-streams:0.11.0.0" + + // Include kafka-clients instrumentation for tests. + testInstrumentation project(':instrumentation:kafka-clients-0.11:javaagent') + + testLibrary "org.apache.kafka:kafka-clients:0.11.0.0" + testLibrary "org.springframework.kafka:spring-kafka:1.3.3.RELEASE" + testLibrary "org.springframework.kafka:spring-kafka-test:1.3.3.RELEASE" + testImplementation "javax.xml.bind:jaxb-api:2.2.3" + testImplementation "org.mockito:mockito-core" + testLibrary "org.assertj:assertj-core" + + + // Include latest version of kafka itself along with latest version of client libs. + // This seems to help with jar compatibility hell. + latestDepTestLibrary "org.apache.kafka:kafka_2.11:2.3.+" + // (Pinning to 2.3.x: 2.4.0 introduces an error when executing compileLatestDepTestGroovy) + // Caused by: java.lang.NoClassDefFoundError: org.I0Itec.zkclient.ZkClient + latestDepTestLibrary "org.apache.kafka:kafka-clients:2.3.+" + latestDepTestLibrary "org.apache.kafka:kafka-streams:2.3.+" + latestDepTestLibrary "org.springframework.kafka:spring-kafka:2.2.+" + latestDepTestLibrary "org.springframework.kafka:spring-kafka-test:2.2.+" + // assertj-core:3.20.0 is incompatible with spring-kafka-test:2.7.2 + latestDepTestLibrary "org.assertj:assertj-core:3.19.0" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.kafka.experimental-span-attributes=true" +} + +// Requires old version of AssertJ for baseline +if (!testLatestDeps) { + configurations.testRuntimeClasspath.resolutionStrategy.force "org.assertj:assertj-core:2.9.1" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/ContextScopeHolder.java b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/ContextScopeHolder.java new file mode 100644 index 000000000..f95fda44a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/ContextScopeHolder.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkastreams; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class ContextScopeHolder { + public static final ThreadLocal HOLDER = new ThreadLocal<>(); + + private Context context; + private Scope scope; + + public void closeScope() { + scope.close(); + } + + public Context getContext() { + return context; + } + + public void set(Context context, Scope scope) { + this.context = context; + this.scope = scope; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsInstrumentationModule.java new file mode 100644 index 000000000..fcef833ce --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkastreams; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class KafkaStreamsInstrumentationModule extends InstrumentationModule { + public KafkaStreamsInstrumentationModule() { + super("kafka-streams", "kafka-streams-0.11", "kafka"); + } + + @Override + public List typeInstrumentations() { + return asList( + new KafkaStreamsSourceNodeRecordDeserializerInstrumentation(), + new StreamTaskStartInstrumentation(), + new StreamTaskStopInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsSourceNodeRecordDeserializerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsSourceNodeRecordDeserializerInstrumentation.java new file mode 100644 index 000000000..f1fee3cc8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsSourceNodeRecordDeserializerInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkastreams; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.record.TimestampType; + +// This is necessary because SourceNodeRecordDeserializer drops the headers. :-( +public class KafkaStreamsSourceNodeRecordDeserializerInstrumentation + implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.kafka.streams.processor.internals.SourceNodeRecordDeserializer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("deserialize")) + .and(takesArgument(0, named("org.apache.kafka.clients.consumer.ConsumerRecord"))) + .and(returns(named("org.apache.kafka.clients.consumer.ConsumerRecord"))), + KafkaStreamsSourceNodeRecordDeserializerInstrumentation.class.getName() + + "$SaveHeadersAdvice"); + } + + @SuppressWarnings("unused") + public static class SaveHeadersAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void saveHeaders( + @Advice.Argument(0) ConsumerRecord incoming, + @Advice.Return(readOnly = false) ConsumerRecord result) { + result = + new ConsumerRecord<>( + result.topic(), + result.partition(), + result.offset(), + result.timestamp(), + TimestampType.CREATE_TIME, + result.checksum(), + result.serializedKeySize(), + result.serializedValueSize(), + result.key(), + result.value(), + incoming.headers()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsTracer.java b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsTracer.java new file mode 100644 index 000000000..e2f94ceca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsTracer.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkastreams; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.javaagent.instrumentation.kafkastreams.TextMapExtractAdapter.GETTER; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.kafka.streams.processor.internals.StampedRecord; + +public class KafkaStreamsTracer extends BaseTracer { + private static final KafkaStreamsTracer TRACER = new KafkaStreamsTracer(); + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.kafka.experimental-span-attributes", false); + + public static KafkaStreamsTracer tracer() { + return TRACER; + } + + public Context startSpan(StampedRecord record) { + Context parentContext = extract(record.value.headers(), GETTER); + Span span = + spanBuilder(parentContext, spanNameForConsume(record), CONSUMER) + .setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "kafka") + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION, record.topic()) + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic") + .setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process") + .startSpan(); + onConsume(span, record); + return withConsumerSpan(parentContext, span); + } + + public String spanNameForConsume(StampedRecord record) { + if (record == null) { + return null; + } + return record.topic() + " process"; + } + + public void onConsume(Span span, StampedRecord record) { + if (record != null) { + span.setAttribute(SemanticAttributes.MESSAGING_KAFKA_PARTITION, record.partition()); + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("kafka.offset", record.offset()); + } + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.kafka-streams-0.11"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/StreamTaskStartInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/StreamTaskStartInstrumentation.java new file mode 100644 index 000000000..cf5327f97 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/StreamTaskStartInstrumentation.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkastreams; + +import static io.opentelemetry.javaagent.instrumentation.kafkastreams.ContextScopeHolder.HOLDER; +import static io.opentelemetry.javaagent.instrumentation.kafkastreams.KafkaStreamsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPackagePrivate; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.kafka.streams.processor.internals.StampedRecord; + +public class StreamTaskStartInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.kafka.streams.processor.internals.PartitionGroup"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPackagePrivate()) + .and(named("nextRecord")) + .and(returns(named("org.apache.kafka.streams.processor.internals.StampedRecord"))), + StreamTaskStartInstrumentation.class.getName() + "$StartSpanAdvice"); + } + + @SuppressWarnings("unused") + public static class StartSpanAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return StampedRecord record) { + if (record == null) { + return; + } + + ContextScopeHolder holder = HOLDER.get(); + if (holder == null) { + // somehow nextRecord() was called outside of process() + return; + } + + Context context = tracer().startSpan(record); + + holder.set(context, context.makeCurrent()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/StreamTaskStopInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/StreamTaskStopInstrumentation.java new file mode 100644 index 000000000..824f519bf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/StreamTaskStopInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkastreams; + +import static io.opentelemetry.javaagent.instrumentation.kafkastreams.ContextScopeHolder.HOLDER; +import static io.opentelemetry.javaagent.instrumentation.kafkastreams.KafkaStreamsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class StreamTaskStopInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.kafka.streams.processor.internals.StreamTask"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("process")).and(takesArguments(0)), + StreamTaskStopInstrumentation.class.getName() + "$StopSpanAdvice"); + } + + @SuppressWarnings("unused") + public static class StopSpanAdvice { + + @Advice.OnMethodEnter + public static ContextScopeHolder onEnter() { + ContextScopeHolder holder = new ContextScopeHolder(); + HOLDER.set(holder); + return holder; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter ContextScopeHolder holder, @Advice.Thrown Throwable throwable) { + HOLDER.remove(); + Context context = holder.getContext(); + if (context != null) { + holder.closeScope(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/TextMapExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/TextMapExtractAdapter.java new file mode 100644 index 000000000..9cc353618 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/TextMapExtractAdapter.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkastreams; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.kafka.common.header.Header; +import org.apache.kafka.common.header.Headers; + +public class TextMapExtractAdapter implements TextMapGetter { + + public static final TextMapExtractAdapter GETTER = new TextMapExtractAdapter(); + + @Override + public Iterable keys(Headers headers) { + return StreamSupport.stream(headers.spliterator(), false) + .map(Header::key) + .collect(Collectors.toList()); + } + + @Override + public String get(Headers headers, String key) { + Header header = headers.lastHeader(key); + if (header == null) { + return null; + } + byte[] value = header.value(); + if (value == null) { + return null; + } + return new String(value, StandardCharsets.UTF_8); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsTest.groovy new file mode 100644 index 000000000..ce2cd5993 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsTest.groovy @@ -0,0 +1,230 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator +import io.opentelemetry.context.Context +import io.opentelemetry.context.propagation.TextMapGetter +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.common.serialization.Serdes +import org.apache.kafka.streams.KafkaStreams +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.kstream.KStream +import org.apache.kafka.streams.kstream.ValueMapper +import org.junit.ClassRule +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.listener.KafkaMessageListenerContainer +import org.springframework.kafka.listener.MessageListener +import org.springframework.kafka.test.rule.KafkaEmbedded +import org.springframework.kafka.test.utils.ContainerTestUtils +import org.springframework.kafka.test.utils.KafkaTestUtils +import spock.lang.Shared + +class KafkaStreamsTest extends AgentInstrumentationSpecification { + + static final STREAM_PENDING = "test.pending" + static final STREAM_PROCESSED = "test.processed" + + @Shared + @ClassRule + KafkaEmbedded embeddedKafka = new KafkaEmbedded(1, true, STREAM_PENDING, STREAM_PROCESSED) + + def "test kafka produce and consume with streams in-between"() { + setup: + def config = new Properties() + def senderProps = KafkaTestUtils.senderProps(embeddedKafka.getBrokersAsString()) + config.putAll(senderProps) + config.put(StreamsConfig.APPLICATION_ID_CONFIG, "test-application") + config.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName()) + config.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName()) + + // CONFIGURE CONSUMER + def consumerFactory = new DefaultKafkaConsumerFactory(KafkaTestUtils.consumerProps("sender", "false", embeddedKafka)) + + def containerProperties + try { + // Different class names for test and latestDepTest. + containerProperties = Class.forName("org.springframework.kafka.listener.config.ContainerProperties").newInstance(STREAM_PROCESSED) + } catch (ClassNotFoundException | NoClassDefFoundError e) { + containerProperties = Class.forName("org.springframework.kafka.listener.ContainerProperties").newInstance(STREAM_PROCESSED) + } + def consumerContainer = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties) + + // create a thread safe queue to store the processed message + def records = new LinkedBlockingQueue>() + + // setup a Kafka message listener + consumerContainer.setupMessageListener(new MessageListener() { + @Override + void onMessage(ConsumerRecord record) { + Span.current().setAttribute("testing", 123) + records.add(record) + } + }) + + // start the container and underlying message listener + consumerContainer.start() + + // wait until the container has the required number of assigned partitions + ContainerTestUtils.waitForAssignment(consumerContainer, embeddedKafka.getPartitionsPerTopic()) + + // CONFIGURE PROCESSOR + def builder + try { + // Different class names for test and latestDepTest. + builder = Class.forName("org.apache.kafka.streams.kstream.KStreamBuilder").newInstance() + } catch (ClassNotFoundException | NoClassDefFoundError e) { + builder = Class.forName("org.apache.kafka.streams.StreamsBuilder").newInstance() + } + KStream textLines = builder.stream(STREAM_PENDING) + def values = textLines + .mapValues(new ValueMapper() { + @Override + String apply(String textLine) { + Span.current().setAttribute("asdf", "testing") + return textLine.toLowerCase() + } + }) + + KafkaStreams streams + try { + // Different api for test and latestDepTest. + values.to(Serdes.String(), Serdes.String(), STREAM_PROCESSED) + streams = new KafkaStreams(builder, config) + } catch (MissingMethodException e) { + def producer = Class.forName("org.apache.kafka.streams.kstream.Produced") + .with(Serdes.String(), Serdes.String()) + values.to(STREAM_PROCESSED, producer) + streams = new KafkaStreams(builder.build(), config) + } + streams.start() + + // CONFIGURE PRODUCER + def producerFactory = new DefaultKafkaProducerFactory(senderProps) + def kafkaTemplate = new KafkaTemplate(producerFactory) + + when: + String greeting = "TESTING TESTING 123!" + kafkaTemplate.send(STREAM_PENDING, greeting) + + then: + // check that the message was received + def received = records.poll(10, TimeUnit.SECONDS) + received.value() == greeting.toLowerCase() + received.key() == null + + assertTraces(1) { + trace(0, 5) { + // PRODUCER span 0 + span(0) { + name STREAM_PENDING + " send" + kind PRODUCER + hasNoParent() + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" STREAM_PENDING + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + } + } + // CONSUMER span 0 + span(1) { + name STREAM_PENDING + " process" + kind CONSUMER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" STREAM_PENDING + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + "kafka.offset" 0 + "kafka.record.queue_time_ms" { it >= 0 } + } + } + // STREAMING span 1 + span(2) { + name STREAM_PENDING + " process" + kind CONSUMER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" STREAM_PENDING + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + "kafka.offset" 0 + "asdf" "testing" + } + } + // STREAMING span 0 + span(3) { + name STREAM_PROCESSED + " send" + kind PRODUCER + childOf span(2) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" STREAM_PROCESSED + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + } + } + // CONSUMER span 0 + span(4) { + name STREAM_PROCESSED + " process" + kind CONSUMER + childOf span(3) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "kafka" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" STREAM_PROCESSED + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_KAFKA_PARTITION.key}" { it >= 0 } + "kafka.offset" 0 + "kafka.record.queue_time_ms" { it >= 0 } + "testing" 123 + } + } + } + } + + def headers = received.headers() + headers.iterator().hasNext() + def traceparent = new String(headers.headers("traceparent").iterator().next().value()) + Context context = W3CTraceContextPropagator.instance.extract(Context.root(), "", new TextMapGetter() { + @Override + Iterable keys(String carrier) { + return Collections.singleton("traceparent") + } + + @Override + String get(String carrier, String key) { + if (key == "traceparent") { + return traceparent + } + return null + } + }) + def spanContext = Span.fromContext(context).getSpanContext() + def streamSendSpan = traces[0][3] + spanContext.traceId == streamSendSpan.traceId + spanContext.spanId == streamSendSpan.spanId + + + cleanup: + producerFactory?.stop() + streams?.close() + consumerContainer?.stop() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/README.md b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/README.md new file mode 100644 index 000000000..773b18ec7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/README.md @@ -0,0 +1,2 @@ +Kotlin coroutine library instrumentation is located at +https://github.com/open-telemetry/opentelemetry-java/tree/master/extensions/kotlin diff --git a/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/gradle.properties b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/gradle.properties new file mode 100644 index 000000000..0d6aa7b61 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/gradle.properties @@ -0,0 +1 @@ +kotlin.stdlib.default.dependency=false diff --git a/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/kotlinx-coroutines-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/kotlinx-coroutines-javaagent.gradle new file mode 100644 index 000000000..35abaab1d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/kotlinx-coroutines-javaagent.gradle @@ -0,0 +1,34 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' +} + +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'org.jetbrains.kotlinx' + module = 'kotlinx-coroutines-core' + versions = "[1.0.0,1.3.8)" + } + // 1.3.9 (and beyond?) have changed how artifact names are resolved due to multiplatform variants + pass { + group = 'org.jetbrains.kotlinx' + module = 'kotlinx-coroutines-core-jvm' + versions = "[1.3.9,)" + } +} +dependencies { + compileOnly "io.opentelemetry:opentelemetry-extension-kotlin" + compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + testImplementation "io.opentelemetry:opentelemetry-extension-kotlin" + testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + // Use first version with flow support since we have tests for it. + testLibrary "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0" +} + +tasks.named('compileTestGroovy').configure { + //Note: look like it should be `classpath += files(sourceSets.test.kotlin.classesDirectory)` + //instead, but kotlin plugin doesn't support it (yet?) + classpath += files(compileTestKotlin.destinationDir) +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentation.java new file mode 100644 index 000000000..ae1134a6e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentation.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import kotlin.coroutines.CoroutineContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class KotlinCoroutinesInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("kotlinx.coroutines.BuildersKt"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf("launch", "launch$default") + .and(takesArgument(1, named("kotlin.coroutines.CoroutineContext"))), + this.getClass().getName() + "$LaunchAdvice"); + transformer.applyAdviceToMethod( + namedOneOf("runBlocking", "runBlocking$default") + .and(takesArgument(0, named("kotlin.coroutines.CoroutineContext"))), + this.getClass().getName() + "$RunBlockingAdvice"); + } + + @SuppressWarnings("unused") + public static class LaunchAdvice { + + @Advice.OnMethodEnter + public static void enter( + @Advice.Argument(value = 1, readOnly = false) CoroutineContext coroutineContext) { + coroutineContext = + KotlinCoroutinesInstrumentationHelper.addOpenTelemetryContext(coroutineContext); + } + } + + @SuppressWarnings("unused") + public static class RunBlockingAdvice { + + @Advice.OnMethodEnter + public static void enter( + @Advice.Argument(value = 0, readOnly = false) CoroutineContext coroutineContext) { + coroutineContext = + KotlinCoroutinesInstrumentationHelper.addOpenTelemetryContext(coroutineContext); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationHelper.java b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationHelper.java new file mode 100644 index 000000000..0f5f8e3a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationHelper.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines; + +import io.opentelemetry.context.Context; +import io.opentelemetry.extension.kotlin.ContextExtensionsKt; +import kotlin.coroutines.CoroutineContext; + +public final class KotlinCoroutinesInstrumentationHelper { + + public static CoroutineContext addOpenTelemetryContext(CoroutineContext coroutineContext) { + Context current = Context.current(); + Context inCoroutine = ContextExtensionsKt.getOpenTelemetryContext(coroutineContext); + if (current == inCoroutine) { + return coroutineContext; + } + return coroutineContext.plus(ContextExtensionsKt.asContextElement(current)); + } + + private KotlinCoroutinesInstrumentationHelper() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java new file mode 100644 index 000000000..c57276355 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class KotlinCoroutinesInstrumentationModule extends InstrumentationModule { + + public KotlinCoroutinesInstrumentationModule() { + super("kotlinx-coroutines"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.extension.kotlin."); + } + + @Override + public List typeInstrumentations() { + return singletonList(new KotlinCoroutinesInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/test/groovy/KotlinCoroutineInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/test/groovy/KotlinCoroutineInstrumentationTest.groovy new file mode 100644 index 000000000..fcf9ce792 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/test/groovy/KotlinCoroutineInstrumentationTest.groovy @@ -0,0 +1,228 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ThreadPoolDispatcherKt + +class KotlinCoroutineInstrumentationTest extends AgentInstrumentationSpecification { + + static dispatchersToTest = [ + Dispatchers.Default, + Dispatchers.IO, + Dispatchers.Unconfined, + ThreadPoolDispatcherKt.newFixedThreadPoolContext(2, "Fixed-Thread-Pool"), + ThreadPoolDispatcherKt.newSingleThreadContext("Single-Thread"), + ] + + def "kotlin traced across channels"() { + setup: + KotlinCoroutineTests kotlinTest = new KotlinCoroutineTests(dispatcher) + + when: + kotlinTest.tracedAcrossChannels() + + then: + assertTraces(1) { + trace(0, 7) { + span(0) { + name "parent" + attributes { + } + } + (0..2).each { + span("produce_$it") { + childOf span(0) + attributes { + } + } + span("consume_$it") { + childOf span(0) + attributes { + } + } + } + } + } + + where: + dispatcher << dispatchersToTest + } + + def "kotlin cancellation prevents trace"() { + setup: + KotlinCoroutineTests kotlinTest = new KotlinCoroutineTests(dispatcher) + + when: + kotlinTest.tracePreventedByCancellation() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + attributes { + } + } + span("preLaunch") { + childOf span(0) + attributes { + } + } + } + } + + where: + dispatcher << dispatchersToTest + } + + def "kotlin propagates across nested jobs"() { + setup: + KotlinCoroutineTests kotlinTest = new KotlinCoroutineTests(dispatcher) + + when: + kotlinTest.tracedAcrossThreadsWithNested() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + attributes { + } + } + span("nested") { + childOf span(0) + attributes { + } + } + } + } + + where: + dispatcher << dispatchersToTest + } + + def "kotlin either deferred completion"() { + setup: + KotlinCoroutineTests kotlinTest = new KotlinCoroutineTests(Dispatchers.Default) + + when: + kotlinTest.traceWithDeferred() + + then: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "parent" + attributes { + } + } + span("future1") { + childOf span(0) + attributes { + } + } + span("keptPromise") { + childOf span(0) + attributes { + } + } + span("keptPromise2") { + childOf span(0) + attributes { + } + } + span("brokenPromise") { + childOf span(0) + attributes { + } + } + } + } + + where: + dispatcher << dispatchersToTest + } + + def "kotlin first completed deferred"() { + setup: + KotlinCoroutineTests kotlinTest = new KotlinCoroutineTests(Dispatchers.Default) + + when: + kotlinTest.tracedWithDeferredFirstCompletions() + + then: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "parent" + attributes { + } + } + span("timeout1") { + childOf span(0) + attributes { + } + } + span("timeout2") { + childOf span(0) + attributes { + } + } + span("timeout3") { + childOf span(0) + attributes { + } + } + } + } + + where: + dispatcher << dispatchersToTest + } + + def "test concurrent suspend functions"() { + setup: + KotlinCoroutineTests kotlinTest = new KotlinCoroutineTests(Dispatchers.Default) + int numIters = 100 + HashSet seenItersA = new HashSet<>() + HashSet seenItersB = new HashSet<>() + HashSet expectedIters = new HashSet<>((0L..(numIters - 1)).toList()) + + when: + kotlinTest.launchConcurrentSuspendFunctions(numIters) + + then: + // This generates numIters each of "a calls a2" and "b calls b2" traces. Each + // trace should have a single pair of spans (a and a2) and each of those spans + // should have the same iteration number (attribute "iter"). + // The traces are in some random order, so let's keep track and make sure we see + // each iteration # exactly once + assertTraces(numIters * 2) { + for (int i = 0; i < numIters * 2; i++) { + trace(i, 2) { + boolean a = false + long iter = -1 + span(0) { + a = span.name.matches("a") + iter = span.getAttributes().get(AttributeKey.longKey("iter")) + (a ? seenItersA : seenItersB).add(iter) + name(a ? "a" : "b") + } + span(1) { + name(a ? "a2" : "b2") + childOf(span(0)) + assert span.getAttributes().get(AttributeKey.longKey("iter")) == iter + + } + } + } + } + assert seenItersA.equals(expectedIters) + assert seenItersB.equals(expectedIters) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/test/kotlin/KotlinCoroutineTests.kt b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/test/kotlin/KotlinCoroutineTests.kt new file mode 100644 index 000000000..4921187b7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kotlinx-coroutines/javaagent/src/test/kotlin/KotlinCoroutineTests.kt @@ -0,0 +1,192 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.extension.kotlin.asContextElement +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.selects.select +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.yield + +class KotlinCoroutineTests(private val dispatcher: CoroutineDispatcher) { + val tracer: Tracer = GlobalOpenTelemetry.getTracer("test") + + fun tracedAcrossChannels() = runTest { + + val producer = produce { + repeat(3) { + tracedChild("produce_$it") + send(it) + } + } + + producer.consumeAsFlow().collect { + tracedChild("consume_$it") + } + } + + fun tracePreventedByCancellation() { + + kotlin.runCatching { + runTest { + tracedChild("preLaunch") + + launch(start = CoroutineStart.UNDISPATCHED) { + throw Exception("Child Error") + } + + yield() + + tracedChild("postLaunch") + } + } + } + + fun tracedAcrossThreadsWithNested() = runTest { + val goodDeferred = async { 1 } + + launch { + goodDeferred.await() + launch { tracedChild("nested") } + } + } + + fun traceWithDeferred() = runTest { + + val keptPromise = CompletableDeferred() + val brokenPromise = CompletableDeferred() + val afterPromise = async { + keptPromise.await() + tracedChild("keptPromise") + } + val afterPromise2 = async { + keptPromise.await() + tracedChild("keptPromise2") + } + val failedAfterPromise = async { + brokenPromise + .runCatching { await() } + .onFailure { tracedChild("brokenPromise") } + } + + launch { + tracedChild("future1") + keptPromise.complete(true) + brokenPromise.completeExceptionally(IllegalStateException()) + } + + listOf(afterPromise, afterPromise2, failedAfterPromise).awaitAll() + } + + /** + * @return Number of expected spans in the trace + */ + fun tracedWithDeferredFirstCompletions() = runTest { + + val children = listOf( + async { + tracedChild("timeout1") + false + }, + async { + tracedChild("timeout2") + false + }, + async { + tracedChild("timeout3") + true + } + ) + + withTimeout(TimeUnit.SECONDS.toMillis(30)) { + select { + children.forEach { child -> + child.onAwait { it } + } + } + } + } + + fun launchConcurrentSuspendFunctions(numIters: Int) { + runBlocking { + for (i in 0 until numIters) { + GlobalScope.launch { + a(i.toLong()) + } + GlobalScope.launch { + b(i.toLong()) + } + } + } + } + + suspend fun a(iter: Long) { + var span = tracer.spanBuilder("a").startSpan() + span.setAttribute("iter", iter) + withContext(span.asContextElement()) { + delay(10) + a2(iter) + } + span.end() + } + + suspend fun a2(iter: Long) { + var span = tracer.spanBuilder("a2").startSpan() + span.setAttribute("iter", iter) + withContext(span.asContextElement()) { + delay(10) + } + span.end() + } + + suspend fun b(iter: Long) { + var span = tracer.spanBuilder("b").startSpan() + span.setAttribute("iter", iter) + withContext(span.asContextElement()) { + delay(10) + b2(iter) + } + span.end() + } + + suspend fun b2(iter: Long) { + var span = tracer.spanBuilder("b2").startSpan() + span.setAttribute("iter", iter) + withContext(span.asContextElement()) { + delay(10) + } + span.end() + } + + fun tracedChild(opName: String) { + tracer.spanBuilder(opName).startSpan().end() + } + + private fun runTest(block: suspend CoroutineScope.() -> T): T { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = parentSpan.makeCurrent() + try { + return runBlocking(dispatcher, block = block) + } finally { + parentSpan.end() + parentScope.close() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/kubernetes-client-7.0-javaagent-unit-tests.gradle b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/kubernetes-client-7.0-javaagent-unit-tests.gradle new file mode 100644 index 000000000..be04c8dbb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/kubernetes-client-7.0-javaagent-unit-tests.gradle @@ -0,0 +1,6 @@ +apply plugin: "otel.java-conventions" + +dependencies { + testImplementation project(':instrumentation:kubernetes-client-7.0:javaagent') + testImplementation "io.kubernetes:client-java-api:7.0.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/groovy/KubernetesRequestUtilsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/groovy/KubernetesRequestUtilsTest.groovy new file mode 100644 index 000000000..19b7f6b5b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/groovy/KubernetesRequestUtilsTest.groovy @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesRequestDigest +import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesResource +import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesVerb +import spock.lang.Specification + +class KubernetesRequestUtilsTest extends Specification { + def "asserting non-resource requests should work"() { + expect: + !KubernetesRequestDigest.isResourceRequest("/api") + !KubernetesRequestDigest.isResourceRequest("/apis") + !KubernetesRequestDigest.isResourceRequest("/apis/v1") + !KubernetesRequestDigest.isResourceRequest("/healthz") + !KubernetesRequestDigest.isResourceRequest("/swagger.json") + !KubernetesRequestDigest.isResourceRequest("/api/v1") + !KubernetesRequestDigest.isResourceRequest("/api/v1/") + !KubernetesRequestDigest.isResourceRequest("/apis/apps/v1") + !KubernetesRequestDigest.isResourceRequest("/apis/apps/v1/") + } + + def "asserting resource requests should work"() { + expect: + KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/foos") + KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/namespaces/default/foos") + KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces") + KubernetesRequestDigest.isResourceRequest("/api/v1/pods") + KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces/default/pods") + } + + def "parsing core resource from url-path should work"(String urlPath, String apiGroup, String apiVersion, String resource, String subResource, String namespace, String name) { + expect: + KubernetesResource.parseCoreResource(urlPath).apiGroup == apiGroup + KubernetesResource.parseCoreResource(urlPath).apiVersion == apiVersion + KubernetesResource.parseCoreResource(urlPath).resource == resource + KubernetesResource.parseCoreResource(urlPath).subResource == subResource + KubernetesResource.parseCoreResource(urlPath).namespace == namespace + KubernetesResource.parseCoreResource(urlPath).name == name + + where: + urlPath | apiGroup | apiVersion | resource | subResource | namespace | name + "/api/v1/pods" | "" | "v1" | "pods" | null | null | null + "/api/v1/namespaces/default/pods" | "" | "v1" | "pods" | null | "default" | null + "/api/v1/namespaces/default/pods/foo" | "" | "v1" | "pods" | null | "default" | "foo" + "/api/v1/namespaces/default/pods/foo/exec" | "" | "v1" | "pods" | "exec" | "default" | "foo" + } + + def "parsing regular non-core resource from url-path should work"(String urlPath, String apiGroup, String apiVersion, String resource, String subResource, String namespace, String name) { + expect: + KubernetesResource.parseRegularResource(urlPath).apiGroup == apiGroup + KubernetesResource.parseRegularResource(urlPath).apiVersion == apiVersion + KubernetesResource.parseRegularResource(urlPath).resource == resource + KubernetesResource.parseRegularResource(urlPath).subResource == subResource + KubernetesResource.parseRegularResource(urlPath).namespace == namespace + KubernetesResource.parseRegularResource(urlPath).name == name + + where: + urlPath | apiGroup | apiVersion | resource | subResource | namespace | name + "/apis/apps/v1/deployments" | "apps" | "v1" | "deployments" | null | null | null + "/apis/apps/v1/namespaces/default/deployments" | "apps" | "v1" | "deployments" | null | "default" | null + "/apis/apps/v1/namespaces/default/deployments/foo" | "apps" | "v1" | "deployments" | null | "default" | "foo" + "/apis/apps/v1/namespaces/default/deployments/foo/status" | "apps" | "v1" | "deployments" | "status" | "default" | "foo" + "/apis/example.io/v1alpha1/foos" | "example.io" | "v1alpha1" | "foos" | null | null | null + "/apis/example.io/v1alpha1/namespaces/default/foos" | "example.io" | "v1alpha1" | "foos" | null | "default" | null + "/apis/example.io/v1alpha1/namespaces/default/foos/foo" | "example.io" | "v1alpha1" | "foos" | null | "default" | "foo" + "/apis/example.io/v1alpha1/namespaces/default/foos/foo/status" | "example.io" | "v1alpha1" | "foos" | "status" | "default" | "foo" + } + + def "parsing kubernetes request verbs should work"(String httpVerb, boolean hasNamePathParam, boolean hasWatchParam, KubernetesVerb kubernetesVerb) { + expect: + KubernetesVerb.of(httpVerb, hasNamePathParam, hasWatchParam) == kubernetesVerb + + where: + httpVerb | hasNamePathParam | hasWatchParam | kubernetesVerb + "GET" | true | false | KubernetesVerb.GET + "GET" | false | true | KubernetesVerb.WATCH + "GET" | false | false | KubernetesVerb.LIST + "POST" | false | false | KubernetesVerb.CREATE + "PUT" | false | false | KubernetesVerb.UPDATE + "PATCH" | false | false | KubernetesVerb.PATCH + "DELETE" | true | false | KubernetesVerb.DELETE + "DELETE" | false | false | KubernetesVerb.DELETE_COLLECTION + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/kubernetes-client-7.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/kubernetes-client-7.0-javaagent.gradle new file mode 100644 index 000000000..d2e80c32f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/kubernetes-client-7.0-javaagent.gradle @@ -0,0 +1,23 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.kubernetes" + module = "client-java-api" + versions = "[7.0.0,)" + assertInverse = true + } +} + +dependencies { + library("io.kubernetes:client-java-api:7.0.0") + + implementation(project(':instrumentation:okhttp:okhttp-3.0:javaagent')) + + testInstrumentation(project(':instrumentation:okhttp:okhttp-3.0:javaagent')) +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.kubernetes-client.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java new file mode 100644 index 000000000..83d295075 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import static io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.kubernetes.client.openapi.ApiCallback; +import io.kubernetes.client.openapi.ApiResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import okhttp3.Call; +import okhttp3.Request; + +public class ApiClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.kubernetes.client.openapi.ApiClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isPublic().and(named("buildRequest")).and(takesArguments(10)), + this.getClass().getName() + "$BuildRequestAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(named("execute")) + .and(takesArguments(2)) + .and(takesArgument(0, named("okhttp3.Call"))), + this.getClass().getName() + "$ExecuteAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(named("executeAsync")) + .and(takesArguments(3)) + .and(takesArgument(0, named("okhttp3.Call"))) + .and(takesArgument(2, named("io.kubernetes.client.openapi.ApiCallback"))), + this.getClass().getName() + "$ExecuteAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildRequestAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return(readOnly = false) Request request) { + Context parentContext = Java8BytecodeBridge.currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + Request.Builder requestWithPropagation = request.newBuilder(); + Context context = tracer().startSpan(parentContext, request, requestWithPropagation); + CurrentContextAndScope.set(parentContext, context); + request = requestWithPropagation.build(); + } + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Return ApiResponse response, @Advice.Thrown Throwable throwable) { + Context context = CurrentContextAndScope.removeAndClose(); + if (context == null) { + return; + } + if (throwable == null) { + tracer().end(context, response); + } else { + tracer().endExceptionally(context, response, throwable); + } + } + } + + @SuppressWarnings("unused") + public static class ExecuteAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Call httpCall, + @Advice.Argument(value = 2, readOnly = false) ApiCallback callback, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + CurrentContextAndScope current = CurrentContextAndScope.remove(); + if (current != null) { + context = current.getContext(); + scope = current.getScope(); + callback = new TracingApiCallback<>(callback, current.getParentContext(), context); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + // else span will be ended in the TracingApiCallback + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/CurrentContextAndScope.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/CurrentContextAndScope.java new file mode 100644 index 000000000..1c64169da --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/CurrentContextAndScope.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Kubernetes instrumentation starts and ends spans in two different methods - there only way to + * pass {@link Scope} between them is to use a thread local. + */ +public final class CurrentContextAndScope { + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + private final Context parentContext; + private final Context context; + private final Scope scope; + + private CurrentContextAndScope(Context parentContext, Context context, Scope scope) { + this.parentContext = parentContext; + this.context = context; + this.scope = scope; + } + + public static void set(Context parentContext, Context context) { + CURRENT.set(new CurrentContextAndScope(parentContext, context, context.makeCurrent())); + } + + @Nullable + public static CurrentContextAndScope remove() { + CurrentContextAndScope contextAndScope = CURRENT.get(); + CURRENT.remove(); + return contextAndScope; + } + + @Nullable + public static Context removeAndClose() { + CurrentContextAndScope contextAndScope = remove(); + if (contextAndScope == null) { + return null; + } + contextAndScope.scope.close(); + return contextAndScope.context; + } + + public Context getParentContext() { + return parentContext; + } + + public Context getContext() { + return context; + } + + public Scope getScope() { + return scope; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientInstrumentationModule.java new file mode 100644 index 000000000..ea00adef7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class KubernetesClientInstrumentationModule extends InstrumentationModule { + + public KubernetesClientInstrumentationModule() { + super("kubernetes-client", "kubernetes-client-7.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ApiClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientTracer.java new file mode 100644 index 000000000..92643f59d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientTracer.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.ApiResponse; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import okhttp3.Request; + +public class KubernetesClientTracer + extends HttpClientTracer> { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty( + "otel.instrumentation.kubernetes-client.experimental-span-attributes", false); + + private static final KubernetesClientTracer TRACER = new KubernetesClientTracer(); + + private KubernetesClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static KubernetesClientTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.kubernetes-client-7.0"; + } + + @Override + protected String method(Request request) { + return request.method(); + } + + @Override + protected URI url(Request request) { + return request.url().uri(); + } + + @Override + protected Integer status(ApiResponse response) { + return response.getStatusCode(); + } + + @Override + protected String requestHeader(Request request, String name) { + return request.header(name); + } + + @Override + protected String responseHeader(ApiResponse response, String name) { + Map> responseHeaders = + response.getHeaders() == null ? Collections.emptyMap() : response.getHeaders(); + return responseHeaders.getOrDefault(name, Collections.emptyList()).stream() + .findFirst() + .orElse(null); + } + + @Override + protected TextMapSetter getSetter() { + return RequestBuilderInjectAdapter.SETTER; + } + + @Override + public void onException(Context context, Throwable throwable) { + super.onException(context, throwable); + if (throwable instanceof ApiException) { + int status = ((ApiException) throwable).getCode(); + if (status != 0) { + Span.fromContext(context).setAttribute(SemanticAttributes.HTTP_STATUS_CODE, status); + } + } + } + + @Override + protected void onRequest(SpanBuilder spanBuilder, Request request) { + super.onRequest(spanBuilder, request); + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + KubernetesRequestDigest digest = KubernetesRequestDigest.parse(request); + spanBuilder + .setAttribute("kubernetes-client.namespace", digest.getResourceMeta().getNamespace()) + .setAttribute("kubernetes-client.name", digest.getResourceMeta().getName()); + } + } + + @Override + protected String spanNameForRequest(Request request) { + return KubernetesRequestDigest.parse(request).toString(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java new file mode 100644 index 000000000..6a7541609 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import java.util.regex.Pattern; +import okhttp3.Request; + +class KubernetesRequestDigest { + + public static final Pattern RESOURCE_URL_PATH_PATTERN = + Pattern.compile("^/(api|apis)(/\\S+)?/v\\d\\w*/\\S+"); + + KubernetesRequestDigest( + String urlPath, + boolean isNonResourceRequest, + KubernetesResource resourceMeta, + KubernetesVerb verb) { + this.urlPath = urlPath; + this.isNonResourceRequest = isNonResourceRequest; + this.resourceMeta = resourceMeta; + this.verb = verb; + } + + public static KubernetesRequestDigest parse(Request request) { + String urlPath = request.url().encodedPath(); + if (!isResourceRequest(urlPath)) { + return nonResource(urlPath); + } + try { + KubernetesResource resourceMeta; + if (urlPath.startsWith("/api/v1")) { + resourceMeta = KubernetesResource.parseCoreResource(urlPath); + } else { + resourceMeta = KubernetesResource.parseRegularResource(urlPath); + } + + return new KubernetesRequestDigest( + urlPath, + /* isNonResourceRequest= */ false, + resourceMeta, + KubernetesVerb.of( + request.method(), hasNamePathParameter(resourceMeta), hasWatchParameter(request))); + } catch (ParseKubernetesResourceException e) { + return nonResource(urlPath); + } + } + + private static KubernetesRequestDigest nonResource(String urlPath) { + return new KubernetesRequestDigest(urlPath, /* isNonResourceRequest= */ true, null, null); + } + + public static boolean isResourceRequest(String urlPath) { + return RESOURCE_URL_PATH_PATTERN.matcher(urlPath).matches(); + } + + private static boolean hasWatchParameter(Request request) { + return !isNullOrEmpty(request.url().queryParameter("watch")); + } + + private static boolean hasNamePathParameter(KubernetesResource resource) { + return !isNullOrEmpty(resource.getName()); + } + + private final String urlPath; + private final boolean isNonResourceRequest; + + private final KubernetesResource resourceMeta; + private final KubernetesVerb verb; + + public String getUrlPath() { + return urlPath; + } + + public boolean isNonResourceRequest() { + return isNonResourceRequest; + } + + public KubernetesResource getResourceMeta() { + return resourceMeta; + } + + public KubernetesVerb getVerb() { + return verb; + } + + @Override + public String toString() { + if (isNonResourceRequest) { + return verb.value() + ' ' + urlPath; + } + + String groupVersion; + if (isNullOrEmpty(resourceMeta.getApiGroup())) { // core resource + groupVersion = ""; + } else { // regular resource + groupVersion = resourceMeta.getApiGroup() + "/" + resourceMeta.getApiVersion(); + } + + String targetResourceName; + if (isNullOrEmpty(resourceMeta.getSubResource())) { + targetResourceName = resourceMeta.getResource(); + } else { // subresource + targetResourceName = resourceMeta.getResource() + "/" + resourceMeta.getSubResource(); + } + + return verb.value() + ' ' + groupVersion + ' ' + targetResourceName; + } + + private static boolean isNullOrEmpty(String s) { + return s == null || s.isEmpty(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java new file mode 100644 index 000000000..57b10b3db --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class KubernetesResource { + + public static final Pattern CORE_RESOURCE_URL_PATH_PATTERN = + Pattern.compile( + "^/api/v1(/namespaces/(?[\\w-]+))?/(?[\\w-]+)(/(?[\\w-]+))?(/(?[\\w-]+))?"); + + public static final Pattern REGULAR_RESOURCE_URL_PATH_PATTERN = + Pattern.compile( + "^/apis/(?\\S+?)/(?\\S+?)(/namespaces/(?[\\w-]+))?/(?[\\w-]+)(/(?[\\w-]+))?(/(?[\\w-]+))?"); + + public static KubernetesResource parseCoreResource(String urlPath) + throws ParseKubernetesResourceException { + Matcher matcher = CORE_RESOURCE_URL_PATH_PATTERN.matcher(urlPath); + if (!matcher.matches()) { + throw new ParseKubernetesResourceException(); + } + return new KubernetesResource( + "", + "v1", + matcher.group("resource"), + matcher.group("subresource"), + matcher.group("namespace"), + matcher.group("name")); + } + + public static KubernetesResource parseRegularResource(String urlPath) + throws ParseKubernetesResourceException { + Matcher matcher = REGULAR_RESOURCE_URL_PATH_PATTERN.matcher(urlPath); + if (!matcher.matches()) { + throw new ParseKubernetesResourceException(); + } + return new KubernetesResource( + matcher.group("group"), + matcher.group("version"), + matcher.group("resource"), + matcher.group("subresource"), + matcher.group("namespace"), + matcher.group("name")); + } + + KubernetesResource( + String apiGroup, + String apiVersion, + String resource, + String subResource, + String namespace, + String name) { + this.apiGroup = apiGroup; + this.apiVersion = apiVersion; + this.resource = resource; + this.subResource = subResource; + this.namespace = namespace; + this.name = name; + } + + private final String apiGroup; + private final String apiVersion; + private final String resource; + private final String subResource; + + private final String namespace; + private final String name; + + public String getApiGroup() { + return apiGroup; + } + + public String getApiVersion() { + return apiVersion; + } + + public String getResource() { + return resource; + } + + public String getSubResource() { + return subResource; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesVerb.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesVerb.java new file mode 100644 index 000000000..8d65b8282 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesVerb.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +public enum KubernetesVerb { + GET("get"), + LIST("list"), + CREATE("create"), + UPDATE("update"), + DELETE("delete"), + PATCH("patch"), + WATCH("watch"), + DELETE_COLLECTION("deleteCollection"); + + private final String value; + + KubernetesVerb(String value) { + this.value = value; + } + + public static KubernetesVerb of( + String httpVerb, boolean hasNamePathParam, boolean hasWatchParam) { + if (hasWatchParam) { + return WATCH; + } + switch (httpVerb) { + case "GET": + if (!hasNamePathParam) { + return LIST; + } + return GET; + case "POST": + return CREATE; + case "PUT": + return UPDATE; + case "PATCH": + return PATCH; + case "DELETE": + if (!hasNamePathParam) { + return DELETE_COLLECTION; + } + return DELETE; + default: + throw new IllegalArgumentException("invalid HTTP verb for kubernetes client"); + } + } + + public String value() { + return value; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ParseKubernetesResourceException.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ParseKubernetesResourceException.java new file mode 100644 index 000000000..2a1b82cde --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ParseKubernetesResourceException.java @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +class ParseKubernetesResourceException extends Exception {} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/RequestBuilderInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/RequestBuilderInjectAdapter.java new file mode 100644 index 000000000..25b5ad043 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/RequestBuilderInjectAdapter.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import io.opentelemetry.context.propagation.TextMapSetter; +import okhttp3.Request; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Helper class to inject span context into request headers. + * + * @author Pavol Loffay + */ +// TODO(anuraaga): Figure out a way to avoid copying this from okhttp instrumentation. +final class RequestBuilderInjectAdapter implements TextMapSetter { + + static final RequestBuilderInjectAdapter SETTER = new RequestBuilderInjectAdapter(); + + @Override + public void set(Request.@Nullable Builder carrier, String key, String value) { + if (carrier == null) { + return; + } + carrier.header(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/TracingApiCallback.java b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/TracingApiCallback.java new file mode 100644 index 000000000..b45673ce0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/TracingApiCallback.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import static io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesClientTracer.tracer; + +import io.kubernetes.client.openapi.ApiCallback; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.ApiResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.List; +import java.util.Map; + +public class TracingApiCallback implements ApiCallback { + private final ApiCallback delegate; + private final Context parentContext; + private final Context context; + + public TracingApiCallback(ApiCallback delegate, Context parentContext, Context context) { + this.delegate = delegate; + this.parentContext = parentContext; + this.context = context; + } + + @Override + public void onFailure(ApiException e, int status, Map> headers) { + tracer().endExceptionally(context, new ApiResponse<>(status, headers), e); + if (delegate != null) { + try (Scope ignored = parentContext.makeCurrent()) { + delegate.onFailure(e, status, headers); + } + } + } + + @Override + public void onSuccess(T t, int status, Map> headers) { + tracer().end(context, new ApiResponse<>(status, headers)); + if (delegate != null) { + try (Scope ignored = parentContext.makeCurrent()) { + delegate.onSuccess(t, status, headers); + } + } + } + + @Override + public void onUploadProgress(long bytesWritten, long contentLength, boolean done) { + if (delegate != null) { + try (Scope ignored = parentContext.makeCurrent()) { + delegate.onUploadProgress(bytesWritten, contentLength, done); + } + } + } + + @Override + public void onDownloadProgress(long bytesRead, long contentLength, boolean done) { + if (delegate != null) { + try (Scope ignored = parentContext.makeCurrent()) { + delegate.onDownloadProgress(bytesRead, contentLength, done); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/test/groovy/KubernetesClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/test/groovy/KubernetesClientTest.groovy new file mode 100644 index 000000000..d530a55ff --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/kubernetes-client-7.0/javaagent/src/test/groovy/KubernetesClientTest.groovy @@ -0,0 +1,199 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.kubernetes.client.openapi.ApiCallback +import io.kubernetes.client.openapi.ApiClient +import io.kubernetes.client.openapi.ApiException +import io.kubernetes.client.openapi.apis.CoreV1Api +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.common.HttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpStatus +import io.opentelemetry.testing.internal.armeria.common.MediaType +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicReference +import spock.lang.Shared + +class KubernetesClientTest extends AgentInstrumentationSpecification { + private static final String TEST_USER_AGENT = "test-user-agent" + + @Shared + def server = new MockWebServerExtension() + + @Shared + CoreV1Api api + + def setupSpec() { + server.start() + def apiClient = new ApiClient() + apiClient.setUserAgent(TEST_USER_AGENT) + apiClient.basePath = server.httpUri().toString() + api = new CoreV1Api(apiClient) + } + + def cleanupSpec() { + server.stop() + } + + def setup() { + server.beforeTestExecution(null) + } + + def "Kubernetes span is registered on a synchronous call"() { + given: + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42")) + + when: + def response = runUnderTrace("parent") { + api.connectGetNamespacedPodProxy("name", "namespace", "path") + } + + then: + response == "42" + server.takeRequest().request().headers().get("traceparent") != null + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 200) + } + } + } + + def "Kubernetes instrumentation handles errors on a synchronous call"() { + given: + server.enqueue(HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42")) + + when: + runUnderTrace("parent") { + api.connectGetNamespacedPodProxy("name", "namespace", "path") + } + + then: + def exception = thrown(ApiException) + server.takeRequest().request().headers().get("traceparent") != null + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, exception) + apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 451, exception) + } + } + } + + def "Kubernetes span is registered on an asynchronous call"() { + given: + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42")) + + when: + def responseBody = new AtomicReference() + def latch = new CountDownLatch(1) + + runUnderTrace("parent") { + api.connectGetNamespacedPodProxyAsync("name", "namespace", "path", new ApiCallbackTemplate() { + @Override + void onSuccess(String result, int statusCode, Map> responseHeaders) { + responseBody.set(result) + latch.countDown() + runInternalSpan("callback") + } + }) + } + + then: + latch.await() + responseBody.get() == "42" + server.takeRequest().request().headers().get("traceparent") != null + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 200) + basicSpan(it, 2, "callback", span(0)) + } + } + } + + def "Kubernetes instrumentation handles errors on an asynchronous call"() { + given: + server.enqueue(HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42")) + + when: + def exception = new AtomicReference() + def latch = new CountDownLatch(1) + + runUnderTrace("parent") { + api.connectGetNamespacedPodProxyAsync("name", "namespace", "path", new ApiCallbackTemplate() { + @Override + void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + exception.set(e) + latch.countDown() + runInternalSpan("callback") + } + }) + } + + then: + latch.await() + exception.get() != null + server.takeRequest().request().headers().get("traceparent") != null + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 451, exception.get()) + basicSpan(it, 2, "callback", span(0)) + } + } + } + + private void apiClientSpan(TraceAssert trace, int index, String spanName, String url, int statusCode, Throwable exception = null) { + boolean hasFailed = exception != null + trace.span(index) { + name spanName + kind CLIENT + childOf trace.span(0) + if (hasFailed) { + status ERROR + errorEvent exception.class, exception.message + } + attributes { + "$SemanticAttributes.HTTP_URL.key" url + "$SemanticAttributes.HTTP_FLAVOR.key" "1.1" + "$SemanticAttributes.HTTP_METHOD.key" "GET" + "$SemanticAttributes.HTTP_USER_AGENT" TEST_USER_AGENT + "$SemanticAttributes.HTTP_STATUS_CODE" statusCode + "$SemanticAttributes.NET_TRANSPORT" IP_TCP + "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" + "$SemanticAttributes.NET_PEER_PORT" server.httpPort() + "kubernetes-client.namespace" "namespace" + "kubernetes-client.name" "name" + } + } + } + + static class ApiCallbackTemplate implements ApiCallback { + @Override + void onFailure(ApiException e, int statusCode, Map> responseHeaders) {} + + @Override + void onSuccess(String result, int statusCode, Map> responseHeaders) {} + + @Override + void onUploadProgress(long bytesWritten, long contentLength, boolean done) {} + + @Override + void onDownloadProgress(long bytesRead, long contentLength, boolean done) {} + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/lettuce-4.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/lettuce-4.0-javaagent.gradle new file mode 100644 index 000000000..8231e9120 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/lettuce-4.0-javaagent.gradle @@ -0,0 +1,22 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "biz.paluch.redis" + module = "lettuce" + versions = "[4.0.Final,)" + assertInverse = true + } +} + + +dependencies { + library "biz.paluch.redis:lettuce:4.0.Final" + + latestDepTestLibrary "biz.paluch.redis:lettuce:4.+" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.lettuce.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/InstrumentationPoints.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/InstrumentationPoints.java new file mode 100644 index 000000000..f0df8c03b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/InstrumentationPoints.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import static com.lambdaworks.redis.protocol.CommandKeyword.SEGFAULT; +import static com.lambdaworks.redis.protocol.CommandType.DEBUG; +import static com.lambdaworks.redis.protocol.CommandType.SHUTDOWN; +import static io.opentelemetry.javaagent.instrumentation.lettuce.v4_0.LettuceSingletons.instrumenter; + +import com.lambdaworks.redis.protocol.AsyncCommand; +import com.lambdaworks.redis.protocol.CommandType; +import com.lambdaworks.redis.protocol.ProtocolKeyword; +import com.lambdaworks.redis.protocol.RedisCommand; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.CancellationException; + +public final class InstrumentationPoints { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBoolean("otel.instrumentation.lettuce.experimental-span-attributes", false); + + private static final Set NON_INSTRUMENTING_COMMANDS = EnumSet.of(SHUTDOWN, DEBUG); + + public static void afterCommand( + RedisCommand command, + Context context, + Throwable throwable, + AsyncCommand asyncCommand) { + if (throwable != null) { + instrumenter().end(context, command, null, throwable); + } else if (expectsResponse(command)) { + asyncCommand.handleAsync( + (value, ex) -> { + if (ex instanceof CancellationException) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span span = Span.fromContext(context); + span.setAttribute("lettuce.command.cancelled", true); + } + // and don't report this as an error + ex = null; + } + instrumenter().end(context, command, null, ex); + return null; + }); + } else { + // No response is expected, so we must finish the span now. + instrumenter().end(context, command, null, null); + } + } + + /** + * Determines whether a redis command should finish its relevant span early (as soon as tags are + * added and the command is executed) because these commands have no return values/call backs, so + * we must close the span early in order to provide info for the users. + * + * @return false if the span should finish early (the command will not have a return value) + */ + public static boolean expectsResponse(RedisCommand command) { + ProtocolKeyword keyword = command.getType(); + return !(isNonInstrumentingCommand(keyword) || isNonInstrumentingKeyword(keyword)); + } + + private static boolean isNonInstrumentingCommand(ProtocolKeyword keyword) { + return keyword instanceof CommandType && NON_INSTRUMENTING_COMMANDS.contains(keyword); + } + + private static boolean isNonInstrumentingKeyword(ProtocolKeyword keyword) { + return keyword == SEGFAULT; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncCommandsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncCommandsInstrumentation.java new file mode 100644 index 000000000..d3e3cc6dc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncCommandsInstrumentation.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.lettuce.v4_0.LettuceSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.lambdaworks.redis.protocol.AsyncCommand; +import com.lambdaworks.redis.protocol.RedisCommand; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class LettuceAsyncCommandsInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.lambdaworks.redis.AbstractRedisAsyncCommands"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("dispatch")) + .and(takesArgument(0, named("com.lambdaworks.redis.protocol.RedisCommand"))), + LettuceAsyncCommandsInstrumentation.class.getName() + "$DispatchAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) RedisCommand command, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = instrumenter().start(currentContext(), command); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) RedisCommand command, + @Advice.Thrown Throwable throwable, + @Advice.Return AsyncCommand asyncCommand, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + InstrumentationPoints.afterCommand(command, context, throwable, asyncCommand); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectAttributesExtractor.java new file mode 100644 index 000000000..2eebf0f8b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectAttributesExtractor.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import com.lambdaworks.redis.RedisURI; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +final class LettuceConnectAttributesExtractor extends AttributesExtractor { + + @Override + protected void onStart(AttributesBuilder attributes, RedisURI redisUri) { + attributes.put(SemanticAttributes.DB_SYSTEM, SemanticAttributes.DbSystemValues.REDIS); + + int database = redisUri.getDatabase(); + if (database != 0) { + attributes.put(SemanticAttributes.DB_REDIS_DATABASE_INDEX, (long) database); + } + } + + @Override + protected void onEnd(AttributesBuilder attributes, RedisURI redisUri, Void unused) {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectInstrumentation.java new file mode 100644 index 000000000..6f5d55886 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectInstrumentation.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.lettuce.v4_0.LettuceSingletons.connectInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.lambdaworks.redis.RedisURI; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class LettuceConnectInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.lambdaworks.redis.RedisClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("connectStandalone")), + LettuceConnectInstrumentation.class.getName() + "$ConnectAdvice"); + } + + @SuppressWarnings("unused") + public static class ConnectAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(1) RedisURI redisUri, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = connectInstrumenter().start(currentContext(), redisUri); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Argument(1) RedisURI redisUri, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + connectInstrumenter().end(context, redisUri, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetAttributesExtractor.java new file mode 100644 index 000000000..d5731f9d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetAttributesExtractor.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import com.lambdaworks.redis.RedisURI; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class LettuceConnectNetAttributesExtractor extends NetAttributesExtractor { + + @Override + @Nullable + public String transport(RedisURI redisUri) { + return null; + } + + @Override + public String peerName(RedisURI redisUri, @Nullable Void unused) { + return redisUri.getHost(); + } + + @Override + public Integer peerPort(RedisURI redisUri, @Nullable Void unused) { + return redisUri.getPort(); + } + + @Override + @Nullable + public String peerIp(RedisURI redisUri, @Nullable Void unused) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceDbAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceDbAttributesExtractor.java new file mode 100644 index 000000000..374cb65b3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceDbAttributesExtractor.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import com.lambdaworks.redis.protocol.RedisCommand; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class LettuceDbAttributesExtractor + extends DbAttributesExtractor, Void> { + @Override + protected String system(RedisCommand request) { + return SemanticAttributes.DbSystemValues.REDIS; + } + + @Override + @Nullable + protected String user(RedisCommand request) { + return null; + } + + @Override + @Nullable + protected String name(RedisCommand request) { + return null; + } + + @Override + @Nullable + protected String connectionString(RedisCommand request) { + return null; + } + + @Override + protected String statement(RedisCommand request) { + return request.getType().name(); + } + + @Override + protected String operation(RedisCommand request) { + return request.getType().name(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceInstrumentationModule.java new file mode 100644 index 000000000..7e49f180c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class LettuceInstrumentationModule extends InstrumentationModule { + public LettuceInstrumentationModule() { + super("lettuce", "lettuce-4.0"); + } + + @Override + public List typeInstrumentations() { + return asList(new LettuceConnectInstrumentation(), new LettuceAsyncCommandsInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSingletons.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSingletons.java new file mode 100644 index 000000000..537e6bdc4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSingletons.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import com.lambdaworks.redis.RedisURI; +import com.lambdaworks.redis.protocol.RedisCommand; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; + +public final class LettuceSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.lettuce-4.0"; + + private static final Instrumenter, Void> INSTRUMENTER; + + private static final Instrumenter CONNECT_INSTRUMENTER; + + static { + DbAttributesExtractor, Void> attributesExtractor = + new LettuceDbAttributesExtractor(); + SpanNameExtractor> spanName = + DbSpanNameExtractor.create(attributesExtractor); + + INSTRUMENTER = + Instrumenter., Void>newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(attributesExtractor) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + + LettuceConnectNetAttributesExtractor connectNetAttributesExtractor = + new LettuceConnectNetAttributesExtractor(); + CONNECT_INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, redisUri -> "CONNECT") + .addAttributesExtractor(connectNetAttributesExtractor) + .addAttributesExtractor( + PeerServiceAttributesExtractor.create(connectNetAttributesExtractor)) + .addAttributesExtractor(new LettuceConnectAttributesExtractor()) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter, Void> instrumenter() { + return INSTRUMENTER; + } + + public static Instrumenter connectInstrumenter() { + return CONNECT_INSTRUMENTER; + } + + private LettuceSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy new file mode 100644 index 000000000..b9a10e440 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy @@ -0,0 +1,472 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import com.lambdaworks.redis.ClientOptions +import com.lambdaworks.redis.RedisClient +import com.lambdaworks.redis.RedisConnectionException +import com.lambdaworks.redis.RedisFuture +import com.lambdaworks.redis.RedisURI +import com.lambdaworks.redis.api.StatefulConnection +import com.lambdaworks.redis.api.async.RedisAsyncCommands +import com.lambdaworks.redis.api.sync.RedisCommands +import com.lambdaworks.redis.codec.Utf8StringCodec +import com.lambdaworks.redis.protocol.AsyncCommand +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CancellationException +import java.util.concurrent.TimeUnit +import java.util.function.BiConsumer +import java.util.function.BiFunction +import java.util.function.Consumer +import java.util.function.Function +import org.testcontainers.containers.FixedHostPortGenericContainer +import spock.lang.Shared +import spock.util.concurrent.AsyncConditions + +class LettuceAsyncClientTest extends AgentInstrumentationSpecification { + public static final String HOST = "localhost" + public static final int DB_INDEX = 0 + // Disable autoreconnect so we do not get stray traces popping up on server shutdown + public static final ClientOptions CLIENT_OPTIONS = new ClientOptions.Builder().autoReconnect(false).build() + + private static FixedHostPortGenericContainer redisServer = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + @Shared + int port + @Shared + int incorrectPort + @Shared + String dbAddr + @Shared + String dbAddrNonExistent + @Shared + String dbUriNonExistent + @Shared + String embeddedDbUri + + @Shared + Map testHashMap = [ + firstname: "John", + lastname : "Doe", + age : "53" + ] + + RedisClient redisClient + StatefulConnection connection + RedisAsyncCommands asyncCommands + RedisCommands syncCommands + + def setupSpec() { + port = PortUtils.findOpenPort() + incorrectPort = PortUtils.findOpenPort() + dbAddr = HOST + ":" + port + "/" + DB_INDEX + dbAddrNonExistent = HOST + ":" + incorrectPort + "/" + DB_INDEX + dbUriNonExistent = "redis://" + dbAddrNonExistent + embeddedDbUri = "redis://" + dbAddr + + redisServer = redisServer.withFixedExposedPort(port, 6379) + } + + def setup() { + redisClient = RedisClient.create(embeddedDbUri) + + redisServer.start() + redisClient.setOptions(CLIENT_OPTIONS) + + connection = redisClient.connect() + asyncCommands = connection.async() + syncCommands = connection.sync() + + syncCommands.set("TESTKEY", "TESTVAL") + + // 1 set + 1 connect trace + ignoreTracesAndClear(2) + } + + def cleanup() { + connection.close() + redisServer.stop() + } + + def "connect using get on ConnectionFuture"() { + setup: + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri) + testConnectionClient.setOptions(CLIENT_OPTIONS) + + when: + StatefulConnection connection = testConnectionClient.connect(new Utf8StringCodec(), + new RedisURI(HOST, port, 3, TimeUnit.SECONDS)) + + then: + connection != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "CONNECT" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" HOST + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + } + } + } + } + + cleanup: + connection.close() + } + + def "connect exception inside the connection future"() { + setup: + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent) + testConnectionClient.setOptions(CLIENT_OPTIONS) + + when: + StatefulConnection connection = testConnectionClient.connect(new Utf8StringCodec(), + new RedisURI(HOST, incorrectPort, 3, TimeUnit.SECONDS)) + + then: + connection == null + thrown RedisConnectionException + assertTraces(1) { + trace(0, 1) { + span(0) { + name "CONNECT" + kind CLIENT + status ERROR + errorEvent RedisConnectionException, String + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" HOST + "${SemanticAttributes.NET_PEER_PORT.key}" incorrectPort + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + } + } + } + } + } + + def "set command using Future get with timeout"() { + setup: + RedisFuture redisFuture = asyncCommands.set("TESTSETKEY", "TESTSETVAL") + String res = redisFuture.get(3, TimeUnit.SECONDS) + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "SET" + "${SemanticAttributes.DB_STATEMENT.key}" "SET" + } + } + } + } + } + + def "get command chained with thenAccept"() { + setup: + def conds = new AsyncConditions() + Consumer consumer = new Consumer() { + @Override + void accept(String res) { + conds.evaluate { + assert res == "TESTVAL" + } + } + } + + when: + RedisFuture redisFuture = asyncCommands.get("TESTKEY") + redisFuture.thenAccept(consumer) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "GET" + "${SemanticAttributes.DB_STATEMENT.key}" "GET" + } + } + } + } + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while still + // recording metrics + def "get non existent key command with handleAsync and chained with thenApply"() { + setup: + def conds = new AsyncConditions() + String successStr = "KEY MISSING" + BiFunction firstStage = new BiFunction() { + @Override + String apply(String res, Throwable throwable) { + conds.evaluate { + assert res == null + assert throwable == null + } + return (res == null ? successStr : res) + } + } + Function secondStage = new Function() { + @Override + Object apply(String input) { + conds.evaluate { + assert input == successStr + } + return null + } + } + + when: + RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY") + redisFuture.handleAsync(firstStage).thenApply(secondStage) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "GET" + "${SemanticAttributes.DB_STATEMENT.key}" "GET" + } + } + } + } + } + + def "command with no arguments using a biconsumer"() { + setup: + def conds = new AsyncConditions() + BiConsumer biConsumer = new BiConsumer() { + @Override + void accept(String keyRetrieved, Throwable throwable) { + conds.evaluate { + assert keyRetrieved != null + } + } + } + + when: + RedisFuture redisFuture = asyncCommands.randomkey() + redisFuture.whenCompleteAsync(biConsumer) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "RANDOMKEY" + "${SemanticAttributes.DB_STATEMENT.key}" "RANDOMKEY" + } + } + } + } + } + + def "hash set and then nest apply to hash getall"() { + setup: + def conds = new AsyncConditions() + + when: + RedisFuture hmsetFuture = asyncCommands.hmset("TESTHM", testHashMap) + hmsetFuture.thenApplyAsync(new Function() { + @Override + Object apply(String setResult) { + waitForTraces(1) // Wait for 'hmset' trace to get written + conds.evaluate { + assert setResult == "OK" + } + RedisFuture> hmGetAllFuture = asyncCommands.hgetall("TESTHM") + hmGetAllFuture.exceptionally(new Function>() { + @Override + Map apply(Throwable throwable) { + println("unexpected:" + throwable.toString()) + throwable.printStackTrace() + assert false + return null + } + }) + hmGetAllFuture.thenAccept(new Consumer>() { + @Override + void accept(Map hmGetAllResult) { + conds.evaluate { + assert testHashMap == hmGetAllResult + } + } + }) + return null + } + }) + + then: + conds.await() + assertTraces(2) { + trace(0, 1) { + span(0) { + name "HMSET" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "HMSET" + "${SemanticAttributes.DB_STATEMENT.key}" "HMSET" + } + } + } + trace(1, 1) { + span(0) { + name "HGETALL" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "HGETALL" + "${SemanticAttributes.DB_STATEMENT.key}" "HGETALL" + } + } + } + } + } + + def "command completes exceptionally"() { + setup: + // turn off auto flush to complete the command exceptionally manually + asyncCommands.setAutoFlushCommands(false) + def conds = new AsyncConditions() + RedisFuture redisFuture = asyncCommands.del("key1", "key2") + boolean completedExceptionally = ((AsyncCommand) redisFuture).completeExceptionally(new IllegalStateException("TestException")) + redisFuture.exceptionally({ + throwable -> + conds.evaluate { + assert throwable != null + assert throwable instanceof IllegalStateException + assert throwable.getMessage() == "TestException" + } + throw throwable + }) + + when: + // now flush and execute the command + asyncCommands.flushCommands() + redisFuture.get() + + then: + conds.await() + completedExceptionally == true + thrown Exception + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DEL" + kind CLIENT + status ERROR + errorEvent(IllegalStateException, "TestException") + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "DEL" + "${SemanticAttributes.DB_STATEMENT.key}" "DEL" + } + } + } + } + } + + def "cancel command before it finishes"() { + setup: + asyncCommands.setAutoFlushCommands(false) + def conds = new AsyncConditions() + RedisFuture redisFuture = asyncCommands.sadd("SKEY", "1", "2") + redisFuture.whenCompleteAsync({ + res, throwable -> + conds.evaluate { + assert throwable != null + assert throwable instanceof CancellationException + } + }) + + when: + boolean cancelSuccess = redisFuture.cancel(true) + asyncCommands.flushCommands() + + then: + conds.await() + cancelSuccess == true + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SADD" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "SADD" + "${SemanticAttributes.DB_STATEMENT.key}" "SADD" + "lettuce.command.cancelled" true + } + } + } + } + } + + def "debug segfault command (returns void) with no argument should produce span"() { + setup: + asyncCommands.debugSegfault() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DEBUG" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "DEBUG" + "${SemanticAttributes.DB_STATEMENT.key}" "DEBUG" + } + } + } + } + } + + + def "shutdown command (returns void) should produce a span"() { + setup: + asyncCommands.shutdown(false) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SHUTDOWN" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "SHUTDOWN" + "${SemanticAttributes.DB_STATEMENT.key}" "SHUTDOWN" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy new file mode 100644 index 000000000..0f9d38bb7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy @@ -0,0 +1,324 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import com.lambdaworks.redis.ClientOptions +import com.lambdaworks.redis.RedisClient +import com.lambdaworks.redis.RedisConnectionException +import com.lambdaworks.redis.api.StatefulConnection +import com.lambdaworks.redis.api.sync.RedisCommands +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.testcontainers.containers.FixedHostPortGenericContainer +import spock.lang.Shared + +class LettuceSyncClientTest extends AgentInstrumentationSpecification { + public static final String HOST = "localhost" + public static final int DB_INDEX = 0 + // Disable autoreconnect so we do not get stray traces popping up on server shutdown + public static final ClientOptions CLIENT_OPTIONS = new ClientOptions.Builder().autoReconnect(false).build() + + private static FixedHostPortGenericContainer redis = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + @Shared + int port + @Shared + int incorrectPort + @Shared + String dbAddr + @Shared + String dbAddrNonExistent + @Shared + String dbUriNonExistent + @Shared + String embeddedDbUri + + @Shared + Map testHashMap = [ + firstname: "John", + lastname : "Doe", + age : "53" + ] + + RedisClient redisClient + StatefulConnection connection + RedisCommands syncCommands + + def setupSpec() { + port = PortUtils.findOpenPort() + incorrectPort = PortUtils.findOpenPort() + dbAddr = HOST + ":" + port + "/" + DB_INDEX + dbAddrNonExistent = HOST + ":" + incorrectPort + "/" + DB_INDEX + dbUriNonExistent = "redis://" + dbAddrNonExistent + embeddedDbUri = "redis://" + dbAddr + + redis = redis.withFixedExposedPort(port, 6379) + } + + def setup() { + //TODO do not restart server for every test + redis.start() + + redisClient = RedisClient.create(embeddedDbUri) + + connection = redisClient.connect() + syncCommands = connection.sync() + + syncCommands.set("TESTKEY", "TESTVAL") + syncCommands.hmset("TESTHM", testHashMap) + + // 2 sets + 1 connect trace + ignoreTracesAndClear(3) + } + + def cleanup() { + connection.close() + redis.stop() + } + + def "connect"() { + setup: + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri) + testConnectionClient.setOptions(CLIENT_OPTIONS) + + when: + StatefulConnection connection = testConnectionClient.connect() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "CONNECT" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" HOST + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + } + } + } + } + + cleanup: + connection.close() + } + + def "connect exception"() { + setup: + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent) + testConnectionClient.setOptions(CLIENT_OPTIONS) + + when: + testConnectionClient.connect() + + then: + thrown RedisConnectionException + assertTraces(1) { + trace(0, 1) { + span(0) { + name "CONNECT" + kind CLIENT + status ERROR + errorEvent RedisConnectionException, String + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" HOST + "${SemanticAttributes.NET_PEER_PORT.key}" incorrectPort + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + } + } + } + } + } + + def "set command"() { + setup: + String res = syncCommands.set("TESTSETKEY", "TESTSETVAL") + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "SET" + "${SemanticAttributes.DB_STATEMENT.key}" "SET" + } + } + } + } + } + + def "get command"() { + setup: + String res = syncCommands.get("TESTKEY") + + expect: + res == "TESTVAL" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "GET" + "${SemanticAttributes.DB_STATEMENT.key}" "GET" + } + } + } + } + } + + def "get non existent key command"() { + setup: + String res = syncCommands.get("NON_EXISTENT_KEY") + + expect: + res == null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "GET" + "${SemanticAttributes.DB_STATEMENT.key}" "GET" + } + } + } + } + } + + def "command with no arguments"() { + setup: + def keyRetrieved = syncCommands.randomkey() + + expect: + keyRetrieved != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "RANDOMKEY" + "${SemanticAttributes.DB_STATEMENT.key}" "RANDOMKEY" + } + } + } + } + } + + def "list command"() { + setup: + long res = syncCommands.lpush("TESTLIST", "TESTLIST ELEMENT") + + expect: + res == 1 + assertTraces(1) { + trace(0, 1) { + span(0) { + name "LPUSH" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "LPUSH" + "${SemanticAttributes.DB_STATEMENT.key}" "LPUSH" + } + } + } + } + } + + def "hash set command"() { + setup: + def res = syncCommands.hmset("user", testHashMap) + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "HMSET" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "HMSET" + "${SemanticAttributes.DB_STATEMENT.key}" "HMSET" + } + } + } + } + } + + def "hash getall command"() { + setup: + Map res = syncCommands.hgetall("TESTHM") + + expect: + res == testHashMap + assertTraces(1) { + trace(0, 1) { + span(0) { + name "HGETALL" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "HGETALL" + "${SemanticAttributes.DB_STATEMENT.key}" "HGETALL" + } + } + } + } + } + + def "debug segfault command (returns void) with no argument should produce span"() { + setup: + syncCommands.debugSegfault() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DEBUG" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "DEBUG" + "${SemanticAttributes.DB_STATEMENT.key}" "DEBUG" + } + } + } + } + } + + def "shutdown command (returns void) should produce a span"() { + setup: + syncCommands.shutdown(false) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SHUTDOWN" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_OPERATION.key}" "SHUTDOWN" + "${SemanticAttributes.DB_STATEMENT.key}" "SHUTDOWN" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/lettuce-5.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/lettuce-5.0-javaagent.gradle new file mode 100644 index 000000000..491d944e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/lettuce-5.0-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.lettuce" + module = "lettuce-core" + versions = "[5.0.0.RELEASE,5.1.0.RELEASE)" + assertInverse = true + } +} + +dependencies { + compileOnly "io.lettuce:lettuce-core:5.0.0.RELEASE" + + implementation project(':instrumentation:lettuce:lettuce-common:library') + + testImplementation "io.lettuce:lettuce-core:5.0.0.RELEASE" + testInstrumentation project(':instrumentation:reactor-3.1:javaagent') +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.lettuce.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndCommandAsyncBiFunction.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndCommandAsyncBiFunction.java new file mode 100644 index 000000000..f5bbf7e5b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndCommandAsyncBiFunction.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.LettuceSingletons.instrumenter; + +import io.lettuce.core.protocol.RedisCommand; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import java.util.concurrent.CancellationException; +import java.util.function.BiFunction; + +/** + * Callback class to close the command span on an error or a success in the RedisFuture returned by + * the lettuce async API. + * + * @param the normal completion result + * @param the error + * @param the return type, should be null since nothing else should happen from tracing + * standpoint after the span is closed + */ +public class EndCommandAsyncBiFunction + implements BiFunction { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBoolean("otel.instrumentation.lettuce.experimental-span-attributes", false); + + private final Context context; + private final RedisCommand command; + + public EndCommandAsyncBiFunction(Context context, RedisCommand command) { + this.context = context; + this.command = command; + } + + @Override + public R apply(T t, Throwable throwable) { + end(context, command, throwable); + return null; + } + + public static void end(Context context, RedisCommand command, Throwable throwable) { + if (throwable instanceof CancellationException) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span.fromContext(context).setAttribute("lettuce.command.cancelled", true); + } + // and don't report this as an error + throwable = null; + } + instrumenter().end(context, command, null, throwable); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndConnectAsyncBiFunction.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndConnectAsyncBiFunction.java new file mode 100644 index 000000000..bd6257499 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndConnectAsyncBiFunction.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.LettuceSingletons.connectInstrumenter; + +import io.lettuce.core.RedisURI; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import java.util.concurrent.CancellationException; +import java.util.function.BiFunction; + +/** + * Callback class to close the connect span on an error or a success in the RedisFuture returned by + * the lettuce async API. + * + * @param the normal completion result + * @param the error + * @param the return type, should be null since nothing else should happen from tracing + * standpoint after the span is closed + */ +public class EndConnectAsyncBiFunction + implements BiFunction { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBoolean("otel.instrumentation.lettuce.experimental-span-attributes", false); + + private final Context context; + private final RedisURI redisUri; + + public EndConnectAsyncBiFunction(Context context, RedisURI redisUri) { + this.context = context; + this.redisUri = redisUri; + } + + @Override + public R apply(T t, Throwable throwable) { + if (throwable instanceof CancellationException) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span.fromContext(context).setAttribute("lettuce.command.cancelled", true); + } + // and don't report this as an error + throwable = null; + } + connectInstrumenter().end(context, redisUri, null, throwable); + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncCommandsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncCommandsInstrumentation.java new file mode 100644 index 000000000..37d32f635 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncCommandsInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.LettuceInstrumentationUtil.expectsResponse; +import static io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.LettuceSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.lettuce.core.protocol.AsyncCommand; +import io.lettuce.core.protocol.RedisCommand; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class LettuceAsyncCommandsInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.lettuce.core.AbstractRedisAsyncCommands"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("dispatch")) + .and(takesArgument(0, named("io.lettuce.core.protocol.RedisCommand"))), + LettuceAsyncCommandsInstrumentation.class.getName() + "$DispatchAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) RedisCommand command, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = instrumenter().start(currentContext(), command); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(0) RedisCommand command, + @Advice.Thrown Throwable throwable, + @Advice.Return AsyncCommand asyncCommand, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + if (throwable != null) { + instrumenter().end(context, command, null, throwable); + return; + } + + // close spans on error or normal completion + if (expectsResponse(command)) { + asyncCommand.handleAsync(new EndCommandAsyncBiFunction<>(context, command)); + } else { + instrumenter().end(context, command, null, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceClientInstrumentation.java new file mode 100644 index 000000000..7d743d9ea --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceClientInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.LettuceSingletons.connectInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPrivate; +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.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.lettuce.core.ConnectionFuture; +import io.lettuce.core.RedisURI; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class LettuceClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.lettuce.core.RedisClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPrivate()) + .and(returns(named("io.lettuce.core.ConnectionFuture"))) + .and(nameStartsWith("connect")) + .and(nameEndsWith("Async")) + .and(takesArgument(1, named("io.lettuce.core.RedisURI"))), + LettuceClientInstrumentation.class.getName() + "$ConnectAdvice"); + } + + @SuppressWarnings("unused") + public static class ConnectAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(1) RedisURI redisUri, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = connectInstrumenter().start(currentContext(), redisUri); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(1) RedisURI redisUri, + @Advice.Thrown Throwable throwable, + @Advice.Return ConnectionFuture connectionFuture, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + if (throwable != null) { + connectInstrumenter().end(context, redisUri, null, throwable); + return; + } + connectionFuture.handleAsync(new EndConnectAsyncBiFunction<>(context, redisUri)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectAttributesExtractor.java new file mode 100644 index 000000000..a41f2a4f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectAttributesExtractor.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import io.lettuce.core.RedisURI; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +final class LettuceConnectAttributesExtractor extends AttributesExtractor { + + @Override + protected void onStart(AttributesBuilder attributes, RedisURI redisUri) { + attributes.put(SemanticAttributes.DB_SYSTEM, SemanticAttributes.DbSystemValues.REDIS); + + int database = redisUri.getDatabase(); + if (database != 0) { + attributes.put(SemanticAttributes.DB_REDIS_DATABASE_INDEX, (long) database); + } + } + + @Override + protected void onEnd(AttributesBuilder attributes, RedisURI redisUri, Void unused) {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetAttributesExtractor.java new file mode 100644 index 000000000..6dcbbb3f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetAttributesExtractor.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import io.lettuce.core.RedisURI; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class LettuceConnectNetAttributesExtractor extends NetAttributesExtractor { + + @Override + @Nullable + public String transport(RedisURI redisUri) { + return null; + } + + @Override + public String peerName(RedisURI redisUri, @Nullable Void unused) { + return redisUri.getHost(); + } + + @Override + public Integer peerPort(RedisURI redisUri, @Nullable Void unused) { + return redisUri.getPort(); + } + + @Override + @Nullable + public String peerIp(RedisURI redisUri, @Nullable Void unused) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceDbAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceDbAttributesExtractor.java new file mode 100644 index 000000000..519ff8d38 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceDbAttributesExtractor.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import io.lettuce.core.protocol.RedisCommand; +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.lettuce.common.LettuceArgSplitter; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class LettuceDbAttributesExtractor + extends DbAttributesExtractor, Void> { + @Override + protected String system(RedisCommand request) { + return SemanticAttributes.DbSystemValues.REDIS; + } + + @Override + @Nullable + protected String user(RedisCommand request) { + return null; + } + + @Override + @Nullable + protected String name(RedisCommand request) { + return null; + } + + @Override + @Nullable + protected String connectionString(RedisCommand request) { + return null; + } + + @Override + protected String statement(RedisCommand request) { + String command = LettuceInstrumentationUtil.getCommandName(request); + List args = + request.getArgs() == null + ? Collections.emptyList() + : LettuceArgSplitter.splitArgs(request.getArgs().toCommandString()); + return RedisCommandSanitizer.sanitize(command, args); + } + + @Override + protected String operation(RedisCommand request) { + return request.getType().name(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceInstrumentationModule.java new file mode 100644 index 000000000..702bae8af --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceInstrumentationModule.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.rx.LettuceReactiveCommandsInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class LettuceInstrumentationModule extends InstrumentationModule { + public LettuceInstrumentationModule() { + super("lettuce", "lettuce-5.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return not(hasClassesNamed("io.lettuce.core.tracing.Tracing")); + } + + @Override + public List typeInstrumentations() { + return asList( + new LettuceAsyncCommandsInstrumentation(), + new LettuceClientInstrumentation(), + new LettuceReactiveCommandsInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceInstrumentationUtil.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceInstrumentationUtil.java new file mode 100644 index 000000000..a433a0b09 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceInstrumentationUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import io.lettuce.core.protocol.RedisCommand; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class LettuceInstrumentationUtil { + + private static final Set nonInstrumentingCommands = + Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("SHUTDOWN", "DEBUG", "OOM", "SEGFAULT"))); + + /** + * Determines whether a redis command should finish its relevant span early (as soon as tags are + * added and the command is executed) because these commands have no return values/call backs, so + * we must close the span early in order to provide info for the users. + * + * @return false if the span should finish early (the command will not have a return value) + */ + public static boolean expectsResponse(RedisCommand command) { + String commandName = LettuceInstrumentationUtil.getCommandName(command); + return !nonInstrumentingCommands.contains(commandName); + } + + /** + * Retrieves the actual redis command name from a RedisCommand object. + * + * @param command the lettuce RedisCommand object + * @return the redis command as a string + */ + public static String getCommandName(RedisCommand command) { + String commandName = "Redis Command"; + if (command != null) { + + // get the redis command name (i.e. GET, SET, HMSET, etc) + if (command.getType() != null) { + commandName = command.getType().name().trim(); + } + } + return commandName; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSingletons.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSingletons.java new file mode 100644 index 000000000..308b329d0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSingletons.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import io.lettuce.core.RedisURI; +import io.lettuce.core.protocol.RedisCommand; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; +import io.opentelemetry.javaagent.instrumentation.api.instrumenter.PeerServiceAttributesExtractor; + +public final class LettuceSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.lettuce-5.0"; + + private static final Instrumenter, Void> INSTRUMENTER; + + private static final Instrumenter CONNECT_INSTRUMENTER; + + static { + DbAttributesExtractor, Void> attributesExtractor = + new LettuceDbAttributesExtractor(); + SpanNameExtractor> spanName = + DbSpanNameExtractor.create(attributesExtractor); + + INSTRUMENTER = + Instrumenter., Void>newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(attributesExtractor) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + + LettuceConnectNetAttributesExtractor connectNetAttributesExtractor = + new LettuceConnectNetAttributesExtractor(); + CONNECT_INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, redisUri -> "CONNECT") + .addAttributesExtractor(connectNetAttributesExtractor) + .addAttributesExtractor( + PeerServiceAttributesExtractor.create(connectNetAttributesExtractor)) + .addAttributesExtractor(new LettuceConnectAttributesExtractor()) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter, Void> instrumenter() { + return INSTRUMENTER; + } + + public static Instrumenter connectInstrumenter() { + return CONNECT_INSTRUMENTER; + } + + private LettuceSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceFluxTerminationRunnable.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceFluxTerminationRunnable.java new file mode 100644 index 000000000..749461e7d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceFluxTerminationRunnable.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.rx; + +import static io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.LettuceSingletons.instrumenter; + +import io.lettuce.core.protocol.RedisCommand; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import java.util.function.Consumer; +import org.reactivestreams.Subscription; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Signal; +import reactor.core.publisher.SignalType; + +public class LettuceFluxTerminationRunnable implements Consumer>, Runnable { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBoolean("otel.instrumentation.lettuce.experimental-span-attributes", false); + + private Context context; + private int numResults; + private final FluxOnSubscribeConsumer onSubscribeConsumer; + + public LettuceFluxTerminationRunnable(RedisCommand command, boolean expectsResponse) { + onSubscribeConsumer = new FluxOnSubscribeConsumer(this, command, expectsResponse); + } + + public FluxOnSubscribeConsumer getOnSubscribeConsumer() { + return onSubscribeConsumer; + } + + private void finishSpan(boolean isCommandCancelled, Throwable throwable) { + if (context != null) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span span = Span.fromContext(context); + span.setAttribute("lettuce.command.results.count", numResults); + if (isCommandCancelled) { + span.setAttribute("lettuce.command.cancelled", true); + } + } + instrumenter().end(context, onSubscribeConsumer.command, null, throwable); + } else { + LoggerFactory.getLogger(Flux.class) + .error( + "Failed to end this.context, LettuceFluxTerminationRunnable cannot find this.context " + + "because it probably wasn't started."); + } + } + + @Override + public void accept(Signal signal) { + if (SignalType.ON_COMPLETE.equals(signal.getType()) + || SignalType.ON_ERROR.equals(signal.getType())) { + finishSpan(/* isCommandCancelled= */ false, signal.getThrowable()); + } else if (SignalType.ON_NEXT.equals(signal.getType())) { + ++numResults; + } + } + + @Override + public void run() { + finishSpan(/* isCommandCancelled= */ true, null); + } + + public static class FluxOnSubscribeConsumer implements Consumer { + + private final LettuceFluxTerminationRunnable owner; + private final RedisCommand command; + private final boolean expectsResponse; + + public FluxOnSubscribeConsumer( + LettuceFluxTerminationRunnable owner, + RedisCommand command, + boolean expectsResponse) { + this.owner = owner; + this.command = command; + this.expectsResponse = expectsResponse; + } + + @Override + public void accept(Subscription subscription) { + owner.context = instrumenter().start(Context.current(), command); + if (!expectsResponse) { + instrumenter().end(owner.context, command, null, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceMonoDualConsumer.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceMonoDualConsumer.java new file mode 100644 index 000000000..9a06db7b3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceMonoDualConsumer.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.rx; + +import static io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.LettuceSingletons.instrumenter; + +import io.lettuce.core.protocol.RedisCommand; +import io.opentelemetry.context.Context; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class LettuceMonoDualConsumer implements Consumer, BiConsumer { + + private Context context; + private final RedisCommand command; + private final boolean finishSpanOnClose; + + public LettuceMonoDualConsumer(RedisCommand command, boolean finishSpanOnClose) { + this.command = command; + this.finishSpanOnClose = finishSpanOnClose; + } + + @Override + public void accept(R r) { + context = instrumenter().start(Context.current(), command); + if (finishSpanOnClose) { + instrumenter().end(context, command, null, null); + } + } + + @Override + public void accept(T t, Throwable throwable) { + if (context != null) { + instrumenter().end(context, command, null, throwable); + } else { + LoggerFactory.getLogger(Mono.class) + .error( + "Failed to finish this.span, BiConsumer cannot find this.span because " + + "it probably wasn't started."); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceReactiveCommandsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceReactiveCommandsInstrumentation.java new file mode 100644 index 000000000..0bba7f487 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceReactiveCommandsInstrumentation.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.rx; + +import static io.opentelemetry.javaagent.instrumentation.lettuce.v5_0.LettuceInstrumentationUtil.expectsResponse; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +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.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.lettuce.core.protocol.RedisCommand; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.function.Supplier; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class LettuceReactiveCommandsInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.lettuce.core.AbstractRedisReactiveCommands"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("createMono")) + .and(takesArgument(0, Supplier.class)) + .and(returns(named("reactor.core.publisher.Mono"))), + LettuceReactiveCommandsInstrumentation.class.getName() + "$CreateMonoAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(nameStartsWith("create")) + .and(nameEndsWith("Flux")) + .and(isPublic()) + .and(takesArgument(0, Supplier.class)) + .and(returns(named("reactor.core.publisher.Flux"))), + LettuceReactiveCommandsInstrumentation.class.getName() + "$CreateFluxAdvice"); + } + + @SuppressWarnings("unused") + public static class CreateMonoAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static RedisCommand extractCommandName( + @Advice.Argument(0) Supplier supplier) { + return supplier.get(); + } + + // throwables wouldn't matter here, because no spans have been started due to redis command not + // being run until the user subscribes to the Mono publisher + @Advice.OnMethodExit(suppress = Throwable.class) + public static void monitorSpan( + @Advice.Enter RedisCommand command, @Advice.Return(readOnly = false) Mono publisher) { + boolean finishSpanOnClose = !expectsResponse(command); + LettuceMonoDualConsumer mdc = new LettuceMonoDualConsumer(command, finishSpanOnClose); + publisher = publisher.doOnSubscribe(mdc); + // register the call back to close the span only if necessary + if (!finishSpanOnClose) { + publisher = publisher.doOnSuccessOrError(mdc); + } + } + } + + @SuppressWarnings("unused") + public static class CreateFluxAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static RedisCommand extractCommandName( + @Advice.Argument(0) Supplier supplier) { + return supplier.get(); + } + + // if there is an exception thrown, then don't make spans + @Advice.OnMethodExit(suppress = Throwable.class) + public static void monitorSpan( + @Advice.Enter RedisCommand command, @Advice.Return(readOnly = false) Flux publisher) { + + boolean expectsResponse = expectsResponse(command); + LettuceFluxTerminationRunnable handler = + new LettuceFluxTerminationRunnable(command, expectsResponse); + publisher = publisher.doOnSubscribe(handler.getOnSubscribeConsumer()); + // don't register extra callbacks to finish the spans if the command being instrumented is one + // of those that return + // Mono (In here a flux is created first and then converted to Mono) + if (expectsResponse) { + publisher = publisher.doOnEach(handler); + publisher = publisher.doOnCancel(handler); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy new file mode 100644 index 000000000..364d4e2c1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy @@ -0,0 +1,476 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.lettuce.core.ClientOptions +import io.lettuce.core.ConnectionFuture +import io.lettuce.core.RedisClient +import io.lettuce.core.RedisFuture +import io.lettuce.core.RedisURI +import io.lettuce.core.api.StatefulConnection +import io.lettuce.core.api.async.RedisAsyncCommands +import io.lettuce.core.api.sync.RedisCommands +import io.lettuce.core.codec.StringCodec +import io.lettuce.core.protocol.AsyncCommand +import io.netty.channel.AbstractChannel +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import java.util.function.BiConsumer +import java.util.function.BiFunction +import java.util.function.Consumer +import java.util.function.Function +import org.testcontainers.containers.FixedHostPortGenericContainer +import spock.lang.Shared +import spock.util.concurrent.AsyncConditions + +class LettuceAsyncClientTest extends AgentInstrumentationSpecification { + public static final String PEER_NAME = "localhost" + public static final String PEER_IP = "127.0.0.1" + public static final int DB_INDEX = 0 + // Disable autoreconnect so we do not get stray traces popping up on server shutdown + public static final ClientOptions CLIENT_OPTIONS = ClientOptions.builder().autoReconnect(false).build() + + private static FixedHostPortGenericContainer redisServer = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + @Shared + int port + @Shared + int incorrectPort + @Shared + String dbAddr + @Shared + String dbAddrNonExistent + @Shared + String dbUriNonExistent + @Shared + String embeddedDbUri + + @Shared + Map testHashMap = [ + firstname: "John", + lastname : "Doe", + age : "53" + ] + + RedisClient redisClient + StatefulConnection connection + RedisAsyncCommands asyncCommands + RedisCommands syncCommands + + def setupSpec() { + port = PortUtils.findOpenPort() + incorrectPort = PortUtils.findOpenPort() + dbAddr = PEER_NAME + ":" + port + "/" + DB_INDEX + dbAddrNonExistent = PEER_NAME + ":" + incorrectPort + "/" + DB_INDEX + dbUriNonExistent = "redis://" + dbAddrNonExistent + embeddedDbUri = "redis://" + dbAddr + + redisServer = redisServer.withFixedExposedPort(port, 6379) + } + + def setup() { + redisClient = RedisClient.create(embeddedDbUri) + + redisServer.start() + redisClient.setOptions(CLIENT_OPTIONS) + + connection = redisClient.connect() + asyncCommands = connection.async() + syncCommands = connection.sync() + + syncCommands.set("TESTKEY", "TESTVAL") + + // 1 set + 1 connect trace + ignoreTracesAndClear(2) + } + + def cleanup() { + connection.close() + redisServer.stop() + } + + def "connect using get on ConnectionFuture"() { + setup: + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri) + testConnectionClient.setOptions(CLIENT_OPTIONS) + + when: + ConnectionFuture connectionFuture = testConnectionClient.connectAsync(StringCodec.UTF8, + new RedisURI(PEER_NAME, port, 3, TimeUnit.SECONDS)) + StatefulConnection connection = connectionFuture.get() + + then: + connection != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "CONNECT" + kind CLIENT + attributes { + "$SemanticAttributes.NET_PEER_NAME.key" PEER_NAME + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_SYSTEM.key" "redis" + } + } + } + } + + cleanup: + connection.close() + } + + def "connect exception inside the connection future"() { + setup: + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent) + testConnectionClient.setOptions(CLIENT_OPTIONS) + + when: + ConnectionFuture connectionFuture = testConnectionClient.connectAsync(StringCodec.UTF8, + new RedisURI(PEER_NAME, incorrectPort, 3, TimeUnit.SECONDS)) + StatefulConnection connection = connectionFuture.get() + + then: + connection == null + thrown ExecutionException + assertTraces(1) { + trace(0, 1) { + span(0) { + name "CONNECT" + kind CLIENT + status ERROR + errorEvent AbstractChannel.AnnotatedConnectException, String + attributes { + "$SemanticAttributes.NET_PEER_NAME.key" PEER_NAME + "$SemanticAttributes.NET_PEER_PORT.key" incorrectPort + "$SemanticAttributes.DB_SYSTEM.key" "redis" + } + } + } + } + } + + def "set command using Future get with timeout"() { + setup: + RedisFuture redisFuture = asyncCommands.set("TESTSETKEY", "TESTSETVAL") + String res = redisFuture.get(3, TimeUnit.SECONDS) + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET TESTSETKEY ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + } + } + } + } + } + + def "get command chained with thenAccept"() { + setup: + def conds = new AsyncConditions() + Consumer consumer = new Consumer() { + @Override + void accept(String res) { + conds.evaluate { + assert res == "TESTVAL" + } + } + } + + when: + RedisFuture redisFuture = asyncCommands.get("TESTKEY") + redisFuture.thenAccept(consumer) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET TESTKEY" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while still + // recording metrics + def "get non existent key command with handleAsync and chained with thenApply"() { + setup: + def conds = new AsyncConditions() + String successStr = "KEY MISSING" + BiFunction firstStage = new BiFunction() { + @Override + String apply(String res, Throwable throwable) { + conds.evaluate { + assert res == null + assert throwable == null + } + return (res == null ? successStr : res) + } + } + Function secondStage = new Function() { + @Override + Object apply(String input) { + conds.evaluate { + assert input == successStr + } + return null + } + } + + when: + RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY") + redisFuture.handleAsync(firstStage).thenApply(secondStage) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET NON_EXISTENT_KEY" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } + + def "command with no arguments using a biconsumer"() { + setup: + def conds = new AsyncConditions() + BiConsumer biConsumer = new BiConsumer() { + @Override + void accept(String keyRetrieved, Throwable throwable) { + conds.evaluate { + assert keyRetrieved != null + } + } + } + + when: + RedisFuture redisFuture = asyncCommands.randomkey() + redisFuture.whenCompleteAsync(biConsumer) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "RANDOMKEY" + "$SemanticAttributes.DB_OPERATION.key" "RANDOMKEY" + } + } + } + } + } + + def "hash set and then nest apply to hash getall"() { + setup: + def conds = new AsyncConditions() + + when: + RedisFuture hmsetFuture = asyncCommands.hmset("TESTHM", testHashMap) + hmsetFuture.thenApplyAsync(new Function() { + @Override + Object apply(String setResult) { + conds.evaluate { + assert setResult == "OK" + } + RedisFuture> hmGetAllFuture = asyncCommands.hgetall("TESTHM") + hmGetAllFuture.exceptionally(new Function>() { + @Override + Map apply(Throwable throwable) { + println("unexpected:" + throwable.toString()) + throwable.printStackTrace() + assert false + return null + } + }) + hmGetAllFuture.thenAccept(new Consumer>() { + @Override + void accept(Map hmGetAllResult) { + conds.evaluate { + assert testHashMap == hmGetAllResult + } + } + }) + return null + } + }) + + then: + conds.await() + assertTraces(2) { + trace(0, 1) { + span(0) { + name "HMSET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "HMSET TESTHM firstname ? lastname ? age ?" + "$SemanticAttributes.DB_OPERATION.key" "HMSET" + } + } + } + trace(1, 1) { + span(0) { + name "HGETALL" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "HGETALL TESTHM" + "$SemanticAttributes.DB_OPERATION.key" "HGETALL" + } + } + } + } + } + + def "command completes exceptionally"() { + setup: + // turn off auto flush to complete the command exceptionally manually + asyncCommands.setAutoFlushCommands(false) + def conds = new AsyncConditions() + RedisFuture redisFuture = asyncCommands.del("key1", "key2") + boolean completedExceptionally = ((AsyncCommand) redisFuture).completeExceptionally(new IllegalStateException("TestException")) + redisFuture.exceptionally({ + throwable -> + conds.evaluate { + assert throwable != null + assert throwable instanceof IllegalStateException + assert throwable.getMessage() == "TestException" + } + throw throwable + }) + + when: + // now flush and execute the command + asyncCommands.flushCommands() + redisFuture.get() + + then: + conds.await() + completedExceptionally == true + thrown Exception + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DEL" + kind CLIENT + status ERROR + errorEvent(IllegalStateException, "TestException") + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "DEL key1 key2" + "$SemanticAttributes.DB_OPERATION.key" "DEL" + } + } + } + } + } + + def "cancel command before it finishes"() { + setup: + asyncCommands.setAutoFlushCommands(false) + def conds = new AsyncConditions() + RedisFuture redisFuture = asyncCommands.sadd("SKEY", "1", "2") + redisFuture.whenCompleteAsync({ + res, throwable -> + conds.evaluate { + assert throwable != null + assert throwable instanceof CancellationException + } + }) + + when: + boolean cancelSuccess = redisFuture.cancel(true) + asyncCommands.flushCommands() + + then: + conds.await() + cancelSuccess == true + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SADD" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SADD SKEY ? ?" + "$SemanticAttributes.DB_OPERATION.key" "SADD" + "lettuce.command.cancelled" true + } + } + } + } + } + + def "debug segfault command (returns void) with no argument should produce span"() { + setup: + asyncCommands.debugSegfault() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DEBUG" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "DEBUG SEGFAULT" + "$SemanticAttributes.DB_OPERATION.key" "DEBUG" + } + } + } + } + } + + + def "shutdown command (returns void) should produce a span"() { + setup: + asyncCommands.shutdown(false) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SHUTDOWN" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SHUTDOWN NOSAVE" + "$SemanticAttributes.DB_OPERATION.key" "SHUTDOWN" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceReactiveClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceReactiveClientTest.groovy new file mode 100644 index 000000000..4257b2496 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceReactiveClientTest.groovy @@ -0,0 +1,401 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.lettuce.core.ClientOptions +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulConnection +import io.lettuce.core.api.reactive.RedisReactiveCommands +import io.lettuce.core.api.sync.RedisCommands +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.function.Consumer +import org.testcontainers.containers.FixedHostPortGenericContainer +import reactor.core.scheduler.Schedulers +import spock.lang.Shared +import spock.util.concurrent.AsyncConditions + +class LettuceReactiveClientTest extends AgentInstrumentationSpecification { + public static final String PEER_HOST = "localhost" + public static final String PEER_IP = "127.0.0.1" + public static final int DB_INDEX = 0 + // Disable autoreconnect so we do not get stray traces popping up on server shutdown + public static final ClientOptions CLIENT_OPTIONS = ClientOptions.builder().autoReconnect(false).build() + + private static FixedHostPortGenericContainer redisServer = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + @Shared + String embeddedDbUri + + + RedisClient redisClient + StatefulConnection connection + RedisReactiveCommands reactiveCommands + RedisCommands syncCommands + + def setupSpec() { + int port = PortUtils.findOpenPort() + String dbAddr = PEER_HOST + ":" + port + "/" + DB_INDEX + embeddedDbUri = "redis://" + dbAddr + + redisServer = redisServer.withFixedExposedPort(port, 6379) + } + + def setup() { + redisClient = RedisClient.create(embeddedDbUri) + + redisServer.start() + redisClient.setOptions(CLIENT_OPTIONS) + + connection = redisClient.connect() + reactiveCommands = connection.reactive() + syncCommands = connection.sync() + + syncCommands.set("TESTKEY", "TESTVAL") + + // 1 set + 1 connect trace + ignoreTracesAndClear(2) + } + + def cleanup() { + connection.close() + redisClient.shutdown() + redisServer.stop() + } + + def "set command with subscribe on a defined consumer"() { + setup: + def conds = new AsyncConditions() + Consumer consumer = new Consumer() { + @Override + void accept(String res) { + conds.evaluate { + assert res == "OK" + } + } + } + + when: + reactiveCommands.set("TESTSETKEY", "TESTSETVAL").subscribe(consumer) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET TESTSETKEY ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + } + } + } + } + } + + def "get command with lambda function"() { + setup: + def conds = new AsyncConditions() + + when: + reactiveCommands.get("TESTKEY").subscribe { res -> conds.evaluate { assert res == "TESTVAL" } } + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET TESTKEY" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while still + // recording metrics + def "get non existent key command"() { + setup: + def conds = new AsyncConditions() + final defaultVal = "NOT THIS VALUE" + + when: + reactiveCommands.get("NON_EXISTENT_KEY").defaultIfEmpty(defaultVal).subscribe { + res -> + conds.evaluate { + assert res == defaultVal + } + } + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET NON_EXISTENT_KEY" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + + } + + def "command with no arguments"() { + setup: + def conds = new AsyncConditions() + + when: + reactiveCommands.randomkey().subscribe { + res -> + conds.evaluate { + assert res == "TESTKEY" + } + } + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "RANDOMKEY" + "$SemanticAttributes.DB_OPERATION.key" "RANDOMKEY" + } + } + } + } + } + + def "command flux publisher "() { + setup: + reactiveCommands.command().subscribe() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "COMMAND" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "COMMAND" + "$SemanticAttributes.DB_OPERATION.key" "COMMAND" + "lettuce.command.results.count" { it > 100 } + } + } + } + } + } + + def "command cancel after 2 on flux publisher "() { + setup: + reactiveCommands.command().take(2).subscribe() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "COMMAND" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "COMMAND" + "$SemanticAttributes.DB_OPERATION.key" "COMMAND" + "lettuce.command.cancelled" true + "lettuce.command.results.count" 2 + } + } + } + } + } + + def "non reactive command should not produce span"() { + when: + def res = reactiveCommands.digest(null) + + then: + res != null + traces.size() == 0 + } + + def "debug segfault command (returns mono void) with no argument should produce span"() { + setup: + reactiveCommands.debugSegfault().subscribe() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DEBUG" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "DEBUG SEGFAULT" + "$SemanticAttributes.DB_OPERATION.key" "DEBUG" + } + } + } + } + } + + def "shutdown command (returns void) with argument should produce span"() { + setup: + reactiveCommands.shutdown(false).subscribe() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SHUTDOWN" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SHUTDOWN NOSAVE" + "$SemanticAttributes.DB_OPERATION.key" "SHUTDOWN" + } + } + } + } + } + + def "blocking subscriber"() { + when: + runUnderTrace("test-parent") { + reactiveCommands.set("a", "1") + .then(reactiveCommands.get("a")) + .block() + } + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "test-parent" + attributes { + } + } + span(1) { + name "SET" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET a ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + } + } + span(2) { + name "GET" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET a" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } + + def "async subscriber"() { + when: + runUnderTrace("test-parent") { + reactiveCommands.set("a", "1") + .then(reactiveCommands.get("a")) + .subscribe() + } + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "test-parent" + attributes { + } + } + span(1) { + name "SET" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET a ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + } + } + span(2) { + name "GET" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET a" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } + + def "async subscriber with specific thread pool"() { + when: + runUnderTrace("test-parent") { + reactiveCommands.set("a", "1") + .then(reactiveCommands.get("a")) + .subscribeOn(Schedulers.elastic()) + .subscribe() + } + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "test-parent" + attributes { + } + } + span(1) { + name "SET" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET a ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + } + } + span(2) { + name "GET" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET a" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy new file mode 100644 index 000000000..eb5b6b505 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy @@ -0,0 +1,324 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.lettuce.core.ClientOptions +import io.lettuce.core.RedisClient +import io.lettuce.core.RedisConnectionException +import io.lettuce.core.api.StatefulConnection +import io.lettuce.core.api.sync.RedisCommands +import io.netty.channel.AbstractChannel +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.testcontainers.containers.FixedHostPortGenericContainer +import spock.lang.Shared + +class LettuceSyncClientTest extends AgentInstrumentationSpecification { + public static final String PEER_NAME = "localhost" + public static final String PEER_IP = "127.0.0.1" + public static final int DB_INDEX = 0 + // Disable autoreconnect so we do not get stray traces popping up on server shutdown + public static final ClientOptions CLIENT_OPTIONS = ClientOptions.builder().autoReconnect(false).build() + + private static FixedHostPortGenericContainer redisServer = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + @Shared + int port + @Shared + int incorrectPort + @Shared + String dbAddr + @Shared + String dbAddrNonExistent + @Shared + String dbUriNonExistent + @Shared + String embeddedDbUri + + @Shared + Map testHashMap = [ + firstname: "John", + lastname : "Doe", + age : "53" + ] + + RedisClient redisClient + StatefulConnection connection + RedisCommands syncCommands + + def setupSpec() { + port = PortUtils.findOpenPort() + incorrectPort = PortUtils.findOpenPort() + dbAddr = PEER_NAME + ":" + port + "/" + DB_INDEX + dbAddrNonExistent = PEER_NAME + ":" + incorrectPort + "/" + DB_INDEX + dbUriNonExistent = "redis://" + dbAddrNonExistent + embeddedDbUri = "redis://" + dbAddr + + redisServer = redisServer.withFixedExposedPort(port, 6379) + } + + def setup() { + redisClient = RedisClient.create(embeddedDbUri) + + redisServer.start() + connection = redisClient.connect() + syncCommands = connection.sync() + + syncCommands.set("TESTKEY", "TESTVAL") + syncCommands.hmset("TESTHM", testHashMap) + + // 2 sets + 1 connect trace + ignoreTracesAndClear(3) + } + + def cleanup() { + connection.close() + redisServer.stop() + } + + def "connect"() { + setup: + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri) + testConnectionClient.setOptions(CLIENT_OPTIONS) + + when: + StatefulConnection connection = testConnectionClient.connect() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "CONNECT" + kind CLIENT + attributes { + "$SemanticAttributes.NET_PEER_NAME.key" PEER_NAME + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_SYSTEM.key" "redis" + } + } + } + } + + cleanup: + connection.close() + } + + def "connect exception"() { + setup: + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent) + testConnectionClient.setOptions(CLIENT_OPTIONS) + + when: + testConnectionClient.connect() + + then: + thrown RedisConnectionException + assertTraces(1) { + trace(0, 1) { + span(0) { + name "CONNECT" + kind CLIENT + status ERROR + errorEvent AbstractChannel.AnnotatedConnectException, String + attributes { + "$SemanticAttributes.NET_PEER_NAME.key" PEER_NAME + "$SemanticAttributes.NET_PEER_PORT.key" incorrectPort + "$SemanticAttributes.DB_SYSTEM.key" "redis" + } + } + } + } + } + + def "set command"() { + setup: + String res = syncCommands.set("TESTSETKEY", "TESTSETVAL") + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SET TESTSETKEY ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + } + } + } + } + } + + def "get command"() { + setup: + String res = syncCommands.get("TESTKEY") + + expect: + res == "TESTVAL" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET TESTKEY" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } + + def "get non existent key command"() { + setup: + String res = syncCommands.get("NON_EXISTENT_KEY") + + expect: + res == null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "GET NON_EXISTENT_KEY" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } + + def "command with no arguments"() { + setup: + def keyRetrieved = syncCommands.randomkey() + + expect: + keyRetrieved != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "RANDOMKEY" + "$SemanticAttributes.DB_OPERATION.key" "RANDOMKEY" + } + } + } + } + } + + def "list command"() { + setup: + long res = syncCommands.lpush("TESTLIST", "TESTLIST ELEMENT") + + expect: + res == 1 + assertTraces(1) { + trace(0, 1) { + span(0) { + name "LPUSH" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "LPUSH TESTLIST ?" + "$SemanticAttributes.DB_OPERATION.key" "LPUSH" + } + } + } + } + } + + def "hash set command"() { + setup: + def res = syncCommands.hmset("user", testHashMap) + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "HMSET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "HMSET user firstname ? lastname ? age ?" + "$SemanticAttributes.DB_OPERATION.key" "HMSET" + } + } + } + } + } + + def "hash getall command"() { + setup: + Map res = syncCommands.hgetall("TESTHM") + + expect: + res == testHashMap + assertTraces(1) { + trace(0, 1) { + span(0) { + name "HGETALL" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "HGETALL TESTHM" + "$SemanticAttributes.DB_OPERATION.key" "HGETALL" + } + } + } + } + } + + def "debug segfault command (returns void) with no argument should produce span"() { + setup: + syncCommands.debugSegfault() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DEBUG" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "DEBUG SEGFAULT" + "$SemanticAttributes.DB_OPERATION.key" "DEBUG" + } + } + } + } + } + + def "shutdown command (returns void) should produce a span"() { + setup: + syncCommands.shutdown(false) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SHUTDOWN" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.DB_STATEMENT.key" "SHUTDOWN NOSAVE" + "$SemanticAttributes.DB_OPERATION.key" "SHUTDOWN" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/lettuce-5.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/lettuce-5.1-javaagent.gradle new file mode 100644 index 000000000..9f68404a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/lettuce-5.1-javaagent.gradle @@ -0,0 +1,26 @@ +apply from: "${rootDir}/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.lettuce" + module = "lettuce-core" + versions = "[5.1.0.RELEASE,)" + assertInverse = true + } +} + +dependencies { + library "io.lettuce:lettuce-core:5.1.0.RELEASE" + + implementation project(':instrumentation:lettuce:lettuce-5.1:library') + + testImplementation project(':instrumentation:lettuce:lettuce-5.1:testing') + + // Only 5.2+ will have command arguments in the db.statement tag. + testLibrary "io.lettuce:lettuce-core:5.2.0.RELEASE" + testInstrumentation project(':instrumentation:reactor-3.1:javaagent') +} + +test { + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/DefaultClientResourcesInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/DefaultClientResourcesInstrumentation.java new file mode 100644 index 000000000..ffc5c3935 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/DefaultClientResourcesInstrumentation.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_1; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.lettuce.core.resource.DefaultClientResources; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DefaultClientResourcesInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.lettuce.core.resource.DefaultClientResources"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(isStatic()).and(named("builder")), + this.getClass().getName() + "$BuilderAdvice"); + } + + @SuppressWarnings("unused") + public static class BuilderAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void methodEnter(@Advice.Return DefaultClientResources.Builder builder) { + builder.tracing(TracingHolder.TRACING); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceInstrumentationModule.java new file mode 100644 index 000000000..c790c386c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceInstrumentationModule.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_1; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class LettuceInstrumentationModule extends InstrumentationModule { + + public LettuceInstrumentationModule() { + super("lettuce", "lettuce-5.1"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("io.lettuce.core.tracing.Tracing"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new DefaultClientResourcesInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/TracingHolder.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/TracingHolder.java new file mode 100644 index 000000000..09a69fae5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/TracingHolder.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_1; + +import io.lettuce.core.tracing.Tracing; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.lettuce.v5_1.LettuceTracing; + +public final class TracingHolder { + + public static final Tracing TRACING = + LettuceTracing.create(GlobalOpenTelemetry.get()).newTracing(); + + private TracingHolder() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy new file mode 100644 index 000000000..04fa6160c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_1 + +import io.lettuce.core.RedisClient +import io.opentelemetry.instrumentation.lettuce.v5_1.AbstractLettuceAsyncClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class LettuceAsyncClientTest extends AbstractLettuceAsyncClientTest implements AgentTestTrait { + @Override + RedisClient createClient(String uri) { + return RedisClient.create(uri) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy new file mode 100644 index 000000000..bdc22ec87 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_1 + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.lettuce.core.RedisClient +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.lettuce.v5_1.AbstractLettuceReactiveClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import reactor.core.scheduler.Schedulers + +class LettuceReactiveClientTest extends AbstractLettuceReactiveClientTest implements AgentTestTrait { + @Override + RedisClient createClient(String uri) { + return RedisClient.create(uri) + } + + // TODO(anuraaga): reactor library instrumentation doesn't seem to handle this case, figure out if + // it should and if so move back to base class. + def "async subscriber with specific thread pool"() { + when: + runUnderTrace("test-parent") { + reactiveCommands.set("a", "1") + .then(reactiveCommands.get("a")) + .subscribeOn(Schedulers.elastic()) + .subscribe() + } + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "test-parent" + attributes { + } + } + span(1) { + name "SET" + kind SpanKind.CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "SET a ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + span(2) { + name "GET" + kind SpanKind.CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET a" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy new file mode 100644 index 000000000..664e50184 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_1 + +import io.lettuce.core.RedisClient +import io.opentelemetry.instrumentation.lettuce.v5_1.AbstractLettuceSyncClientAuthTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class LettuceSyncClientAuthTest extends AbstractLettuceSyncClientAuthTest implements AgentTestTrait { + @Override + RedisClient createClient(String uri) { + return RedisClient.create(uri) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy new file mode 100644 index 000000000..0b1d15baa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_1 + +import io.lettuce.core.RedisClient +import io.opentelemetry.instrumentation.lettuce.v5_1.AbstractLettuceSyncClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class LettuceSyncClientTest extends AbstractLettuceSyncClientTest implements AgentTestTrait { + @Override + RedisClient createClient(String uri) { + return RedisClient.create(uri) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/lettuce-5.1-library.gradle b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/lettuce-5.1-library.gradle new file mode 100644 index 000000000..10afb66f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/lettuce-5.1-library.gradle @@ -0,0 +1,15 @@ +apply plugin: "otel.library-instrumentation" +apply plugin: "net.ltgt.nullaway" + +dependencies { + library "io.lettuce:lettuce-core:5.1.0.RELEASE" + + implementation project(':instrumentation:lettuce:lettuce-common:library') + + testImplementation project(':instrumentation:lettuce:lettuce-5.1:testing') + testImplementation project(':instrumentation:reactor-3.1:library') +} + +test { + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTracing.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTracing.java new file mode 100644 index 000000000..b11c9e509 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTracing.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1; + +import io.lettuce.core.tracing.Tracing; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; + +/** Entrypoint for tracing Lettuce or clients. */ +public final class LettuceTracing { + + /** Returns a new {@link LettuceTracing} configured with the given {@link OpenTelemetry}. */ + public static LettuceTracing create(OpenTelemetry openTelemetry) { + return new LettuceTracing(openTelemetry); + } + + private final Tracer tracer; + + private LettuceTracing(OpenTelemetry openTelemetry) { + tracer = openTelemetry.getTracer("io.opentelemetry.javaagent.lettuce-5.1"); + } + + /** + * Returns a new {@link Tracing} which can be used with methods like {@link + * io.lettuce.core.resource.ClientResources.Builder#tracing(Tracing)}. + */ + public Tracing newTracing() { + return new OpenTelemetryTracing(tracer); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/OpenTelemetryTracing.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/OpenTelemetryTracing.java new file mode 100644 index 000000000..f2bebb5dd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/OpenTelemetryTracing.java @@ -0,0 +1,353 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1; + +import static io.opentelemetry.instrumentation.lettuce.common.LettuceArgSplitter.splitArgs; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; + +import io.lettuce.core.output.CommandOutput; +import io.lettuce.core.protocol.CompleteableCommand; +import io.lettuce.core.protocol.RedisCommand; +import io.lettuce.core.tracing.TraceContext; +import io.lettuce.core.tracing.TraceContextProvider; +import io.lettuce.core.tracing.Tracer; +import io.lettuce.core.tracing.TracerProvider; +import io.lettuce.core.tracing.Tracing; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; +import io.opentelemetry.instrumentation.api.db.RedisCommandUtil; +import io.opentelemetry.instrumentation.api.tracer.AttributeSetter; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.checkerframework.checker.nullness.qual.Nullable; + +final class OpenTelemetryTracing implements Tracing { + + private final TracerProvider tracerProvider; + + OpenTelemetryTracing(io.opentelemetry.api.trace.Tracer tracer) { + this.tracerProvider = new OpenTelemetryTracerProvider(tracer); + } + + @Override + public TracerProvider getTracerProvider() { + return tracerProvider; + } + + @Override + public TraceContextProvider initialTraceContextProvider() { + return new OpenTelemetryTraceContextProvider(); + } + + @Override + public boolean isEnabled() { + return true; + } + + // Added in lettuce 5.2, ignored in 6.0+ + // @Override + public boolean includeCommandArgsInSpanTags() { + return true; + } + + @Override + @Nullable + public Endpoint createEndpoint(SocketAddress socketAddress) { + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress address = (InetSocketAddress) socketAddress; + + String ip = address.getAddress() == null ? null : address.getAddress().getHostAddress(); + return new OpenTelemetryEndpoint(ip, address.getPort(), address.getHostString()); + } + return null; + } + + private static class OpenTelemetryTracerProvider implements TracerProvider { + + private final Tracer openTelemetryTracer; + + OpenTelemetryTracerProvider(io.opentelemetry.api.trace.Tracer tracer) { + openTelemetryTracer = new OpenTelemetryTracer(tracer); + } + + @Override + public Tracer getTracer() { + return openTelemetryTracer; + } + } + + private static class OpenTelemetryTraceContextProvider implements TraceContextProvider { + + @Override + public TraceContext getTraceContext() { + return new OpenTelemetryTraceContext(); + } + } + + private static class OpenTelemetryTraceContext implements TraceContext { + private final Context context; + + OpenTelemetryTraceContext() { + this.context = Context.current(); + } + + public Context getSpanContext() { + return context; + } + } + + private static class OpenTelemetryEndpoint implements Endpoint { + @Nullable final String ip; + final int port; + @Nullable final String name; + + OpenTelemetryEndpoint(@Nullable String ip, int port, @Nullable String name) { + this.ip = ip; + this.port = port; + this.name = name; + } + } + + private static class OpenTelemetryTracer extends Tracer { + + private final io.opentelemetry.api.trace.Tracer tracer; + + OpenTelemetryTracer(io.opentelemetry.api.trace.Tracer tracer) { + this.tracer = tracer; + } + + @Override + public OpenTelemetrySpan nextSpan() { + return nextSpan(Context.current()); + } + + @Override + public OpenTelemetrySpan nextSpan(TraceContext traceContext) { + if (!(traceContext instanceof OpenTelemetryTraceContext)) { + return nextSpan(); + } + + Context context = ((OpenTelemetryTraceContext) traceContext).getSpanContext(); + return nextSpan(context); + } + + private OpenTelemetrySpan nextSpan(Context context) { + // Name will be updated later, we create with an arbitrary one here to store other data before + // the span starts. + SpanBuilder spanBuilder = + tracer + .spanBuilder("redis") + .setSpanKind(SpanKind.CLIENT) + .setParent(context) + .setAttribute(SemanticAttributes.DB_SYSTEM, DbSystemValues.REDIS); + return new OpenTelemetrySpan(spanBuilder); + } + } + + // The order that callbacks will be called in or which thread they are called from is not well + // defined. We go ahead and buffer all data until we know we have a span. This implementation is + // particularly safe, synchronizing all accesses. Relying on implementation details would allow + // reducing synchronization but the impact should be minimal. + private static class OpenTelemetrySpan extends Tracer.Span { + private final SpanBuilder spanBuilder; + + @Nullable + private String name; + + @Nullable + private List events; + + @Nullable + private Throwable error; + + @Nullable + private Span span; + + @Nullable + private String args; + + @Nullable + private String intranetErrorMessage; + + private long startTime; + + OpenTelemetrySpan(SpanBuilder spanBuilder) { + this.spanBuilder = spanBuilder; + } + + @Override + public synchronized Tracer.Span name(String name) { + if (span != null) { + span.updateName(name); + } + + this.name = name; + + return this; + } + + @Override + public synchronized Tracer.Span remoteEndpoint(Endpoint endpoint) { + if (endpoint instanceof OpenTelemetryEndpoint) { + if (span != null) { + fillEndpoint(span::setAttribute, (OpenTelemetryEndpoint) endpoint); + } else { + fillEndpoint(spanBuilder::setAttribute, (OpenTelemetryEndpoint) endpoint); + } + } + return this; + } + + // Added and called in 6.0+ + // @Override + public synchronized Tracer.Span start(RedisCommand command) { + start(); + + Span span = this.span; + if (span == null) { + throw new IllegalStateException("Span started but null, this is a programming error."); + } + span.updateName(command.getType().name()); + + if (command.getArgs() != null) { + args = command.getArgs().toCommandString(); + } + + if (command instanceof CompleteableCommand) { + CompleteableCommand completeableCommand = (CompleteableCommand) command; + completeableCommand.onComplete( + (o, throwable) -> { + if (throwable != null) { + span.recordException(throwable); + } + + CommandOutput output = command.getOutput(); + if (output != null) { + String error = output.getError(); + if (error != null) { + span.setStatus(StatusCode.ERROR, error); + } + } + + finish(span); + }); + } + + return this; + } + + // Not called by Lettuce in 6.0+ (though we call it ourselves above). + @Override + public synchronized Tracer.Span start() { + startTime = System.currentTimeMillis(); + span = spanBuilder.startSpan(); + if (name != null) { + span.updateName(name); + } + + if (events != null) { + for (int i = 0; i < events.size(); i += 2) { + span.addEvent((String) events.get(i), (Instant) events.get(i + 1)); + } + events = null; + } + + if (error != null) { + span.setStatus(StatusCode.ERROR); + span.recordException(error); + error = null; + } else if (intranetErrorMessage != null) { + span.addEvent("redis_error", Attributes.builder().put("exception_message", intranetErrorMessage).build()); + span.setStatus(StatusCode.ERROR); + intranetErrorMessage = null; + } + + return this; + } + + @Override + public synchronized Tracer.Span annotate(String value) { + if (span != null) { + span.addEvent(value); + } else { + if (events == null) { + events = new ArrayList<>(); + } + events.add(value); + events.add(Instant.now()); + } + return this; + } + + @Override + public synchronized Tracer.Span tag(String key, String value) { + if (key.equals("redis.args")) { + args = value; + return this; + } + if ("error".equals(key)) { + if (span != null) { + span.addEvent("redis_error", Attributes.builder().put("exception_message", value).build()); + span.setStatus(StatusCode.ERROR); + } else { + intranetErrorMessage = value; + } + return this; + } + if (span != null) { + span.setAttribute(key, value); + } else { + spanBuilder.setAttribute(key, value); + } + return this; + } + + @Override + public synchronized Tracer.Span error(Throwable throwable) { + if (span != null) { + span.recordException(throwable); + } else { + this.error = throwable; + } + return this; + } + + @Override + public synchronized void finish() { + if (span != null) { + finish(span); + } + } + + private void finish(Span span) { + if (name != null) { + if (RedisCommandUtil.skipEnd(name, error, startTime)) { + return; + } + String statement = RedisCommandSanitizer.sanitize(name, splitArgs(args)); + span.setAttribute(SemanticAttributes.DB_STATEMENT, statement); + } + span.end(); + } + + private static void fillEndpoint(AttributeSetter span, OpenTelemetryEndpoint endpoint) { + span.setAttribute(SemanticAttributes.NET_TRANSPORT, IP_TCP); + NetPeerAttributes.INSTANCE.setNetPeer(span, endpoint.name, endpoint.ip, endpoint.port); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncSyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncSyncClientTest.groovy new file mode 100644 index 000000000..f93192e3a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncSyncClientTest.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import io.lettuce.core.RedisClient +import io.lettuce.core.resource.ClientResources +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class LettuceAsyncSyncClientTest extends AbstractLettuceAsyncClientTest implements LibraryTestTrait { + @Override + RedisClient createClient(String uri) { + return RedisClient.create( + ClientResources.builder() + .tracing(LettuceTracing.create(getOpenTelemetry()).newTracing()) + .build(), + uri) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy new file mode 100644 index 000000000..af2660971 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import io.lettuce.core.RedisClient +import io.lettuce.core.resource.ClientResources +import io.opentelemetry.instrumentation.reactor.TracingOperator +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import spock.lang.Shared + +class LettuceReactiveClientTest extends AbstractLettuceReactiveClientTest implements LibraryTestTrait { + @Shared + TracingOperator tracingOperator = TracingOperator.create() + + @Override + RedisClient createClient(String uri) { + return RedisClient.create( + ClientResources.builder() + .tracing(LettuceTracing.create(getOpenTelemetry()).newTracing()) + .build(), + uri) + } + + def setupSpec() { + tracingOperator.registerOnEachOperator() + } + + def cleanupSpec() { + tracingOperator.resetOnEachOperator() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy new file mode 100644 index 000000000..1d770b074 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import io.lettuce.core.RedisClient +import io.lettuce.core.resource.ClientResources +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class LettuceSyncClientAuthTest extends AbstractLettuceSyncClientAuthTest implements LibraryTestTrait { + @Override + RedisClient createClient(String uri) { + return RedisClient.create( + ClientResources.builder() + .tracing(LettuceTracing.create(getOpenTelemetry()).newTracing()) + .build(), + uri) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy new file mode 100644 index 000000000..79ddc5a95 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import io.lettuce.core.RedisClient +import io.lettuce.core.resource.ClientResources +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class LettuceSyncClientTest extends AbstractLettuceSyncClientTest implements LibraryTestTrait { + @Override + RedisClient createClient(String uri) { + return RedisClient.create( + ClientResources.builder() + .tracing(LettuceTracing.create(getOpenTelemetry()).newTracing()) + .build(), + uri) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/lettuce-5.1-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/lettuce-5.1-testing.gradle new file mode 100644 index 000000000..021ecadf7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/lettuce-5.1-testing.gradle @@ -0,0 +1,14 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "io.lettuce:lettuce-core:5.1.0.RELEASE" + + implementation "org.testcontainers:testcontainers" + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.groovy new file mode 100644 index 000000000..fd663c4b1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.groovy @@ -0,0 +1,377 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.lettuce.core.ConnectionFuture +import io.lettuce.core.RedisClient +import io.lettuce.core.RedisFuture +import io.lettuce.core.RedisURI +import io.lettuce.core.api.StatefulConnection +import io.lettuce.core.api.async.RedisAsyncCommands +import io.lettuce.core.api.sync.RedisCommands +import io.lettuce.core.codec.StringCodec +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import java.util.function.BiConsumer +import java.util.function.BiFunction +import java.util.function.Consumer +import java.util.function.Function +import org.testcontainers.containers.FixedHostPortGenericContainer +import spock.lang.Shared +import spock.util.concurrent.AsyncConditions + +abstract class AbstractLettuceAsyncClientTest extends InstrumentationSpecification { + public static final String HOST = "127.0.0.1" + public static final int DB_INDEX = 0 + + private static FixedHostPortGenericContainer redisServer = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + abstract RedisClient createClient(String uri) + + @Shared + int port + @Shared + int incorrectPort + @Shared + String dbAddr + @Shared + String dbAddrNonExistent + @Shared + String dbUriNonExistent + @Shared + String embeddedDbUri + + @Shared + Map testHashMap = [ + firstname: "John", + lastname : "Doe", + age : "53" + ] + + RedisClient redisClient + StatefulConnection connection + RedisAsyncCommands asyncCommands + RedisCommands syncCommands + + def setupSpec() { + port = PortUtils.findOpenPort() + incorrectPort = PortUtils.findOpenPort() + dbAddr = HOST + ":" + port + "/" + DB_INDEX + dbAddrNonExistent = HOST + ":" + incorrectPort + "/" + DB_INDEX + dbUriNonExistent = "redis://" + dbAddrNonExistent + embeddedDbUri = "redis://" + dbAddr + + redisServer = redisServer.withFixedExposedPort(port, 6379) + } + + def setup() { + redisClient = createClient(embeddedDbUri) + + redisServer.start() + redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + + connection = redisClient.connect() + asyncCommands = connection.async() + syncCommands = connection.sync() + + syncCommands.set("TESTKEY", "TESTVAL") + + // 1 set + ignoreTracesAndClear(1) + } + + def cleanup() { + connection.close() + redisServer.stop() + } + + def "connect using get on ConnectionFuture"() { + setup: + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri) + testConnectionClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + + when: + ConnectionFuture connectionFuture = testConnectionClient.connectAsync(StringCodec.UTF8, + RedisURI.create("redis://${HOST}:${port}?timeout=3s")) + StatefulConnection connection = connectionFuture.get() + + then: + connection != null + // Lettuce tracing does not trace connect + assertTraces(0) {} + + cleanup: + connection.close() + } + + def "connect exception inside the connection future"() { + setup: + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent) + testConnectionClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + + when: + ConnectionFuture connectionFuture = testConnectionClient.connectAsync(StringCodec.UTF8, + RedisURI.create("redis://${HOST}:${incorrectPort}?timeout=3s")) + StatefulConnection connection = connectionFuture.get() + + then: + connection == null + thrown ExecutionException + // Lettuce tracing does not trace connect + assertTraces(0) {} + } + + def "set command using Future get with timeout"() { + setup: + RedisFuture redisFuture = asyncCommands.set("TESTSETKEY", "TESTSETVAL") + String res = redisFuture.get(3, TimeUnit.SECONDS) + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "SET TESTSETKEY ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "get command chained with thenAccept"() { + setup: + def conds = new AsyncConditions() + Consumer consumer = new Consumer() { + @Override + void accept(String res) { + conds.evaluate { + assert res == "TESTVAL" + } + } + } + + when: + RedisFuture redisFuture = asyncCommands.get("TESTKEY") + redisFuture.thenAccept(consumer) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET TESTKEY" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while still + // recording metrics + def "get non existent key command with handleAsync and chained with thenApply"() { + setup: + def conds = new AsyncConditions() + String successStr = "KEY MISSING" + BiFunction firstStage = new BiFunction() { + @Override + String apply(String res, Throwable throwable) { + conds.evaluate { + assert res == null + assert throwable == null + } + return (res == null ? successStr : res) + } + } + Function secondStage = new Function() { + @Override + Object apply(String input) { + conds.evaluate { + assert input == successStr + } + return null + } + } + + when: + RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY") + redisFuture.handleAsync(firstStage).thenApply(secondStage) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET NON_EXISTENT_KEY" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "command with no arguments using a biconsumer"() { + setup: + def conds = new AsyncConditions() + BiConsumer biConsumer = new BiConsumer() { + @Override + void accept(String keyRetrieved, Throwable throwable) { + conds.evaluate { + assert keyRetrieved != null + } + } + } + + when: + RedisFuture redisFuture = asyncCommands.randomkey() + redisFuture.whenCompleteAsync(biConsumer) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_STATEMENT.key}" "RANDOMKEY" + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "hash set and then nest apply to hash getall"() { + setup: + def conds = new AsyncConditions() + + when: + RedisFuture hmsetFuture = asyncCommands.hmset("TESTHM", testHashMap) + hmsetFuture.thenApplyAsync(new Function() { + @Override + Object apply(String setResult) { + conds.evaluate { + assert setResult == "OK" + } + RedisFuture> hmGetAllFuture = asyncCommands.hgetall("TESTHM") + hmGetAllFuture.exceptionally(new Function>() { + @Override + Map apply(Throwable throwable) { + println("unexpected:" + throwable.toString()) + throwable.printStackTrace() + assert false + return null + } + }) + hmGetAllFuture.thenAccept(new Consumer>() { + @Override + void accept(Map hmGetAllResult) { + conds.evaluate { + assert testHashMap == hmGetAllResult + } + } + }) + return null + } + }) + + then: + conds.await() + assertTraces(2) { + trace(0, 1) { + span(0) { + name "HMSET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "HMSET TESTHM firstname ? lastname ? age ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + trace(1, 1) { + span(0) { + name "HGETALL" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "HGETALL TESTHM" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.groovy new file mode 100644 index 000000000..f204e1f0c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.groovy @@ -0,0 +1,370 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulConnection +import io.lettuce.core.api.reactive.RedisReactiveCommands +import io.lettuce.core.api.sync.RedisCommands +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.function.Consumer +import org.testcontainers.containers.FixedHostPortGenericContainer +import spock.lang.Shared +import spock.util.concurrent.AsyncConditions + +abstract class AbstractLettuceReactiveClientTest extends InstrumentationSpecification { + public static final String HOST = "127.0.0.1" + public static final int DB_INDEX = 0 + + private static FixedHostPortGenericContainer redisServer = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + abstract RedisClient createClient(String uri) + + @Shared + int port + @Shared + String embeddedDbUri + + RedisClient redisClient + StatefulConnection connection + RedisReactiveCommands reactiveCommands + RedisCommands syncCommands + + def setupSpec() { + port = PortUtils.findOpenPort() + String dbAddr = HOST + ":" + port + "/" + DB_INDEX + embeddedDbUri = "redis://" + dbAddr + + redisServer = redisServer.withFixedExposedPort(port, 6379) + } + + def setup() { + redisClient = createClient(embeddedDbUri) + + redisServer.start() + redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + + connection = redisClient.connect() + reactiveCommands = connection.reactive() + syncCommands = connection.sync() + + syncCommands.set("TESTKEY", "TESTVAL") + + // 1 set + ignoreTracesAndClear(1) + } + + def cleanup() { + connection.close() + redisClient.shutdown() + redisServer.stop() + } + + def "set command with subscribe on a defined consumer"() { + setup: + def conds = new AsyncConditions() + Consumer consumer = new Consumer() { + @Override + void accept(String res) { + conds.evaluate { + assert res == "OK" + } + } + } + + when: + reactiveCommands.set("TESTSETKEY", "TESTSETVAL").subscribe(consumer) + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "SET TESTSETKEY ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "get command with lambda function"() { + setup: + def conds = new AsyncConditions() + + when: + reactiveCommands.get("TESTKEY").subscribe { res -> conds.evaluate { assert res == "TESTVAL" } } + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET TESTKEY" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while still + // recording metrics + def "get non existent key command"() { + setup: + def conds = new AsyncConditions() + final defaultVal = "NOT THIS VALUE" + + when: + reactiveCommands.get("NON_EXISTENT_KEY").defaultIfEmpty(defaultVal).subscribe { + res -> + conds.evaluate { + assert res == defaultVal + } + } + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET NON_EXISTENT_KEY" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + + } + + def "command with no arguments"() { + setup: + def conds = new AsyncConditions() + + when: + reactiveCommands.randomkey().subscribe { + res -> + conds.evaluate { + assert res == "TESTKEY" + } + } + + then: + conds.await() + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_STATEMENT.key}" "RANDOMKEY" + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "command flux publisher "() { + setup: + reactiveCommands.command().subscribe() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "COMMAND" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_STATEMENT.key}" "COMMAND" + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "non reactive command should not produce span"() { + when: + def res = reactiveCommands.digest() + + then: + res != null + traces.size() == 0 + } + + def "blocking subscriber"() { + when: + runUnderTrace("test-parent") { + reactiveCommands.set("a", "1") + .then(reactiveCommands.get("a")) + .block() + } + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "test-parent" + attributes { + } + } + span(1) { + name "SET" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "SET a ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + span(2) { + name "GET" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET a" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "async subscriber"() { + when: + runUnderTrace("test-parent") { + reactiveCommands.set("a", "1") + .then(reactiveCommands.get("a")) + .subscribe() + } + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "test-parent" + attributes { + } + } + span(1) { + name "SET" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "SET a ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + span(2) { + name "GET" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET a" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.groovy new file mode 100644 index 000000000..f54964c48 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.groovy @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP + +import io.lettuce.core.RedisClient +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.testcontainers.containers.FixedHostPortGenericContainer +import spock.lang.Shared + +abstract class AbstractLettuceSyncClientAuthTest extends InstrumentationSpecification { + public static final String HOST = "127.0.0.1" + public static final int DB_INDEX = 0 + + private static FixedHostPortGenericContainer redisServer = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + abstract RedisClient createClient(String uri) + + @Shared + int port + @Shared + String password + @Shared + String dbAddr + @Shared + String embeddedDbUri + + RedisClient redisClient + + def setupSpec() { + port = PortUtils.findOpenPort() + dbAddr = HOST + ":" + port + "/" + DB_INDEX + embeddedDbUri = "redis://" + dbAddr + password = "password" + + redisServer = redisServer + .withFixedExposedPort(port, 6379) + .withCommand("redis-server", "--requirepass $password") + } + + def setup() { + redisClient = createClient(embeddedDbUri) + redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + redisServer.start() + } + + def cleanup() { + redisServer.stop() + } + + def "auth command"() { + setup: + def res = redisClient.connect().sync().auth(password) + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "AUTH" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "AUTH ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.groovy new file mode 100644 index 000000000..057d184bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.groovy @@ -0,0 +1,496 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP +import static java.nio.charset.StandardCharsets.UTF_8 + +import io.lettuce.core.RedisClient +import io.lettuce.core.RedisConnectionException +import io.lettuce.core.RedisException +import io.lettuce.core.ScriptOutputType +import io.lettuce.core.api.StatefulConnection +import io.lettuce.core.api.sync.RedisCommands +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.testcontainers.containers.FixedHostPortGenericContainer +import spock.lang.Shared + +abstract class AbstractLettuceSyncClientTest extends InstrumentationSpecification { + public static final String HOST = "127.0.0.1" + public static final int DB_INDEX = 0 + + private static FixedHostPortGenericContainer redisServer = new FixedHostPortGenericContainer<>("redis:6.2.3-alpine") + + abstract RedisClient createClient(String uri) + + @Shared + int port + @Shared + int incorrectPort + @Shared + String dbAddr + @Shared + String dbAddrNonExistent + @Shared + String dbUriNonExistent + @Shared + String embeddedDbUri + @Shared + String embeddedDbLocalhostUri + + @Shared + Map testHashMap = [ + firstname: "John", + lastname : "Doe", + age : "53" + ] + + RedisClient redisClient + StatefulConnection connection + RedisCommands syncCommands + + def setupSpec() { + port = PortUtils.findOpenPort() + incorrectPort = PortUtils.findOpenPort() + dbAddr = HOST + ":" + port + "/" + DB_INDEX + dbAddrNonExistent = HOST + ":" + incorrectPort + "/" + DB_INDEX + dbUriNonExistent = "redis://" + dbAddrNonExistent + embeddedDbUri = "redis://" + dbAddr + embeddedDbLocalhostUri = "redis://localhost:" + port + "/" + DB_INDEX + + redisServer = redisServer.withFixedExposedPort(port, 6379) + } + + def setup() { + redisClient = createClient(embeddedDbUri) + redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + + redisServer.start() + connection = redisClient.connect() + syncCommands = connection.sync() + + syncCommands.set("TESTKEY", "TESTVAL") + syncCommands.hmset("TESTHM", testHashMap) + + // 2 sets + ignoreTracesAndClear(2) + } + + def cleanup() { + connection.close() + redisServer.stop() + } + + def "connect"() { + setup: + RedisClient testConnectionClient = createClient(embeddedDbUri) + testConnectionClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + + when: + StatefulConnection connection = testConnectionClient.connect() + + then: + // Lettuce tracing does not trace connect + assertTraces(0) {} + + cleanup: + connection.close() + } + + def "connect exception"() { + setup: + RedisClient testConnectionClient = createClient(dbUriNonExistent) + testConnectionClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + + when: + testConnectionClient.connect() + + then: + thrown RedisConnectionException + // Lettuce tracing does not trace connect + assertTraces(0) {} + } + + def "set command"() { + setup: + String res = syncCommands.set("TESTSETKEY", "TESTSETVAL") + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "SET TESTSETKEY ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "set command localhost"() { + setup: + RedisClient testConnectionClient = createClient(embeddedDbLocalhostUri) + testConnectionClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) + StatefulConnection connection = testConnectionClient.connect() + String res = connection.sync().set("TESTSETKEY", "TESTSETVAL") + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "SET TESTSETKEY ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "get command"() { + setup: + String res = syncCommands.get("TESTKEY") + + expect: + res == "TESTVAL" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET TESTKEY" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "get non existent key command"() { + setup: + String res = syncCommands.get("NON_EXISTENT_KEY") + + expect: + res == null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "GET NON_EXISTENT_KEY" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "command with no arguments"() { + setup: + def keyRetrieved = syncCommands.randomkey() + + expect: + keyRetrieved != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RANDOMKEY" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_STATEMENT.key}" "RANDOMKEY" + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "list command"() { + setup: + long res = syncCommands.lpush("TESTLIST", "TESTLIST ELEMENT") + + expect: + res == 1 + assertTraces(1) { + trace(0, 1) { + span(0) { + name "LPUSH" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "LPUSH TESTLIST ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "hash set command"() { + setup: + def res = syncCommands.hmset("user", testHashMap) + + expect: + res == "OK" + assertTraces(1) { + trace(0, 1) { + span(0) { + name "HMSET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "HMSET user firstname ? lastname ? age ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "hash getall command"() { + setup: + Map res = syncCommands.hgetall("TESTHM") + + expect: + res == testHashMap + assertTraces(1) { + trace(0, 1) { + span(0) { + name "HGETALL" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "HGETALL TESTHM" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "eval command"() { + given: + def script = "redis.call('lpush', KEYS[1], ARGV[1], ARGV[2]); return redis.call('llen', KEYS[1])" + + when: + def result = syncCommands.eval(script, ScriptOutputType.INTEGER, ["TESTLIST"] as String[], "abc", "def") + + then: + result == 2 + + def b64Script = Base64.encoder.encodeToString(script.getBytes(UTF_8)) + assertTraces(1) { + trace(0, 1) { + span(0) { + name "EVAL" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "EVAL $b64Script 1 TESTLIST ? ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "mset command"() { + when: + def res = syncCommands.mset([ + "key1": "value1", + "key2": "value2" + ]) + + then: + res == "OK" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "MSET" + kind CLIENT + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "MSET key1 ? key2 ?" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + } + } + } + } + + def "debug segfault command (returns void) with no argument produces no span"() { + setup: + syncCommands.debugSegfault() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DEBUG" + kind CLIENT + // Disconnect not an actual error even though an exception is recorded. + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "DEBUG SEGFAULT" + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + if (Boolean.getBoolean("testLatestDeps")) { + // Seems to only be recorded with Lettuce 6+ + errorEvent(RedisException, "Connection disconnected", 2) + } + } + } + } + } + + def "shutdown command (returns void) produces no span"() { + setup: + syncCommands.shutdown(false) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SHUTDOWN" + kind CLIENT + if (Boolean.getBoolean("testLatestDeps")) { + // Seems to only be treated as an error with Lettuce 6+ + status ERROR + } + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" port + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "SHUTDOWN NOSAVE" + if (!Boolean.getBoolean("testLatestDeps")) { + // Lettuce adds this tag before 6.0 + // TODO(anuraaga): Filter this out? + "error" "Connection disconnected" + } + } + event(0) { + eventName "redis.encode.start" + } + event(1) { + eventName "redis.encode.end" + } + if (Boolean.getBoolean("testLatestDeps")) { + errorEvent(RedisException, "Connection disconnected", 2) + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.groovy new file mode 100644 index 000000000..7e90d6821 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.groovy @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1 + +import groovy.transform.PackageScope +import io.lettuce.core.ClientOptions + +@PackageScope +final class LettuceTestUtil { + + static final ClientOptions CLIENT_OPTIONS + + static { + def options = ClientOptions.builder() + // Disable autoreconnect so we do not get stray traces popping up on server shutdown + .autoReconnect(false) + if (Boolean.getBoolean("testLatestDeps")) { + // Force RESP2 on 6+ for consistency in tests + options + .pingBeforeActivateConnection(false) + .protocolVersion(Class.forName("io.lettuce.core.protocol.ProtocolVersion").getField("RESP2").get(null)) + } + CLIENT_OPTIONS = options.build() + } + + private LettuceTestUtil() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/lettuce-common-library.gradle b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/lettuce-common-library.gradle new file mode 100644 index 000000000..4936b5e30 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/lettuce-common-library.gradle @@ -0,0 +1,9 @@ +// not applying $rootDir/gradle/instrumentation.gradle because that brings running tests with agent +// infrastructure, and this module only wants to run unit tests + +ext.mavenGroupId = 'io.opentelemetry.javaagent.instrumentation' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +archivesBaseName = projectDir.parentFile.name diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/src/main/java/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitter.java b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/src/main/java/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitter.java new file mode 100644 index 000000000..39ab126fd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/src/main/java/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitter.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class LettuceArgSplitter { + private static final Pattern KEY_PATTERN = + Pattern.compile("((key|value)<(?[^>]+)>|(?[0-9A-Za-z=]+))(\\s+|$)"); + + // this method removes the key|value<...> wrappers around redis keys or values and splits the args + // string + public static List splitArgs(@Nullable String args) { + if (args == null || args.isEmpty()) { + return Collections.emptyList(); + } + + List argsList = new ArrayList<>(); + Matcher m = KEY_PATTERN.matcher(args); + while (m.find()) { + String wrapped = m.group("wrapped"); + if (wrapped != null) { + argsList.add(wrapped); + } else { + argsList.add(m.group("plain")); + } + } + return argsList; + } + + private LettuceArgSplitter() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.groovy b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.groovy new file mode 100644 index 000000000..a708f83ee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/lettuce/lettuce-common/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.common + +import spock.lang.Specification +import spock.lang.Unroll + +class LettuceArgSplitterTest extends Specification { + @Unroll + def "should properly split #desc"() { + expect: + LettuceArgSplitter.splitArgs(args) == result + + where: + desc | args | result + "a null value" | null | [] + "an empty value" | "" | [] + "a single key" | "key" | ["key"] + "a single value" | "value" | ["value"] + "a plain string" | "teststring" | ["teststring"] + "an integer" | "42" | ["42"] + "a base64 value" | "TeST123==" | ["TeST123=="] + "a complex list of args" | "key aSDFgh4321= 5 test value" | ["key", "aSDFgh4321=", "5", "test", "val"] + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/compile-stub.gradle b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/compile-stub.gradle new file mode 100644 index 000000000..b1a828515 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/compile-stub.gradle @@ -0,0 +1,9 @@ +apply plugin: "otel.java-conventions" + +// liberty jars are not available as a maven dependency so we provide stripped +// down versions of liberty classes that we can use from integration code without +// having to do everything with reflection + +// disable checkstyle +// Abbreviation in name 'getRequestURI' must contain no more than '2' consecutive capital letters. [AbbreviationAsWordInName] +project.tasks['checkstyleMain'].enabled = false diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/ws/http/channel/internal/inbound/HttpInboundServiceContextImpl.java b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/ws/http/channel/internal/inbound/HttpInboundServiceContextImpl.java new file mode 100644 index 000000000..15cb06a12 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/ws/http/channel/internal/inbound/HttpInboundServiceContextImpl.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.ws.http.channel.internal.inbound; + +import com.ibm.wsspi.http.channel.HttpRequestMessage; + +// https://github.com/OpenLiberty/open-liberty/blob/master/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/channel/internal/inbound/HttpInboundServiceContextImpl.java +public class HttpInboundServiceContextImpl { + + public HttpRequestMessage getRequest() { + throw new UnsupportedOperationException(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/ws/http/dispatcher/internal/channel/HttpDispatcherLink.java b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/ws/http/dispatcher/internal/channel/HttpDispatcherLink.java new file mode 100644 index 000000000..c8cd3b15f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/ws/http/dispatcher/internal/channel/HttpDispatcherLink.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.ws.http.dispatcher.internal.channel; + +// https://github.com/OpenLiberty/open-liberty/blob/master/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/dispatcher/internal/channel/HttpDispatcherLink.java +public class HttpDispatcherLink { + + public int getRemotePort() { + throw new UnsupportedOperationException(); + } + + public String getRemoteHostAddress() { + throw new UnsupportedOperationException(); + } + + public String getRequestedHost() { + throw new UnsupportedOperationException(); + } + + public int getRequestedPort() { + throw new UnsupportedOperationException(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/genericbnf/HeaderField.java b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/genericbnf/HeaderField.java new file mode 100644 index 000000000..3b72443e9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/genericbnf/HeaderField.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.wsspi.genericbnf; + +// https://github.com/OpenLiberty/open-liberty/blob/master/dev/com.ibm.ws.transport.http/src/com/ibm/wsspi/genericbnf/HeaderField.java +public interface HeaderField { + + String asString(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/HttpRequestMessage.java b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/HttpRequestMessage.java new file mode 100644 index 000000000..43f6af09b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/HttpRequestMessage.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.wsspi.http.channel; + +import com.ibm.wsspi.genericbnf.HeaderField; +import java.util.List; + +// https://github.com/OpenLiberty/open-liberty/blob/master/dev/com.ibm.ws.transport.http/src/com/ibm/wsspi/http/channel/HttpRequestMessage.java +public interface HttpRequestMessage { + + String getMethod(); + + HeaderField getHeader(String paramString); + + String getScheme(); + + String getRequestURI(); + + String getQueryString(); + + String getVersion(); + + List getAllHeaderNames(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/values/StatusCodes.java b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/values/StatusCodes.java new file mode 100644 index 000000000..70dc2ebb4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/values/StatusCodes.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.wsspi.http.channel.values; + +// https://github.com/OpenLiberty/open-liberty/blob/master/dev/com.ibm.ws.transport.http/src/com/ibm/wsspi/http/channel/values/StatusCodes.java +public class StatusCodes { + + public int getIntCode() { + throw new UnsupportedOperationException(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/liberty-dispatcher-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/liberty-dispatcher-javaagent.gradle new file mode 100644 index 000000000..2840e2139 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/liberty-dispatcher-javaagent.gradle @@ -0,0 +1,10 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +// liberty and liberty-dispatcher are loaded into different class loaders +// liberty module has access to servlet api while liberty-dispatcher does not + +dependencies { + // liberty jars are not available as a maven dependency so we compile against + // stub classes + compileOnly project(':instrumentation:liberty:compile-stub') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyConnectionWrapper.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyConnectionWrapper.java new file mode 100644 index 000000000..24132c2a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyConnectionWrapper.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; + +import com.ibm.ws.http.dispatcher.internal.channel.HttpDispatcherLink; +import com.ibm.wsspi.http.channel.HttpRequestMessage; + +public class LibertyConnectionWrapper { + private final HttpDispatcherLink httpDispatcherLink; + private final HttpRequestMessage httpRequestMessage; + + public LibertyConnectionWrapper( + HttpDispatcherLink httpDispatcherLink, HttpRequestMessage httpRequestMessage) { + this.httpDispatcherLink = httpDispatcherLink; + this.httpRequestMessage = httpRequestMessage; + } + + public int peerPort() { + return httpDispatcherLink.getRemotePort(); + } + + public String peerHostIP() { + return httpDispatcherLink.getRemoteHostAddress(); + } + + public String getProtocol() { + return httpRequestMessage.getVersion(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherInstrumentationModule.java new file mode 100644 index 000000000..488280b5e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class LibertyDispatcherInstrumentationModule extends InstrumentationModule { + + public LibertyDispatcherInstrumentationModule() { + super("liberty", "liberty-dispatcher"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new LibertyDispatcherLinkInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherLinkInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherLinkInstrumentation.java new file mode 100644 index 000000000..5c597355e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherLinkInstrumentation.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; + +import static io.opentelemetry.javaagent.instrumentation.liberty.dispatcher.LibertyDispatcherTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.ibm.ws.http.channel.internal.inbound.HttpInboundServiceContextImpl; +import com.ibm.ws.http.dispatcher.internal.channel.HttpDispatcherLink; +import com.ibm.wsspi.http.channel.values.StatusCodes; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Instrumenting + * https://github.com/OpenLiberty/open-liberty/blob/master/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/dispatcher/internal/channel/HttpDispatcherLink.java + * We instrument sendResponse method that is called when no application has been deployed under + * requested context root or something goes horribly wrong and server responds with Internal Server + * Error + */ +public class LibertyDispatcherLinkInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.ibm.ws.http.dispatcher.internal.channel.HttpDispatcherLink"); + } + + @Override + public void transform(TypeTransformer transformer) { + // https://github.com/OpenLiberty/open-liberty/blob/master/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/dispatcher/internal/channel/HttpDispatcherLink.java + transformer.applyAdviceToMethod( + named("sendResponse") + .and(takesArgument(0, named("com.ibm.wsspi.http.channel.values.StatusCodes"))) + .and(takesArgument(1, named(String.class.getName()))) + .and(takesArgument(2, named(Exception.class.getName()))) + .and(takesArgument(3, named(boolean.class.getName()))), + this.getClass().getName() + "$SendResponseAdvice"); + } + + @SuppressWarnings("unused") + public static class SendResponseAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This HttpDispatcherLink httpDispatcherLink, + @Advice.FieldValue("isc") HttpInboundServiceContextImpl isc, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + LibertyRequestWrapper requestWrapper = + new LibertyRequestWrapper(httpDispatcherLink, isc.getRequest()); + LibertyConnectionWrapper connectionWrapper = + new LibertyConnectionWrapper(httpDispatcherLink, isc.getRequest()); + context = + tracer() + .startSpan( + requestWrapper, connectionWrapper, null, "HTTP " + requestWrapper.getMethod()); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Argument(value = 0) StatusCodes statusCode, + @Advice.Argument(value = 2) Exception failure, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + + LibertyResponseWrapper responseWrapper = new LibertyResponseWrapper(statusCode); + + Throwable t = failure != null ? failure : throwable; + if (t != null) { + tracer().endExceptionally(context, t, responseWrapper); + return; + } + + tracer().end(context, responseWrapper); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherTracer.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherTracer.java new file mode 100644 index 000000000..55db58351 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherTracer.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import java.net.URI; +import java.net.URISyntaxException; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LibertyDispatcherTracer + extends HttpServerTracer< + LibertyRequestWrapper, LibertyResponseWrapper, LibertyConnectionWrapper, Void> { + private static final Logger log = LoggerFactory.getLogger(LibertyDispatcherTracer.class); + private static final LibertyDispatcherTracer TRACER = new LibertyDispatcherTracer(); + + public static LibertyDispatcherTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.liberty-dispatcher"; + } + + @Override + @Nullable + protected Integer peerPort(LibertyConnectionWrapper libertyConnectionWrapper) { + return libertyConnectionWrapper.peerPort(); + } + + @Override + @Nullable + protected String peerHostIP(LibertyConnectionWrapper libertyConnectionWrapper) { + return libertyConnectionWrapper.peerHostIP(); + } + + @Override + protected String flavor( + LibertyConnectionWrapper libertyConnectionWrapper, + LibertyRequestWrapper libertyRequestWrapper) { + return libertyConnectionWrapper.getProtocol(); + } + + private static final TextMapGetter GETTER = + new TextMapGetter() { + + @Override + public Iterable keys(LibertyRequestWrapper carrier) { + return carrier.getAllHeaderNames(); + } + + @Override + public String get(LibertyRequestWrapper carrier, String key) { + return carrier.getHeaderValue(key); + } + }; + + @Override + protected TextMapGetter getGetter() { + return GETTER; + } + + @Override + protected String url(LibertyRequestWrapper libertyRequestWrapper) { + try { + return new URI( + libertyRequestWrapper.getScheme(), + null, + libertyRequestWrapper.getServerName(), + libertyRequestWrapper.getServerPort(), + libertyRequestWrapper.getRequestUri(), + libertyRequestWrapper.getQueryString(), + null) + .toString(); + } catch (URISyntaxException e) { + log.debug("Failed to construct request URI", e); + return null; + } + } + + @Override + protected String method(LibertyRequestWrapper libertyRequestWrapper) { + return libertyRequestWrapper.getMethod(); + } + + @Override + protected @Nullable String requestHeader( + LibertyRequestWrapper libertyRequestWrapper, String name) { + return libertyRequestWrapper.getHeaderValue(name); + } + + @Override + protected int responseStatus(LibertyResponseWrapper libertyResponseWrapper) { + return libertyResponseWrapper.getStatus(); + } + + @Override + @Nullable + public Context getServerContext(Void none) { + return null; + } + + @Override + protected void attachServerContext(Context context, Void none) { + // This advice is only used when server didn't find matching application or got an internal + // error. Nothing that is called within this advice should require access to the span. + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyRequestWrapper.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyRequestWrapper.java new file mode 100644 index 000000000..2fcc791af --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyRequestWrapper.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; + +import com.ibm.ws.http.dispatcher.internal.channel.HttpDispatcherLink; +import com.ibm.wsspi.genericbnf.HeaderField; +import com.ibm.wsspi.http.channel.HttpRequestMessage; +import java.util.List; + +public class LibertyRequestWrapper { + private final HttpDispatcherLink httpDispatcherLink; + private final HttpRequestMessage httpRequestMessage; + + public LibertyRequestWrapper( + HttpDispatcherLink httpDispatcherLink, HttpRequestMessage httpRequestMessage) { + this.httpDispatcherLink = httpDispatcherLink; + this.httpRequestMessage = httpRequestMessage; + } + + public String getMethod() { + return httpRequestMessage.getMethod(); + } + + public String getScheme() { + return httpRequestMessage.getScheme(); + } + + public String getRequestUri() { + return httpRequestMessage.getRequestURI(); + } + + public String getQueryString() { + return httpRequestMessage.getQueryString(); + } + + public String getServerName() { + return httpDispatcherLink.getRequestedHost(); + } + + public int getServerPort() { + return httpDispatcherLink.getRequestedPort(); + } + + public List getAllHeaderNames() { + return httpRequestMessage.getAllHeaderNames(); + } + + public String getHeaderValue(String name) { + HeaderField hf = httpRequestMessage.getHeader(name); + return hf != null ? hf.asString() : null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyResponseWrapper.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyResponseWrapper.java new file mode 100644 index 000000000..0950b8f2f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty-dispatcher/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyResponseWrapper.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; + +import com.ibm.wsspi.http.channel.values.StatusCodes; + +public class LibertyResponseWrapper { + private final StatusCodes code; + + public LibertyResponseWrapper(StatusCodes code) { + this.code = code; + } + + public int getStatus() { + return code.getIntCode(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/liberty-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/liberty-javaagent.gradle new file mode 100644 index 000000000..e08c0bbb9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/liberty-javaagent.gradle @@ -0,0 +1,11 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +// liberty and liberty-dispatcher are loaded into different class loaders +// liberty module has access to servlet api while liberty-dispatcher does not + +dependencies { + compileOnly "javax.servlet:javax.servlet-api:3.0.1" + + implementation project(':instrumentation:servlet:servlet-common:javaagent') + implementation project(':instrumentation:servlet:servlet-3.0:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyHttpServerTracer.java new file mode 100644 index 000000000..52a2a3b41 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyHttpServerTracer.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.servlet.v3_0.Servlet3HttpServerTracer; +import javax.servlet.http.HttpServletRequest; + +public class LibertyHttpServerTracer extends Servlet3HttpServerTracer { + private static final LibertyHttpServerTracer TRACER = new LibertyHttpServerTracer(); + + public static LibertyHttpServerTracer tracer() { + return TRACER; + } + + public Context startSpan(HttpServletRequest request) { + return startSpan(request, "HTTP " + request.getMethod(), /* servlet= */ false); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.liberty"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyInstrumentationModule.java new file mode 100644 index 000000000..cb9b0b100 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyInstrumentationModule.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +/** + * Instrumenting request handling in Liberty. + * + *
    + *
  • On entry to WebApp.handleRequest remember request. {@link + * LibertyWebAppInstrumentation.HandleRequestAdvice} + *
  • On call to WebApp.isForbidden (called from WebApp.handleRequest) start span based on + * remembered request. We don't start span immediately at the start or handleRequest because + * HttpServletRequest isn't usable yet. {@link LibertyWebAppInstrumentation.IsForbiddenAdvice} + *
  • On exit from WebApp.handleRequest close the span. {@link + * LibertyWebAppInstrumentation.HandleRequestAdvice} + *
+ */ +@AutoService(InstrumentationModule.class) +public class LibertyInstrumentationModule extends InstrumentationModule { + + public LibertyInstrumentationModule() { + super("liberty"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new LibertyWebAppInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyWebAppInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyWebAppInstrumentation.java new file mode 100644 index 000000000..437874aac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertyWebAppInstrumentation.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty; + +import static io.opentelemetry.javaagent.instrumentation.liberty.LibertyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterAdviceHelper; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class LibertyWebAppInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.ibm.ws.webcontainer.webapp.WebApp"); + } + + @Override + public void transform(TypeTransformer transformer) { + // https://github.com/OpenLiberty/open-liberty/blob/master/dev/com.ibm.ws.webcontainer/src/com/ibm/ws/webcontainer/webapp/WebApp.java + transformer.applyAdviceToMethod( + named("handleRequest") + .and(takesArgument(0, named("javax.servlet.ServletRequest"))) + .and(takesArgument(1, named("javax.servlet.ServletResponse"))) + .and(takesArgument(2, named("com.ibm.wsspi.http.HttpInboundConnection"))), + this.getClass().getName() + "$HandleRequestAdvice"); + + // isForbidden is called from handleRequest + transformer.applyAdviceToMethod( + named("isForbidden").and(takesArgument(0, named(String.class.getName()))), + this.getClass().getName() + "$IsForbiddenAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0) ServletRequest request, + @Advice.Argument(value = 1) ServletResponse response) { + + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + return; + } + + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + // it is a bit too early to start span at this point because calling + // some methods on HttpServletRequest will give a NPE + // just remember the request and use it a bit later to start the span + ThreadLocalContext.startRequest(httpServletRequest, (HttpServletResponse) response); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(0) ServletRequest servletRequest, + @Advice.Argument(1) ServletResponse servletResponse, + @Advice.Thrown Throwable throwable) { + ThreadLocalContext ctx = ThreadLocalContext.endRequest(); + if (ctx == null) { + return; + } + + Context context = ctx.getContext(); + Scope scope = ctx.getScope(); + if (scope == null) { + return; + } + scope.close(); + + if (context == null) { + // an existing span was found + return; + } + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + tracer().setPrincipal(context, request); + + if (throwable != null) { + tracer().endExceptionally(context, throwable, response); + return; + } + + if (ServletAndFilterAdviceHelper.mustEndOnHandlerMethodExit(tracer(), request)) { + tracer().end(context, response); + } + } + } + + @SuppressWarnings("unused") + public static class IsForbiddenAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + ThreadLocalContext ctx = ThreadLocalContext.get(); + if (ctx == null || !ctx.startSpan()) { + return; + } + + Context context = tracer().startSpan(ctx.getRequest()); + Scope scope = context.makeCurrent(); + + ctx.setContext(context); + ctx.setScope(scope); + + // Must be set here since Liberty RequestProcessors can use startAsync outside of servlet + // scope. + tracer().setAsyncListenerResponse(ctx.getRequest(), ctx.getResponse()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/ThreadLocalContext.java b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/ThreadLocalContext.java new file mode 100644 index 000000000..698887ec0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/liberty/liberty/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/ThreadLocalContext.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.liberty; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class ThreadLocalContext { + + private static final ThreadLocal local = new ThreadLocal<>(); + + private final HttpServletRequest request; + private final HttpServletResponse response; + private Context context; + private Scope scope; + private boolean started; + + private ThreadLocalContext(HttpServletRequest request, HttpServletResponse response) { + this.request = request; + this.response = response; + } + + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } + + public Scope getScope() { + return scope; + } + + public void setScope(Scope scope) { + this.scope = scope; + } + + public HttpServletRequest getRequest() { + return request; + } + + public HttpServletResponse getResponse() { + return response; + } + + /** + * Test whether span should be started. + * + * @return true when span should be started, false when span was already started + */ + public boolean startSpan() { + boolean b = started; + started = true; + return !b; + } + + public static void startRequest(HttpServletRequest request, HttpServletResponse response) { + ThreadLocalContext ctx = new ThreadLocalContext(request, response); + local.set(ctx); + } + + public static ThreadLocalContext get() { + return local.get(); + } + + public static ThreadLocalContext endRequest() { + ThreadLocalContext ctx = local.get(); + if (ctx != null) { + local.remove(); + } + return ctx; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/log4j-1.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/log4j-1.2-javaagent.gradle new file mode 100644 index 000000000..8607dc435 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/log4j-1.2-javaagent.gradle @@ -0,0 +1,26 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "log4j" + module = "log4j" + versions = "[1.2,)" + // version 1.2.15 has a bad dependency on javax.jms:jms:1.1 which was released as pom only + skip('1.2.15') + } +} + +dependencies { + // 1.2 introduces MDC and there's no version earlier than 1.2.4 available + library "log4j:log4j:1.2.4" +} + +configurations { + // In order to test the real log4j library we need to remove the log4j transitive + // dependency 'log4j-over-slf4j' brought in by :testing-common which would shadow + // the log4j module under test using a proxy to slf4j instead. + testImplementation.exclude group: 'org.slf4j', module: 'log4j-over-slf4j' + + // See: https://stackoverflow.com/a/9047963/2749853 + testImplementation.exclude group: 'javax.jms', module: 'jms' +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/CategoryInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/CategoryInstrumentation.java new file mode 100644 index 000000000..77f01f90a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/CategoryInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.v1_2; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.log4j.spi.LoggingEvent; + +public class CategoryInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.log4j.Category"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("callAppenders")) + .and(takesArguments(1)) + .and(takesArgument(0, named("org.apache.log4j.spi.LoggingEvent"))), + CategoryInstrumentation.class.getName() + "$CallAppendersAdvice"); + } + + @SuppressWarnings("unused") + public static class CallAppendersAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) LoggingEvent event) { + InstrumentationContext.get(LoggingEvent.class, Span.class) + .put(event, Java8BytecodeBridge.currentSpan()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/Log4j1InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/Log4j1InstrumentationModule.java new file mode 100644 index 000000000..7b1328b66 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/Log4j1InstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.v1_2; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Log4j1InstrumentationModule extends InstrumentationModule { + public Log4j1InstrumentationModule() { + super("log4j", "log4j-1.2"); + } + + @Override + public List typeInstrumentations() { + return asList(new CategoryInstrumentation(), new LoggingEventInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/LoggingEventInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/LoggingEventInstrumentation.java new file mode 100644 index 000000000..c08c9e550 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v1_2/LoggingEventInstrumentation.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.v1_2; + +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.SPAN_ID; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_FLAGS; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_ID; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import java.util.Hashtable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.log4j.MDC; +import org.apache.log4j.spi.LoggingEvent; + +public class LoggingEventInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.log4j.spi.LoggingEvent"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("getMDC")) + .and(takesArguments(1)) + .and(takesArgument(0, String.class)), + LoggingEventInstrumentation.class.getName() + "$GetMdcAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("getMDCCopy")).and(takesArguments(0)), + LoggingEventInstrumentation.class.getName() + "$GetMdcCopyAdvice"); + } + + @SuppressWarnings("unused") + public static class GetMdcAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This LoggingEvent event, + @Advice.Argument(0) String key, + @Advice.Return(readOnly = false) Object value) { + if (TRACE_ID.equals(key) || SPAN_ID.equals(key) || TRACE_FLAGS.equals(key)) { + if (value != null) { + // Assume already instrumented event if traceId/spanId/sampled is present. + return; + } + + Span span = InstrumentationContext.get(LoggingEvent.class, Span.class).get(event); + if (span == null || !span.getSpanContext().isValid()) { + return; + } + + SpanContext spanContext = span.getSpanContext(); + switch (key) { + case TRACE_ID: + value = spanContext.getTraceId(); + break; + case SPAN_ID: + value = spanContext.getSpanId(); + break; + case TRACE_FLAGS: + value = spanContext.getTraceFlags().asHex(); + break; + default: + // do nothing + } + } + } + } + + @SuppressWarnings("unused") + public static class GetMdcCopyAdvice { + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This LoggingEvent event, + @Advice.FieldValue(value = "mdcCopyLookupRequired", readOnly = false) boolean copyRequired, + @Advice.FieldValue(value = "mdcCopy", readOnly = false) Hashtable mdcCopy) { + // this advice basically replaces the original method + + if (copyRequired) { + copyRequired = false; + + Hashtable mdc = new Hashtable(); + + Hashtable originalMdc = MDC.getContext(); + if (originalMdc != null) { + mdc.putAll(originalMdc); + } + + // Assume already instrumented event if traceId is present. + if (!mdc.containsKey(TRACE_ID)) { + Span span = InstrumentationContext.get(LoggingEvent.class, Span.class).get(event); + if (span != null && span.getSpanContext().isValid()) { + SpanContext spanContext = span.getSpanContext(); + mdc.put(TRACE_ID, spanContext.getTraceId()); + mdc.put(SPAN_ID, spanContext.getSpanId()); + mdc.put(TRACE_FLAGS, spanContext.getTraceFlags().asHex()); + } + } + + mdcCopy = mdc; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/groovy/ListAppender.groovy b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/groovy/ListAppender.groovy new file mode 100644 index 000000000..6960ee0c7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/groovy/ListAppender.groovy @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.apache.log4j.AppenderSkeleton +import org.apache.log4j.spi.LoggingEvent + +class ListAppender extends AppenderSkeleton { + static events = new ArrayList() + + @Override + protected void append(LoggingEvent loggingEvent) { + events.add(loggingEvent) + } + + @Override + boolean requiresLayout() { + return false + } + + @Override + void close() { + } + + static clearEvents() { + events.clear() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/groovy/Log4j1MdcTest.groovy b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/groovy/Log4j1MdcTest.groovy new file mode 100644 index 000000000..5e18d3d0c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/groovy/Log4j1MdcTest.groovy @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.TraceUtils +import org.apache.log4j.LogManager + +class Log4j1MdcTest extends AgentInstrumentationSpecification { + def cleanup() { + ListAppender.clearEvents() + } + + def "no ids when no span"() { + given: + def logger = LogManager.getLogger('TestLogger') + + when: + logger.info("log message 1") + logger.info("log message 2") + + then: + def events = ListAppender.events + + events.size() == 2 + events[0].message == "log message 1" + events[0].getMDC("trace_id") == null + events[0].getMDC("span_id") == null + events[0].getMDC("trace_flags") == null + + events[1].message == "log message 2" + events[1].getMDC("trace_id") == null + events[1].getMDC("span_id") == null + events[1].getMDC("trace_flags") == null + } + + def "ids when span"() { + given: + def logger = LogManager.getLogger('TestLogger') + + when: + def span1 = TraceUtils.runUnderTrace("test") { + logger.info("log message 1") + Span.current() + } + + logger.info("log message 2") + + def span2 = TraceUtils.runUnderTrace("test 2") { + logger.info("log message 3") + Span.current() + } + + then: + def events = ListAppender.events + + events.size() == 3 + events[0].message == "log message 1" + events[0].getMDC("trace_id") == span1.spanContext.traceId + events[0].getMDC("span_id") == span1.spanContext.spanId + events[0].getMDC("trace_flags") == "01" + + events[1].message == "log message 2" + events[1].getMDC("trace_id") == null + events[1].getMDC("span_id") == null + events[1].getMDC("trace_flags") == null + + events[2].message == "log message 3" + // this explicit getMDCCopy() call here is to make sure that whole instrumentation is tested + events[2].getMDCCopy() + events[2].getMDC("trace_id") == span2.spanContext.traceId + events[2].getMDC("span_id") == span2.spanContext.spanId + events[2].getMDC("trace_flags") == "01" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/resources/log4j.properties b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/resources/log4j.properties new file mode 100644 index 000000000..e7104aab6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-1.2/javaagent/src/test/resources/log4j.properties @@ -0,0 +1,2 @@ +log4j.rootLogger=INFO, LIST +log4j.appender.LIST=ListAppender diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/log4j-2-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/log4j-2-testing.gradle new file mode 100644 index 000000000..dbe8f86df --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/log4j-2-testing.gradle @@ -0,0 +1,15 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "org.apache.logging.log4j:log4j-core:2.7" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" + + annotationProcessor "org.apache.logging.log4j:log4j-core:2.7" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/groovy/Log4j2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/groovy/Log4j2Test.groovy new file mode 100644 index 000000000..8028d0096 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/groovy/Log4j2Test.groovy @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.log4j.v2_13_2.ListAppender +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.TraceUtils +import org.apache.logging.log4j.LogManager + +abstract class Log4j2Test extends InstrumentationSpecification { + def cleanup() { + ListAppender.get().clearEvents() + } + + def "no ids when no span"() { + given: + def logger = LogManager.getLogger("TestLogger") + + when: + logger.info("log message 1") + logger.info("log message 2") + + def events = ListAppender.get().getEvents() + + then: + events.size() == 2 + events[0].message == "log message 1" + events[0].contextData["trace_id"] == null + events[0].contextData["span_id"] == null + events[0].contextData["trace_flags"] == null + + events[1].message == "log message 2" + events[1].contextData["trace_id"] == null + events[1].contextData["span_id"] == null + events[1].contextData["trace_flags"] == null + } + + def "ids when span"() { + given: + def logger = LogManager.getLogger("TestLogger") + + when: + Span span1 = TraceUtils.runUnderTrace("test") { + logger.info("log message 1") + Span.current() + } + + logger.info("log message 2") + + Span span2 = TraceUtils.runUnderTrace("test 2") { + logger.info("log message 3") + Span.current() + } + + def events = ListAppender.get().getEvents() + + then: + events.size() == 3 + events[0].message == "log message 1" + events[0].contextData["trace_id"] == span1.spanContext.traceId + events[0].contextData["span_id"] == span1.spanContext.spanId + events[0].contextData["trace_flags"] == "01" + + events[1].message == "log message 2" + events[1].contextData["trace_id"] == null + events[1].contextData["span_id"] == null + events[1].contextData["trace_flags"] == null + + events[2].message == "log message 3" + events[2].contextData["trace_id"] == span2.spanContext.traceId + events[2].contextData["span_id"] == span2.spanContext.spanId + events[2].contextData["trace_flags"] == "01" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/java/io/opentelemetry/instrumentation/log4j/v2_13_2/ListAppender.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/java/io/opentelemetry/instrumentation/log4j/v2_13_2/ListAppender.java new file mode 100644 index 000000000..c91754875 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/java/io/opentelemetry/instrumentation/log4j/v2_13_2/ListAppender.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.v2_13_2; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; + +@Plugin( + name = "ListAppender", + category = "Core", + elementType = Appender.ELEMENT_TYPE, + printObject = true) +public class ListAppender extends AbstractAppender { + + public static ListAppender get() { + return INSTANCE; + } + + private static final ListAppender INSTANCE = new ListAppender(); + + private final List events = Collections.synchronizedList(new ArrayList<>()); + + public ListAppender() { + super("ListAppender", null, null, /* ignoreExceptions= */ true); + } + + public List getEvents() { + return events; + } + + public void clearEvents() { + events.clear(); + } + + @Override + public void append(LogEvent logEvent) { + // Event object may be reused by the framework so copy the data we need. + LoggedEvent copied = + new LoggedEvent( + logEvent.getMessage().getFormattedMessage(), + new HashMap<>(logEvent.getContextData().toMap())); + events.add(copied); + } + + @PluginFactory + public static ListAppender createAppender(@PluginAttribute("name") String name) { + if (!name.equals("ListAppender")) { + throw new IllegalArgumentException( + "Use name=\"ListAppender\" in log4j2-test.xml instead of " + name); + } + return INSTANCE; + } + + public static class LoggedEvent { + private final String message; + private final Map contextData; + + LoggedEvent(String message, Map contextData) { + this.message = message; + this.contextData = contextData; + } + + public String getMessage() { + return message; + } + + public Map getContextData() { + return contextData; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/resources/log4j2-test.xml b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/resources/log4j2-test.xml new file mode 100644 index 000000000..17138ea6e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2-testing/src/main/resources/log4j2-test.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/log4j-2.13.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/log4j-2.13.2-javaagent.gradle new file mode 100644 index 000000000..19f2a2101 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/log4j-2.13.2-javaagent.gradle @@ -0,0 +1,41 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: "org.unbroken-dome.test-sets" + +muzzle { + pass { + group = "org.apache.logging.log4j" + module = "log4j-core" + versions = "[2.13.2,)" + assertInverse = true + } +} + +testSets { + // Very different codepaths when threadlocals are enabled or not so we check both. + // Regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2403 + testDisableThreadLocals { + dirName = "test" + } +} + +dependencies { + library "org.apache.logging.log4j:log4j-core:2.13.2" + + implementation project(':instrumentation:log4j:log4j-2.13.2:library') + + testImplementation project(':instrumentation:log4j:log4j-2-testing') +} + +// Threadlocals are always false if is.webapp is true, so we make sure to override it because as of +// now testing-common includes jetty / servlet. +test { + jvmArgs "-Dlog4j2.is.webapp=false" + jvmArgs "-Dlog4j2.enable.threadlocals=true" +} + +testDisableThreadLocals { + jvmArgs "-Dlog4j2.is.webapp=false" + jvmArgs "-Dlog4j2.enable.threadlocals=false" +} + +check.dependsOn testDisableThreadLocals \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_13_2/BugFixingInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_13_2/BugFixingInstrumentation.java new file mode 100644 index 000000000..e96a23974 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_13_2/BugFixingInstrumentation.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.v2_13_2; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.logging.log4j.core.config.Property; + +// Log4 J 2.13.2 has a critical bug that prevents using ContextDataProvider with many log4j.xml +// configurations. We introduce a patch which should be low enough overhead to keep on higher +// versions too. +// +// https://github.com/apache/logging-log4j2/commit/e5394028c000008505991c45b8ce593422f7ac55 +public class BugFixingInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named( + "org.apache.logging.log4j.core.impl.ThreadContextDataInjector$ForCopyOnWriteThreadContextMap"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("injectContextData")) + .and(takesArgument(0, List.class)), + BugFixingInstrumentation.class.getName() + "$BugFixingAdvice"); + } + + @SuppressWarnings("unused") + public static class BugFixingAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(value = 0, readOnly = false) List props) { + if (props == null) { + props = Collections.emptyList(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_13_2/Log4j2InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_13_2/Log4j2InstrumentationModule.java new file mode 100644 index 000000000..8a508b66e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_13_2/Log4j2InstrumentationModule.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.v2_13_2; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Arrays; +import java.util.List; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class Log4j2InstrumentationModule extends InstrumentationModule { + public Log4j2InstrumentationModule() { + super("log4j", "log4j-2.13.2"); + } + + @Override + public List helperResourceNames() { + return singletonList( + "META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("org.apache.logging.log4j.core.util.ContextDataProvider"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new BugFixingInstrumentation(), new ResourceInjectingTypeInstrumentation()); + } + + // A type instrumentation is needed to trigger resource injection. + public static class ResourceInjectingTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // we cannot use ContextDataProvider here because one of the classes that we inject implements + // this interface, causing the interface to be loaded while it's being transformed, which + // leads to duplicate class definition error after the interface is transformed and the + // triggering class loader tries to load it. + return named("org.apache.logging.log4j.core.impl.ThreadContextDataInjector"); + } + + @Override + public void transform(TypeTransformer transformer) { + // Nothing to transform, this type instrumentation is only used for injecting resources. + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/test/groovy/AutoLog4j2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/test/groovy/AutoLog4j2Test.groovy new file mode 100644 index 000000000..260ff5915 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/javaagent/src/test/groovy/AutoLog4j2Test.groovy @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class AutoLog4j2Test extends Log4j2Test implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/README.md b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/README.md new file mode 100644 index 000000000..c47b33eee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/README.md @@ -0,0 +1,60 @@ +# Log4j 2 Integration + +This module integrates instrumentation with Log4j 2 by injecting the trace ID and span ID from a +mounted span into +Log4j's [context data](https://logging.apache.org/log4j/2.x/manual/thread-context.html). + +**Note**: Depending on your application, you may run into a [critical bug](https://issues.apache.org/jira/browse/LOG4J2-2838) +with Log4j 2.13.2. If log messages show a `NullPointerException` when adding this instrumentation, +please update to 2.13.3 or higher. The only change between 2.13.2 and 2.13.3 is the fix to this +issue. + +To use it, just add the module to your application's runtime classpath. + +**Maven** + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-log4j-2.13.2 + 0.17.0-alpha + runtime + + +``` + +**Gradle** + +```kotlin +dependencies { + runtimeOnly("io.opentelemetry.instrumentation:opentelemetry-log4j-2.13.2:0.17.0-alpha") +} +``` + +Log4j will automatically pick up our integration and will have these keys added to the context when +a log statement is made when a span is active. + +- `trace_id` +- `span_id` +- `trace_flags` + +You can use these keys when defining an appender in your `log4j.xml` configuration, for example + +```xml + + + + + + + + + + + + + +``` diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/log4j-2.13.2-library.gradle b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/log4j-2.13.2-library.gradle new file mode 100644 index 000000000..9d4162295 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/log4j-2.13.2-library.gradle @@ -0,0 +1,11 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library "org.apache.logging.log4j:log4j-core:2.13.2" + + // Library instrumentation cannot be applied to 2.13.2 due to a bug in Log4J. The agent works + // around it. + testLibrary "org.apache.logging.log4j:log4j-core:2.13.3" + + testImplementation project(':instrumentation:log4j:log4j-2-testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/main/java/io/opentelemetry/instrumentation/log4j/v2_13_2/OpenTelemetryContextDataProvider.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/main/java/io/opentelemetry/instrumentation/log4j/v2_13_2/OpenTelemetryContextDataProvider.java new file mode 100644 index 000000000..db87e187f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/main/java/io/opentelemetry/instrumentation/log4j/v2_13_2/OpenTelemetryContextDataProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.v2_13_2; + +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.SPAN_ID; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_FLAGS; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_ID; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.logging.log4j.core.util.ContextDataProvider; + +/** + * Implementation of Log4j 2's {@link ContextDataProvider} which is loaded via SPI. {@link + * #supplyContextData()} is called when a log entry is created. + */ +public class OpenTelemetryContextDataProvider implements ContextDataProvider { + + /** + * Returns context from the current span when available. + * + * @return A map containing string versions of the traceId, spanId, and traceFlags, which can then + * be accessed from layout components + */ + @Override + public Map supplyContextData() { + Span currentSpan = Span.current(); + if (!currentSpan.getSpanContext().isValid()) { + return Collections.emptyMap(); + } + + Map contextData = new HashMap<>(); + SpanContext spanContext = currentSpan.getSpanContext(); + contextData.put(TRACE_ID, spanContext.getTraceId()); + contextData.put(SPAN_ID, spanContext.getSpanId()); + contextData.put(TRACE_FLAGS, spanContext.getTraceFlags().asHex()); + return contextData; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider new file mode 100644 index 000000000..e4eb8009d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.log4j.v2_13_2.OpenTelemetryContextDataProvider diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/test/groovy/LibraryLog4j2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/test/groovy/LibraryLog4j2Test.groovy new file mode 100644 index 000000000..c5fa9fb7b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.13.2/library/src/test/groovy/LibraryLog4j2Test.groovy @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class LibraryLog4j2Test extends Log4j2Test implements LibraryTestTrait { +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/log4j-2.7-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/log4j-2.7-javaagent.gradle new file mode 100644 index 000000000..35c77a483 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/log4j-2.7-javaagent.gradle @@ -0,0 +1,18 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.logging.log4j" + module = "log4j-core" + versions = "[2.7,2.13.2)" + assertInverse = true + } +} + +dependencies { + library "org.apache.logging.log4j:log4j-core:2.7" + + testImplementation project(':instrumentation:log4j:log4j-2-testing') + + latestDepTestLibrary "org.apache.logging.log4j:log4j-core:2.13.1" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/ContextDataInjectorFactoryInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/ContextDataInjectorFactoryInstrumentation.java new file mode 100644 index 000000000..8899bbd1c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/ContextDataInjectorFactoryInstrumentation.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.v2_7; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.logging.log4j.core.ContextDataInjector; + +public class ContextDataInjectorFactoryInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.logging.log4j.core.impl.ContextDataInjectorFactory"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(isStatic()) + .and(named("createInjector")) + .and(returns(named("org.apache.logging.log4j.core.ContextDataInjector"))), + this.getClass().getName() + "$CreateInjectorAdvice"); + } + + @SuppressWarnings("unused") + public static class CreateInjectorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) + ContextDataInjector injector) { + injector = new SpanDecoratingContextDataInjector(injector); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/Log4j27InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/Log4j27InstrumentationModule.java new file mode 100644 index 000000000..9ba59a79f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/Log4j27InstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.v2_7; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class Log4j27InstrumentationModule extends InstrumentationModule { + public Log4j27InstrumentationModule() { + super("log4j", "log4j-2.7"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("org.apache.logging.log4j.core.impl.ContextDataInjectorFactory") + .and(not(hasClassesNamed("org.apache.logging.log4j.core.util.ContextDataProvider"))); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ContextDataInjectorFactoryInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/SpanDecoratingContextDataInjector.java b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/SpanDecoratingContextDataInjector.java new file mode 100644 index 000000000..853381d0d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/v2_7/SpanDecoratingContextDataInjector.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.v2_7; + +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.SPAN_ID; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_FLAGS; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_ID; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import java.util.List; +import org.apache.logging.log4j.core.ContextDataInjector; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.apache.logging.log4j.util.SortedArrayStringMap; +import org.apache.logging.log4j.util.StringMap; + +public final class SpanDecoratingContextDataInjector implements ContextDataInjector { + private final ContextDataInjector delegate; + + public SpanDecoratingContextDataInjector(ContextDataInjector delegate) { + this.delegate = delegate; + } + + @Override + public StringMap injectContextData(List list, StringMap stringMap) { + StringMap contextData = delegate.injectContextData(list, stringMap); + + if (contextData.containsKey(TRACE_ID)) { + // Assume already instrumented event if traceId is present. + return contextData; + } + + SpanContext currentContext = Java8BytecodeBridge.currentSpan().getSpanContext(); + if (!currentContext.isValid()) { + return contextData; + } + + StringMap newContextData = new SortedArrayStringMap(contextData); + newContextData.putValue(TRACE_ID, currentContext.getTraceId()); + newContextData.putValue(SPAN_ID, currentContext.getSpanId()); + newContextData.putValue(TRACE_FLAGS, currentContext.getTraceFlags().asHex()); + return newContextData; + } + + @Override + public ReadOnlyStringMap rawContextData() { + return delegate.rawContextData(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/test/groovy/Log4j27Test.groovy b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/test/groovy/Log4j27Test.groovy new file mode 100644 index 000000000..24e5b98f4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/log4j/log4j-2.7/javaagent/src/test/groovy/Log4j27Test.groovy @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class Log4j27Test extends Log4j2Test implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/logback-1.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/logback-1.0-javaagent.gradle new file mode 100644 index 000000000..700fb34fd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/logback-1.0-javaagent.gradle @@ -0,0 +1,20 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "ch.qos.logback" + module = "logback-classic" + versions = "[1.0.0,1.2.3]" + } +} + +dependencies { + implementation project(':instrumentation:logback-1.0:library') + + library "ch.qos.logback:logback-classic:1.0.0" + + testImplementation project(':instrumentation:logback-1.0:testing') + + // 1.3+ contains breaking changes, check back after it stabilizes. + latestDepTestLibrary "ch.qos.logback:logback-classic:1.2.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackInstrumentationModule.java new file mode 100644 index 000000000..2b3cc31ac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.logback.v1_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class LogbackInstrumentationModule extends InstrumentationModule { + public LogbackInstrumentationModule() { + super("logback", "logback-1.0"); + } + + @Override + public List typeInstrumentations() { + return asList(new LoggerInstrumentation(), new LoggingEventInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LoggerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LoggerInstrumentation.java new file mode 100644 index 000000000..578686247 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LoggerInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.logback.v1_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class LoggerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("ch.qos.logback.classic.Logger"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("callAppenders")) + .and(takesArguments(1)) + .and(takesArgument(0, named("ch.qos.logback.classic.spi.ILoggingEvent"))), + LoggerInstrumentation.class.getName() + "$CallAppendersAdvice"); + } + + @SuppressWarnings("unused") + public static class CallAppendersAdvice { + + @Advice.OnMethodEnter + public static void onEnter(@Advice.Argument(value = 0, readOnly = false) ILoggingEvent event) { + InstrumentationContext.get(ILoggingEvent.class, Span.class) + .put(event, Java8BytecodeBridge.currentSpan()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LoggingEventInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LoggingEventInstrumentation.java new file mode 100644 index 000000000..e9e1388bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LoggingEventInstrumentation.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.logback.v1_0; + +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.SPAN_ID; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_FLAGS; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_ID; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.instrumentation.logback.v1_0.internal.UnionMap; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner.Typing; +import net.bytebuddy.matcher.ElementMatcher; + +public class LoggingEventInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("ch.qos.logback.classic.spi.ILoggingEvent"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("ch.qos.logback.classic.spi.ILoggingEvent")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(namedOneOf("getMDCPropertyMap", "getMdc")) + .and(takesArguments(0)), + LoggingEventInstrumentation.class.getName() + "$GetMdcAdvice"); + } + + @SuppressWarnings("unused") + public static class GetMdcAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This ILoggingEvent event, + @Advice.Return(typing = Typing.DYNAMIC, readOnly = false) Map contextData) { + if (contextData != null && contextData.containsKey(TRACE_ID)) { + // Assume already instrumented event if traceId is present. + return; + } + + Span currentSpan = InstrumentationContext.get(ILoggingEvent.class, Span.class).get(event); + if (currentSpan == null || !currentSpan.getSpanContext().isValid()) { + return; + } + + Map spanContextData = new HashMap<>(); + SpanContext spanContext = currentSpan.getSpanContext(); + spanContextData.put(TRACE_ID, spanContext.getTraceId()); + spanContextData.put(SPAN_ID, spanContext.getSpanId()); + spanContextData.put(TRACE_FLAGS, spanContext.getTraceFlags().asHex()); + + if (contextData == null) { + contextData = spanContextData; + } else { + contextData = new UnionMap<>(contextData, spanContextData); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.groovy b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.groovy new file mode 100644 index 000000000..dfb90d616 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.groovy @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.logback.v1_0 + +import io.opentelemetry.instrumentation.logback.v1_0.AbstractLogbackTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class LogbackTest extends AbstractLogbackTest implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/test/resources/logback.xml new file mode 100644 index 000000000..3434fbaaa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/javaagent/src/test/resources/logback.xml @@ -0,0 +1,19 @@ + + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/README.md b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/README.md new file mode 100644 index 000000000..9e2792e29 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/README.md @@ -0,0 +1,55 @@ +# Logback Integration + +This module integrates instrumentation with Logback by injecting the trace ID and span ID from a +mounted span using a custom Logback appender. + +To use it, add the module to your application's runtime classpath and add the appender to your +`logback.xml`. + +**Maven** + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-logback-1.0 + 0.17.0-alpha + runtime + + +``` + +**Gradle** + +```kotlin +dependencies { + runtimeOnly("io.opentelemetry.instrumentation:opentelemetry-logback-1.0:0.17.0-alpha") +} +``` + +**logback.xml** + +```xml + + + + + %d{HH:mm:ss.SSS} trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %msg%n + + + + + + + + ... + +``` + +Logging events will automatically have context information from the span context injected. The +following attributes are available for use: + +- `trace_id` +- `span_id` +- `trace_flags` diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/logback-1.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/logback-1.0-library.gradle new file mode 100644 index 000000000..32289ac60 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/logback-1.0-library.gradle @@ -0,0 +1,10 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library "ch.qos.logback:logback-classic:1.0.0" + + testImplementation project(':instrumentation:logback-1.0:testing') + + // 1.3+ contains breaking changes, check back after it stabilizes. + latestDepTestLibrary "ch.qos.logback:logback-classic:1.2.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/LoggingEventWrapper.java b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/LoggingEventWrapper.java new file mode 100644 index 000000000..419afd3b5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/LoggingEventWrapper.java @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.v1_0; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.LoggerContextVO; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Marker; + +final class LoggingEventWrapper implements ILoggingEvent { + private final ILoggingEvent event; + private final Map mdcPropertyMap; + @Nullable private final LoggerContextVO vo; + + LoggingEventWrapper(ILoggingEvent event, Map mdcPropertyMap) { + this.event = event; + this.mdcPropertyMap = mdcPropertyMap; + + LoggerContextVO oldVo = event.getLoggerContextVO(); + if (oldVo != null) { + vo = new LoggerContextVO(oldVo.getName(), mdcPropertyMap, oldVo.getBirthTime()); + } else { + vo = null; + } + } + + @Override + public Object[] getArgumentArray() { + return event.getArgumentArray(); + } + + @Override + public Level getLevel() { + return event.getLevel(); + } + + @Override + public String getLoggerName() { + return event.getLoggerName(); + } + + @Override + public String getThreadName() { + return event.getThreadName(); + } + + @Override + public IThrowableProxy getThrowableProxy() { + return event.getThrowableProxy(); + } + + @Override + public void prepareForDeferredProcessing() { + event.prepareForDeferredProcessing(); + } + + @Override + public LoggerContextVO getLoggerContextVO() { + return vo; + } + + @Override + public String getMessage() { + return event.getMessage(); + } + + @Override + public long getTimeStamp() { + return event.getTimeStamp(); + } + + @Override + public StackTraceElement[] getCallerData() { + return event.getCallerData(); + } + + @Override + public boolean hasCallerData() { + return event.hasCallerData(); + } + + @Override + public Marker getMarker() { + return event.getMarker(); + } + + @Override + public String getFormattedMessage() { + return event.getFormattedMessage(); + } + + @Override + public Map getMDCPropertyMap() { + return mdcPropertyMap; + } + + /** + * A synonym for {@link #getMDCPropertyMap}. + * + * @deprecated Use {@link #getMDCPropertyMap()}. + */ + @Override + @Deprecated + public Map getMdc() { + return event.getMDCPropertyMap(); + } + + @Override + public String toString() { + return event.toString(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/OpenTelemetryAppender.java b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/OpenTelemetryAppender.java new file mode 100644 index 000000000..a6a59f1d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/OpenTelemetryAppender.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.v1_0; + +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.SPAN_ID; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_FLAGS; +import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_ID; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.UnsynchronizedAppenderBase; +import ch.qos.logback.core.spi.AppenderAttachable; +import ch.qos.logback.core.spi.AppenderAttachableImpl; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.instrumentation.logback.v1_0.internal.UnionMap; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class OpenTelemetryAppender extends UnsynchronizedAppenderBase + implements AppenderAttachable { + + private final AppenderAttachableImpl aai = new AppenderAttachableImpl<>(); + + public static ILoggingEvent wrapEvent(ILoggingEvent event) { + Span currentSpan = Span.current(); + if (!currentSpan.getSpanContext().isValid()) { + return event; + } + + Map eventContext = event.getMDCPropertyMap(); + if (eventContext != null && eventContext.containsKey(TRACE_ID)) { + // Assume already instrumented event if traceId is present. + return event; + } + + Map contextData = new HashMap<>(); + SpanContext spanContext = currentSpan.getSpanContext(); + contextData.put(TRACE_ID, spanContext.getTraceId()); + contextData.put(SPAN_ID, spanContext.getSpanId()); + contextData.put(TRACE_FLAGS, spanContext.getTraceFlags().asHex()); + + if (eventContext == null) { + eventContext = contextData; + } else { + eventContext = new UnionMap<>(eventContext, contextData); + } + + return new LoggingEventWrapper(event, eventContext); + } + + @Override + protected void append(ILoggingEvent event) { + aai.appendLoopOnAppenders(wrapEvent(event)); + } + + @Override + public void addAppender(Appender appender) { + aai.addAppender(appender); + } + + @Override + public Iterator> iteratorForAppenders() { + return aai.iteratorForAppenders(); + } + + @Override + public Appender getAppender(String name) { + return aai.getAppender(name); + } + + @Override + public boolean isAttached(Appender appender) { + return aai.isAttached(appender); + } + + @Override + public void detachAndStopAllAppenders() { + aai.detachAndStopAllAppenders(); + } + + @Override + public boolean detachAppender(Appender appender) { + return aai.detachAppender(appender); + } + + @Override + public boolean detachAppender(String name) { + return aai.detachAppender(name); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/internal/UnionMap.java b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/internal/UnionMap.java new file mode 100644 index 000000000..bb1cde85c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/v1_0/internal/UnionMap.java @@ -0,0 +1,203 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.v1_0.internal; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * An immutable view over two maps, with keys resolving from the first map first, or otherwise the + * second if not present in the first. + */ +public final class UnionMap extends AbstractMap { + + private final Map first; + private final Map second; + private int size = -1; + private Set> entrySet; + + public UnionMap(Map first, Map second) { + this.first = first; + this.second = second; + } + + @Override + public int size() { + if (size >= 0) { + return size; + } + + final Map a; + final Map b; + if (first.size() >= second.size()) { + a = first; + b = second; + } else { + a = second; + b = first; + } + + int size = a.size(); + if (!b.isEmpty()) { + for (K k : b.keySet()) { + if (!a.containsKey(k)) { + size++; + } + } + } + + return this.size = size; + } + + @Override + public boolean isEmpty() { + return first.isEmpty() && second.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return first.containsKey(key) || second.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return first.containsValue(value) || second.containsValue(value); + } + + @Override + public V get(Object key) { + V value = first.get(key); + return value != null ? value : second.get(key); + } + + @Override + public V put(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public Set> entrySet() { + if (entrySet != null) { + return entrySet; + } + + // Check for dupes first to reduce allocations on the vastly more common case where there aren't + // any. + boolean secondHasDupes = false; + for (Entry entry : second.entrySet()) { + if (first.containsKey(entry.getKey())) { + secondHasDupes = true; + break; + } + } + + final Set> filteredSecond; + if (!secondHasDupes) { + filteredSecond = second.entrySet(); + } else { + filteredSecond = new LinkedHashSet<>(); + for (Entry entry : second.entrySet()) { + if (!first.containsKey(entry.getKey())) { + filteredSecond.add(entry); + } + } + } + return entrySet = + Collections.unmodifiableSet(new ConcatenatedSet<>(first.entrySet(), filteredSecond)); + } + + // Member sets must be deduped by caller. + static final class ConcatenatedSet extends AbstractSet { + + private final Set first; + private final Set second; + + private final int size; + + ConcatenatedSet(Set first, Set second) { + this.first = first; + this.second = second; + + size = first.size() + second.size(); + } + + @Override + public int size() { + return size; + } + + @Override + public boolean add(T t) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator iterator() { + return new ConcatenatedSetIterator(); + } + + class ConcatenatedSetIterator implements Iterator { + final Iterator firstItr = first.iterator(); + final Iterator secondItr = second.iterator(); + + ConcatenatedSetIterator() {} + + @Override + public boolean hasNext() { + return firstItr.hasNext() || secondItr.hasNext(); + } + + @Override + public T next() { + if (firstItr.hasNext()) { + return firstItr.next(); + } + return secondItr.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/v1_0/LogbackTest.groovy b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/v1_0/LogbackTest.groovy new file mode 100644 index 000000000..7d69285e0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/v1_0/LogbackTest.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.v1_0 + +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class LogbackTest extends AbstractLogbackTest implements LibraryTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/v1_0/internal/UnionMapTest.groovy b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/v1_0/internal/UnionMapTest.groovy new file mode 100644 index 000000000..e5a86b777 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/v1_0/internal/UnionMapTest.groovy @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.v1_0.internal + +import spock.lang.Specification + +class UnionMapTest extends Specification { + + def "maps"() { + when: + def union = new UnionMap(first, second) + + then: + union['cat'] == 'meow' + union['dog'] == 'bark' + union['foo'] == 'bar' + union['hello'] == 'world' + union['giraffe'] == null + + !union.isEmpty() + union.size() == 4 + union.containsKey('cat') + union.containsKey('dog') + union.containsKey('foo') + union.containsKey('hello') + !union.containsKey('giraffe') + + def set = union.entrySet() + !set.isEmpty() + set.size() == 4 + def copy = new ArrayList(set) + copy.size() == 4 + + where: + first | second + [cat: 'meow', dog: 'bark'] | [foo: 'bar', hello: 'world'] + // Overlapping entries in second does not affect the union. + [cat: 'meow', dog: 'bark'] | [foo: 'bar', hello: 'world', cat: 'moo'] + } + + def "both empty"() { + when: + def union = new UnionMap(Collections.emptyMap(), Collections.emptyMap()) + + then: + union.isEmpty() + union.size() == 0 + union['cat'] == null + + def set = union.entrySet() + set.isEmpty() + set.size() == 0 + def copy = new ArrayList(set) + copy.size() == 0 + } + + def "one empty"() { + when: + def union = new UnionMap(first, second) + + then: + !union.isEmpty() + union.size() == 1 + union['cat'] == 'meow' + union['dog'] == null + + def set = union.entrySet() + !set.isEmpty() + set.size() == 1 + def copy = new ArrayList(set) + copy.size() == 1 + + where: + first | second + [cat: 'meow'] | Collections.emptyMap() + Collections.emptyMap() | [cat: 'meow'] + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/resources/logback.xml new file mode 100644 index 000000000..564fcd9be --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/library/src/test/resources/logback.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/testing/logback-1.0-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/testing/logback-1.0-testing.gradle new file mode 100644 index 000000000..185c68943 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/testing/logback-1.0-testing.gradle @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: "otel.java-conventions" + +dependencies { + compileOnly project(":instrumentation:logback-1.0:library") + + api project(':testing-common') + + api "ch.qos.logback:logback-classic:1.0.0" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/logback-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/v1_0/AbstractLogbackTest.groovy b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/v1_0/AbstractLogbackTest.groovy new file mode 100644 index 000000000..caa5329af --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/logback-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/v1_0/AbstractLogbackTest.groovy @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.v1_0 + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.TraceUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Shared + +abstract class AbstractLogbackTest extends InstrumentationSpecification { + + private static final Logger logger = LoggerFactory.getLogger("test") + + @Shared + ListAppender listAppender + + def setupSpec() { + ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) logger + def topLevelListAppender = logbackLogger.getAppender("LIST") + if (topLevelListAppender != null) { + // Auto instrumentation test. + listAppender = topLevelListAppender as ListAppender + } else { + // Library instrumentation test. + listAppender = (logbackLogger.getAppender("OTEL") as OpenTelemetryAppender) + .getAppender("LIST") as ListAppender + } + } + + def setup() { + listAppender.list.clear() + } + + def "no ids when no span"() { + when: + logger.info("log message 1") + logger.info("log message 2") + + def events = listAppender.list + + then: + events.size() == 2 + events[0].message == "log message 1" + events[0].getMDCPropertyMap().get("trace_id") == null + events[0].getMDCPropertyMap().get("span_id") == null + events[0].getMDCPropertyMap().get("trace_flags") == null + + events[1].message == "log message 2" + events[1].getMDCPropertyMap().get("trace_id") == null + events[1].getMDCPropertyMap().get("span_id") == null + events[1].getMDCPropertyMap().get("trace_flags") == null + } + + def "ids when span"() { + when: + Span span1 = TraceUtils.runUnderTrace("test") { + logger.info("log message 1") + Span.current() + } + + logger.info("log message 2") + + Span span2 = TraceUtils.runUnderTrace("test 2") { + logger.info("log message 3") + Span.current() + } + + def events = listAppender.list + + then: + events.size() == 3 + events[0].message == "log message 1" + events[0].getMDCPropertyMap().get("trace_id") == span1.spanContext.traceId + events[0].getMDCPropertyMap().get("span_id") == span1.spanContext.spanId + events[0].getMDCPropertyMap().get("trace_flags") == "01" + + events[1].message == "log message 2" + events[1].getMDCPropertyMap().get("trace_id") == null + events[1].getMDCPropertyMap().get("span_id") == null + events[1].getMDCPropertyMap().get("trace_flags") == null + + events[2].message == "log message 3" + events[2].getMDCPropertyMap().get("trace_id") == span2.spanContext.traceId + events[2].getMDCPropertyMap().get("span_id") == span2.spanContext.spanId + events[2].getMDCPropertyMap().get("trace_flags") == "01" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/methods-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/methods-javaagent.gradle new file mode 100644 index 000000000..cc8e1ad7b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/methods-javaagent.gradle @@ -0,0 +1,15 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + coreJdk = true + } +} + +dependencies { + compileOnly project(':javaagent-tooling') +} + +tasks.withType(Test).configureEach { + jvmArgs "-Dotel.instrumentation.methods.include=package.ClassName[method1,method2];MethodTest\$ConfigTracedCallable[call]" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentation.java new file mode 100644 index 000000000..f518bfd67 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentation.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.methods; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import java.util.Set; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class MethodInstrumentation implements TypeInstrumentation { + private final String className; + private final Set methodNames; + + public MethodInstrumentation(String className, Set methodNames) { + this.className = className; + this.methodNames = methodNames; + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed(className); + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(named(className)); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf(methodNames.toArray(new String[0])), + MethodInstrumentation.class.getName() + "$MethodAdvice"); + } + + @SuppressWarnings("unused") + public static class MethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, method)) { + return; + } + + context = instrumenter().start(parentContext, method); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + scope.close(); + instrumenter().end(context, method, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentationModule.java new file mode 100644 index 000000000..3f7ff5439 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentationModule.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.methods; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * TraceConfig Instrumentation does not extend Default. + * + *

Instead it directly implements Instrumenter#instrument() and adds one default Instrumenter for + * every configured class+method-list. + * + *

If this becomes a more common use case the building logic should be abstracted out into a + * super class. + */ +@AutoService(InstrumentationModule.class) +public class MethodInstrumentationModule extends InstrumentationModule { + + private static final String TRACE_METHODS_CONFIG = "otel.instrumentation.methods.include"; + + private final List typeInstrumentations; + + public MethodInstrumentationModule() { + super("methods"); + + Map> classMethodsToTrace = + MethodsConfigurationParser.parse(Config.get().getString(TRACE_METHODS_CONFIG)); + + typeInstrumentations = + classMethodsToTrace.entrySet().stream() + .filter(e -> !e.getValue().isEmpty()) + .map(e -> new MethodInstrumentation(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } + + // the default configuration has empty "otel.instrumentation.methods.include", and so doesn't + // generate any TypeInstrumentation for muzzle to analyze + @Override + public List getMuzzleHelperClassNames() { + return typeInstrumentations.isEmpty() + ? emptyList() + : singletonList("io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons"); + } + + @Override + public List typeInstrumentations() { + return typeInstrumentations; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodSingletons.java b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodSingletons.java new file mode 100644 index 000000000..0e6f3a8cb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodSingletons.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.methods; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import java.lang.reflect.Method; + +public final class MethodSingletons { + private static final String INSTRUMENTATION_NAME = + "io.opentelemetry.javaagent.external-annotations"; + + private static final Instrumenter INSTRUMENTER; + + static { + SpanNameExtractor spanName = SpanNames::fromMethod; + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .newInstrumenter(SpanKindExtractor.alwaysInternal()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private MethodSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/test/groovy/MethodTest.groovy b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/test/groovy/MethodTest.groovy new file mode 100644 index 000000000..8fc96b75c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/methods/javaagent/src/test/groovy/MethodTest.groovy @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.Callable + +class MethodTest extends AgentInstrumentationSpecification { + + static class ConfigTracedCallable implements Callable { + @Override + String call() throws Exception { + return "Hello!" + } + } + + def "test configuration based trace"() { + expect: + new ConfigTracedCallable().call() == "Hello!" + + and: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "ConfigTracedCallable.call" + attributes { + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/mongo-3.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/mongo-3.1-javaagent.gradle new file mode 100644 index 000000000..dde787b89 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/mongo-3.1-javaagent.gradle @@ -0,0 +1,18 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.mongodb" + module = "mongo-java-driver" + versions = "[3.1,)" + assertInverse = true + } +} + +dependencies { + implementation(project(':instrumentation:mongo:mongo-3.1:library')) + + library "org.mongodb:mongo-java-driver:3.1.0" + + testImplementation project(':instrumentation:mongo:mongo-3.1:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoClientInstrumentationModule.java new file mode 100644 index 000000000..d027261f1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoClientInstrumentationModule.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v3_1; + +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import com.mongodb.MongoClientOptions; +import com.mongodb.event.CommandListener; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class MongoClientInstrumentationModule extends InstrumentationModule { + + public MongoClientInstrumentationModule() { + super("mongo", "mongo-3.1"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new MongoClientOptionsBuilderInstrumentation()); + } + + private static final class MongoClientOptionsBuilderInstrumentation + implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.MongoClientOptions$Builder") + .and( + declaresMethod( + named("addCommandListener") + .and(isPublic()) + .and( + takesArguments(1) + .and(takesArgument(0, named("com.mongodb.event.CommandListener")))))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")).and(takesArguments(0)), + MongoClientInstrumentationModule.class.getName() + "$MongoClientAdvice"); + } + } + + @SuppressWarnings("unused") + public static class MongoClientAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void injectTraceListener( + @Advice.This MongoClientOptions.Builder builder, + @Advice.FieldValue("commandListeners") List commandListeners) { + for (CommandListener commandListener : commandListeners) { + if (commandListener == MongoInstrumentationSingletons.LISTENER) { + return; + } + } + builder.addCommandListener(MongoInstrumentationSingletons.LISTENER); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoInstrumentationSingletons.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoInstrumentationSingletons.java new file mode 100644 index 000000000..4299899a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoInstrumentationSingletons.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v3_1; + +import com.mongodb.event.CommandListener; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.mongo.v3_1.MongoTracing; + +public final class MongoInstrumentationSingletons { + + public static final CommandListener LISTENER = + MongoTracing.create(GlobalOpenTelemetry.get()).newCommandListener(); + + private MongoInstrumentationSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/test/groovy/MongoClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/test/groovy/MongoClientTest.groovy new file mode 100644 index 000000000..8f8289d56 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/javaagent/src/test/groovy/MongoClientTest.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.mongodb.MongoClientOptions +import io.opentelemetry.instrumentation.mongo.v3_1.AbstractMongo31ClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class MongoClientTest extends AbstractMongo31ClientTest implements AgentTestTrait { + @Override + void configureMongoClientOptions(MongoClientOptions.Builder options) { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/mongo-3.1-library.gradle b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/mongo-3.1-library.gradle new file mode 100644 index 000000000..aaf01f8d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/mongo-3.1-library.gradle @@ -0,0 +1,8 @@ +apply plugin: "otel.library-instrumentation" +apply plugin: "net.ltgt.nullaway" + +dependencies { + library "org.mongodb:mongo-java-driver:3.1.0" + + testImplementation project(':instrumentation:mongo:mongo-3.1:testing') +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTracer.java new file mode 100644 index 000000000..7b4b52c05 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTracer.java @@ -0,0 +1,294 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.mongo.v3_1; + +import static java.util.Arrays.asList; + +import com.mongodb.ServerAddress; +import com.mongodb.connection.ConnectionDescription; +import com.mongodb.event.CommandStartedEvent; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.bson.BsonValue; +import org.bson.json.JsonWriter; +import org.bson.json.JsonWriterSettings; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class MongoClientTracer + extends DatabaseClientTracer { + + private final int maxNormalizedQueryLength; + @Nullable private final JsonWriterSettings jsonWriterSettings; + + MongoClientTracer(OpenTelemetry openTelemetry, int maxNormalizedQueryLength) { + super(openTelemetry, NetPeerAttributes.INSTANCE); + this.maxNormalizedQueryLength = maxNormalizedQueryLength; + this.jsonWriterSettings = createJsonWriterSettings(maxNormalizedQueryLength); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.mongo-common"; + } + + @Override + // TODO(anuraaga): Migrate off of StringWriter to avoid synchronization. + @SuppressWarnings("JdkObsolete") + protected String sanitizeStatement(BsonDocument command) { + StringWriter stringWriter = new StringWriter(128); + // jsonWriterSettings is generally not null but could be due to security manager or unknown + // API incompatibilities, which we can't detect by Muzzle because we use reflection. + JsonWriter jsonWriter = + jsonWriterSettings != null + ? new JsonWriter(stringWriter, jsonWriterSettings) + : new JsonWriter(stringWriter); + writeScrubbed(command, jsonWriter, /* isRoot= */ true); + // If using MongoDB driver >= 3.7, the substring invocation will be a no-op due to use of + // JsonWriterSettings.Builder.maxLength in the static initializer for JSON_WRITER_SETTINGS + StringBuffer buf = stringWriter.getBuffer(); + if (buf.length() <= maxNormalizedQueryLength) { + return buf.toString(); + } + return buf.substring(0, maxNormalizedQueryLength); + } + + @Override + protected String spanName( + CommandStartedEvent event, BsonDocument document, String normalizedQuery) { + return conventionSpanName(dbName(event), event.getCommandName(), collectionName(event)); + } + + @Override + protected String dbSystem(CommandStartedEvent event) { + return DbSystemValues.MONGODB; + } + + @Override + protected void onConnection(SpanBuilder span, CommandStartedEvent event) { + String collection = collectionName(event); + if (collection != null) { + span.setAttribute(SemanticAttributes.DB_MONGODB_COLLECTION, collection); + } + super.onConnection(span, event); + } + + @Override + protected String dbName(CommandStartedEvent event) { + return event.getDatabaseName(); + } + + @Override + @Nullable + protected String dbConnectionString(CommandStartedEvent event) { + ConnectionDescription connectionDescription = event.getConnectionDescription(); + if (connectionDescription != null) { + ServerAddress sa = connectionDescription.getServerAddress(); + if (sa != null) { + // https://docs.mongodb.com/manual/reference/connection-string/ + String host = sa.getHost(); + int port = sa.getPort(); + if (host != null && port != 0) { + return "mongodb://" + host + ":" + port; + } + } + } + return null; + } + + @Override + @Nullable + protected InetSocketAddress peerAddress(CommandStartedEvent event) { + if (event.getConnectionDescription() != null + && event.getConnectionDescription().getServerAddress() != null) { + return event.getConnectionDescription().getServerAddress().getSocketAddress(); + } else { + return null; + } + } + + @Override + protected String dbStatement( + CommandStartedEvent event, BsonDocument command, String sanitizedStatement) { + return sanitizedStatement; + } + + @Override + protected String dbOperation( + CommandStartedEvent event, BsonDocument command, String sanitizedStatement) { + return event.getCommandName(); + } + + @Nullable private static final Method IS_TRUNCATED_METHOD; + + static { + IS_TRUNCATED_METHOD = + Arrays.stream(JsonWriter.class.getMethods()) + .filter(method -> method.getName().equals("isTruncated")) + .findFirst() + .orElse(null); + } + + @Nullable + private static JsonWriterSettings createJsonWriterSettings(int maxNormalizedQueryLength) { + JsonWriterSettings settings = null; + try { + // The static JsonWriterSettings.builder() method was introduced in the 3.5 release + Optional buildMethod = + Arrays.stream(JsonWriterSettings.class.getMethods()) + .filter(method -> method.getName().equals("builder")) + .findFirst(); + if (buildMethod.isPresent()) { + Class builderClass = buildMethod.get().getReturnType(); + Object builder = buildMethod.get().invoke(null, (Object[]) null); + + // The JsonWriterSettings.Builder.indent method was introduced in the 3.5 release, + // but checking anyway + Optional indentMethod = + Arrays.stream(builderClass.getMethods()) + .filter(method -> method.getName().equals("indent")) + .findFirst(); + if (indentMethod.isPresent()) { + indentMethod.get().invoke(builder, false); + } + + // The JsonWriterSettings.Builder.maxLength method was introduced in the 3.7 release + Optional maxLengthMethod = + Arrays.stream(builderClass.getMethods()) + .filter(method -> method.getName().equals("maxLength")) + .findFirst(); + if (maxLengthMethod.isPresent()) { + maxLengthMethod.get().invoke(builder, maxNormalizedQueryLength); + } + settings = + (JsonWriterSettings) + builderClass.getMethod("build", (Class[]) null).invoke(builder, (Object[]) null); + } + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) { + // Ignore + } + if (settings == null) { + try { + // Constructor removed in 4.0+ so use reflection. 4.0+ will have used the builder above. + settings = JsonWriterSettings.class.getConstructor(Boolean.TYPE).newInstance(false); + } catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException ignored) { + // Ignore + } + } + + return settings; + } + + private static final String HIDDEN_CHAR = "?"; + + private static boolean writeScrubbed(BsonDocument origin, JsonWriter writer, boolean isRoot) { + writer.writeStartDocument(); + boolean firstField = true; + for (Map.Entry entry : origin.entrySet()) { + writer.writeName(entry.getKey()); + // the first field of the root document is the command name, so we preserve its value + // (which for most CRUD commands is the collection name) + if (isRoot && firstField && entry.getValue().isString()) { + writer.writeString(entry.getValue().asString().getValue()); + } else { + if (writeScrubbed(entry.getValue(), writer)) { + return true; + } + } + firstField = false; + } + writer.writeEndDocument(); + return false; + } + + private static boolean writeScrubbed(BsonArray origin, JsonWriter writer) { + writer.writeStartArray(); + for (BsonValue value : origin) { + if (writeScrubbed(value, writer)) { + return true; + } + } + writer.writeEndArray(); + return false; + } + + private static boolean writeScrubbed(BsonValue origin, JsonWriter writer) { + if (origin.isDocument()) { + return writeScrubbed(origin.asDocument(), writer, /* isRoot= */ false); + } else if (origin.isArray()) { + return writeScrubbed(origin.asArray(), writer); + } else { + writer.writeString(HIDDEN_CHAR); + return isTruncated(writer); + } + } + + private static boolean isTruncated(JsonWriter writer) { + if (IS_TRUNCATED_METHOD == null) { + return false; + } else { + try { + return (boolean) IS_TRUNCATED_METHOD.invoke(writer, (Object[]) null); + } catch (IllegalAccessException | InvocationTargetException ignored) { + return false; + } + } + } + + private static final Set COMMANDS_WITH_COLLECTION_NAME_AS_VALUE = + new HashSet<>( + asList( + "aggregate", + "count", + "distinct", + "mapReduce", + "geoSearch", + "delete", + "find", + "killCursors", + "findAndModify", + "insert", + "update", + "create", + "drop", + "createIndexes", + "listIndexes")); + + @Nullable + private static String collectionName(CommandStartedEvent event) { + if (event.getCommandName().equals("getMore")) { + if (event.getCommand().containsKey("collection")) { + BsonValue collectionValue = event.getCommand().get("collection"); + if (collectionValue.isString()) { + return event.getCommand().getString("collection").getValue(); + } + } + } else if (COMMANDS_WITH_COLLECTION_NAME_AS_VALUE.contains(event.getCommandName())) { + BsonValue commandValue = event.getCommand().get(event.getCommandName()); + if (commandValue != null && commandValue.isString()) { + return commandValue.asString().getValue(); + } + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoTracing.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoTracing.java new file mode 100644 index 000000000..f4ac61dab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoTracing.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.mongo.v3_1; + +import com.mongodb.event.CommandListener; +import io.opentelemetry.api.OpenTelemetry; + +/** Entrypoint to OpenTelemetry instrumentation of the MongoDB client. */ +public final class MongoTracing { + + /** Returns a new {@link MongoTracing} configured with the given {@link OpenTelemetry}. */ + public static MongoTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + /** Returns a new {@link MongoTracingBuilder} configured with the given {@link OpenTelemetry}. */ + public static MongoTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new MongoTracingBuilder(openTelemetry); + } + + private final MongoClientTracer tracer; + + MongoTracing(OpenTelemetry openTelemetry, int maxNormalizedQueryLength) { + this.tracer = new MongoClientTracer(openTelemetry, maxNormalizedQueryLength); + } + + /** + * Returns a new {@link CommandListener} that can be used with methods like {@link + * com.mongodb.MongoClientOptions.Builder#addCommandListener(CommandListener)}. + */ + public CommandListener newCommandListener() { + return new TracingCommandListener(tracer); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoTracingBuilder.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoTracingBuilder.java new file mode 100644 index 000000000..f06ed9ecf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoTracingBuilder.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.mongo.v3_1; + +import io.opentelemetry.api.OpenTelemetry; + +/** A builder of {@link MongoTracing}. */ +public final class MongoTracingBuilder { + + // Visible for testing + static final int DEFAULT_MAX_NORMALIZED_QUERY_LENGTH = 32 * 1024; + + private final OpenTelemetry openTelemetry; + + private int maxNormalizedQueryLength = DEFAULT_MAX_NORMALIZED_QUERY_LENGTH; + + MongoTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Sets the max length of recorded queries after normalization. Defaults to {@value + * DEFAULT_MAX_NORMALIZED_QUERY_LENGTH}. + */ + public MongoTracingBuilder setMaxNormalizedQueryLength(int maxNormalizedQueryLength) { + this.maxNormalizedQueryLength = maxNormalizedQueryLength; + return this; + } + + /** Returns a new {@link MongoTracing} with the settings of this {@link MongoTracingBuilder}. */ + public MongoTracing build() { + return new MongoTracing(openTelemetry, maxNormalizedQueryLength); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/TracingCommandListener.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/TracingCommandListener.java new file mode 100644 index 000000000..ed28ef3b7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/TracingCommandListener.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.mongo.v3_1; + +import com.mongodb.event.CommandFailedEvent; +import com.mongodb.event.CommandListener; +import com.mongodb.event.CommandStartedEvent; +import com.mongodb.event.CommandSucceededEvent; +import io.opentelemetry.context.Context; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +final class TracingCommandListener implements CommandListener { + + private final MongoClientTracer tracer; + private final Map contextMap; + + TracingCommandListener(MongoClientTracer tracer) { + this.tracer = tracer; + contextMap = new ConcurrentHashMap<>(); + } + + @Override + public void commandStarted(CommandStartedEvent event) { + Context context = tracer.startSpan(Context.current(), event, event.getCommand()); + contextMap.put(event.getRequestId(), context); + } + + @Override + public void commandSucceeded(CommandSucceededEvent event) { + Context context = contextMap.remove(event.getRequestId()); + if (context != null) { + tracer.end(context); + } + } + + @Override + public void commandFailed(CommandFailedEvent event) { + Context context = contextMap.remove(event.getRequestId()); + if (context != null) { + tracer.endExceptionally(context, event.getThrowable()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTest.groovy new file mode 100644 index 000000000..48a5c607e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTest.groovy @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.mongo.v3_1 + +import com.mongodb.MongoClientOptions +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class MongoClientTest extends AbstractMongo31ClientTest implements LibraryTestTrait { + @Override + void configureMongoClientOptions(MongoClientOptions.Builder options) { + options.addCommandListener(MongoTracing.create(openTelemetry).newCommandListener()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTracerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTracerTest.groovy new file mode 100644 index 000000000..8961b8cc0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/mongo/v3_1/MongoClientTracerTest.groovy @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.mongo.v3_1 + +import static io.opentelemetry.instrumentation.mongo.v3_1.MongoTracingBuilder.DEFAULT_MAX_NORMALIZED_QUERY_LENGTH +import static java.util.Arrays.asList + +import com.mongodb.event.CommandStartedEvent +import io.opentelemetry.api.OpenTelemetry +import org.bson.BsonArray +import org.bson.BsonDocument +import org.bson.BsonInt32 +import org.bson.BsonString +import spock.lang.Specification + +class MongoClientTracerTest extends Specification { + def 'should sanitize statements to json'() { + setup: + def tracer = new MongoClientTracer(OpenTelemetry.noop(), DEFAULT_MAX_NORMALIZED_QUERY_LENGTH) + + expect: + sanitizeStatementAcrossVersions(tracer, + new BsonDocument("cmd", new BsonInt32(1))) == + '{"cmd": "?"}' + + sanitizeStatementAcrossVersions(tracer, + new BsonDocument("cmd", new BsonInt32(1)) + .append("sub", new BsonDocument("a", new BsonInt32(1)))) == + '{"cmd": "?", "sub": {"a": "?"}}' + + sanitizeStatementAcrossVersions(tracer, + new BsonDocument("cmd", new BsonInt32(1)) + .append("sub", new BsonArray(asList(new BsonInt32(1))))) == + '{"cmd": "?", "sub": ["?"]}' + } + + def 'should only preserve string value if it is the value of the first top-level key'() { + setup: + def tracer = new MongoClientTracer(OpenTelemetry.noop(), DEFAULT_MAX_NORMALIZED_QUERY_LENGTH) + + expect: + sanitizeStatementAcrossVersions(tracer, + new BsonDocument("cmd", new BsonString("c")) + .append("f", new BsonString("c")) + .append("sub", new BsonString("c"))) == + '{"cmd": "c", "f": "?", "sub": "?"}' + } + + def 'should truncate simple command'() { + setup: + def tracer = new MongoClientTracer(OpenTelemetry.noop(), 20) + + def normalized = sanitizeStatementAcrossVersions(tracer, + new BsonDocument("cmd", new BsonString("c")) + .append("f1", new BsonString("c1")) + .append("f2", new BsonString("c2"))) + expect: + // this can vary because of different whitespace for different mongo versions + normalized == '{"cmd": "c", "f1": "' || normalized == '{"cmd": "c", "f1" ' + } + + def 'should truncate array'() { + setup: + def tracer = new MongoClientTracer(OpenTelemetry.noop(), 27) + + def normalized = sanitizeStatementAcrossVersions(tracer, + new BsonDocument("cmd", new BsonString("c")) + .append("f1", new BsonArray(asList(new BsonString("c1"), new BsonString("c2")))) + .append("f2", new BsonString("c3"))) + expect: + // this can vary because of different whitespace for different mongo versions + normalized == '{"cmd": "c", "f1": ["?", "?' || normalized == '{"cmd": "c", "f1": ["?",' + } + + def 'test span name with no dbName'() { + setup: + def tracer = new MongoClientTracer(OpenTelemetry.noop(), DEFAULT_MAX_NORMALIZED_QUERY_LENGTH) + def event = new CommandStartedEvent( + 0, null, null, command, new BsonDocument(command, new BsonInt32(1))) + + when: + def spanName = tracer.spanName(event, null, null) + + then: + spanName == command + + where: + command = "listDatabases" + } + + def sanitizeStatementAcrossVersions(MongoClientTracer tracer, BsonDocument query) { + return sanitizeAcrossVersions(tracer.sanitizeStatement(query)) + } + + def sanitizeAcrossVersions(String json) { + json = json.replaceAll('\\{ ', '{') + json = json.replaceAll(' }', '}') + json = json.replaceAll(' :', ':') + return json + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/testing/mongo-3.1-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/testing/mongo-3.1-testing.gradle new file mode 100644 index 000000000..e47338e89 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/testing/mongo-3.1-testing.gradle @@ -0,0 +1,11 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':instrumentation:mongo:mongo-testing') + + compileOnly "org.mongodb:mongo-java-driver:3.1.0" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/v3_1/AbstractMongo31ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/v3_1/AbstractMongo31ClientTest.groovy new file mode 100644 index 000000000..a15c632b1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/v3_1/AbstractMongo31ClientTest.groovy @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.mongo.v3_1 + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.mongodb.MongoClient +import com.mongodb.MongoClientOptions +import com.mongodb.MongoTimeoutException +import com.mongodb.ServerAddress +import com.mongodb.client.MongoCollection +import com.mongodb.client.MongoDatabase +import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest +import io.opentelemetry.instrumentation.test.utils.PortUtils +import org.bson.BsonDocument +import org.bson.BsonString +import org.bson.Document +import spock.lang.Shared + +abstract class AbstractMongo31ClientTest extends AbstractMongoClientTest> { + + abstract void configureMongoClientOptions(MongoClientOptions.Builder options); + + @Shared + MongoClient client + + def setupSpec() throws Exception { + def options = MongoClientOptions.builder().description("some-description") + configureMongoClientOptions(options) + client = new MongoClient(new ServerAddress("localhost", port), options.build()) + } + + def cleanupSpec() throws Exception { + client?.close() + client = null + } + + @Override + void createCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + void createCollectionNoDescription(String dbName, String collectionName) { + def options = MongoClientOptions.builder() + configureMongoClientOptions(options) + MongoDatabase db = new MongoClient(new ServerAddress("localhost", port), options.build()).getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName) { + def clientOptions = client.mongoClientOptions + def newClientOptions = MongoClientOptions.builder(clientOptions).build() + MongoDatabase db = new MongoClient(new ServerAddress("localhost", port), newClientOptions).getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + void createCollectionCallingBuildTwice(String dbName, String collectionName) { + def options = MongoClientOptions.builder().description("some-description") + configureMongoClientOptions(options) + options.build() + MongoDatabase db = new MongoClient(new ServerAddress("localhost", port), options.build()).getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + int getCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + return db.getCollection(collectionName).count() + } + + @Override + MongoCollection setupInsert(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int insert(MongoCollection collection) { + collection.insertOne(new Document("password", "SECRET")) + return collection.count() + } + + @Override + MongoCollection setupUpdate(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + def coll = db.getCollection(collectionName) + coll.insertOne(new Document("password", "OLDPW")) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int update(MongoCollection collection) { + def result = collection.updateOne( + new BsonDocument("password", new BsonString("OLDPW")), + new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW")))) + collection.count() + return result.modifiedCount + } + + @Override + MongoCollection setupDelete(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + def coll = db.getCollection(collectionName) + coll.insertOne(new Document("password", "SECRET")) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int delete(MongoCollection collection) { + def result = collection.deleteOne(new BsonDocument("password", new BsonString("SECRET"))) + collection.count() + return result.deletedCount + } + + @Override + MongoCollection setupGetMore(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def coll = db.getCollection(collectionName) + coll.insertMany([new Document("_id", 0), new Document("_id", 1), new Document("_id", 2)]) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + void getMore(MongoCollection collection) { + collection.find().filter(new Document("_id", new Document('$gte', 0))) + .batchSize(2).into(new ArrayList()) + } + + @Override + void error(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + collection.updateOne(new BsonDocument(), new BsonDocument()) + } + + def "test client failure"() { + setup: + def options = MongoClientOptions.builder().serverSelectionTimeout(10).build() + def client = new MongoClient(new ServerAddress("localhost", PortUtils.UNUSABLE_PORT), [], options) + + when: + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + + then: + thrown(MongoTimeoutException) + // Unfortunately not caught by our instrumentation. + assertTraces(0) {} + + where: + dbName = "test_db" + collectionName = createCollectionName() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/mongo-3.7-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/mongo-3.7-javaagent.gradle new file mode 100644 index 000000000..7ccd0b1ea --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/mongo-3.7-javaagent.gradle @@ -0,0 +1,28 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.mongodb" + module = "mongo-java-driver" + versions = "[3.7, 4.0)" + assertInverse = true + } + pass { + group = "org.mongodb" + module = "mongodb-driver-core" + // this instrumentation is backwards compatible with early versions of the new API that shipped in 3.7 + // the legacy API instrumented in mongo-3.1 continues to be shipped in 4.x, but doesn't conflict here + // because they are triggered by different types: MongoClientSettings(new) vs MongoClientOptions(legacy) + versions = "[3.7, 4.0)" + assertInverse = true + } +} + +dependencies { + implementation(project(':instrumentation:mongo:mongo-3.1:library')) + + // a couple of test attribute verifications don't pass until 3.8.0 + library "org.mongodb:mongo-java-driver:3.8.0" + + testImplementation project(':instrumentation:mongo:mongo-testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/BaseClusterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/BaseClusterInstrumentation.java new file mode 100644 index 000000000..eeaff9612 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/BaseClusterInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v3_7; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.mongodb.async.SingleResultCallback; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class BaseClusterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "com.mongodb.connection.BaseCluster", "com.mongodb.internal.connection.BaseCluster"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("selectServerAsync")) + .and(takesArgument(0, named("com.mongodb.selector.ServerSelector"))) + .and(takesArgument(1, named("com.mongodb.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg1Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 1, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/InternalStreamConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/InternalStreamConnectionInstrumentation.java new file mode 100644 index 000000000..0023b03b8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/InternalStreamConnectionInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v3_7; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.mongodb.async.SingleResultCallback; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class InternalStreamConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.internal.connection.InternalStreamConnection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("openAsync")) + .and(takesArgument(0, named("com.mongodb.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg0Advice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("readAsync")) + .and(takesArgument(1, named("com.mongodb.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("writeAsync")) + .and(takesArgument(1, named("com.mongodb.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg0Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 0, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg1Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 1, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoClientInstrumentationModule.java new file mode 100644 index 000000000..924a03444 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoClientInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v3_7; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class MongoClientInstrumentationModule extends InstrumentationModule { + + public MongoClientInstrumentationModule() { + super("mongo", "mongo-3.7"); + } + + @Override + public List typeInstrumentations() { + return asList( + new MongoClientSettingsBuilderInstrumentation(), + new InternalStreamConnectionInstrumentation(), + new BaseClusterInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoClientSettingsBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoClientSettingsBuilderInstrumentation.java new file mode 100644 index 000000000..82fac8982 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoClientSettingsBuilderInstrumentation.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v3_7; + +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.mongodb.MongoClientSettings; +import com.mongodb.event.CommandListener; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class MongoClientSettingsBuilderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.MongoClientSettings$Builder") + .and( + declaresMethod( + named("addCommandListener") + .and(isPublic()) + .and( + takesArguments(1) + .and(takesArgument(0, named("com.mongodb.event.CommandListener")))))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")).and(takesArguments(0)), + this.getClass().getName() + "$AddCommandListenerAdvice"); + } + + @SuppressWarnings("unused") + public static class AddCommandListenerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void injectTraceListener( + @Advice.This MongoClientSettings.Builder builder, + @Advice.FieldValue("commandListeners") List commandListeners) { + for (CommandListener commandListener : commandListeners) { + if (commandListener == MongoInstrumentationSingletons.LISTENER) { + return; + } + } + builder.addCommandListener(MongoInstrumentationSingletons.LISTENER); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoInstrumentationSingletons.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoInstrumentationSingletons.java new file mode 100644 index 000000000..b01c3fe33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/MongoInstrumentationSingletons.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v3_7; + +import com.mongodb.event.CommandListener; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.mongo.v3_1.MongoTracing; + +public final class MongoInstrumentationSingletons { + + public static final CommandListener LISTENER = + MongoTracing.create(GlobalOpenTelemetry.get()).newCommandListener(); + + private MongoInstrumentationSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/SingleResultCallbackWrapper.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/SingleResultCallbackWrapper.java new file mode 100644 index 000000000..b5fbf7dc8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_7/SingleResultCallbackWrapper.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v3_7; + +import com.mongodb.async.SingleResultCallback; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class SingleResultCallbackWrapper implements SingleResultCallback { + private final Context context; + private final SingleResultCallback delegate; + + public SingleResultCallbackWrapper(Context context, SingleResultCallback delegate) { + this.context = context; + this.delegate = delegate; + } + + @Override + public void onResult(Object server, Throwable throwable) { + try (Scope ignored = context.makeCurrent()) { + delegate.onResult(server, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/test/groovy/MongoClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/test/groovy/MongoClientTest.groovy new file mode 100644 index 000000000..b51bef171 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-3.7/javaagent/src/test/groovy/MongoClientTest.groovy @@ -0,0 +1,193 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.mongodb.MongoClientSettings +import com.mongodb.MongoTimeoutException +import com.mongodb.ServerAddress +import com.mongodb.client.MongoClient +import com.mongodb.client.MongoClients +import com.mongodb.client.MongoCollection +import com.mongodb.client.MongoDatabase +import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import org.bson.BsonDocument +import org.bson.BsonString +import org.bson.Document +import spock.lang.Shared + +class MongoClientTest extends AbstractMongoClientTest> implements AgentTestTrait { + + @Shared + MongoClient client + + def setupSpec() throws Exception { + client = MongoClients.create(MongoClientSettings.builder() + .applyToClusterSettings({ builder -> + builder.hosts(Arrays.asList( + new ServerAddress("localhost", port))) + .description("some-description") + }) + .build()) + } + + def cleanupSpec() throws Exception { + client?.close() + client = null + } + + @Override + void createCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + void createCollectionNoDescription(String dbName, String collectionName) { + MongoDatabase db = MongoClients.create("mongodb://localhost:${port}").getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName) { + def clientSettings = MongoClientSettings.builder() + .applyToClusterSettings({ builder -> + builder.hosts(Arrays.asList( + new ServerAddress("localhost", port))) + .description("some-description") + }) + .build() + def newClientSettings = MongoClientSettings.builder(clientSettings).build() + MongoDatabase db = MongoClients.create(newClientSettings).getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + void createCollectionCallingBuildTwice(String dbName, String collectionName) { + def clientSettings = MongoClientSettings.builder() + .applyToClusterSettings({ builder -> + builder.hosts(Arrays.asList( + new ServerAddress("localhost", port))) + .description("some-description") + }) + clientSettings.build() + MongoDatabase db = MongoClients.create(clientSettings.build()).getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + int getCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + return db.getCollection(collectionName).count() + } + + @Override + MongoCollection setupInsert(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int insert(MongoCollection collection) { + collection.insertOne(new Document("password", "SECRET")) + return collection.count() + } + + @Override + MongoCollection setupUpdate(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + def coll = db.getCollection(collectionName) + coll.insertOne(new Document("password", "OLDPW")) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int update(MongoCollection collection) { + def result = collection.updateOne( + new BsonDocument("password", new BsonString("OLDPW")), + new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW")))) + collection.count() + return result.modifiedCount + } + + @Override + MongoCollection setupDelete(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + def coll = db.getCollection(collectionName) + coll.insertOne(new Document("password", "SECRET")) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int delete(MongoCollection collection) { + def result = collection.deleteOne(new BsonDocument("password", new BsonString("SECRET"))) + collection.count() + return result.deletedCount + } + + @Override + MongoCollection setupGetMore(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def coll = db.getCollection(collectionName) + coll.insertMany([new Document("_id", 0), new Document("_id", 1), new Document("_id", 2)]) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + void getMore(MongoCollection collection) { + collection.find().filter(new Document("_id", new Document('$gte', 0))) + .batchSize(2).into(new ArrayList()) + } + + @Override + void error(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + collection.updateOne(new BsonDocument(), new BsonDocument()) + } + + def "test client failure"() { + setup: + def client = MongoClients.create("mongodb://localhost:" + UNUSABLE_PORT + "/?connectTimeoutMS=10") + + when: + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + + then: + thrown(MongoTimeoutException) + // Unfortunately not caught by our instrumentation. + assertTraces(0) {} + + where: + dbName = "test_db" + collectionName = createCollectionName() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/mongo-4.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/mongo-4.0-javaagent.gradle new file mode 100644 index 000000000..762cd5f30 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/mongo-4.0-javaagent.gradle @@ -0,0 +1,22 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.mongodb" + module = "mongodb-driver-core" + versions = "[4.0,)" + assertInverse = true + } +} + +dependencies { + implementation(project(':instrumentation:mongo:mongo-3.1:library')) + + library "org.mongodb:mongodb-driver-core:4.0.0" + + testLibrary "org.mongodb:mongodb-driver-sync:4.0.0" + testLibrary "org.mongodb:mongodb-driver-reactivestreams:4.0.0" + + testImplementation project(':instrumentation:mongo:mongo-testing') + testImplementation "de.flapdoodle.embed:de.flapdoodle.embed.mongo:1.50.5" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/BaseClusterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/BaseClusterInstrumentation.java new file mode 100644 index 000000000..458bf5b24 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/BaseClusterInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.mongodb.internal.async.SingleResultCallback; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class BaseClusterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.internal.connection.BaseCluster"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("selectServerAsync")) + .and(takesArgument(0, named("com.mongodb.selector.ServerSelector"))) + .and(takesArgument(1, named("com.mongodb.internal.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg1Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 1, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/DefaultConnectionPoolInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/DefaultConnectionPoolInstrumentation.java new file mode 100644 index 000000000..a7e5d78a5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/DefaultConnectionPoolInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.mongodb.internal.async.SingleResultCallback; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DefaultConnectionPoolInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.internal.connection.DefaultConnectionPool"); + } + + @Override + public void transform(TypeTransformer transformer) { + // instrumentation needed since 4.3.0-beta3 + transformer.applyAdviceToMethod( + named("getAsync") + .and(takesArgument(0, named("com.mongodb.internal.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackAdvice"); + } + + public static class SingleResultCallbackAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 0, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/InternalStreamConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/InternalStreamConnectionInstrumentation.java new file mode 100644 index 000000000..8bec4a1d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/InternalStreamConnectionInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.mongodb.internal.async.SingleResultCallback; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class InternalStreamConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.internal.connection.InternalStreamConnection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("openAsync")) + .and(takesArgument(0, named("com.mongodb.internal.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg0Advice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("readAsync")) + .and(takesArgument(1, named("com.mongodb.internal.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("writeAsync")) + .and(takesArgument(1, named("com.mongodb.internal.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg0Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 0, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg1Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 1, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientInstrumentationModule.java new file mode 100644 index 000000000..4d32ec847 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class MongoClientInstrumentationModule extends InstrumentationModule { + + public MongoClientInstrumentationModule() { + super("mongo", "mongo-4.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new MongoClientSettingsBuilderInstrumentation(), + new InternalStreamConnectionInstrumentation(), + new BaseClusterInstrumentation(), + new DefaultConnectionPoolInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientSettingsBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientSettingsBuilderInstrumentation.java new file mode 100644 index 000000000..6060031bd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientSettingsBuilderInstrumentation.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.mongodb.MongoClientSettings; +import com.mongodb.event.CommandListener; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class MongoClientSettingsBuilderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.MongoClientSettings$Builder") + .and( + declaresMethod( + named("addCommandListener") + .and(isPublic()) + .and( + takesArguments(1) + .and(takesArgument(0, named("com.mongodb.event.CommandListener")))))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")).and(takesArguments(0)), + this.getClass().getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void injectTraceListener( + @Advice.This MongoClientSettings.Builder builder, + @Advice.FieldValue("commandListeners") List commandListeners) { + for (CommandListener commandListener : commandListeners) { + if (commandListener == MongoInstrumentationSingletons.LISTENER) { + return; + } + } + builder.addCommandListener(MongoInstrumentationSingletons.LISTENER); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoInstrumentationSingletons.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoInstrumentationSingletons.java new file mode 100644 index 000000000..6e59cb7a2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoInstrumentationSingletons.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +import com.mongodb.event.CommandListener; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.mongo.v3_1.MongoTracing; + +public final class MongoInstrumentationSingletons { + + public static final CommandListener LISTENER = + MongoTracing.create(GlobalOpenTelemetry.get()).newCommandListener(); + + private MongoInstrumentationSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/SingleResultCallbackWrapper.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/SingleResultCallbackWrapper.java new file mode 100644 index 000000000..730ffe4c6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/SingleResultCallbackWrapper.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +import com.mongodb.internal.async.SingleResultCallback; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class SingleResultCallbackWrapper implements SingleResultCallback { + private final Context context; + private final SingleResultCallback delegate; + + public SingleResultCallbackWrapper(Context context, SingleResultCallback delegate) { + this.context = context; + this.delegate = delegate; + } + + @Override + public void onResult(Object server, Throwable throwable) { + try (Scope ignored = context.makeCurrent()) { + delegate.onResult(server, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/Mongo4ReactiveClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/Mongo4ReactiveClientTest.groovy new file mode 100644 index 000000000..abae4cc10 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/Mongo4ReactiveClientTest.groovy @@ -0,0 +1,212 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.mongodb.MongoClientSettings +import com.mongodb.ServerAddress +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.UpdateResult +import com.mongodb.reactivestreams.client.MongoClient +import com.mongodb.reactivestreams.client.MongoClients +import com.mongodb.reactivestreams.client.MongoCollection +import com.mongodb.reactivestreams.client.MongoDatabase +import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch +import org.bson.BsonDocument +import org.bson.BsonString +import org.bson.Document +import org.junit.AssumptionViolatedException +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import spock.lang.Shared + +class Mongo4ReactiveClientTest extends AbstractMongoClientTest> implements AgentTestTrait { + + @Shared + MongoClient client + + def setupSpec() throws Exception { + client = MongoClients.create("mongodb://localhost:$port") + } + + def cleanupSpec() throws Exception { + client?.close() + client = null + } + + @Override + void createCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName).subscribe(toSubscriber {}) + } + + @Override + void createCollectionNoDescription(String dbName, String collectionName) { + MongoDatabase db = MongoClients.create("mongodb://localhost:${port}").getDatabase(dbName) + db.createCollection(collectionName).subscribe(toSubscriber {}) + } + + @Override + void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName) { + throw new AssumptionViolatedException("not tested on 4.0") + } + + @Override + void createCollectionCallingBuildTwice(String dbName, String collectionName) { + def settings = MongoClientSettings.builder() + .applyToClusterSettings({ builder -> + builder.hosts(Arrays.asList( + new ServerAddress("localhost", port))) + }) + settings.build() + MongoDatabase db = MongoClients.create(settings.build()).getDatabase(dbName) + db.createCollection(collectionName).subscribe(toSubscriber {}) + } + + @Override + int getCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + def count = new CompletableFuture() + db.getCollection(collectionName).estimatedDocumentCount().subscribe(toSubscriber { count.complete(it) }) + return count.join() + } + + @Override + MongoCollection setupInsert(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName).subscribe(toSubscriber { latch1.countDown() }) + latch1.await() + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int insert(MongoCollection collection) { + def count = new CompletableFuture() + collection.insertOne(new Document("password", "SECRET")).subscribe(toSubscriber { + collection.estimatedDocumentCount().subscribe(toSubscriber { count.complete(it) }) + }) + return count.join() + } + + @Override + MongoCollection setupUpdate(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName).subscribe(toSubscriber { latch1.countDown() }) + latch1.await() + def coll = db.getCollection(collectionName) + def latch2 = new CountDownLatch(1) + coll.insertOne(new Document("password", "OLDPW")).subscribe(toSubscriber { latch2.countDown() }) + latch2.await() + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int update(MongoCollection collection) { + def result = new CompletableFuture() + def count = new CompletableFuture() + collection.updateOne( + new BsonDocument("password", new BsonString("OLDPW")), + new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW")))).subscribe(toSubscriber { + result.complete(it) + collection.estimatedDocumentCount().subscribe(toSubscriber { count.complete(it) }) + }) + return result.join().modifiedCount + } + + @Override + MongoCollection setupDelete(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName).subscribe(toSubscriber { latch1.countDown() }) + latch1.await() + def coll = db.getCollection(collectionName) + def latch2 = new CountDownLatch(1) + coll.insertOne(new Document("password", "SECRET")).subscribe(toSubscriber { latch2.countDown() }) + latch2.await() + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int delete(MongoCollection collection) { + def result = new CompletableFuture() + def count = new CompletableFuture() + collection.deleteOne(new BsonDocument("password", new BsonString("SECRET"))).subscribe(toSubscriber { + result.complete(it) + collection.estimatedDocumentCount().subscribe(toSubscriber { count.complete(it) }) + }) + return result.join().deletedCount + } + + @Override + MongoCollection setupGetMore(String dbName, String collectionName) { + throw new AssumptionViolatedException("not tested on reactive") + } + + @Override + void getMore(MongoCollection collection) { + throw new AssumptionViolatedException("not tested on reactive") + } + + @Override + void error(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch = new CountDownLatch(1) + db.createCollection(collectionName).subscribe(toSubscriber { + latch.countDown() + }) + latch.await() + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + def result = new CompletableFuture() + collection.updateOne(new BsonDocument(), new BsonDocument()).subscribe(toSubscriber { + result.complete(it) + }) + throw result.join() + } + + def Subscriber toSubscriber(Closure closure) { + return new Subscriber() { + boolean hasResult + + @Override + void onSubscribe(Subscription s) { + s.request(1) // must request 1 value to trigger async call + } + + @Override + void onNext(Object o) { hasResult = true; closure.call(o) } + + @Override + void onError(Throwable t) { hasResult = true; closure.call(t) } + + @Override + void onComplete() { + if (!hasResult) { + hasResult = true + closure.call() + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/MongoClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/MongoClientTest.groovy new file mode 100644 index 000000000..ab6e99b43 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/MongoClientTest.groovy @@ -0,0 +1,158 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.mongodb.MongoClientSettings +import com.mongodb.ServerAddress +import com.mongodb.client.MongoClient +import com.mongodb.client.MongoClients +import com.mongodb.client.MongoCollection +import com.mongodb.client.MongoDatabase +import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import org.bson.BsonDocument +import org.bson.BsonString +import org.bson.Document +import org.junit.AssumptionViolatedException +import spock.lang.Shared + +class MongoClientTest extends AbstractMongoClientTest> implements AgentTestTrait { + + @Shared + MongoClient client + + def setupSpec() throws Exception { + client = MongoClients.create("mongodb://localhost:$port") + } + + def cleanupSpec() throws Exception { + client?.close() + client = null + } + + @Override + void createCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + void createCollectionNoDescription(String dbName, String collectionName) { + MongoDatabase db = MongoClients.create("mongodb://localhost:${port}").getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName) { + throw new AssumptionViolatedException("not tested on 4.0") + } + + @Override + void createCollectionCallingBuildTwice(String dbName, String collectionName) { + def settings = MongoClientSettings.builder() + .applyToClusterSettings({ builder -> + builder.hosts(Arrays.asList( + new ServerAddress("localhost", port))) + }) + settings.build() + MongoDatabase db = MongoClients.create(settings.build()).getDatabase(dbName) + db.createCollection(collectionName) + } + + @Override + int getCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + return db.getCollection(collectionName).estimatedDocumentCount() + } + + @Override + MongoCollection setupInsert(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int insert(MongoCollection collection) { + collection.insertOne(new Document("password", "SECRET")) + return collection.estimatedDocumentCount() + } + + @Override + MongoCollection setupUpdate(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + def coll = db.getCollection(collectionName) + coll.insertOne(new Document("password", "OLDPW")) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int update(MongoCollection collection) { + def result = collection.updateOne( + new BsonDocument("password", new BsonString("OLDPW")), + new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW")))) + collection.estimatedDocumentCount() + return result.modifiedCount + } + + @Override + MongoCollection setupDelete(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + def coll = db.getCollection(collectionName) + coll.insertOne(new Document("password", "SECRET")) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int delete(MongoCollection collection) { + def result = collection.deleteOne(new BsonDocument("password", new BsonString("SECRET"))) + collection.estimatedDocumentCount() + return result.deletedCount + } + + @Override + MongoCollection setupGetMore(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def coll = db.getCollection(collectionName) + coll.insertMany([new Document("_id", 0), new Document("_id", 1), new Document("_id", 2)]) + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + void getMore(MongoCollection collection) { + collection.find().filter(new Document("_id", new Document('$gte', 0))) + .batchSize(2).into(new ArrayList()) + } + + @Override + void error(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName) + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + collection.updateOne(new BsonDocument(), new BsonDocument()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/mongo-async-3.3-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/mongo-async-3.3-javaagent.gradle new file mode 100644 index 000000000..3c34c040b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/mongo-async-3.3-javaagent.gradle @@ -0,0 +1,21 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.mongodb" + module = "mongodb-driver-async" + versions = "[3.3,)" + extraDependency 'org.mongodb:mongo-java-driver' + assertInverse = true + } +} + +dependencies { + implementation(project(':instrumentation:mongo:mongo-3.1:library')) + + library "org.mongodb:mongodb-driver-async:3.3.0" + + testImplementation project(':instrumentation:mongo:mongo-testing') + + testInstrumentation project(':instrumentation:mongo:mongo-3.7:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/BaseClusterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/BaseClusterInstrumentation.java new file mode 100644 index 000000000..bb98263ac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/BaseClusterInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongoasync.v3_3; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.mongodb.async.SingleResultCallback; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class BaseClusterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.connection.BaseCluster"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("selectServerAsync")) + .and(takesArgument(0, named("com.mongodb.selector.ServerSelector"))) + .and(takesArgument(1, named("com.mongodb.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg1Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 1, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/InternalStreamConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/InternalStreamConnectionInstrumentation.java new file mode 100644 index 000000000..5c261b4d3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/InternalStreamConnectionInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongoasync.v3_3; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.mongodb.async.SingleResultCallback; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class InternalStreamConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.connection.InternalStreamConnection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("openAsync")) + .and(takesArgument(0, named("com.mongodb.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg0Advice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("readAsync")) + .and(takesArgument(1, named("com.mongodb.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("sendMessageAsync")) + .and(takesArgument(2, named("com.mongodb.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg2Advice"); + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg0Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 0, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg1Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 1, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } + + @SuppressWarnings("unused") + public static class SingleResultCallbackArg2Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 2, readOnly = false) SingleResultCallback callback) { + callback = new SingleResultCallbackWrapper(Java8BytecodeBridge.currentContext(), callback); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoAsyncClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoAsyncClientInstrumentationModule.java new file mode 100644 index 000000000..37e194072 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoAsyncClientInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongoasync.v3_3; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class MongoAsyncClientInstrumentationModule extends InstrumentationModule { + + public MongoAsyncClientInstrumentationModule() { + super("mongo-async", "mongo-async-3.3", "mongo"); + } + + @Override + public List typeInstrumentations() { + return asList( + new MongoClientSettingsBuildersInstrumentation(), + new InternalStreamConnectionInstrumentation(), + new BaseClusterInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoClientSettingsBuildersInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoClientSettingsBuildersInstrumentation.java new file mode 100644 index 000000000..2a82fd93b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoClientSettingsBuildersInstrumentation.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongoasync.v3_3; + +import static net.bytebuddy.matcher.ElementMatchers.declaresField; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.mongodb.async.client.MongoClientSettings; +import com.mongodb.event.CommandListener; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class MongoClientSettingsBuildersInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.async.client.MongoClientSettings$Builder") + .and( + declaresMethod( + named("addCommandListener") + .and(isPublic()) + .and( + takesArguments(1) + .and(takesArgument(0, named("com.mongodb.event.CommandListener")))))) + .and(declaresField(named("commandListeners"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")).and(takesArguments(0)), + this.getClass().getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void injectTraceListener( + @Advice.This MongoClientSettings.Builder builder, + @Advice.FieldValue("commandListeners") List commandListeners) { + for (CommandListener commandListener : commandListeners) { + if (commandListener == MongoInstrumentationSingletons.LISTENER) { + return; + } + } + builder.addCommandListener(MongoInstrumentationSingletons.LISTENER); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoInstrumentationSingletons.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoInstrumentationSingletons.java new file mode 100644 index 000000000..2c8c3cc38 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoInstrumentationSingletons.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongoasync.v3_3; + +import com.mongodb.event.CommandListener; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.mongo.v3_1.MongoTracing; + +public final class MongoInstrumentationSingletons { + + public static final CommandListener LISTENER = + MongoTracing.create(GlobalOpenTelemetry.get()).newCommandListener(); + + private MongoInstrumentationSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/SingleResultCallbackWrapper.java b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/SingleResultCallbackWrapper.java new file mode 100644 index 000000000..ebfbbe32a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/SingleResultCallbackWrapper.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongoasync.v3_3; + +import com.mongodb.async.SingleResultCallback; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class SingleResultCallbackWrapper implements SingleResultCallback { + private final Context context; + private final SingleResultCallback delegate; + + public SingleResultCallbackWrapper(Context context, SingleResultCallback delegate) { + this.context = context; + this.delegate = delegate; + } + + @Override + public void onResult(Object server, Throwable throwable) { + try (Scope ignored = context.makeCurrent()) { + delegate.onResult(server, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/test/groovy/MongoAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/test/groovy/MongoAsyncClientTest.groovy new file mode 100644 index 000000000..f00d21c65 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-async-3.3/javaagent/src/test/groovy/MongoAsyncClientTest.groovy @@ -0,0 +1,211 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.mongodb.ConnectionString +import com.mongodb.async.SingleResultCallback +import com.mongodb.async.client.MongoClient +import com.mongodb.async.client.MongoClientSettings +import com.mongodb.async.client.MongoClients +import com.mongodb.async.client.MongoCollection +import com.mongodb.async.client.MongoDatabase +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.UpdateResult +import com.mongodb.connection.ClusterSettings +import io.opentelemetry.instrumentation.mongo.testing.AbstractMongoClientTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch +import org.bson.BsonDocument +import org.bson.BsonString +import org.bson.Document +import org.junit.AssumptionViolatedException +import spock.lang.Shared + +class MongoAsyncClientTest extends AbstractMongoClientTest> implements AgentTestTrait{ + + @Shared + MongoClient client + + def setupSpec() throws Exception { + client = MongoClients.create( + MongoClientSettings.builder() + .clusterSettings( + ClusterSettings.builder() + .description("some-description") + .applyConnectionString(new ConnectionString("mongodb://localhost:$port")) + .build()) + .build()) + } + + def cleanupSpec() throws Exception { + client?.close() + client = null + } + + @Override + void createCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + db.createCollection(collectionName, toCallback {}) + } + + @Override + void createCollectionNoDescription(String dbName, String collectionName) { + MongoDatabase db = MongoClients.create("mongodb://localhost:$port").getDatabase(dbName) + db.createCollection(collectionName, toCallback {}) + } + + @Override + void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName) { + def clientSettings = client.settings + def newClientSettings = MongoClientSettings.builder(clientSettings).build() + MongoDatabase db = MongoClients.create(newClientSettings).getDatabase(dbName) + db.createCollection(collectionName, toCallback {}) + } + + @Override + void createCollectionCallingBuildTwice(String dbName, String collectionName) { + def settings = MongoClientSettings.builder() + .clusterSettings( + ClusterSettings.builder() + .description("some-description") + .applyConnectionString(new ConnectionString("mongodb://localhost:$port")) + .build()) + settings.build() + MongoDatabase db = MongoClients.create(settings.build()).getDatabase(dbName) + db.createCollection(collectionName, toCallback {}) + } + + @Override + int getCollection(String dbName, String collectionName) { + MongoDatabase db = client.getDatabase(dbName) + def count = new CompletableFuture() + db.getCollection(collectionName).count toCallback { count.complete(it) } + return count.join() + } + + @Override + MongoCollection setupInsert(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName, toCallback { latch1.countDown() }) + latch1.await() + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int insert(MongoCollection collection) { + def count = new CompletableFuture() + collection.insertOne(new Document("password", "SECRET"), toCallback { + collection.count toCallback { count.complete(it) } + }) + return count.get() + } + + @Override + MongoCollection setupUpdate(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName, toCallback { latch1.countDown() }) + latch1.await() + def coll = db.getCollection(collectionName) + def latch2 = new CountDownLatch(1) + coll.insertOne(new Document("password", "OLDPW"), toCallback { latch2.countDown() }) + latch2.await() + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int update(MongoCollection collection) { + def result = new CompletableFuture() + def count = new CompletableFuture() + collection.updateOne( + new BsonDocument("password", new BsonString("OLDPW")), + new BsonDocument('$set', new BsonDocument("password", new BsonString("NEWPW"))), toCallback { + result.complete(it) + collection.count toCallback { count.complete(it) } + }) + return result.get().modifiedCount + } + + @Override + MongoCollection setupDelete(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch1 = new CountDownLatch(1) + db.createCollection(collectionName, toCallback { latch1.countDown() }) + latch1.await() + def coll = db.getCollection(collectionName) + def latch2 = new CountDownLatch(1) + coll.insertOne(new Document("password", "SECRET"), toCallback { latch2.countDown() }) + latch2.await() + return coll + } + ignoreTracesAndClear(1) + return collection + } + + @Override + int delete(MongoCollection collection) { + def result = new CompletableFuture() + def count = new CompletableFuture() + collection.deleteOne(new BsonDocument("password", new BsonString("SECRET")), toCallback { + result.complete(it) + collection.count toCallback { count.complete(it) } + }) + return result.get().deletedCount + } + + @Override + MongoCollection setupGetMore(String dbName, String collectionName) { + throw new AssumptionViolatedException("not tested on async") + } + + @Override + void getMore(MongoCollection collection) { + throw new AssumptionViolatedException("not tested on async") + } + + @Override + void error(String dbName, String collectionName) { + MongoCollection collection = runUnderTrace("setup") { + MongoDatabase db = client.getDatabase(dbName) + def latch = new CountDownLatch(1) + db.createCollection(collectionName, toCallback { + latch.countDown() + }) + latch.await() + return db.getCollection(collectionName) + } + ignoreTracesAndClear(1) + def result = new CompletableFuture() + collection.updateOne(new BsonDocument(), new BsonDocument(), toCallback { + result.complete(it) + }) + throw result.join() + } + + SingleResultCallback toCallback(Closure closure) { + return new SingleResultCallback() { + @Override + void onResult(Object result, Throwable t) { + if (t) { + closure.call(t) + } else { + closure.call(result) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-testing/mongo-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-testing/mongo-testing.gradle new file mode 100644 index 000000000..b331b272d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-testing/mongo-testing.gradle @@ -0,0 +1,10 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + api "org.testcontainers:mongodb:${versions["org.testcontainers"]}" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/testing/AbstractMongoClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/testing/AbstractMongoClientTest.groovy new file mode 100644 index 000000000..9887da696 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/mongo/mongo-testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/testing/AbstractMongoClientTest.groovy @@ -0,0 +1,364 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.mongo.testing + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.atomic.AtomicInteger +import org.slf4j.LoggerFactory +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import spock.lang.Shared + +abstract class AbstractMongoClientTest extends InstrumentationSpecification { + + @Shared + GenericContainer mongodb + + @Shared + int port + + def setupSpec() { + mongodb = new GenericContainer("mongo:3.2") + .withExposedPorts(27017) + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("mongodb"))) + mongodb.start() + + port = mongodb.getMappedPort(27017) + } + + def cleanupSpec() throws Exception { + mongodb.stop() + } + + // Different client versions have different APIs to do these operations. If adding a test for a new + // version, refer to existing ones on how to implement these operations. + + abstract void createCollection(String dbName, String collectionName) + + abstract void createCollectionNoDescription(String dbName, String collectionName) + + // Tests the fix for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/457 + // TracingCommandListener might get added multiple times if clientOptions are built using existing clientOptions or when calling a build method twice. + // This test asserts that duplicate traces are not created in those cases. + abstract void createCollectionWithAlreadyBuiltClientOptions(String dbName, String collectionName) + + abstract void createCollectionCallingBuildTwice(String dbName, String collectionName) + + abstract int getCollection(String dbName, String collectionName) + + abstract T setupInsert(String dbName, String collectionName) + abstract int insert(T collection) + + abstract T setupUpdate(String dbName, String collectionName) + abstract int update(T collection) + + abstract T setupDelete(String dbName, String collectionName) + abstract int delete(T collection) + + abstract T setupGetMore(String dbName, String collectionName) + abstract void getMore(T collection) + + abstract void error(String dbName, String collectionName) + + def "test port open"() { + when: + new Socket("localhost", port) + + then: + noExceptionThrown() + } + + def "test create collection"() { + when: + runUnderTrace("parent") { + createCollection(dbName, collectionName) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "create", collectionName, dbName, span(0)) { + assert it == "{\"create\":\"$collectionName\",\"capped\":\"?\"}" || + it == "{\"create\": \"$collectionName\", \"capped\": \"?\", \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test create collection no description"() { + when: + runUnderTrace("parent") { + createCollectionNoDescription(dbName, collectionName) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "create", collectionName, dbName, span(0), { + assert it == "{\"create\":\"$collectionName\",\"capped\":\"?\"}" || + it == "{\"create\": \"$collectionName\", \"capped\": \"?\", \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + }) + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test create collection calling build twice"() { + when: + runUnderTrace("parent") { + createCollectionCallingBuildTwice(dbName, collectionName) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "create", collectionName, dbName, span(0)) { + assert it == "{\"create\":\"$collectionName\",\"capped\":\"?\"}" || + it == "{\"create\": \"$collectionName\", \"capped\": \"?\", \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test get collection"() { + when: + def count = runUnderTrace("parent") { + getCollection(dbName, collectionName) + } + + then: + count == 0 + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "count", collectionName, dbName, span(0)) { + assert it == "{\"count\":\"$collectionName\",\"query\":{}}" || + it == "{\"count\":\"$collectionName\"}" || + it == "{\"count\": \"$collectionName\", \"query\": {}, \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test insert"() { + when: + def collection = setupInsert(dbName, collectionName) + def count = runUnderTrace("parent") { + insert(collection) + } + + then: + count == 1 + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "insert", collectionName, dbName, span(0)) { + assert it == "{\"insert\":\"$collectionName\",\"ordered\":\"?\",\"documents\":[{\"_id\":\"?\",\"password\":\"?\"}]}" || + it == "{\"insert\": \"$collectionName\", \"ordered\": \"?\", \"\$db\": \"?\", \"documents\": [{\"_id\": \"?\", \"password\": \"?\"}]}" + true + } + mongoSpan(it, 2, "count", collectionName, dbName, span(0)) { + assert it == "{\"count\":\"$collectionName\",\"query\":{}}" || + it == "{\"count\":\"$collectionName\"}" || + it == "{\"count\": \"$collectionName\", \"query\": {}, \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test update"() { + when: + def collection = setupUpdate(dbName, collectionName) + int modifiedCount = runUnderTrace("parent") { + update(collection) + } + + then: + modifiedCount == 1 + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "update", collectionName, dbName, span(0)) { + assert it == "{\"update\":\"$collectionName\",\"ordered\":\"?\",\"updates\":[{\"q\":{\"password\":\"?\"},\"u\":{\"\$set\":{\"password\":\"?\"}}}]}" || + it == "{\"update\": \"?\", \"ordered\": \"?\", \"\$db\": \"?\", \"updates\": [{\"q\": {\"password\": \"?\"}, \"u\": {\"\$set\": {\"password\": \"?\"}}}]}" + true + } + mongoSpan(it, 2, "count", collectionName, dbName, span(0)) { + assert it == "{\"count\":\"$collectionName\",\"query\":{}}" || + it == "{\"count\":\"$collectionName\"}" || + it == "{\"count\": \"$collectionName\", \"query\": {}, \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test delete"() { + when: + def collection = setupDelete(dbName, collectionName) + int deletedCount = runUnderTrace("parent") { + delete(collection) + } + + then: + deletedCount == 1 + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "delete", collectionName, dbName, span(0)) { + assert it == "{\"delete\":\"$collectionName\",\"ordered\":\"?\",\"deletes\":[{\"q\":{\"password\":\"?\"},\"limit\":\"?\"}]}" || + it == "{\"delete\": \"?\", \"ordered\": \"?\", \"\$db\": \"?\", \"deletes\": [{\"q\": {\"password\": \"?\"}, \"limit\": \"?\"}]}" + true + } + mongoSpan(it, 2, "count", collectionName, dbName, span(0)) { + assert it == "{\"count\":\"$collectionName\",\"query\":{}}" || + it == "{\"count\":\"$collectionName\"}" || + it == "{\"count\": \"$collectionName\", \"query\": {}, \"\$db\": \"?\", \"\$readPreference\": {\"mode\": \"?\"}}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test collection name for getMore command"() { + when: + def collection = setupGetMore(dbName, collectionName) + runUnderTrace("parent") { + getMore(collection) + } + + then: + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "find", collectionName, dbName, span(0)) { + assert it == '{"find":"' + collectionName + '","filter":{"_id":{"$gte":"?"}},"batchSize":"?"}' + true + } + mongoSpan(it, 2, "getMore", collectionName, dbName, span(0)) { + assert it == '{"getMore":"?","collection":"?","batchSize":"?"}' + true + } + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test error"() { + when: + error(dbName, collectionName) + + then: + thrown(IllegalArgumentException) + // Unfortunately not caught by our instrumentation. + assertTraces(0) {} + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + def "test create collection with already built ClientOptions"() { + when: + runUnderTrace("parent") { + createCollectionWithAlreadyBuiltClientOptions(dbName, collectionName) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + mongoSpan(it, 1, "create", collectionName, dbName, span(0)) { + assert it == "{\"create\":\"$collectionName\",\"capped\":\"?\"}" + true + } + } + } + + where: + dbName = "test_db" + collectionName = createCollectionName() + } + + private static final AtomicInteger collectionIndex = new AtomicInteger() + + def createCollectionName() { + return "testCollection-${collectionIndex.getAndIncrement()}" + } + + def mongoSpan(TraceAssert trace, int index, + String operation, String collection, + String dbName, Object parentSpan, + Closure statementEval, Throwable exception = null) { + trace.span(index) { + name { operation + " " + dbName + "." + collection } + kind CLIENT + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + attributes { + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" { + statementEval.call(it.replaceAll(" ", "")) + } + "$SemanticAttributes.DB_SYSTEM.key" "mongodb" + "$SemanticAttributes.DB_CONNECTION_STRING.key" "mongodb://localhost:" + port + "$SemanticAttributes.DB_NAME.key" dbName + "$SemanticAttributes.DB_OPERATION.key" operation + "$SemanticAttributes.DB_MONGODB_COLLECTION.key" collection + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/netty-3.8-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/netty-3.8-javaagent.gradle new file mode 100644 index 000000000..41eef6131 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/netty-3.8-javaagent.gradle @@ -0,0 +1,40 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.netty" + module = "netty" + versions = "[3.8.0.Final,4)" + assertInverse = true + } + fail { + group = "io.netty" + module = "netty-all" + versions = "[,]" + excludeDependency 'io.netty:netty-tcnative' + } +} + +dependencies { + compileOnly "io.netty:netty:3.8.0.Final" + + testLibrary "io.netty:netty:3.8.0.Final" + testLibrary "com.ning:async-http-client:1.8.0" + + latestDepTestLibrary "io.netty:netty:3.10.+" + latestDepTestLibrary "com.ning:async-http-client:1.9.+" +} + +// We need to force the dependency to the earliest supported version because other libraries declare newer versions. +if (!testLatestDeps) { + configurations.each { + it.resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + //specifying a fixed version for all libraries with io.netty' group + if (details.requested.group == 'io.netty') { + details.useVersion "3.8.0.Final" + } + } + } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/ChannelFutureListenerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/ChannelFutureListenerInstrumentation.java new file mode 100644 index 000000000..8981d05ca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/ChannelFutureListenerInstrumentation.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.client.NettyHttpClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; + +public class ChannelFutureListenerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.jboss.netty.channel.ChannelFutureListener"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.jboss.netty.channel.ChannelFutureListener")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("operationComplete")) + .and(takesArgument(0, named("org.jboss.netty.channel.ChannelFuture"))), + ChannelFutureListenerInstrumentation.class.getName() + "$OperationCompleteAdvice"); + } + + @SuppressWarnings("unused") + public static class OperationCompleteAdvice { + + @Advice.OnMethodEnter + public static Scope activateScope(@Advice.Argument(0) ChannelFuture future) { + /* + Idea here is: + - To return scope only if we have captured it. + - To capture scope only in case of error. + */ + Throwable cause = future.getCause(); + if (cause == null) { + return null; + } + + ContextStore contextStore = + InstrumentationContext.get(Channel.class, ChannelTraceContext.class); + + ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(future.getChannel(), ChannelTraceContext.Factory.INSTANCE); + Context parentContext = channelTraceContext.getConnectionContext(); + if (parentContext == null) { + return null; + } + Scope parentScope = parentContext.makeCurrent(); + if (channelTraceContext.createConnectionSpan()) { + tracer().connectionFailure(parentContext, future.getChannel(), cause); + } + return parentScope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void deactivateScope(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/ChannelTraceContext.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/ChannelTraceContext.java new file mode 100644 index 000000000..994a749d3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/ChannelTraceContext.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import java.util.Objects; + +public class ChannelTraceContext { + + public static class Factory implements ContextStore.Factory { + public static final Factory INSTANCE = new Factory(); + + @Override + public ChannelTraceContext create() { + return new ChannelTraceContext(); + } + } + + private Context connectionContext; + private Context clientParentContext; + private Context context; + private boolean connectionSpanCreated; + + public Context getConnectionContext() { + return connectionContext; + } + + public void setConnectionContext(Context connectionContext) { + this.connectionContext = connectionContext; + } + + public Context getClientParentContext() { + return clientParentContext; + } + + public void setClientParentContext(Context clientParentContext) { + this.clientParentContext = clientParentContext; + } + + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } + + public boolean createConnectionSpan() { + if (connectionSpanCreated) { + return false; + } + connectionSpanCreated = true; + return true; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ChannelTraceContext)) { + return false; + } + ChannelTraceContext other = (ChannelTraceContext) obj; + return Objects.equals(connectionContext, other.connectionContext) + && Objects.equals(clientParentContext, other.clientParentContext) + && Objects.equals(context, other.context); + } + + @Override + public int hashCode() { + return Objects.hash(connectionContext, clientParentContext, context); + } + + @Override + public String toString() { + return "ChannelTraceContext{" + + "connectionContext=" + + connectionContext + + ", clientParentContext=" + + clientParentContext + + ", context=" + + context + + '}'; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/DefaultChannelPipelineInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/DefaultChannelPipelineInstrumentation.java new file mode 100644 index 000000000..a3891db1e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/DefaultChannelPipelineInstrumentation.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8; + +import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.server.NettyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DefaultChannelPipelineInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.netty.channel.DefaultChannelPipeline"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("notifyHandlerException")) + .and(takesArgument(1, named(Throwable.class.getName()))), + DefaultChannelPipelineInstrumentation.class.getName() + "$NotifyHandlerExceptionAdvice"); + } + + @SuppressWarnings("unused") + public static class NotifyHandlerExceptionAdvice { + + @Advice.OnMethodEnter + public static void onEnter(@Advice.Argument(1) Throwable throwable) { + if (throwable != null) { + tracer().onException(Java8BytecodeBridge.currentContext(), throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelInstrumentation.java new file mode 100644 index 000000000..b5ee5f70b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelInstrumentation.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.netty.channel.Channel; + +public class NettyChannelInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.jboss.netty.channel.Channel"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.jboss.netty.channel.Channel")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("connect")) + .and(returns(named("org.jboss.netty.channel.ChannelFuture"))), + NettyChannelInstrumentation.class.getName() + "$ChannelConnectAdvice"); + } + + @SuppressWarnings("unused") + public static class ChannelConnectAdvice { + + @Advice.OnMethodEnter + public static void onEnter(@Advice.This Channel channel) { + Context context = Java8BytecodeBridge.currentContext(); + Span span = Java8BytecodeBridge.spanFromContext(context); + if (span.getSpanContext().isValid()) { + ContextStore contextStore = + InstrumentationContext.get(Channel.class, ChannelTraceContext.class); + + if (contextStore + .putIfAbsent(channel, ChannelTraceContext.Factory.INSTANCE) + .getConnectionContext() + == null) { + contextStore.get(channel).setConnectionContext(context); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelPipelineInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelPipelineInstrumentation.java new file mode 100644 index 000000000..982c23b6d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelPipelineInstrumentation.java @@ -0,0 +1,169 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.client.HttpClientRequestTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.client.HttpClientResponseTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.client.HttpClientTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.server.HttpServerRequestTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.server.HttpServerResponseTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.server.HttpServerTracingHandler; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandler; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.handler.codec.http.HttpClientCodec; +import org.jboss.netty.handler.codec.http.HttpRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpRequestEncoder; +import org.jboss.netty.handler.codec.http.HttpResponseDecoder; +import org.jboss.netty.handler.codec.http.HttpResponseEncoder; +import org.jboss.netty.handler.codec.http.HttpServerCodec; + +public class NettyChannelPipelineInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.jboss.netty.channel.ChannelPipeline"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.jboss.netty.channel.ChannelPipeline")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(nameStartsWith("add")) + .and(takesArgument(1, named("org.jboss.netty.channel.ChannelHandler"))), + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineAdd2ArgsAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(nameStartsWith("add")) + .and(takesArgument(2, named("org.jboss.netty.channel.ChannelHandler"))), + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineAdd3ArgsAdvice"); + } + + /** + * When certain handlers are added to the pipeline, we want to add our corresponding tracing + * handlers. If those handlers are later removed, we may want to remove our handlers. That is not + * currently implemented. + */ + public static class ChannelPipelineAdviceUtil { + public static void wrapHandler( + ContextStore contextStore, + ChannelPipeline pipeline, + ChannelHandler handler) { + try { + // Server pipeline handlers + if (handler instanceof HttpServerCodec) { + pipeline.addLast( + HttpServerTracingHandler.class.getName(), new HttpServerTracingHandler(contextStore)); + } else if (handler instanceof HttpRequestDecoder) { + pipeline.addLast( + HttpServerRequestTracingHandler.class.getName(), + new HttpServerRequestTracingHandler(contextStore)); + } else if (handler instanceof HttpResponseEncoder) { + pipeline.addLast( + HttpServerResponseTracingHandler.class.getName(), + new HttpServerResponseTracingHandler(contextStore)); + } else + // Client pipeline handlers + if (handler instanceof HttpClientCodec) { + pipeline.addLast( + HttpClientTracingHandler.class.getName(), new HttpClientTracingHandler(contextStore)); + } else if (handler instanceof HttpRequestEncoder) { + pipeline.addLast( + HttpClientRequestTracingHandler.class.getName(), + new HttpClientRequestTracingHandler(contextStore)); + } else if (handler instanceof HttpResponseDecoder) { + pipeline.addLast( + HttpClientResponseTracingHandler.class.getName(), + new HttpClientResponseTracingHandler(contextStore)); + } + } finally { + CallDepthThreadLocalMap.reset(ChannelPipeline.class); + } + } + } + + @SuppressWarnings("unused") + public static class ChannelPipelineAdd2ArgsAdvice { + + @Advice.OnMethodEnter + public static int checkDepth( + @Advice.This ChannelPipeline pipeline, @Advice.Argument(1) ChannelHandler handler) { + // Pipelines are created once as a factory and then copied multiple times using the same add + // methods as we are hooking. If our handler has already been added we need to remove it so we + // don't end up with duplicates (this throws an exception) + if (pipeline.get(handler.getClass().getName()) != null) { + pipeline.remove(handler.getClass().getName()); + } + return CallDepthThreadLocalMap.incrementCallDepth(ChannelPipeline.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void addHandler( + @Advice.Enter int depth, + @Advice.This ChannelPipeline pipeline, + @Advice.Argument(1) ChannelHandler handler) { + if (depth > 0) { + return; + } + + ContextStore contextStore = + InstrumentationContext.get(Channel.class, ChannelTraceContext.class); + + ChannelPipelineAdviceUtil.wrapHandler(contextStore, pipeline, handler); + } + } + + @SuppressWarnings("unused") + public static class ChannelPipelineAdd3ArgsAdvice { + + @Advice.OnMethodEnter + public static int checkDepth( + @Advice.This ChannelPipeline pipeline, @Advice.Argument(2) ChannelHandler handler) { + // Pipelines are created once as a factory and then copied multiple times using the same add + // methods as we are hooking. If our handler has already been added we need to remove it so we + // don't end up with duplicates (this throws an exception) + if (pipeline.get(handler.getClass().getName()) != null) { + pipeline.remove(handler.getClass().getName()); + } + return CallDepthThreadLocalMap.incrementCallDepth(ChannelPipeline.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void addHandler( + @Advice.Enter int depth, + @Advice.This ChannelPipeline pipeline, + @Advice.Argument(2) ChannelHandler handler) { + if (depth > 0) { + return; + } + + ContextStore contextStore = + InstrumentationContext.get(Channel.class, ChannelTraceContext.class); + + ChannelPipelineAdviceUtil.wrapHandler(contextStore, pipeline, handler); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyInstrumentationModule.java new file mode 100644 index 000000000..77d66eb82 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class NettyInstrumentationModule extends InstrumentationModule { + public NettyInstrumentationModule() { + super("netty", "netty-3.8"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ChannelFutureListenerInstrumentation(), + new NettyChannelInstrumentation(), + new NettyChannelPipelineInstrumentation(), + new DefaultChannelPipelineInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientRequestTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientRequestTracingHandler.java new file mode 100644 index 000000000..db4d5d042 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientRequestTracingHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; + +import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.client.NettyHttpClientTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelDownstreamHandler; +import org.jboss.netty.handler.codec.http.HttpRequest; + +public class HttpClientRequestTracingHandler extends SimpleChannelDownstreamHandler { + + private final ContextStore contextStore; + + public HttpClientRequestTracingHandler(ContextStore contextStore) { + this.contextStore = contextStore; + } + + @Override + public void writeRequested(ChannelHandlerContext ctx, MessageEvent event) { + Object message = event.getMessage(); + if (!(message instanceof HttpRequest)) { + ctx.sendDownstream(event); + return; + } + + ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE); + + Context parentContext = channelTraceContext.getConnectionContext(); + if (parentContext != null) { + channelTraceContext.setConnectionContext(null); + } else { + parentContext = Context.current(); + } + + if (!tracer().shouldStartSpan(parentContext)) { + ctx.sendDownstream(event); + return; + } + + Context context = tracer().startSpan(parentContext, ctx, (HttpRequest) message); + channelTraceContext.setContext(context); + channelTraceContext.setClientParentContext(parentContext); + + try (Scope ignored = context.makeCurrent()) { + ctx.sendDownstream(event); + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientResponseTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientResponseTracingHandler.java new file mode 100644 index 000000000..89161bfa7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientResponseTracingHandler.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; + +import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.client.NettyHttpClientTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.HttpResponse; + +public class HttpClientResponseTracingHandler extends SimpleChannelUpstreamHandler { + + private final ContextStore contextStore; + + public HttpClientResponseTracingHandler(ContextStore contextStore) { + this.contextStore = contextStore; + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent msg) { + ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE); + + Context context = channelTraceContext.getContext(); + if (context == null) { + ctx.sendUpstream(msg); + return; + } + + if (msg.getMessage() instanceof HttpResponse) { + tracer().end(context, (HttpResponse) msg.getMessage()); + } + + // We want the callback in the scope of the parent, not the client span + Context parentContext = channelTraceContext.getClientParentContext(); + if (parentContext != null) { + try (Scope ignored = parentContext.makeCurrent()) { + ctx.sendUpstream(msg); + } + } else { + ctx.sendUpstream(msg); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientTracingHandler.java new file mode 100644 index 000000000..de6474c83 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/HttpClientTracingHandler.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; + +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.CombinedSimpleChannelHandler; +import org.jboss.netty.channel.Channel; + +public class HttpClientTracingHandler + extends CombinedSimpleChannelHandler< + HttpClientResponseTracingHandler, HttpClientRequestTracingHandler> { + + public HttpClientTracingHandler(ContextStore contextStore) { + super( + new HttpClientResponseTracingHandler(contextStore), + new HttpClientRequestTracingHandler(contextStore)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyHttpClientTracer.java new file mode 100644 index 000000000..820daa9b4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyHttpClientTracer.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.client.NettyResponseInjectAdapter.SETTER; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_UDP; +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.HOST; + +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.socket.DatagramChannel; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; + +public class NettyHttpClientTracer + extends HttpClientTracer { + private static final NettyHttpClientTracer TRACER = new NettyHttpClientTracer(); + + private NettyHttpClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static NettyHttpClientTracer tracer() { + return TRACER; + } + + public Context startSpan(Context parentContext, ChannelHandlerContext ctx, HttpRequest request) { + SpanBuilder spanBuilder = spanBuilder(parentContext, spanNameForRequest(request), CLIENT); + onRequest(spanBuilder, request); + NetPeerAttributes.INSTANCE.setNetPeer( + spanBuilder, (InetSocketAddress) ctx.getChannel().getRemoteAddress()); + + Context context = withClientSpan(parentContext, spanBuilder.startSpan()); + inject(context, request.headers(), SETTER); + return context; + } + + public void connectionFailure(Context parentContext, Channel channel, Throwable throwable) { + SpanBuilder spanBuilder = spanBuilder(parentContext, "CONNECT", CLIENT); + spanBuilder.setAttribute( + SemanticAttributes.NET_TRANSPORT, channel instanceof DatagramChannel ? IP_UDP : IP_TCP); + NetPeerAttributes.INSTANCE.setNetPeer( + spanBuilder, (InetSocketAddress) channel.getRemoteAddress()); + + Context context = withClientSpan(parentContext, spanBuilder.startSpan()); + tracer().endExceptionally(context, throwable); + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.getMethod().getName(); + } + + @Override + @Nullable + protected String flavor(HttpRequest httpRequest) { + return httpRequest.getProtocolVersion().getText(); + } + + @Override + protected URI url(HttpRequest request) throws URISyntaxException { + URI uri = new URI(request.getUri()); + if ((uri.getHost() == null || uri.getHost().equals("")) && request.headers().contains(HOST)) { + return new URI("http://" + request.headers().get(HOST) + request.getUri()); + } else { + return uri; + } + } + + @Override + protected Integer status(HttpResponse httpResponse) { + return httpResponse.getStatus().getCode(); + } + + @Override + protected String requestHeader(HttpRequest httpRequest, String name) { + return httpRequest.headers().get(name); + } + + @Override + protected String responseHeader(HttpResponse httpResponse, String name) { + return httpResponse.headers().get(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.netty-3.8"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyResponseInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyResponseInjectAdapter.java new file mode 100644 index 000000000..d093107d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyResponseInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.jboss.netty.handler.codec.http.HttpHeaders; + +public class NettyResponseInjectAdapter implements TextMapSetter { + + public static final NettyResponseInjectAdapter SETTER = new NettyResponseInjectAdapter(); + + @Override + public void set(HttpHeaders headers, String key, String value) { + headers.set(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerRequestTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerRequestTracingHandler.java new file mode 100644 index 000000000..d0f400662 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerRequestTracingHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.server; + +import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.server.NettyHttpServerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.HttpRequest; + +public class HttpServerRequestTracingHandler extends SimpleChannelUpstreamHandler { + + private final ContextStore contextStore; + + public HttpServerRequestTracingHandler(ContextStore contextStore) { + this.contextStore = contextStore; + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) { + ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE); + + Object message = event.getMessage(); + if (!(message instanceof HttpRequest)) { + Context serverContext = tracer().getServerContext(channelTraceContext); + if (serverContext == null) { + ctx.sendUpstream(event); + } else { + try (Scope ignored = serverContext.makeCurrent()) { + ctx.sendUpstream(event); + } + } + return; + } + + HttpRequest request = (HttpRequest) message; + + Context context = + tracer() + .startSpan( + request, ctx.getChannel(), channelTraceContext, "HTTP " + request.getMethod()); + try (Scope ignored = context.makeCurrent()) { + ctx.sendUpstream(event); + // the span is ended normally in HttpServerResponseTracingHandler + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerResponseTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerResponseTracingHandler.java new file mode 100644 index 000000000..59cea4e52 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerResponseTracingHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.server; + +import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.server.NettyHttpServerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelDownstreamHandler; +import org.jboss.netty.handler.codec.http.HttpResponse; + +public class HttpServerResponseTracingHandler extends SimpleChannelDownstreamHandler { + + private final ContextStore contextStore; + + public HttpServerResponseTracingHandler(ContextStore contextStore) { + this.contextStore = contextStore; + } + + @Override + public void writeRequested(ChannelHandlerContext ctx, MessageEvent msg) { + ChannelTraceContext channelTraceContext = + contextStore.putIfAbsent(ctx.getChannel(), ChannelTraceContext.Factory.INSTANCE); + + Context context = tracer().getServerContext(channelTraceContext); + if (context == null || !(msg.getMessage() instanceof HttpResponse)) { + ctx.sendDownstream(msg); + return; + } + + try (Scope ignored = context.makeCurrent()) { + ctx.sendDownstream(msg); + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + tracer().end(context, (HttpResponse) msg.getMessage()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerTracingHandler.java new file mode 100644 index 000000000..b733cd9c6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/HttpServerTracingHandler.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.server; + +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.CombinedSimpleChannelHandler; +import org.jboss.netty.channel.Channel; + +public class HttpServerTracingHandler + extends CombinedSimpleChannelHandler< + HttpServerRequestTracingHandler, HttpServerResponseTracingHandler> { + + public HttpServerTracingHandler(ContextStore contextStore) { + super( + new HttpServerRequestTracingHandler(contextStore), + new HttpServerResponseTracingHandler(contextStore)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyHttpServerTracer.java new file mode 100644 index 000000000..2c5a5d4ee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyHttpServerTracer.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.server; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.HOST; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.ChannelTraceContext; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; + +public class NettyHttpServerTracer + extends HttpServerTracer { + private static final NettyHttpServerTracer TRACER = new NettyHttpServerTracer(); + + public static NettyHttpServerTracer tracer() { + return TRACER; + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.getMethod().getName(); + } + + @Override + protected String requestHeader(HttpRequest httpRequest, String name) { + return httpRequest.headers().get(name); + } + + @Override + protected int responseStatus(HttpResponse httpResponse) { + return httpResponse.getStatus().getCode(); + } + + @Override + protected String bussinessStatus(HttpResponse httpResponse) { + return null; + } + + @Override + protected String bussinessMessage(HttpResponse httpResponse) { + return null; + } + + @Override + protected void attachServerContext(Context context, ChannelTraceContext channelTraceContext) { + channelTraceContext.setContext(context); + } + + @Override + public Context getServerContext(ChannelTraceContext channelTraceContext) { + return channelTraceContext.getContext(); + } + + @Override + protected String url(HttpRequest request) { + String uri = request.getUri(); + if (isRelativeUrl(uri) && request.headers().contains(HOST)) { + return "http://" + request.headers().get(HOST) + request.getUri(); + } else { + return uri; + } + } + + @Override + protected String peerHostIP(Channel channel) { + SocketAddress socketAddress = channel.getRemoteAddress(); + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getAddress().getHostAddress(); + } + return null; + } + + @Override + protected String flavor(Channel channel, HttpRequest request) { + return request.getProtocolVersion().toString(); + } + + @Override + protected TextMapGetter getGetter() { + return NettyRequestExtractAdapter.GETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.netty-3.8"; + } + + @Override + protected Integer peerPort(Channel channel) { + SocketAddress socketAddress = channel.getRemoteAddress(); + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getPort(); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyRequestExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyRequestExtractAdapter.java new file mode 100644 index 000000000..f54bd90b6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyRequestExtractAdapter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.server; + +import io.opentelemetry.context.propagation.TextMapGetter; +import org.jboss.netty.handler.codec.http.HttpRequest; + +public class NettyRequestExtractAdapter implements TextMapGetter { + + public static final NettyRequestExtractAdapter GETTER = new NettyRequestExtractAdapter(); + + @Override + public Iterable keys(HttpRequest request) { + return request.headers().names(); + } + + @Override + public String get(HttpRequest request, String key) { + return request.headers().get(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/util/CombinedSimpleChannelHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/util/CombinedSimpleChannelHandler.java new file mode 100644 index 000000000..5fb3fba4f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/util/CombinedSimpleChannelHandler.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v3_8.util; + +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.ChildChannelStateEvent; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelDownstreamHandler; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.channel.WriteCompletionEvent; + +public class CombinedSimpleChannelHandler< + UPSTREAM extends SimpleChannelUpstreamHandler, + DOWNSTREAM extends SimpleChannelDownstreamHandler> + extends SimpleChannelHandler { + + private final UPSTREAM upstream; + private final DOWNSTREAM downstream; + + public CombinedSimpleChannelHandler(UPSTREAM upstream, DOWNSTREAM downstream) { + this.upstream = upstream; + this.downstream = downstream; + } + + @Override + public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { + upstream.handleUpstream(ctx, e); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + upstream.messageReceived(ctx, e); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + upstream.exceptionCaught(ctx, e); + } + + @Override + public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + upstream.channelOpen(ctx, e); + } + + @Override + public void channelBound(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + upstream.channelBound(ctx, e); + } + + @Override + public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + upstream.channelConnected(ctx, e); + } + + @Override + public void channelInterestChanged(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + upstream.channelInterestChanged(ctx, e); + } + + @Override + public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + upstream.channelDisconnected(ctx, e); + } + + @Override + public void channelUnbound(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + upstream.channelUnbound(ctx, e); + } + + @Override + public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + upstream.channelClosed(ctx, e); + } + + @Override + public void writeComplete(ChannelHandlerContext ctx, WriteCompletionEvent e) throws Exception { + upstream.writeComplete(ctx, e); + } + + @Override + public void childChannelOpen(ChannelHandlerContext ctx, ChildChannelStateEvent e) + throws Exception { + upstream.childChannelOpen(ctx, e); + } + + @Override + public void childChannelClosed(ChannelHandlerContext ctx, ChildChannelStateEvent e) + throws Exception { + upstream.childChannelClosed(ctx, e); + } + + @Override + public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { + downstream.handleDownstream(ctx, e); + } + + @Override + public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + downstream.writeRequested(ctx, e); + } + + @Override + public void bindRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + downstream.bindRequested(ctx, e); + } + + @Override + public void connectRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + downstream.connectRequested(ctx, e); + } + + @Override + public void setInterestOpsRequested(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + downstream.setInterestOpsRequested(ctx, e); + } + + @Override + public void disconnectRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + downstream.disconnectRequested(ctx, e); + } + + @Override + public void unbindRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + downstream.unbindRequested(ctx, e); + } + + @Override + public void closeRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + downstream.closeRequested(ctx, e); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ClientTest.groovy new file mode 100644 index 000000000..46eb79218 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ClientTest.groovy @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.ning.http.client.AsyncCompletionHandler +import com.ning.http.client.AsyncHttpClient +import com.ning.http.client.AsyncHttpClientConfig +import com.ning.http.client.Request +import com.ning.http.client.RequestBuilder +import com.ning.http.client.Response +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.SpanAssert +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.nio.channels.ClosedChannelException +import spock.lang.AutoCleanup +import spock.lang.Shared + +class Netty38ClientTest extends HttpClientTest implements AgentTestTrait { + + @Shared + @AutoCleanup + AsyncHttpClient client = new AsyncHttpClient(getClientConfig()) + + def getClientConfig() { + def builder = new AsyncHttpClientConfig.Builder() + .setUserAgent("test-user-agent") + + if (builder.metaClass.getMetaMethod("setConnectTimeout", int) != null) { + builder.setConnectTimeout(CONNECT_TIMEOUT_MS) + } else { + builder.setRequestTimeoutInMs(CONNECT_TIMEOUT_MS) + } + if (builder.metaClass.getMetaMethod("setFollowRedirect", boolean) != null) { + builder.setFollowRedirect(true) + } else { + builder.setFollowRedirects(true) + } + if (builder.metaClass.getMetaMethod("setMaxRedirects", int) != null) { + builder.setMaxRedirects(3) + } else { + builder.setMaximumNumberOfRedirects(3) + } + + return builder.build() + } + + @Override + Request buildRequest(String method, URI uri, Map headers) { + def requestBuilder = new RequestBuilder(method) + .setUrl(uri.toString()) + headers.entrySet().each { + requestBuilder.addHeader(it.key, it.value) + } + return requestBuilder.build() + } + + @Override + int sendRequest(Request request, String method, URI uri, Map headers) { + return client.executeRequest(request).get().statusCode + } + + @Override + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + // TODO(anuraaga): Do we also need to test ListenableFuture callback? + client.executeRequest(request, new AsyncCompletionHandler() { + @Override + Void onCompleted(Response response) throws Exception { + requestResult.complete(response.statusCode) + return null + } + + @Override + void onThrowable(Throwable throwable) { + requestResult.complete(throwable) + } + }) + } + + @Override + String userAgent() { + return "test-user-agent" + } + + @Override + String expectedClientSpanName(URI uri, String method) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return "CONNECT" + default: + return super.expectedClientSpanName(uri, method) + } + } + + @Override + void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + exception = exception.getCause() != null ? exception.getCause() : new ConnectException("Connection refused: localhost/127.0.0.1:61") + break + case "https://192.0.2.1/": // non routable address + exception = exception.getCause() != null ? exception.getCause() : new ClosedChannelException() + } + super.assertClientSpanErrorEvent(spanAssert, uri, exception) + } + + @Override + Set> httpAttributes(URI uri) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return [] + } + return super.httpAttributes(uri) + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testHttps() { + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ServerTest.groovy new file mode 100644 index 000000000..067d373df --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ServerTest.groovy @@ -0,0 +1,163 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.forPath +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE +import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.LOCATION +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1 + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import org.jboss.netty.bootstrap.ServerBootstrap +import org.jboss.netty.buffer.ChannelBuffer +import org.jboss.netty.buffer.ChannelBuffers +import org.jboss.netty.channel.ChannelHandlerContext +import org.jboss.netty.channel.ChannelPipeline +import org.jboss.netty.channel.ChannelPipelineFactory +import org.jboss.netty.channel.DefaultChannelPipeline +import org.jboss.netty.channel.DownstreamMessageEvent +import org.jboss.netty.channel.ExceptionEvent +import org.jboss.netty.channel.FailedChannelFuture +import org.jboss.netty.channel.MessageEvent +import org.jboss.netty.channel.SimpleChannelHandler +import org.jboss.netty.channel.SucceededChannelFuture +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory +import org.jboss.netty.handler.codec.http.DefaultHttpResponse +import org.jboss.netty.handler.codec.http.HttpRequest +import org.jboss.netty.handler.codec.http.HttpResponse +import org.jboss.netty.handler.codec.http.HttpResponseStatus +import org.jboss.netty.handler.codec.http.HttpServerCodec +import org.jboss.netty.handler.codec.http.QueryStringDecoder +import org.jboss.netty.handler.logging.LoggingHandler +import org.jboss.netty.logging.InternalLogLevel +import org.jboss.netty.logging.InternalLoggerFactory +import org.jboss.netty.logging.Slf4JLoggerFactory +import org.jboss.netty.util.CharsetUtil + +class Netty38ServerTest extends HttpServerTest implements AgentTestTrait { + + static final LoggingHandler LOGGING_HANDLER + static { + InternalLoggerFactory.setDefaultFactory(new Slf4JLoggerFactory()) + LOGGING_HANDLER = new LoggingHandler(SERVER_LOGGER.name, InternalLogLevel.DEBUG, true) + } + + ChannelPipeline channelPipeline() { + ChannelPipeline channelPipeline = new DefaultChannelPipeline() + channelPipeline.addFirst("logger", LOGGING_HANDLER) + + channelPipeline.addLast("http-codec", new HttpServerCodec()) + channelPipeline.addLast("controller", new SimpleChannelHandler() { + @Override + void messageReceived(ChannelHandlerContext ctx, MessageEvent msg) throws Exception { + if (msg.getMessage() instanceof HttpRequest) { + def uri = URI.create((msg.getMessage() as HttpRequest).getUri()) + HttpServerTest.ServerEndpoint endpoint = forPath(uri.path) + ctx.sendDownstream controller(endpoint) { + HttpResponse response + ChannelBuffer responseContent = null + switch (endpoint) { + case SUCCESS: + case ERROR: + responseContent = ChannelBuffers.copiedBuffer(endpoint.body, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + case INDEXED_CHILD: + responseContent = ChannelBuffers.EMPTY_BUFFER + endpoint.collectSpanAttributes { new QueryStringDecoder(uri).getParameters().get(it).find() } + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + case QUERY_PARAM: + responseContent = ChannelBuffers.copiedBuffer(uri.query, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + case REDIRECT: + responseContent = ChannelBuffers.EMPTY_BUFFER + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + response.headers().set(LOCATION, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + default: + responseContent = ChannelBuffers.copiedBuffer(NOT_FOUND.body, CharsetUtil.UTF_8) + response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status)) + response.setContent(responseContent) + break + } + response.headers().set(CONTENT_TYPE, "text/plain") + if (responseContent) { + response.headers().set(CONTENT_LENGTH, responseContent.readableBytes()) + } + return new DownstreamMessageEvent( + ctx.getChannel(), + new SucceededChannelFuture(ctx.getChannel()), + response, + ctx.getChannel().getRemoteAddress()) + } + } + } + + @Override + void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent ex) throws Exception { + def message = ex.cause == null ? " " + ex.message : ex.cause.message == null ? "" : ex.cause.message + ChannelBuffer buffer = ChannelBuffers.copiedBuffer(message, CharsetUtil.UTF_8) + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR) + response.setContent(buffer) + response.headers().set(CONTENT_TYPE, "text/plain") + response.headers().set(CONTENT_LENGTH, buffer.readableBytes()) + ctx.sendDownstream(new DownstreamMessageEvent( + ctx.getChannel(), + new FailedChannelFuture(ctx.getChannel(), ex.getCause()), + response, + ctx.getChannel().getRemoteAddress())) + } + }) + + return channelPipeline + } + + @Override + ServerBootstrap startServer(int port) { + ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory()) + bootstrap.setParentHandler(LOGGING_HANDLER) + bootstrap.setPipelineFactory(new ChannelPipelineFactory() { + @Override + ChannelPipeline getPipeline() throws Exception { + return channelPipeline() + } + }) + + InetSocketAddress address = new InetSocketAddress(port) + bootstrap.bind(address) + return bootstrap + } + + @Override + void stopServer(ServerBootstrap server) { + server?.shutdown() + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "HTTP GET" + } + + @Override + boolean testConcurrency() { + return true + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/netty-4-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/netty-4-common-javaagent.gradle new file mode 100644 index 000000000..d4cb315f4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/netty-4-common-javaagent.gradle @@ -0,0 +1,5 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly "io.netty:netty-codec-http:4.0.0.Final" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/AbstractNettyChannelPipelineInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/AbstractNettyChannelPipelineInstrumentation.java new file mode 100644 index 000000000..eeb8ae923 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/AbstractNettyChannelPipelineInstrumentation.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.common; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public abstract class AbstractNettyChannelPipelineInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.netty.channel.ChannelPipeline"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.netty.channel.ChannelPipeline")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("remove")) + .and(takesArgument(0, named("io.netty.channel.ChannelHandler"))), + AbstractNettyChannelPipelineInstrumentation.class.getName() + + "$ChannelPipelineRemoveAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("remove")).and(takesArgument(0, String.class)), + AbstractNettyChannelPipelineInstrumentation.class.getName() + + "$ChannelPipelineRemoveByNameAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("remove")).and(takesArgument(0, Class.class)), + AbstractNettyChannelPipelineInstrumentation.class.getName() + + "$ChannelPipelineRemoveByClassAdvice"); + } + + @SuppressWarnings("unused") + public static class ChannelPipelineRemoveAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void removeHandler( + @Advice.This ChannelPipeline pipeline, @Advice.Argument(0) ChannelHandler handler) { + ContextStore contextStore = + InstrumentationContext.get(ChannelHandler.class, ChannelHandler.class); + ChannelHandler ourHandler = contextStore.get(handler); + if (ourHandler != null) { + pipeline.remove(ourHandler); + contextStore.put(handler, null); + } + } + } + + @SuppressWarnings("unused") + public static class ChannelPipelineRemoveByNameAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void removeHandler( + @Advice.This ChannelPipeline pipeline, @Advice.Argument(0) String name) { + ChannelHandler handler = pipeline.get(name); + if (handler == null) { + return; + } + + ContextStore contextStore = + InstrumentationContext.get(ChannelHandler.class, ChannelHandler.class); + ChannelHandler ourHandler = contextStore.get(handler); + if (ourHandler != null) { + pipeline.remove(ourHandler); + contextStore.put(handler, null); + } + } + } + + @SuppressWarnings("unused") + public static class ChannelPipelineRemoveByClassAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void removeHandler( + @Advice.This ChannelPipeline pipeline, + @Advice.Argument(0) Class handlerClass) { + ChannelHandler handler = pipeline.get(handlerClass); + if (handler == null) { + return; + } + + ContextStore contextStore = + InstrumentationContext.get(ChannelHandler.class, ChannelHandler.class); + ChannelHandler ourHandler = contextStore.get(handler); + if (ourHandler != null) { + pipeline.remove(ourHandler); + contextStore.put(handler, null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/FutureListenerWrappers.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/FutureListenerWrappers.java new file mode 100644 index 000000000..c8b903f25 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/FutureListenerWrappers.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.common; + +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.GenericProgressiveFutureListener; +import io.netty.util.concurrent.ProgressiveFuture; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.caching.Cache; + +public final class FutureListenerWrappers { + // Instead of ContextStore use Cache with weak keys and weak values to store link between original + // listener and wrapper. ContextStore works fine when wrapper is stored in a field on original + // listener, but when listener class is a lambda instead of field it gets stored in a map with + // weak keys where original listener is key and wrapper is value. As wrapper has a strong + // reference to original listener this causes a memory leak. + // Also note that it's ok if the value is collected prior to the key, since this cache is only + // used to remove the wrapped listener from the netty future, and if the value is collected prior + // to the key, that means it's no longer used (referenced) by the netty future anyways. + private static final Cache< + GenericFutureListener>, GenericFutureListener>> + wrappers = Cache.newBuilder().setWeakKeys().setWeakValues().build(); + + @SuppressWarnings("unchecked") + public static GenericFutureListener wrap( + Context context, GenericFutureListener> delegate) { + if (delegate instanceof WrappedFutureListener + || delegate instanceof WrappedProgressiveFutureListener) { + return delegate; + } + return wrappers.computeIfAbsent( + delegate, + key -> { + if (delegate instanceof GenericProgressiveFutureListener) { + return new WrappedProgressiveFutureListener( + context, (GenericProgressiveFutureListener>) delegate); + } else { + return new WrappedFutureListener(context, (GenericFutureListener>) delegate); + } + }); + } + + public static GenericFutureListener> getWrapper( + GenericFutureListener> delegate) { + GenericFutureListener> wrapper = wrappers.get(delegate); + return wrapper == null ? delegate : wrapper; + } + + private static final class WrappedFutureListener implements GenericFutureListener> { + + private final Context context; + private final GenericFutureListener> delegate; + + private WrappedFutureListener(Context context, GenericFutureListener> delegate) { + this.context = context; + this.delegate = delegate; + } + + @Override + public void operationComplete(Future future) throws Exception { + try (Scope ignored = context.makeCurrent()) { + delegate.operationComplete(future); + } + } + } + + private static final class WrappedProgressiveFutureListener + implements GenericProgressiveFutureListener> { + + private final Context context; + private final GenericProgressiveFutureListener> delegate; + + private WrappedProgressiveFutureListener( + Context context, GenericProgressiveFutureListener> delegate) { + this.context = context; + this.delegate = delegate; + } + + @Override + public void operationProgressed(ProgressiveFuture progressiveFuture, long l, long l1) + throws Exception { + try (Scope ignored = context.makeCurrent()) { + delegate.operationProgressed(progressiveFuture, l, l1); + } + } + + @Override + public void operationComplete(ProgressiveFuture progressiveFuture) throws Exception { + try (Scope ignored = context.makeCurrent()) { + delegate.operationComplete(progressiveFuture); + } + } + } + + private FutureListenerWrappers() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/NettyFutureInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/NettyFutureInstrumentation.java new file mode 100644 index 000000000..6e5acc550 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/NettyFutureInstrumentation.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.common; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isArray; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class NettyFutureInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.netty.util.concurrent.Future"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.netty.util.concurrent.Future")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("addListener")) + .and(takesArgument(0, named("io.netty.util.concurrent.GenericFutureListener"))), + NettyFutureInstrumentation.class.getName() + "$AddListenerAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("addListeners")).and(takesArgument(0, isArray())), + NettyFutureInstrumentation.class.getName() + "$AddListenersAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("removeListener")) + .and(takesArgument(0, named("io.netty.util.concurrent.GenericFutureListener"))), + NettyFutureInstrumentation.class.getName() + "$RemoveListenerAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("removeListeners")).and(takesArgument(0, isArray())), + NettyFutureInstrumentation.class.getName() + "$RemoveListenersAdvice"); + } + + @SuppressWarnings("unused") + public static class AddListenerAdvice { + + @Advice.OnMethodEnter + public static void wrapListener( + @Advice.Argument(value = 0, readOnly = false) + GenericFutureListener> listener) { + listener = FutureListenerWrappers.wrap(Java8BytecodeBridge.currentContext(), listener); + } + } + + @SuppressWarnings("unused") + public static class AddListenersAdvice { + + @Advice.OnMethodEnter + public static void wrapListener( + @Advice.Argument(value = 0, readOnly = false) + GenericFutureListener>[] listeners) { + + Context context = Java8BytecodeBridge.currentContext(); + @SuppressWarnings("unchecked") + GenericFutureListener>[] wrappedListeners = + new GenericFutureListener[listeners.length]; + for (int i = 0; i < listeners.length; ++i) { + wrappedListeners[i] = FutureListenerWrappers.wrap(context, listeners[i]); + } + listeners = wrappedListeners; + } + } + + @SuppressWarnings("unused") + public static class RemoveListenerAdvice { + + @Advice.OnMethodEnter + public static void wrapListener( + @Advice.Argument(value = 0, readOnly = false) + GenericFutureListener> listener) { + listener = FutureListenerWrappers.getWrapper(listener); + } + } + + @SuppressWarnings("unused") + public static class RemoveListenersAdvice { + + @Advice.OnMethodEnter + public static void wrapListener( + @Advice.Argument(value = 0, readOnly = false) + GenericFutureListener>[] listeners) { + + @SuppressWarnings("unchecked") + GenericFutureListener>[] wrappedListeners = + new GenericFutureListener[listeners.length]; + for (int i = 0; i < listeners.length; ++i) { + wrappedListeners[i] = FutureListenerWrappers.getWrapper(listeners[i]); + } + listeners = wrappedListeners; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/client/NettyResponseInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/client/NettyResponseInjectAdapter.java new file mode 100644 index 000000000..730c59df5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/client/NettyResponseInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.common.client; + +import io.netty.handler.codec.http.HttpHeaders; +import io.opentelemetry.context.propagation.TextMapSetter; + +public class NettyResponseInjectAdapter implements TextMapSetter { + + public static final NettyResponseInjectAdapter SETTER = new NettyResponseInjectAdapter(); + + @Override + public void set(HttpHeaders headers, String key, String value) { + headers.set(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/server/NettyRequestExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/server/NettyRequestExtractAdapter.java new file mode 100644 index 000000000..6450a1c7a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/common/server/NettyRequestExtractAdapter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.common.server; + +import io.netty.handler.codec.http.HttpRequest; +import io.opentelemetry.context.propagation.TextMapGetter; + +public class NettyRequestExtractAdapter implements TextMapGetter { + + public static final NettyRequestExtractAdapter GETTER = new NettyRequestExtractAdapter(); + + @Override + public Iterable keys(HttpRequest request) { + return request.headers().names(); + } + + @Override + public String get(HttpRequest request, String key) { + return request.headers().get(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/netty-4.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/netty-4.0-javaagent.gradle new file mode 100644 index 000000000..c4796ec19 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/netty-4.0-javaagent.gradle @@ -0,0 +1,42 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.netty" + module = "netty-codec-http" + versions = "[4.0.0.Final,4.1.0.Final)" + assertInverse = true + } + pass { + group = "io.netty" + module = "netty-all" + versions = "[4.0.0.Final,4.1.0.Final)" + excludeDependency 'io.netty:netty-tcnative' + assertInverse = true + } + fail { + group = "io.netty" + module = "netty" + versions = "[,]" + } +} + +dependencies { + library "io.netty:netty-codec-http:4.0.0.Final" + implementation project(':instrumentation:netty:netty-4-common:javaagent') + latestDepTestLibrary "io.netty:netty-codec-http:4.0.56.Final" +} + +// We need to force the dependency to the earliest supported version because other libraries declare newer versions. +if (!testLatestDeps) { + configurations.each { + it.resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + //specifying a fixed version for all libraries with io.netty' group + if (details.requested.group == 'io.netty' && details.requested.name != "netty-bom") { + details.useVersion "4.0.0.Final" + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/AbstractChannelHandlerContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/AbstractChannelHandlerContextInstrumentation.java new file mode 100644 index 000000000..c90b63e9b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/AbstractChannelHandlerContextInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.NettyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AbstractChannelHandlerContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + // Different classes depending on Netty version + return namedOneOf( + "io.netty.channel.AbstractChannelHandlerContext", + "io.netty.channel.DefaultChannelHandlerContext"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("notifyHandlerException")) + .and(takesArgument(0, named(Throwable.class.getName()))), + AbstractChannelHandlerContextInstrumentation.class.getName() + + "$NotifyHandlerExceptionAdvice"); + } + + @SuppressWarnings("unused") + public static class NotifyHandlerExceptionAdvice { + + @Advice.OnMethodEnter + public static void onEnter(@Advice.Argument(0) Throwable throwable) { + if (throwable != null) { + tracer().onException(Java8BytecodeBridge.currentContext(), throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/AttributeKeys.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/AttributeKeys.java new file mode 100644 index 000000000..2decb2af8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/AttributeKeys.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0; + +import io.netty.util.AttributeKey; +import io.opentelemetry.context.Context; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class AttributeKeys { + + private static final ClassValue>> mapSupplier = + new ClassValue>>() { + @Override + protected ConcurrentMap> computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + }; + + public static final AttributeKey CONNECT_CONTEXT = + attributeKey(AttributeKeys.class.getName() + ".connect-context"); + public static final AttributeKey WRITE_CONTEXT = + attributeKey(AttributeKeys.class.getName() + ".write-context"); + + // this is the context that has the server span + public static final AttributeKey SERVER_SPAN = + attributeKey(AttributeKeys.class.getName() + ".server-span"); + + public static final AttributeKey CLIENT_CONTEXT = + attributeKey(AttributeKeys.class.getName() + ".client-context"); + + public static final AttributeKey CLIENT_PARENT_CONTEXT = + attributeKey(AttributeKeys.class.getName() + ".client-parent-context"); + + /** + * Generate an attribute key or reuse the one existing in the global app map. This implementation + * creates attributes only once even if the current class is loaded by several class loaders and + * prevents an issue with Apache Atlas project were this class loaded by multiple class loaders, + * while the Attribute class is loaded by a third class loader and used internally for the + * cassandra driver. + * + *

Keep this API public for vendor instrumentations + */ + @SuppressWarnings("unchecked") + public static AttributeKey attributeKey(String key) { + ConcurrentMap> classLoaderMap = mapSupplier.get(AttributeKey.class); + return (AttributeKey) classLoaderMap.computeIfAbsent(key, AttributeKey::new); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelFutureListenerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelFutureListenerInstrumentation.java new file mode 100644 index 000000000..41ed6aa95 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelFutureListenerInstrumentation.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.NettyHttpClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.netty.channel.ChannelFuture; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ChannelFutureListenerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.netty.channel.ChannelFutureListener"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.netty.channel.ChannelFutureListener")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("operationComplete")) + .and(takesArgument(0, named("io.netty.channel.ChannelFuture"))), + ChannelFutureListenerInstrumentation.class.getName() + "$OperationCompleteAdvice"); + } + + @SuppressWarnings("unused") + public static class OperationCompleteAdvice { + + @Advice.OnMethodEnter + public static Scope activateScope(@Advice.Argument(0) ChannelFuture future) { + /* + Idea here is: + - To return scope only if we have captured it. + - To capture scope only in case of error. + */ + Context parentContext = future.channel().attr(AttributeKeys.CONNECT_CONTEXT).getAndRemove(); + if (parentContext == null) { + return null; + } + Throwable cause = future.cause(); + if (cause == null) { + return null; + } + + Scope parentScope = parentContext.makeCurrent(); + if (tracer().shouldStartSpan(parentContext)) { + tracer().connectionFailure(parentContext, future.channel(), cause); + } + return parentScope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void deactivateScope(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelInstrumentation.java new file mode 100644 index 000000000..a8ae44319 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.netty.channel.Channel; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This instrumentation preserves the context that was active during call to any "write" operation + * on Netty Channel in that channel's attribute. This context is later used by our various tracing + * handlers to scope the work. + */ +public class ChannelInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.netty.channel.Channel"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.netty.channel.Channel")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(namedOneOf("write", "writeAndFlush")), + ChannelInstrumentation.class.getName() + "$AttachContextAdvice"); + } + + @SuppressWarnings("unused") + public static class AttachContextAdvice { + + @Advice.OnMethodEnter + public static void attachContext(@Advice.This Channel channel) { + channel + .attr(AttributeKeys.WRITE_CONTEXT) + .compareAndSet(null, Java8BytecodeBridge.currentContext()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyChannelPipelineInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyChannelPipelineInstrumentation.java new file mode 100644 index 000000000..37a63302b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyChannelPipelineInstrumentation.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpRequestEncoder; +import io.netty.handler.codec.http.HttpResponseDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.util.Attribute; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.netty.common.AbstractNettyChannelPipelineInstrumentation; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.HttpClientRequestTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.HttpClientResponseTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.HttpClientTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.HttpServerRequestTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.HttpServerResponseTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.HttpServerTracingHandler; +import net.bytebuddy.asm.Advice; + +public class NettyChannelPipelineInstrumentation + extends AbstractNettyChannelPipelineInstrumentation { + + @Override + public void transform(TypeTransformer transformer) { + super.transform(transformer); + + transformer.applyAdviceToMethod( + isMethod() + .and(nameStartsWith("add")) + .and(takesArgument(2, named("io.netty.channel.ChannelHandler"))), + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineAddAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("connect")).and(returns(named("io.netty.channel.ChannelFuture"))), + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineConnectAdvice"); + } + + /** + * When certain handlers are added to the pipeline, we want to add our corresponding tracing + * handlers. If those handlers are later removed, we may want to remove our handlers. That is not + * currently implemented. + */ + @SuppressWarnings("unused") + public static class ChannelPipelineAddAdvice { + + @Advice.OnMethodEnter + public static int trackCallDepth() { + return CallDepthThreadLocalMap.incrementCallDepth(ChannelPipeline.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void addHandler( + @Advice.Enter int callDepth, + @Advice.This ChannelPipeline pipeline, + @Advice.Argument(2) ChannelHandler handler) { + if (callDepth > 0) { + return; + } + CallDepthThreadLocalMap.reset(ChannelPipeline.class); + + ChannelHandler ourHandler = null; + // Server pipeline handlers + if (handler instanceof HttpServerCodec) { + ourHandler = new HttpServerTracingHandler(); + } else if (handler instanceof HttpRequestDecoder) { + ourHandler = new HttpServerRequestTracingHandler(); + } else if (handler instanceof HttpResponseEncoder) { + ourHandler = new HttpServerResponseTracingHandler(); + // Client pipeline handlers + } else if (handler instanceof HttpClientCodec) { + ourHandler = new HttpClientTracingHandler(); + } else if (handler instanceof HttpRequestEncoder) { + ourHandler = new HttpClientRequestTracingHandler(); + } else if (handler instanceof HttpResponseDecoder) { + ourHandler = new HttpClientResponseTracingHandler(); + } + + if (ourHandler != null) { + try { + pipeline.addLast(ourHandler.getClass().getName(), ourHandler); + // associate our handle with original handler so they could be removed together + InstrumentationContext.get(ChannelHandler.class, ChannelHandler.class) + .putIfAbsent(handler, ourHandler); + } catch (IllegalArgumentException e) { + // Prevented adding duplicate handlers. + } + } + } + } + + @SuppressWarnings("unused") + public static class ChannelPipelineConnectAdvice { + + @Advice.OnMethodEnter + public static void addParentSpan(@Advice.This ChannelPipeline pipeline) { + Context context = Java8BytecodeBridge.currentContext(); + Attribute attribute = pipeline.channel().attr(AttributeKeys.CONNECT_CONTEXT); + attribute.compareAndSet(null, context); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyInstrumentationModule.java new file mode 100644 index 000000000..6dfe72e41 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyInstrumentationModule.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.netty.common.NettyFutureInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class NettyInstrumentationModule extends InstrumentationModule { + public NettyInstrumentationModule() { + super("netty", "netty-4.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // Class added in 4.1.0 and not in 4.0.56 to avoid resolving this instrumentation completely + // when using 4.1. + return not(hasClassesNamed("io.netty.handler.codec.http.CombinedHttpHeaders")); + } + + @Override + public List typeInstrumentations() { + return asList( + new ChannelInstrumentation(), + new NettyFutureInstrumentation(), + new ChannelFutureListenerInstrumentation(), + new NettyChannelPipelineInstrumentation(), + new AbstractChannelHandlerContextInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientRequestTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientRequestTracingHandler.java new file mode 100644 index 000000000..c2ade479a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientRequestTracingHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.NettyHttpClientTracer.tracer; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpRequest; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.AttributeKeys; + +public class HttpClientRequestTracingHandler extends ChannelOutboundHandlerAdapter { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) { + if (!(msg instanceof HttpRequest)) { + ctx.write(msg, prm); + return; + } + + Context parentContext = ctx.channel().attr(AttributeKeys.WRITE_CONTEXT).getAndRemove(); + if (parentContext == null) { + parentContext = Context.current(); + } + + if (!tracer().shouldStartSpan(parentContext)) { + ctx.write(msg, prm); + return; + } + + NettyRequestWrapper requestWrapper = new NettyRequestWrapper((HttpRequest) msg, ctx); + Context context = tracer().startSpan(parentContext, ctx, requestWrapper); + ctx.channel().attr(AttributeKeys.CLIENT_CONTEXT).set(context); + ctx.channel().attr(AttributeKeys.CLIENT_PARENT_CONTEXT).set(parentContext); + + try (Scope ignored = context.makeCurrent()) { + ctx.write(msg, prm); + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientResponseTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientResponseTracingHandler.java new file mode 100644 index 000000000..fcd423a73 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientResponseTracingHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.NettyHttpClientTracer.tracer; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.util.Attribute; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.AttributeKeys; + +public class HttpClientResponseTracingHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + Context context = ctx.channel().attr(AttributeKeys.CLIENT_CONTEXT).get(); + if (context == null) { + ctx.fireChannelRead(msg); + return; + } + + if (msg instanceof HttpResponse) { + tracer().end(context, (HttpResponse) msg); + } + + // We want the callback in the scope of the parent, not the client span + Attribute parentAttr = ctx.channel().attr(AttributeKeys.CLIENT_PARENT_CONTEXT); + Context parentContext = parentAttr.get(); + if (parentContext != null) { + try (Scope ignored = parentContext.makeCurrent()) { + ctx.fireChannelRead(msg); + } + } else { + ctx.fireChannelRead(msg); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientTracingHandler.java new file mode 100644 index 000000000..332b56072 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/HttpClientTracingHandler.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + +import io.netty.channel.CombinedChannelDuplexHandler; + +public class HttpClientTracingHandler + extends CombinedChannelDuplexHandler< + HttpClientResponseTracingHandler, HttpClientRequestTracingHandler> { + + public HttpClientTracingHandler() { + super(new HttpClientResponseTracingHandler(), new HttpClientRequestTracingHandler()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyHttpClientTracer.java new file mode 100644 index 000000000..994f361a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyHttpClientTracer.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + +import static io.netty.handler.codec.http.HttpHeaders.Names.HOST; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.javaagent.instrumentation.netty.common.client.NettyResponseInjectAdapter.SETTER; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_UDP; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramChannel; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponse; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class NettyHttpClientTracer + extends HttpClientTracer { + private static final NettyHttpClientTracer TRACER = new NettyHttpClientTracer(); + + private NettyHttpClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static NettyHttpClientTracer tracer() { + return TRACER; + } + + public Context startSpan( + Context parentContext, ChannelHandlerContext ctx, NettyRequestWrapper request) { + SpanBuilder spanBuilder = spanBuilder(parentContext, spanNameForRequest(request), CLIENT); + onRequest(spanBuilder, request); + NetPeerAttributes.INSTANCE.setNetPeer( + spanBuilder, (InetSocketAddress) ctx.channel().remoteAddress()); + + Context context = withClientSpan(parentContext, spanBuilder.startSpan()); + inject(context, request.headers(), SETTER); + return context; + } + + public void connectionFailure(Context parentContext, Channel channel, Throwable throwable) { + SpanBuilder spanBuilder = spanBuilder(parentContext, "CONNECT", CLIENT); + spanBuilder.setAttribute( + SemanticAttributes.NET_TRANSPORT, channel instanceof DatagramChannel ? IP_UDP : IP_TCP); + NetPeerAttributes.INSTANCE.setNetPeer(spanBuilder, (InetSocketAddress) channel.remoteAddress()); + + Context context = withClientSpan(parentContext, spanBuilder.startSpan()); + tracer().endExceptionally(context, throwable); + } + + @Override + protected String method(NettyRequestWrapper httpRequest) { + return httpRequest.method().name(); + } + + @Override + @Nullable + protected String flavor(NettyRequestWrapper httpRequest) { + return httpRequest.protocolVersion().text(); + } + + @Override + protected URI url(NettyRequestWrapper request) throws URISyntaxException { + URI uri = new URI(request.uri()); + if ((uri.getHost() == null || uri.getHost().equals("")) && request.headers().contains(HOST)) { + String protocol = request.isHttps() ? "https://" : "http://"; + uri = new URI(protocol + request.headers().get(HOST) + request.uri()); + } + return uri; + } + + @Override + protected Integer status(HttpResponse httpResponse) { + return httpResponse.getStatus().code(); + } + + @Override + protected String requestHeader(NettyRequestWrapper httpRequest, String name) { + return httpRequest.headers().get(name); + } + + @Override + protected String responseHeader(HttpResponse httpResponse, String name) { + return httpResponse.headers().get(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.netty-4.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyRequestWrapper.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyRequestWrapper.java new file mode 100644 index 000000000..17a2ca51c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyRequestWrapper.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; + +public class NettyRequestWrapper { + private static final Class sslHandlerClass = getSslHandlerClass(); + + @SuppressWarnings("unchecked") + private static Class getSslHandlerClass() { + try { + return (Class) + Class.forName( + "io.netty.handler.ssl.SslHandler", + false, + HttpClientRequestTracingHandler.class.getClassLoader()); + } catch (ClassNotFoundException exception) { + return null; + } + } + + private final HttpRequest request; + private final ChannelHandlerContext ctx; + + public NettyRequestWrapper(HttpRequest request, ChannelHandlerContext ctx) { + this.request = request; + this.ctx = ctx; + } + + public HttpRequest request() { + return request; + } + + public boolean isHttps() { + return sslHandlerClass != null && ctx.pipeline().get(sslHandlerClass) != null; + } + + public HttpHeaders headers() { + return request.headers(); + } + + public HttpVersion protocolVersion() { + return request.getProtocolVersion(); + } + + public String uri() { + return request.getUri(); + } + + public HttpMethod method() { + return request().getMethod(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerRequestTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerRequestTracingHandler.java new file mode 100644 index 000000000..8c939a96d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerRequestTracingHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.server; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.NettyHttpServerTracer.tracer; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.HttpRequest; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class HttpServerRequestTracingHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + Channel channel = ctx.channel(); + + if (!(msg instanceof HttpRequest)) { + Context serverContext = tracer().getServerContext(channel); + if (serverContext == null) { + ctx.fireChannelRead(msg); + } else { + try (Scope ignored = serverContext.makeCurrent()) { + ctx.fireChannelRead(msg); + } + } + return; + } + + HttpRequest request = (HttpRequest) msg; + Context context = tracer().startSpan(request, channel, channel, "HTTP " + request.getMethod()); + try (Scope ignored = context.makeCurrent()) { + ctx.fireChannelRead(msg); + // the span is ended normally in HttpServerResponseTracingHandler + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerResponseTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerResponseTracingHandler.java new file mode 100644 index 000000000..4914cb079 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerResponseTracingHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.server; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.NettyHttpServerTracer.tracer; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class HttpServerResponseTracingHandler extends ChannelOutboundHandlerAdapter { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) { + Context context = tracer().getServerContext(ctx.channel()); + if (context == null || !(msg instanceof HttpResponse)) { + ctx.write(msg, prm); + return; + } + + try (Scope ignored = context.makeCurrent()) { + ctx.write(msg, prm); + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + tracer().end(context, (HttpResponse) msg); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerTracingHandler.java new file mode 100644 index 000000000..b77be5590 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/HttpServerTracingHandler.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.server; + +import io.netty.channel.CombinedChannelDuplexHandler; + +public class HttpServerTracingHandler + extends CombinedChannelDuplexHandler< + HttpServerRequestTracingHandler, HttpServerResponseTracingHandler> { + + public HttpServerTracingHandler() { + super(new HttpServerRequestTracingHandler(), new HttpServerResponseTracingHandler()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/NettyHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/NettyHttpServerTracer.java new file mode 100644 index 000000000..479d05956 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/NettyHttpServerTracer.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.server; + +import static io.netty.handler.codec.http.HttpHeaders.Names.HOST; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import io.opentelemetry.javaagent.instrumentation.netty.common.server.NettyRequestExtractAdapter; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.AttributeKeys; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public class NettyHttpServerTracer + extends HttpServerTracer { + private static final NettyHttpServerTracer TRACER = new NettyHttpServerTracer(); + + public static NettyHttpServerTracer tracer() { + return TRACER; + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.getMethod().name(); + } + + @Override + protected String requestHeader(HttpRequest httpRequest, String name) { + return httpRequest.headers().get(name); + } + + @Override + protected int responseStatus(HttpResponse httpResponse) { + return httpResponse.getStatus().code(); + } + + @Override + protected String bussinessStatus(HttpResponse httpResponse) { + return null; + } + + @Override + protected String bussinessMessage(HttpResponse httpResponse) { + return null; + } + + @Override + protected void attachServerContext(Context context, Channel channel) { + channel.attr(AttributeKeys.SERVER_SPAN).set(context); + } + + @Override + public Context getServerContext(Channel channel) { + return channel.attr(AttributeKeys.SERVER_SPAN).get(); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.netty-4.0"; + } + + @Override + protected TextMapGetter getGetter() { + return NettyRequestExtractAdapter.GETTER; + } + + @Override + protected String url(HttpRequest request) { + String uri = request.getUri(); + if (isRelativeUrl(uri) && request.headers().contains(HOST)) { + return "http://" + request.headers().get(HOST) + request.getUri(); + } else { + return uri; + } + } + + @Override + protected String peerHostIP(Channel channel) { + SocketAddress socketAddress = channel.remoteAddress(); + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getAddress().getHostAddress(); + } + return null; + } + + @Override + protected String flavor(Channel channel, HttpRequest request) { + return request.getProtocolVersion().toString(); + } + + @Override + protected Integer peerPort(Channel channel) { + SocketAddress socketAddress = channel.remoteAddress(); + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getPort(); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelFutureTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelFutureTest.groovy new file mode 100644 index 000000000..4d611fdda --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelFutureTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.channel.ChannelHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.util.concurrent.Future +import io.netty.util.concurrent.GenericFutureListener +import io.netty.util.concurrent.GenericProgressiveFutureListener +import io.netty.util.concurrent.ProgressiveFuture +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class ChannelFutureTest extends AgentInstrumentationSpecification { + // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2705 + def "should clean up wrapped listeners"() { + given: + def channel = new EmbeddedChannel(new EmptyChannelHandler()) + def counter = new AtomicInteger() + + def listener1 = newListener(counter) + channel.closeFuture().addListener(listener1) + channel.closeFuture().removeListener(listener1) + + def listener2 = newListener(counter) + def listener3 = newProgressiveListener(counter) + channel.closeFuture().addListeners(listener2, listener3) + channel.closeFuture().removeListeners(listener2, listener3) + + when: + channel.close().await(5, TimeUnit.SECONDS) + + then: + counter.get() == 0 + } + + private static GenericFutureListener newListener(AtomicInteger counter) { + new GenericFutureListener() { + void operationComplete(Future future) throws Exception { + counter.incrementAndGet() + } + } + } + + private static GenericFutureListener newProgressiveListener(AtomicInteger counter) { + new GenericProgressiveFutureListener() { + void operationProgressed(ProgressiveFuture future, long progress, long total) throws Exception { + counter.incrementAndGet() + } + + void operationComplete(Future future) throws Exception { + counter.incrementAndGet() + } + } + } + + private static class EmptyChannelHandler implements ChannelHandler { + @Override + void handlerAdded(ChannelHandlerContext ctx) throws Exception { + } + + @Override + void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + } + + @Override + void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelPipelineTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelPipelineTest.groovy new file mode 100644 index 000000000..ed6b36e35 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelPipelineTest.groovy @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.channel.ChannelHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.DefaultChannelPipeline +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.http.HttpClientCodec +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.HttpClientTracingHandler +import spock.lang.Unroll + +class ChannelPipelineTest extends AgentInstrumentationSpecification { + // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1373 + @Unroll + def "test remove our handler #testName"() { + setup: + def channel = new EmbeddedChannel(new EmptyChannelHandler()) + def channelPipeline = new DefaultChannelPipeline(channel) + def handler = new HttpClientCodec() + + when: + // no handlers + channelPipeline.first() == null + channelPipeline.last() == null + + then: + // add handler + channelPipeline.addLast("http", handler) + channelPipeline.first() == handler + // our handler was also added + channelPipeline.last().getClass() == HttpClientTracingHandler + + and: + removeMethod.call(channelPipeline, handler) + // removing handler also removes our handler + channelPipeline.first() == null || "io.netty.channel.DefaultChannelPipeline\$TailHandler" == channelPipeline.first().getClass().getName() + channelPipeline.last() == null + + where: + testName | removeMethod + "by instance" | { pipeline, h -> pipeline.remove(h) } + "by class" | { pipeline, h -> pipeline.remove(h.getClass()) } + "by name" | { pipeline, h -> pipeline.remove("http") } + } + + private static class EmptyChannelHandler implements ChannelHandler { + @Override + void handlerAdded(ChannelHandlerContext ctx) throws Exception { + } + + @Override + void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + } + + @Override + void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ClientHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ClientHandler.java new file mode 100644 index 000000000..c96c9697d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ClientHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +/* +Bridges from async Netty world to the sync world of our http client tests. +When request initiated by a test gets a response, calls a given callback and completes given +future with response's status code. +*/ +public class ClientHandler extends SimpleChannelInboundHandler { + private final CompletableFuture responseCode; + + public ClientHandler(CompletableFuture responseCode) { + this.responseCode = responseCode; + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof HttpResponse) { + ctx.pipeline().remove(this); + + HttpResponse response = (HttpResponse) msg; + responseCode.complete(response.getStatus().code()); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + responseCode.completeExceptionally(cause); + ctx.close(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy new file mode 100644 index 000000000..7fca4ea22 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.bootstrap.Bootstrap +import io.netty.buffer.Unpooled +import io.netty.channel.Channel +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.ChannelPipeline +import io.netty.channel.EventLoopGroup +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.http.DefaultFullHttpRequest +import io.netty.handler.codec.http.HttpClientCodec +import io.netty.handler.codec.http.HttpHeaders +import io.netty.handler.codec.http.HttpMethod +import io.netty.handler.codec.http.HttpVersion +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import spock.lang.Shared + +class Netty40ClientTest extends HttpClientTest implements AgentTestTrait { + + @Shared + private EventLoopGroup eventLoopGroup = new NioEventLoopGroup() + + @Shared + private Bootstrap bootstrap + + def setupSpec() { + bootstrap = new Bootstrap() + bootstrap.group(eventLoopGroup) + .channel(NioSocketChannel) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline() + pipeline.addLast(new HttpClientCodec()) + } + }) + } + + def cleanupSpec() { + eventLoopGroup?.shutdownGracefully() + } + + @Override + DefaultFullHttpRequest buildRequest(String method, URI uri, Map headers) { + def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), uri.toString(), Unpooled.EMPTY_BUFFER) + HttpHeaders.setHost(request, uri.host) + request.headers().set("user-agent", userAgent()) + headers.each { k, v -> request.headers().set(k, v) } + return request + } + + @Override + int sendRequest(DefaultFullHttpRequest request, String method, URI uri, Map headers) { + def channel = bootstrap.connect(uri.host, getPort(uri)).sync().channel() + def result = new CompletableFuture() + channel.pipeline().addLast(new ClientHandler(result)) + channel.writeAndFlush(request).get() + return result.get(20, TimeUnit.SECONDS) + } + + @Override + void sendRequestWithCallback(DefaultFullHttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + Channel ch + try { + ch = bootstrap.connect(uri.host, getPort(uri)).sync().channel() + } catch (Exception exception) { + requestResult.complete(exception) + return + } + def result = new CompletableFuture() + result.whenComplete { status, throwable -> + requestResult.complete({ status }, throwable) + } + ch.pipeline().addLast(new ClientHandler(result)) + ch.writeAndFlush(request) + } + + @Override + String expectedClientSpanName(URI uri, String method) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return "CONNECT" + default: + return super.expectedClientSpanName(uri, method) + } + } + + @Override + Set> httpAttributes(URI uri) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return [] + } + return super.httpAttributes(uri) + } + + @Override + String userAgent() { + return "Netty" + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testHttps() { + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ServerTest.groovy new file mode 100644 index 000000000..ec16802dc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ServerTest.groovy @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH +import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1 +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.netty.bootstrap.ServerBootstrap +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelPipeline +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.nio.NioServerSocketChannel +import io.netty.handler.codec.http.DefaultFullHttpResponse +import io.netty.handler.codec.http.FullHttpResponse +import io.netty.handler.codec.http.HttpHeaders +import io.netty.handler.codec.http.HttpRequest +import io.netty.handler.codec.http.HttpRequestDecoder +import io.netty.handler.codec.http.HttpResponseEncoder +import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.handler.codec.http.QueryStringDecoder +import io.netty.handler.logging.LogLevel +import io.netty.handler.logging.LoggingHandler +import io.netty.util.CharsetUtil +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest + +class Netty40ServerTest extends HttpServerTest implements AgentTestTrait { + + static final LoggingHandler LOGGING_HANDLER = new LoggingHandler(SERVER_LOGGER.name, LogLevel.DEBUG) + + @Override + EventLoopGroup startServer(int port) { + def eventLoopGroup = new NioEventLoopGroup() + + ServerBootstrap bootstrap = new ServerBootstrap() + .group(eventLoopGroup) + .handler(LOGGING_HANDLER) + .childHandler([ + initChannel: { ch -> + ChannelPipeline pipeline = ch.pipeline() + pipeline.addFirst("logger", LOGGING_HANDLER) + + def handlers = [new HttpRequestDecoder(), new HttpResponseEncoder()] + handlers.each { pipeline.addLast(it) } + pipeline.addLast([ + channelRead0 : { ctx, msg -> + if (msg instanceof HttpRequest) { + def uri = URI.create((msg as HttpRequest).uri) + ServerEndpoint endpoint = ServerEndpoint.forPath(uri.path) + ctx.write controller(endpoint) { + ByteBuf content = null + FullHttpResponse response + switch (endpoint) { + case SUCCESS: + case ERROR: + content = Unpooled.copiedBuffer(endpoint.body, CharsetUtil.UTF_8) + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) + break + case INDEXED_CHILD: + content = Unpooled.EMPTY_BUFFER + endpoint.collectSpanAttributes { new QueryStringDecoder(uri).parameters().get(it).find() } + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) + break + case QUERY_PARAM: + content = Unpooled.copiedBuffer(uri.query, CharsetUtil.UTF_8) + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) + break + case REDIRECT: + content = Unpooled.EMPTY_BUFFER + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) + response.headers().set(HttpHeaders.Names.LOCATION, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + default: + content = Unpooled.copiedBuffer(NOT_FOUND.body, CharsetUtil.UTF_8) + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(NOT_FOUND.status), content) + break + } + response.headers().set(CONTENT_TYPE, "text/plain") + if (content) { + response.headers().set(CONTENT_LENGTH, content.readableBytes()) + } + return response + } + } + }, + exceptionCaught : { ChannelHandlerContext ctx, Throwable cause -> + ByteBuf content = Unpooled.copiedBuffer(cause.message, CharsetUtil.UTF_8) + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR, content) + response.headers().set(CONTENT_TYPE, "text/plain") + response.headers().set(CONTENT_LENGTH, content.readableBytes()) + ctx.write(response) + }, + channelReadComplete: { it.flush() } + ] as SimpleChannelInboundHandler) + } + ] as ChannelInitializer).channel(NioServerSocketChannel) + bootstrap.bind(port).sync() + + return eventLoopGroup + } + + @Override + void stopServer(EventLoopGroup server) { + server?.shutdownGracefully() + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "HTTP GET" + } + + @Override + boolean testConcurrency() { + return true + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/netty-4.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/netty-4.1-javaagent.gradle new file mode 100644 index 000000000..9a2cc5b3d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/netty-4.1-javaagent.gradle @@ -0,0 +1,57 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.netty" + module = "netty-codec-http" + versions = "[4.1.0.Final,5.0.0)" + assertInverse = true + } + pass { + group = "io.netty" + module = "netty-all" + versions = "[4.1.0.Final,5.0.0)" + excludeDependency 'io.netty:netty-tcnative' + assertInverse = true + } + fail { + group = "io.netty" + module = "netty" + versions = "[,]" + } +} + +dependencies { + library "io.netty:netty-codec-http:4.1.0.Final" + api project(':instrumentation:netty:netty-4.1:library') + implementation project(':instrumentation:netty:netty-4-common:javaagent') + + //Contains logging handler + testLibrary "io.netty:netty-handler:4.1.0.Final" + testLibrary "io.netty:netty-transport-native-epoll:4.1.0.Final:linux-x86_64" + + // first version with kqueue, add it only as a compile time dependency + testCompileOnly "io.netty:netty-transport-native-kqueue:4.1.11.Final:osx-x86_64" + + latestDepTestLibrary enforcedPlatform("io.netty:netty-bom:(,5.0)") + latestDepTestLibrary "io.netty:netty-codec-http:(,5.0)" + latestDepTestLibrary "io.netty:netty-handler:(,5.0)" + latestDepTestLibrary "io.netty:netty-transport-native-epoll:(,5.0):linux-x86_64" + latestDepTestLibrary "io.netty:netty-transport-native-kqueue:(,5.0):osx-x86_64" +} + +test { + systemProperty "testLatestDeps", testLatestDeps +} + +if (!testLatestDeps) { + // No BOM for 4.1.0 so we can't use enforcedPlatform to override our transitive version + // management, so hook into the resolutionStrategy. + configurations.each { + it.resolutionStrategy.eachDependency { + if (it.requested.group == "io.netty" && it.requested.name != "netty-bom" && !it.requested.name.startsWith("netty-transport-native")) { + it.useVersion("4.1.0.Final") + } + } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/AbstractChannelHandlerContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/AbstractChannelHandlerContextInstrumentation.java new file mode 100644 index 000000000..a95d3a640 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/AbstractChannelHandlerContextInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.server.NettyHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AbstractChannelHandlerContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.netty.channel.AbstractChannelHandlerContext"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("invokeExceptionCaught")) + .and(takesArgument(0, named(Throwable.class.getName()))), + AbstractChannelHandlerContextInstrumentation.class.getName() + + "$InvokeExceptionCaughtAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeExceptionCaughtAdvice { + + @Advice.OnMethodEnter + public static void onEnter(@Advice.Argument(0) Throwable throwable) { + if (throwable != null) { + tracer().onException(Java8BytecodeBridge.currentContext(), throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/ChannelFutureListenerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/ChannelFutureListenerInstrumentation.java new file mode 100644 index 000000000..9fd04e6e5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/ChannelFutureListenerInstrumentation.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.NettyHttpClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.netty.channel.ChannelFuture; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ChannelFutureListenerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.netty.channel.ChannelFutureListener"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.netty.channel.ChannelFutureListener")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("operationComplete")) + .and(takesArgument(0, named("io.netty.channel.ChannelFuture"))), + ChannelFutureListenerInstrumentation.class.getName() + "$OperationCompleteAdvice"); + } + + @SuppressWarnings("unused") + public static class OperationCompleteAdvice { + + @Advice.OnMethodEnter + public static Scope activateScope(@Advice.Argument(0) ChannelFuture future) { + /* + Idea here is: + - To return scope only if we have captured it. + - To capture scope only in case of error. + */ + Context parentContext = future.channel().attr(AttributeKeys.CONNECT_CONTEXT).getAndRemove(); + if (parentContext == null) { + return null; + } + Throwable cause = future.cause(); + if (cause != null) { + // 异常记录 + Scope parentScope = parentContext.makeCurrent(); + if (tracer().shouldStartSpan(parentContext, SpanKind.CLIENT)) { + tracer().connectionFailure(parentContext, future.channel(), cause); + } + return parentScope; + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void deactivateScope(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/ChannelInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/ChannelInstrumentation.java new file mode 100644 index 000000000..bb4b03b59 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/ChannelInstrumentation.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.netty.channel.Channel; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This instrumentation preserves the context that was active during call to any "write" operation + * on Netty Channel in that channel's attribute. This context is later used by our various tracing + * handlers to scope the work. + */ +public class ChannelInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.netty.channel.Channel"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.netty.channel.Channel")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(namedOneOf("write", "writeAndFlush")), + ChannelInstrumentation.class.getName() + "$AttachContextAdvice"); + } + + @SuppressWarnings("unused") + public static class AttachContextAdvice { + + @Advice.OnMethodEnter + public static void attachContext(@Advice.This Channel channel) { + channel + .attr(AttributeKeys.WRITE_CONTEXT) + .compareAndSet(null, Java8BytecodeBridge.currentContext()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyChannelPipelineInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyChannelPipelineInstrumentation.java new file mode 100644 index 000000000..30be5ed07 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyChannelPipelineInstrumentation.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpRequestEncoder; +import io.netty.handler.codec.http.HttpResponseDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.util.Attribute; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.netty.common.AbstractNettyChannelPipelineInstrumentation; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientRequestTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientResponseTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.server.HttpServerRequestTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.server.HttpServerResponseTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.server.HttpServerTracingHandler; +import net.bytebuddy.asm.Advice; + +public class NettyChannelPipelineInstrumentation + extends AbstractNettyChannelPipelineInstrumentation { + + @Override + public void transform(TypeTransformer transformer) { + super.transform(transformer); + + transformer.applyAdviceToMethod( + isMethod() + .and(nameStartsWith("add")) + .and(takesArgument(1, String.class)) + .and(takesArgument(2, named("io.netty.channel.ChannelHandler"))), + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineAddAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("connect")).and(returns(named("io.netty.channel.ChannelFuture"))), + NettyChannelPipelineInstrumentation.class.getName() + "$ChannelPipelineConnectAdvice"); + } + + /** + * When certain handlers are added to the pipeline, we want to add our corresponding tracing + * handlers. If those handlers are later removed, we also remove our handlers. Support for + * replacing handlers and removeFirst/removeLast is currently not implemented. + */ + @SuppressWarnings("unused") + public static class ChannelPipelineAddAdvice { + + @Advice.OnMethodEnter + public static int trackCallDepth(@Advice.Argument(2) ChannelHandler handler) { + // Previously we used one unique call depth tracker for all handlers, using + // ChannelPipeline.class as a key. + // The problem with this approach is that it does not work with netty's + // io.netty.channel.ChannelInitializer which provides an `initChannel` that can be used to + // `addLast` other handlers. In that case the depth would exceed 0 and handlers added from + // initializers would not be considered. + // Using the specific handler key instead of the generic ChannelPipeline.class will help us + // both to handle such cases and avoid adding our additional handlers in case of internal + // calls of `addLast` to other method overloads with a compatible signature. + return CallDepthThreadLocalMap.incrementCallDepth(handler.getClass()); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void addHandler( + @Advice.Enter int callDepth, + @Advice.This ChannelPipeline pipeline, + @Advice.Argument(1) String handlerName, + @Advice.Argument(2) ChannelHandler handler) { + if (callDepth > 0) { + return; + } + CallDepthThreadLocalMap.reset(handler.getClass()); + + String name = handlerName; + if (name == null) { + ChannelHandlerContext context = pipeline.context(handler); + if (context == null) { + // probably a ChannelInitializer that was used and removed + // see the comment above in @Advice.OnMethodEnter + return; + } + name = context.name(); + } + + ChannelHandler ourHandler = null; + // Server pipeline handlers + if (handler instanceof HttpServerCodec) { + ourHandler = new HttpServerTracingHandler(); + } else if (handler instanceof HttpRequestDecoder) { + ourHandler = new HttpServerRequestTracingHandler(); + } else if (handler instanceof HttpResponseEncoder) { + ourHandler = new HttpServerResponseTracingHandler(); + // Client pipeline handlers + } else if (handler instanceof HttpClientCodec) { + ourHandler = new HttpClientTracingHandler(); + } else if (handler instanceof HttpRequestEncoder) { + ourHandler = new HttpClientRequestTracingHandler(); + } else if (handler instanceof HttpResponseDecoder) { + ourHandler = new HttpClientResponseTracingHandler(); + } + + if (ourHandler != null) { + try { + pipeline.addAfter(name, ourHandler.getClass().getName(), ourHandler); + // associate our handle with original handler so they could be removed together + InstrumentationContext.get(ChannelHandler.class, ChannelHandler.class) + .putIfAbsent(handler, ourHandler); + } catch (IllegalArgumentException e) { + // Prevented adding duplicate handlers. + } + } + } + } + + @SuppressWarnings("unused") + public static class ChannelPipelineConnectAdvice { + + @Advice.OnMethodEnter + public static void addParentSpan(@Advice.This ChannelPipeline pipeline) { + Attribute attribute = pipeline.channel().attr(AttributeKeys.CONNECT_CONTEXT); + attribute.compareAndSet(null, Java8BytecodeBridge.currentContext()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyInstrumentationModule.java new file mode 100644 index 000000000..17807e2c9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.netty.common.NettyFutureInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class NettyInstrumentationModule extends InstrumentationModule { + public NettyInstrumentationModule() { + super("netty", "netty-4.1"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // Class added in 4.1.0 and not in 4.0.56 to avoid resolving this instrumentation completely + // when using 4.0. + return hasClassesNamed("io.netty.handler.codec.http.CombinedHttpHeaders"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ChannelInstrumentation(), + new NettyFutureInstrumentation(), + new ChannelFutureListenerInstrumentation(), + new NettyChannelPipelineInstrumentation(), + new AbstractChannelHandlerContextInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientRequestTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientRequestTracingHandler.java new file mode 100644 index 000000000..b5dd85002 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientRequestTracingHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.client; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.NettyHttpClientTracer.tracer; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.Attribute; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; + +public class HttpClientRequestTracingHandler extends ChannelOutboundHandlerAdapter { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) { + if (!(msg instanceof HttpRequest)) { + ctx.write(msg, prm); + return; + } + + Context parentContext = ctx.channel().attr(AttributeKeys.WRITE_CONTEXT).getAndRemove(); + if (parentContext == null) { + parentContext = Context.current(); + } + + if (!tracer().shouldStartSpan(parentContext, (HttpRequest) msg)) { + ctx.write(msg, prm); + return; + } + + NettyRequestWrapper requestWrapper = new NettyRequestWrapper((HttpRequest) msg, ctx); + Context context = tracer().startSpan(parentContext, ctx, requestWrapper); + + Attribute clientContextAttr = ctx.channel().attr(AttributeKeys.CLIENT_CONTEXT); + Attribute parentContextAttr = ctx.channel().attr(AttributeKeys.CLIENT_PARENT_CONTEXT); + clientContextAttr.set(context); + parentContextAttr.set(parentContext); + + try (Scope ignored = context.makeCurrent()) { + ctx.write(msg, prm); + // span is ended normally in HttpClientResponseTracingHandler + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + clientContextAttr.remove(); + parentContextAttr.remove(); + throw throwable; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientResponseTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientResponseTracingHandler.java new file mode 100644 index 000000000..d416d71e2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientResponseTracingHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.client; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.NettyHttpClientTracer.tracer; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; + +public class HttpClientResponseTracingHandler extends ChannelInboundHandlerAdapter { + + private static final AttributeKey HTTP_RESPONSE = + AttributeKey.valueOf(HttpClientResponseTracingHandler.class, "http-response"); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + Attribute clientContextAttr = ctx.channel().attr(AttributeKeys.CLIENT_CONTEXT); + Context context = clientContextAttr.get(); + if (context == null) { + ctx.fireChannelRead(msg); + return; + } + + Attribute parentContextAttr = ctx.channel().attr(AttributeKeys.CLIENT_PARENT_CONTEXT); + Context parentContext = parentContextAttr.get(); + + if (msg instanceof FullHttpResponse) { + tracer().end(context, (HttpResponse) msg); + clientContextAttr.remove(); + parentContextAttr.remove(); + } else if (msg instanceof HttpResponse) { + // Headers before body have been received, store them to use when finishing the span. + ctx.channel().attr(HTTP_RESPONSE).set((HttpResponse) msg); + } else if (msg instanceof LastHttpContent) { + // Not a FullHttpResponse so this is content that has been received after headers. Finish the + // span using what we stored in attrs. + tracer().end(context, ctx.channel().attr(HTTP_RESPONSE).getAndRemove()); + clientContextAttr.remove(); + parentContextAttr.remove(); + } + + // We want the callback in the scope of the parent, not the client span + if (parentContext != null) { + try (Scope ignored = parentContext.makeCurrent()) { + ctx.fireChannelRead(msg); + } + } else { + ctx.fireChannelRead(msg); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientTracingHandler.java new file mode 100644 index 000000000..d549aa823 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/HttpClientTracingHandler.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.client; + +import io.netty.channel.CombinedChannelDuplexHandler; + +public class HttpClientTracingHandler + extends CombinedChannelDuplexHandler< + HttpClientResponseTracingHandler, HttpClientRequestTracingHandler> { + + public HttpClientTracingHandler() { + super(new HttpClientResponseTracingHandler(), new HttpClientRequestTracingHandler()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/NettyHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/NettyHttpClientTracer.java new file mode 100644 index 000000000..c08c9078a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/NettyHttpClientTracer.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.client; + +import static io.netty.handler.codec.http.HttpHeaderNames.HOST; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.javaagent.instrumentation.netty.common.client.NettyResponseInjectAdapter.SETTER; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_UDP; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramChannel; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class NettyHttpClientTracer + extends HttpClientTracer { + private static final NettyHttpClientTracer TRACER = new NettyHttpClientTracer(); + + private NettyHttpClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static NettyHttpClientTracer tracer() { + return TRACER; + } + + public Context startSpan( + Context parentContext, ChannelHandlerContext ctx, NettyRequestWrapper request) { + SpanBuilder spanBuilder = spanBuilder(parentContext, spanNameForRequest(request), CLIENT); + onRequest(spanBuilder, request); + NetPeerAttributes.INSTANCE.setNetPeer( + spanBuilder, (InetSocketAddress) ctx.channel().remoteAddress()); + + Context context = withClientSpan(parentContext, spanBuilder.startSpan()); + inject(context, request.headers(), SETTER); + return context; + } + + public void connectionFailure(Context parentContext, Channel channel, Throwable throwable) { + SpanBuilder spanBuilder = spanBuilder(parentContext, "CONNECT", CLIENT); + spanBuilder.setAttribute( + SemanticAttributes.NET_TRANSPORT, channel instanceof DatagramChannel ? IP_UDP : IP_TCP); + NetPeerAttributes.INSTANCE.setNetPeer(spanBuilder, (InetSocketAddress) channel.remoteAddress()); + + Context context = withClientSpan(parentContext, spanBuilder.startSpan()); + tracer().endExceptionally(context, throwable); + } + + @Override + protected String method(NettyRequestWrapper httpRequest) { + return httpRequest.method().name(); + } + + @Override + @Nullable + protected String flavor(NettyRequestWrapper httpRequest) { + return httpRequest.protocolVersion().text(); + } + + @Override + protected URI url(NettyRequestWrapper request) throws URISyntaxException { + URI uri = new URI(request.uri()); + if ((uri.getHost() == null || uri.getHost().equals("")) && request.headers().contains(HOST)) { + String protocol = request.isHttps() ? "https://" : "http://"; + uri = new URI(protocol + request.headers().get(HOST) + request.uri()); + } + return uri; + } + + @Override + protected Integer status(HttpResponse httpResponse) { + return httpResponse.status().code(); + } + + @Override + protected String requestHeader(NettyRequestWrapper httpRequest, String name) { + return httpRequest.headers().get(name); + } + + @Override + protected String responseHeader(HttpResponse httpResponse, String name) { + return httpResponse.headers().get(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + public boolean shouldStartSpan(Context parentContext, HttpRequest request) { + if (!super.shouldStartSpan(parentContext)) { + return false; + } + // The AWS SDK uses Netty for asynchronous clients but constructs a request signature before + // beginning transport. This means we MUST suppress Netty spans we would normally create or + // they will inject their own trace header, which does not match what was present when the + // signature was computed, breaking the SDK request completely. We have not found how to + // cleanly propagate context from the SDK instrumentation, which executes on an application + // thread, to Netty instrumentation, which executes on event loops. If it's possible, it may + // require instrumenting internal classes. Using a header which is more or less guaranteed to + // always exist is arguably more stable. + if (request.headers().contains("amz-sdk-invocation-id")) { + return false; + } + return true; + } + + // TODO (trask) how best to prevent people from use this one instead of the above? + // should all shouldStartSpan methods take REQUEST so that they can be suppressed by REQUEST + // attributes? + @Override + @Deprecated + public boolean shouldStartSpan(Context parentContext) { + throw new UnsupportedOperationException(); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.netty-4.1"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/NettyRequestWrapper.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/NettyRequestWrapper.java new file mode 100644 index 000000000..584d065f4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/client/NettyRequestWrapper.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.client; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; + +public class NettyRequestWrapper { + private static final Class sslHandlerClass = getSslHandlerClass(); + + @SuppressWarnings("unchecked") + private static Class getSslHandlerClass() { + try { + return (Class) + Class.forName( + "io.netty.handler.ssl.SslHandler", + false, + HttpClientRequestTracingHandler.class.getClassLoader()); + } catch (ClassNotFoundException exception) { + return null; + } + } + + private final HttpRequest request; + private final ChannelHandlerContext ctx; + + public NettyRequestWrapper(HttpRequest request, ChannelHandlerContext ctx) { + this.request = request; + this.ctx = ctx; + } + + public HttpRequest request() { + return request; + } + + public boolean isHttps() { + return sslHandlerClass != null && ctx.pipeline().get(sslHandlerClass) != null; + } + + public HttpHeaders headers() { + return request.headers(); + } + + public HttpVersion protocolVersion() { + return request.protocolVersion(); + } + + public String uri() { + return request.uri(); + } + + public HttpMethod method() { + return request().method(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerRequestTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerRequestTracingHandler.java new file mode 100644 index 000000000..19270ae97 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerRequestTracingHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.server; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.server.NettyHttpServerTracer.tracer; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.HttpRequest; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class HttpServerRequestTracingHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + Channel channel = ctx.channel(); + + if (!(msg instanceof HttpRequest)) { + Context serverContext = tracer().getServerContext(channel); + if (serverContext == null) { + ctx.fireChannelRead(msg); + } else { + try (Scope ignored = serverContext.makeCurrent()) { + ctx.fireChannelRead(msg); + } + } + return; + } + + HttpRequest request = (HttpRequest) msg; + Context context = tracer().startSpan(request, channel, channel, "HTTP " + request.method()); + try (Scope ignored = context.makeCurrent()) { + ctx.fireChannelRead(msg); + // the span is ended normally in HttpServerResponseTracingHandler + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerResponseTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerResponseTracingHandler.java new file mode 100644 index 000000000..adf6d7be4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerResponseTracingHandler.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.server; + +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.server.NettyHttpServerTracer.tracer; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.AttributeKey; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class HttpServerResponseTracingHandler extends ChannelOutboundHandlerAdapter { + + private static final AttributeKey HTTP_RESPONSE = + AttributeKey.valueOf(HttpServerResponseTracingHandler.class, "http-response"); + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) { + Context context = tracer().getServerContext(ctx.channel()); + if (context == null) { + ctx.write(msg, prm); + return; + } + + final ChannelPromise writePromise; + + if (msg instanceof LastHttpContent) { + if (prm.isVoid()) { + // Some frameworks don't actually listen for response completion and optimize for + // allocations by using a singleton, unnotifiable promise. Hopefully these frameworks don't + // have observability features or they'd be way off... + writePromise = ctx.newPromise(); + } else { + writePromise = prm; + } + // Going to finish the span after the write of the last content finishes. + if (msg instanceof FullHttpResponse) { + // Headers and body all sent together, we have the response information in the msg. + writePromise.addListener(future -> finish(context, writePromise, (FullHttpResponse) msg)); + } else { + // Body sent after headers. We stored the response information in the context when + // encountering HttpResponse (which was not FullHttpResponse since it's not + // LastHttpContent). + writePromise.addListener( + future -> finish(context, writePromise, ctx.channel().attr(HTTP_RESPONSE).get())); + } + } else { + writePromise = prm; + if (msg instanceof HttpResponse) { + // Headers before body has been sent, store them to use when finishing the span. + ctx.channel().attr(HTTP_RESPONSE).set((HttpResponse) msg); + } + } + + try (Scope ignored = context.makeCurrent()) { + ctx.write(msg, writePromise); + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + } + + private static void finish(Context context, ChannelFuture future, HttpResponse response) { + if (future.isSuccess()) { + tracer().end(context, response); + } else { + tracer().endExceptionally(context, future.cause()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerTracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerTracingHandler.java new file mode 100644 index 000000000..cdb1be0fc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/HttpServerTracingHandler.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.server; + +import io.netty.channel.CombinedChannelDuplexHandler; + +public class HttpServerTracingHandler + extends CombinedChannelDuplexHandler< + HttpServerRequestTracingHandler, HttpServerResponseTracingHandler> { + + public HttpServerTracingHandler() { + super(new HttpServerRequestTracingHandler(), new HttpServerResponseTracingHandler()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/NettyHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/NettyHttpServerTracer.java new file mode 100644 index 000000000..876771e9f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/server/NettyHttpServerTracer.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1.server; + +import static io.netty.handler.codec.http.HttpHeaderNames.HOST; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.netty.common.server.NettyRequestExtractAdapter; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public class NettyHttpServerTracer + extends HttpServerTracer { + private static final NettyHttpServerTracer TRACER = new NettyHttpServerTracer(); + + public static NettyHttpServerTracer tracer() { + return TRACER; + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.method().name(); + } + + @Override + protected String requestHeader(HttpRequest httpRequest, String name) { + return httpRequest.headers().get(name); + } + + @Override + protected int responseStatus(HttpResponse httpResponse) { + return httpResponse.status().code(); + } + + @Override + protected String bussinessStatus(HttpResponse httpResponse) { + return null; + } + + @Override + protected String bussinessMessage(HttpResponse httpResponse) { + return null; + } + + @Override + protected void attachServerContext(Context context, Channel channel) { + channel.attr(AttributeKeys.SERVER_SPAN).set(context); + } + + @Override + public Context getServerContext(Channel channel) { + return channel.attr(AttributeKeys.SERVER_SPAN).get(); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.netty-4.1"; + } + + @Override + protected TextMapGetter getGetter() { + return NettyRequestExtractAdapter.GETTER; + } + + @Override + protected String url(HttpRequest request) { + String uri = request.uri(); + if (isRelativeUrl(uri) && request.headers().contains(HOST)) { + return "http://" + request.headers().get(HOST) + request.uri(); + } else { + return uri; + } + } + + @Override + protected String peerHostIP(Channel channel) { + SocketAddress socketAddress = channel.remoteAddress(); + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getAddress().getHostAddress(); + } + return null; + } + + @Override + protected String flavor(Channel channel, HttpRequest request) { + return request.protocolVersion().toString(); + } + + @Override + protected Integer peerPort(Channel channel) { + SocketAddress socketAddress = channel.remoteAddress(); + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getPort(); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelFutureTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelFutureTest.groovy new file mode 100644 index 000000000..dd4f174bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelFutureTest.groovy @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.util.concurrent.Future +import io.netty.util.concurrent.GenericFutureListener +import io.netty.util.concurrent.GenericProgressiveFutureListener +import io.netty.util.concurrent.ProgressiveFuture +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class ChannelFutureTest extends AgentInstrumentationSpecification { + // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2705 + def "should clean up wrapped listeners"() { + given: + def channel = new EmbeddedChannel() + def counter = new AtomicInteger() + + def listener1 = newListener(counter) + channel.closeFuture().addListener(listener1) + channel.closeFuture().removeListener(listener1) + + def listener2 = newListener(counter) + def listener3 = newProgressiveListener(counter) + channel.closeFuture().addListeners(listener2, listener3) + channel.closeFuture().removeListeners(listener2, listener3) + + when: + channel.close().await(5, TimeUnit.SECONDS) + + then: + counter.get() == 0 + } + + private static GenericFutureListener newListener(AtomicInteger counter) { + new GenericFutureListener() { + void operationComplete(Future future) throws Exception { + counter.incrementAndGet() + } + } + } + + private static GenericFutureListener newProgressiveListener(AtomicInteger counter) { + new GenericProgressiveFutureListener() { + void operationProgressed(ProgressiveFuture future, long progress, long total) throws Exception { + counter.incrementAndGet() + } + + void operationComplete(Future future) throws Exception { + counter.incrementAndGet() + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelPipelineTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelPipelineTest.groovy new file mode 100644 index 000000000..ad0e26303 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelPipelineTest.groovy @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.channel.DefaultChannelPipeline +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.handler.codec.http.HttpClientCodec +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientTracingHandler +import spock.lang.Unroll + +class ChannelPipelineTest extends AgentInstrumentationSpecification { + // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1373 + @Unroll + def "test remove our handler #testName"() { + setup: + def channel = new EmbeddedChannel() + def channelPipeline = new DefaultChannelPipeline(channel) + def handler = new HttpClientCodec() + + when: + // no handlers + channelPipeline.first() == null + + then: + // add handler + channelPipeline.addLast("http", handler) + channelPipeline.first() == handler + // our handler was also added + channelPipeline.last().getClass() == HttpClientTracingHandler + + and: + removeMethod.call(channelPipeline, handler) + // removing handler also removes our handler + channelPipeline.first() == null + + where: + testName | removeMethod + "by instance" | { pipeline, h -> pipeline.remove(h) } + "by class" | { pipeline, h -> pipeline.remove(h.getClass()) } + "by name" | { pipeline, h -> pipeline.remove("http") } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ClientHandler.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ClientHandler.java new file mode 100644 index 000000000..299d90a6d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ClientHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.AttributeKey; +import java.util.concurrent.CompletableFuture; + +/* +Bridges from async Netty world to the sync world of our http client tests. +When request initiated by a test gets a response, calls a given callback and completes given +future with response's status code. +*/ +public class ClientHandler extends SimpleChannelInboundHandler { + + private static final AttributeKey HTTP_RESPONSE = + AttributeKey.valueOf(ClientHandler.class, "http-response"); + + private final CompletableFuture responseCode; + + public ClientHandler(CompletableFuture responseCode) { + this.responseCode = responseCode; + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof FullHttpResponse) { + ctx.pipeline().remove(this); + FullHttpResponse response = (FullHttpResponse) msg; + responseCode.complete(response.getStatus().code()); + } else if (msg instanceof HttpResponse) { + // Headers before body have been received, store them to use when finishing the span. + ctx.channel().attr(HTTP_RESPONSE).set((HttpResponse) msg); + } else if (msg instanceof LastHttpContent) { + ctx.pipeline().remove(this); + responseCode.complete(ctx.channel().attr(HTTP_RESPONSE).get().getStatus().code()); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + responseCode.completeExceptionally(cause); + ctx.close(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientTest.groovy new file mode 100644 index 000000000..a3ab35cbd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientTest.groovy @@ -0,0 +1,359 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static org.junit.Assume.assumeTrue + +import io.netty.bootstrap.Bootstrap +import io.netty.buffer.Unpooled +import io.netty.channel.Channel +import io.netty.channel.ChannelHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.ChannelPipeline +import io.netty.channel.EventLoopGroup +import io.netty.channel.embedded.EmbeddedChannel +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.http.DefaultFullHttpRequest +import io.netty.handler.codec.http.HttpClientCodec +import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpMethod +import io.netty.handler.codec.http.HttpVersion +import io.netty.handler.ssl.SslContext +import io.netty.handler.ssl.SslContextBuilder +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientTracingHandler +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +class Netty41ClientTest extends HttpClientTest implements AgentTestTrait { + + @Shared + private EventLoopGroup eventLoopGroup = buildEventLoopGroup() + + @Shared + private Bootstrap bootstrap = buildBootstrap(false) + + @Shared + private Bootstrap httpsBootstrap = buildBootstrap(true) + + def cleanupSpec() { + eventLoopGroup?.shutdownGracefully() + } + + Bootstrap buildBootstrap(boolean https) { + Bootstrap bootstrap = new Bootstrap() + bootstrap.group(eventLoopGroup) + .channel(getChannelClass()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline() + if (https) { + SslContext sslContext = SslContextBuilder.forClient().build() + pipeline.addLast(sslContext.newHandler(socketChannel.alloc())) + } + pipeline.addLast(new HttpClientCodec()) + } + }) + + return bootstrap + } + + EventLoopGroup buildEventLoopGroup() { + return new NioEventLoopGroup() + } + + Class getChannelClass() { + return NioSocketChannel + } + + Bootstrap getBootstrap(URI uri) { + return uri.getScheme() == "https" ? httpsBootstrap : bootstrap + } + + @Override + DefaultFullHttpRequest buildRequest(String method, URI uri, Map headers) { + def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), uri.toString(), Unpooled.EMPTY_BUFFER) + request.headers().set(HttpHeaderNames.HOST, uri.host) + headers.each { k, v -> request.headers().set(k, v) } + return request + } + + @Override + int sendRequest(DefaultFullHttpRequest request, String method, URI uri, Map headers) { + def channel = getBootstrap(uri).connect(uri.host, getPort(uri)).sync().channel() + def result = new CompletableFuture() + channel.pipeline().addLast(new ClientHandler(result)) + channel.writeAndFlush(request).get() + return result.get(20, TimeUnit.SECONDS) + } + + @Override + void sendRequestWithCallback(DefaultFullHttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + Channel ch + try { + ch = getBootstrap(uri).connect(uri.host, getPort(uri)).sync().channel() + } catch (Exception exception) { + requestResult.complete(exception) + return + } + def result = new CompletableFuture() + result.whenComplete { status, throwable -> + requestResult.complete({ status }, throwable) + } + ch.pipeline().addLast(new ClientHandler(result)) + ch.writeAndFlush(request) + } + + @Override + String expectedClientSpanName(URI uri, String method) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return "CONNECT" + default: + return super.expectedClientSpanName(uri, method) + } + } + + @Override + Set> httpAttributes(URI uri) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return [] + } + return super.httpAttributes(uri) + } + + @Override + boolean testRedirects() { + false + } + + def "test connection reuse and second request with lazy execute"() { + setup: + //Create a simple Netty pipeline + EventLoopGroup group = new NioEventLoopGroup() + Bootstrap b = new Bootstrap() + b.group(group) + .channel(NioSocketChannel) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline() + pipeline.addLast(new HttpClientCodec()) + } + }) + def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, resolveAddress("/success").toString(), Unpooled.EMPTY_BUFFER) + request.headers().set(HttpHeaderNames.HOST, "localhost") + Channel ch = null + + when: + // note that this is a purely asynchronous request + runUnderTrace("parent1") { + ch = b.connect("localhost", server.httpPort()).sync().channel() + ch.write(request) + ch.flush() + } + // This is a cheap/easy way to block/ensure that the first request has finished and check reported spans midway through + // the complex sequence of events + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent1") + clientSpan(it, 1, span(0)) + serverSpan(it, 2, span(1)) + } + } + + then: + // now run a second request through the same channel + runUnderTrace("parent2") { + ch.write(request) + ch.flush() + } + + assertTraces(2) { + trace(0, 3) { + basicSpan(it, 0, "parent1") + clientSpan(it, 1, span(0)) + serverSpan(it, 2, span(1)) + } + trace(1, 3) { + basicSpan(it, 0, "parent2") + clientSpan(it, 1, span(0)) + serverSpan(it, 2, span(1)) + } + } + + + cleanup: + group.shutdownGracefully() + } + + def "when a handler is added to the netty pipeline we add our tracing handler"() { + setup: + def channel = new EmbeddedChannel() + def pipeline = channel.pipeline() + + when: + pipeline.addLast("name", new HttpClientCodec()) + + then: + // The first one returns the removed tracing handler + pipeline.remove(HttpClientTracingHandler.getName()) != null + } + + def "when a handler is added to the netty pipeline we add ONLY ONE tracing handler"() { + setup: + def channel = new EmbeddedChannel() + def pipeline = channel.pipeline() + + when: + pipeline.addLast("name", new HttpClientCodec()) + // The first one returns the removed tracing handler + pipeline.remove(HttpClientTracingHandler.getName()) + // There is only one + pipeline.remove(HttpClientTracingHandler.getName()) == null + + then: + thrown NoSuchElementException + } + + def "handlers of different types can be added"() { + setup: + def channel = new EmbeddedChannel() + def pipeline = channel.pipeline() + + when: + pipeline.addLast("some_handler", new SimpleHandler()) + pipeline.addLast("a_traced_handler", new HttpClientCodec()) + + then: + // The first one returns the removed tracing handler + null != pipeline.remove(HttpClientTracingHandler.getName()) + null != pipeline.remove("some_handler") + null != pipeline.remove("a_traced_handler") + } + + def "calling pipeline.addLast methods that use overloaded methods does not cause infinite loop"() { + setup: + def channel = new EmbeddedChannel() + + when: + channel.pipeline().addLast(new SimpleHandler(), new OtherSimpleHandler()) + + then: + null != channel.pipeline().remove('Netty41ClientTest$SimpleHandler#0') + null != channel.pipeline().remove('Netty41ClientTest$OtherSimpleHandler#0') + } + + def "when a traced handler is added from an initializer we still detect it and add our channel handlers"() { + // This test method replicates a scenario similar to how reactor 0.8.x register the `HttpClientCodec` handler + // into the pipeline. + setup: + assumeTrue(Boolean.getBoolean("testLatestDeps")) + def channel = new EmbeddedChannel() + + when: + channel.pipeline().addLast(new TracedHandlerFromInitializerHandler()) + + then: + null != channel.pipeline().get(HttpClientTracingHandler.getName()) + null != channel.pipeline().remove("added_in_initializer") + null == channel.pipeline().get(HttpClientTracingHandler.getName()) + } + + def "request with trace annotated method #method"() { + given: + def annotatedClass = new TracedClass() + + when: + def responseCode = runUnderTrace("parent") { + annotatedClass.tracedMethod(method) + } + + then: + responseCode == 200 + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "parent") + span(1) { + childOf span(0) + name "tracedMethod" + attributes { + } + } + clientSpan(it, 2, span(1), method) + serverSpan(it, 3, span(2)) + } + } + + where: + method << BODY_METHODS + } + + class TracedClass { + int tracedMethod(String method) { + def uri = resolveAddress("/success") + runUnderTrace("tracedMethod") { + doRequest(method, uri) + } + } + } + + static class SimpleHandler implements ChannelHandler { + @Override + void handlerAdded(ChannelHandlerContext ctx) throws Exception { + } + + @Override + void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + } + + @Override + void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + } + } + + static class OtherSimpleHandler implements ChannelHandler { + @Override + void handlerAdded(ChannelHandlerContext ctx) throws Exception { + } + + @Override + void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + } + + @Override + void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + } + } + + static class TracedHandlerFromInitializerHandler extends ChannelInitializer implements ChannelHandler { + @Override + protected void initChannel(Channel ch) throws Exception { + // This replicates how reactor 0.8.x add the HttpClientCodec + ch.pipeline().addLast("added_in_initializer", new HttpClientCodec()) + } + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + return new SingleNettyConnection(host, port) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41NativeClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41NativeClientTest.groovy new file mode 100644 index 000000000..7ac92ec3e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41NativeClientTest.groovy @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.channel.Channel +import io.netty.channel.EventLoopGroup +import io.netty.channel.epoll.Epoll +import io.netty.channel.epoll.EpollEventLoopGroup +import io.netty.channel.epoll.EpollSocketChannel +import io.netty.channel.kqueue.KQueue +import io.netty.channel.kqueue.KQueueEventLoopGroup +import io.netty.channel.kqueue.KQueueSocketChannel +import org.junit.Assume + +// netty client test with epoll/kqueue native library +class Netty41NativeClientTest extends Netty41ClientTest { + + EventLoopGroup buildEventLoopGroup() { + // linux + if (Epoll.isAvailable()) { + return new EpollEventLoopGroup() + } + // mac + if (KQueueHelper.isAvailable()) { + return new KQueueEventLoopGroup() + } + + // skip test when native library was not found + Assume.assumeTrue("Native library was not found", false) + return super.buildEventLoopGroup() + } + + @Override + Class getChannelClass() { + if (Epoll.isAvailable()) { + return EpollSocketChannel + } + if (KQueueHelper.isAvailable()) { + return KQueueSocketChannel + } + return null + } + + static class KQueueHelper { + static boolean isAvailable() { + try { + return KQueue.isAvailable() + } catch (NoClassDefFoundError error) { + // kqueue is available only in latest dep tests + // in regular tests we only have a compile time dependency because kqueue support was added + // after 4.1.0 + return false + } + } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ServerTest.groovy new file mode 100644 index 000000000..d4bf07873 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ServerTest.groovy @@ -0,0 +1,132 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1 +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.netty.bootstrap.ServerBootstrap +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelPipeline +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.nio.NioServerSocketChannel +import io.netty.handler.codec.http.DefaultFullHttpResponse +import io.netty.handler.codec.http.FullHttpResponse +import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpRequest +import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.handler.codec.http.HttpServerCodec +import io.netty.handler.codec.http.QueryStringDecoder +import io.netty.handler.logging.LogLevel +import io.netty.handler.logging.LoggingHandler +import io.netty.util.CharsetUtil +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest + +class Netty41ServerTest extends HttpServerTest implements AgentTestTrait { + + static final LoggingHandler LOGGING_HANDLER = new LoggingHandler(SERVER_LOGGER.name, LogLevel.DEBUG) + + @Override + EventLoopGroup startServer(int port) { + def eventLoopGroup = new NioEventLoopGroup() + + ServerBootstrap bootstrap = new ServerBootstrap() + .group(eventLoopGroup) + .handler(LOGGING_HANDLER) + .childHandler([ + initChannel: { ch -> + ChannelPipeline pipeline = ch.pipeline() + pipeline.addFirst("logger", LOGGING_HANDLER) + + def handlers = [new HttpServerCodec()] + handlers.each { pipeline.addLast(it) } + pipeline.addLast([ + channelRead0 : { ctx, msg -> + if (msg instanceof HttpRequest) { + def uri = URI.create((msg as HttpRequest).uri()) + ServerEndpoint endpoint = ServerEndpoint.forPath(uri.path) + ctx.write controller(endpoint) { + ByteBuf content = null + FullHttpResponse response + switch (endpoint) { + case SUCCESS: + case ERROR: + content = Unpooled.copiedBuffer(endpoint.body, CharsetUtil.UTF_8) + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) + break + case INDEXED_CHILD: + content = Unpooled.EMPTY_BUFFER + endpoint.collectSpanAttributes { new QueryStringDecoder(uri).parameters().get(it).find() } + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) + break + case QUERY_PARAM: + content = Unpooled.copiedBuffer(uri.query, CharsetUtil.UTF_8) + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) + break + case REDIRECT: + content = Unpooled.EMPTY_BUFFER + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) + response.headers().set(HttpHeaderNames.LOCATION, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + default: + content = Unpooled.copiedBuffer(NOT_FOUND.body, CharsetUtil.UTF_8) + response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(NOT_FOUND.status), content) + break + } + response.headers().set(CONTENT_TYPE, "text/plain") + if (content) { + response.headers().set(CONTENT_LENGTH, content.readableBytes()) + } + return response + } + } + }, + exceptionCaught : { ChannelHandlerContext ctx, Throwable cause -> + ByteBuf content = Unpooled.copiedBuffer(cause.message, CharsetUtil.UTF_8) + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR, content) + response.headers().set(CONTENT_TYPE, "text/plain") + response.headers().set(CONTENT_LENGTH, content.readableBytes()) + ctx.write(response) + }, + channelReadComplete: { it.flush() } + ] as SimpleChannelInboundHandler) + } + ] as ChannelInitializer).channel(NioServerSocketChannel) + bootstrap.bind(port).sync() + + return eventLoopGroup + } + + @Override + void stopServer(EventLoopGroup server) { + server?.shutdownGracefully() + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "HTTP GET" + } + + @Override + boolean testConcurrency() { + return true + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/SingleNettyConnection.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/SingleNettyConnection.java new file mode 100644 index 000000000..eb670a76a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/SingleNettyConnection.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; +import io.opentelemetry.instrumentation.test.base.SingleConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/* +Netty does not actually support proper http pipelining and has no way to correlate incoming response +message with some sent request. This means that without some support from the higher level protocol +we cannot concurrently send several requests across the same channel. Thus doRequest method of this +class is synchronised. Yes, it seems kinda pointless, but at least we test that our instrumentation +does not wreak havoc on Netty channel. + */ +public class SingleNettyConnection implements SingleConnection { + private final String host; + private final int port; + private final Channel channel; + + public SingleNettyConnection(String host, int port) { + this.host = host; + this.port = port; + EventLoopGroup group = new NioEventLoopGroup(); + Bootstrap bootstrap = new Bootstrap(); + bootstrap + .group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) { + ChannelPipeline pipeline = socketChannel.pipeline(); + pipeline.addLast(new HttpClientCodec()); + } + }); + + ChannelFuture channelFuture = bootstrap.connect(host, port); + channelFuture.awaitUninterruptibly(); + if (!channelFuture.isSuccess()) { + throw new IllegalStateException(channelFuture.cause()); + } else { + channel = channelFuture.channel(); + } + } + + @Override + public synchronized int doRequest(String path, Map headers) + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture result = new CompletableFuture<>(); + + channel.pipeline().addLast(new ClientHandler(result)); + + String url; + try { + url = new URL("http", host, port, path).toString(); + } catch (MalformedURLException e) { + throw new ExecutionException(e); + } + + HttpRequest request = + new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, url, Unpooled.EMPTY_BUFFER); + request.headers().set(HttpHeaderNames.HOST, host); + headers.forEach((k, v) -> request.headers().set(k, v)); + + channel.writeAndFlush(request).get(); + return result.get(20, TimeUnit.SECONDS); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/library/netty-4.1-library.gradle b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/library/netty-4.1-library.gradle new file mode 100644 index 000000000..ede863f83 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/library/netty-4.1-library.gradle @@ -0,0 +1,5 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "io.netty:netty-codec-http:4.1.0.Final" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AttributeKeys.java b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AttributeKeys.java new file mode 100644 index 000000000..41e98378b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AttributeKeys.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4_1; + +import io.netty.util.AttributeKey; +import io.opentelemetry.context.Context; + +public final class AttributeKeys { + + public static final AttributeKey CONNECT_CONTEXT = + AttributeKey.valueOf(AttributeKeys.class, "connect-context"); + public static final AttributeKey WRITE_CONTEXT = + AttributeKey.valueOf(AttributeKeys.class, "passed-context"); + + // this is the context that has the server span + // + // note: this attribute key is also used by ratpack instrumentation + public static final AttributeKey SERVER_SPAN = + AttributeKey.valueOf(AttributeKeys.class, "server-span"); + + public static final AttributeKey CLIENT_CONTEXT = + AttributeKey.valueOf(AttributeKeys.class, "client-context"); + + public static final AttributeKey CLIENT_PARENT_CONTEXT = + AttributeKey.valueOf(AttributeKeys.class, "client-parent-context"); + + private AttributeKeys() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/okhttp-2.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/okhttp-2.2-javaagent.gradle new file mode 100644 index 000000000..621cee0fb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/okhttp-2.2-javaagent.gradle @@ -0,0 +1,20 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +/* +Note: The Interceptor class for OkHttp was not introduced until 2.2+, so we need to make sure the +instrumentation is not loaded unless the dependency is 2.2+. +*/ +muzzle { + pass { + group = "com.squareup.okhttp" + module = "okhttp" + versions = "[2.2,3)" + assertInverse = true + } +} + +dependencies { + library("com.squareup.okhttp:okhttp:2.2.0") + + latestDepTestLibrary "com.squareup.okhttp:okhttp:[2.6,3)" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/DispatcherInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/DispatcherInstrumentation.java new file mode 100644 index 000000000..bf913fb03 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/DispatcherInstrumentation.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DispatcherInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.squareup.okhttp.Dispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("enqueue").and(takesArgument(0, named("com.squareup.okhttp.Call$AsyncCall"))), + DispatcherInstrumentation.class.getName() + "$AttachStateAdvice"); + } + + @SuppressWarnings("unused") + public static class AttachStateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State onEnter(@Advice.Argument(0) Runnable call) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(call)) { + ContextStore contextStore = + InstrumentationContext.get(Runnable.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, call, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2InstrumentationModule.java new file mode 100644 index 000000000..5de870504 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2InstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class OkHttp2InstrumentationModule extends InstrumentationModule { + public OkHttp2InstrumentationModule() { + super("okhttp", "okhttp-2.2"); + } + + @Override + public List typeInstrumentations() { + return asList(new OkHttpClientInstrumentation(), new DispatcherInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttpClientInstrumentation.java new file mode 100644 index 000000000..8978471d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttpClientInstrumentation.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.OkHttpClient; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OkHttpClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.squareup.okhttp.OkHttpClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodExit + public static void addTracingInterceptor(@Advice.This OkHttpClient client) { + for (Interceptor interceptor : client.interceptors()) { + if (interceptor instanceof TracingInterceptor) { + return; + } + } + + client.interceptors().add(new TracingInterceptor()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttpClientTracer.java new file mode 100644 index 000000000..d6334bdf2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttpClientTracer.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; + +import static io.opentelemetry.javaagent.instrumentation.okhttp.v2_2.RequestBuilderInjectAdapter.SETTER; + +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import java.net.URISyntaxException; + +public class OkHttpClientTracer extends HttpClientTracer { + private static final OkHttpClientTracer TRACER = new OkHttpClientTracer(); + + private OkHttpClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static OkHttpClientTracer tracer() { + return TRACER; + } + + @Override + protected String method(Request request) { + return request.method(); + } + + @Override + protected URI url(Request request) throws URISyntaxException { + return request.url().toURI(); + } + + @Override + protected Integer status(Response response) { + return response.code(); + } + + @Override + protected String requestHeader(Request request, String name) { + return request.header(name); + } + + @Override + protected String responseHeader(Response response, String name) { + return response.header(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.okhttp-2.2"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/RequestBuilderInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/RequestBuilderInjectAdapter.java new file mode 100644 index 000000000..4ca1bdc4f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/RequestBuilderInjectAdapter.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; + +import com.squareup.okhttp.Request; +import io.opentelemetry.context.propagation.TextMapSetter; + +public class RequestBuilderInjectAdapter implements TextMapSetter { + public static final RequestBuilderInjectAdapter SETTER = new RequestBuilderInjectAdapter(); + + @Override + public void set(Request.Builder carrier, String key, String value) { + carrier.header(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/TracingInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/TracingInterceptor.java new file mode 100644 index 000000000..63ef1f708 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/TracingInterceptor.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; + +import static io.opentelemetry.javaagent.instrumentation.okhttp.v2_2.OkHttpClientTracer.tracer; + +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.io.IOException; + +public class TracingInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + Context parentContext = Context.current(); + if (!tracer().shouldStartSpan(parentContext)) { + return chain.proceed(chain.request()); + } + + Request.Builder requestBuilder = chain.request().newBuilder(); + Context context = tracer().startSpan(parentContext, chain.request(), requestBuilder); + + Response response; + try (Scope ignored = context.makeCurrent()) { + response = chain.proceed(requestBuilder.build()); + } catch (Exception e) { + tracer().endExceptionally(context, e); + throw e; + } + tracer().end(context, response); + return response; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/HeadersUtil.groovy b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/HeadersUtil.groovy new file mode 100644 index 000000000..79c874501 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/HeadersUtil.groovy @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class HeadersUtil { + static headersToArray(Map headers) { + String[] headersArr = new String[headers.size() * 2] + headers.eachWithIndex { k, v, i -> + headersArr[i] = k + headersArr[i + 1] = v + } + + headersArr + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/OkHttp2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/OkHttp2Test.groovy new file mode 100644 index 000000000..bb22ebfb9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/OkHttp2Test.groovy @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.squareup.okhttp.Callback +import com.squareup.okhttp.MediaType +import com.squareup.okhttp.OkHttpClient +import com.squareup.okhttp.Request +import com.squareup.okhttp.RequestBody +import com.squareup.okhttp.Response +import com.squareup.okhttp.internal.http.HttpMethod +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.util.concurrent.TimeUnit +import spock.lang.Shared + +class OkHttp2Test extends HttpClientTest implements AgentTestTrait { + @Shared + def client = new OkHttpClient() + + def setupSpec() { + client.setConnectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + } + + @Override + Request buildRequest(String method, URI uri, Map headers) { + def body = HttpMethod.requiresRequestBody(method) ? RequestBody.create(MediaType.parse("text/plain"), "") : null + def request = new Request.Builder() + .url(uri.toURL()) + .method(method, body) + headers.forEach({ key, value -> request.header(key, value) }) + return request.build() + } + + @Override + int sendRequest(Request request, String method, URI uri, Map headers) { + return client.newCall(request).execute().code() + } + + @Override + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + client.newCall(request).enqueue(new Callback() { + @Override + void onFailure(Request req, IOException e) { + requestResult.complete(e) + } + + @Override + void onResponse(Response response) throws IOException { + requestResult.complete(response.code()) + } + }) + } + + @Override + boolean testRedirects() { + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/okhttp-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/okhttp-3.0-javaagent.gradle new file mode 100644 index 000000000..d95f7039f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/okhttp-3.0-javaagent.gradle @@ -0,0 +1,27 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.squareup.okhttp3" + module = "okhttp" + versions = "[3.0,)" + assertInverse = true + } +} + +/* +Note: there is a bit of dependency exclusion magic going on. +We have to exclude all transitive dependencies on 'okhttp' because we would like to force +specific version. We cannot use . Unfortunately we cannot just force version on +a dependency because this doesn't work well with version ranges - it doesn't select latest. +And we cannot use configurations to exclude this dependency from everywhere in one go +because it looks like exclusions using configurations excludes dependency even if it explicit +not transitive. + */ +dependencies { + implementation project(':instrumentation:okhttp:okhttp-3.0:library') + + library("com.squareup.okhttp3:okhttp:3.0.0") + + testImplementation project(':instrumentation:okhttp:okhttp-3.0:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Instrumentation.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Instrumentation.java new file mode 100644 index 000000000..d83bb0bf7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Instrumentation.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v3_0; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import okhttp3.OkHttpClient; + +public class OkHttp3Instrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("okhttp3.OkHttpClient$Builder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void trackCallDepth(@Advice.Local("callDepth") int callDepth) { + callDepth = CallDepthThreadLocalMap.incrementCallDepth(OkHttpClient.Builder.class); + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void addTracingInterceptor( + @Advice.This OkHttpClient.Builder builder, @Advice.Local("callDepth") int callDepth) { + // No-args constructor is automatically called by constructors with args, but we only want to + // run once from the constructor with args because that is where the dedupe needs to happen. + if (callDepth > 0) { + return; + } + CallDepthThreadLocalMap.reset(OkHttpClient.Builder.class); + if (builder.interceptors().contains(OkHttp3Interceptors.TRACING_INTERCEPTOR)) { + return; + } + builder.addInterceptor(OkHttp3Interceptors.TRACING_INTERCEPTOR); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3InstrumentationModule.java new file mode 100644 index 000000000..a28865a43 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3InstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v3_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class OkHttp3InstrumentationModule extends InstrumentationModule { + + public OkHttp3InstrumentationModule() { + super("okhttp", "okhttp-3.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new OkHttp3Instrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Interceptors.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Interceptors.java new file mode 100644 index 000000000..abd1e6bd5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Interceptors.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v3_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.okhttp.v3_0.OkHttpTracing; +import okhttp3.Interceptor; + +/** Holder of singleton interceptors for adding to instrumented clients. */ +public class OkHttp3Interceptors { + + public static final Interceptor TRACING_INTERCEPTOR = + OkHttpTracing.create(GlobalOpenTelemetry.get()).newInterceptor(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Test.groovy new file mode 100644 index 000000000..2b8a096a2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Test.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.okhttp.v3_0 + +import io.opentelemetry.instrumentation.okhttp.v3_0.AbstractOkHttp3Test +import io.opentelemetry.instrumentation.test.AgentTestTrait +import okhttp3.OkHttpClient + +class OkHttp3Test extends AbstractOkHttp3Test implements AgentTestTrait { + @Override + OkHttpClient.Builder configureClient(OkHttpClient.Builder clientBuilder) { + return clientBuilder + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/okhttp-3.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/okhttp-3.0-library.gradle new file mode 100644 index 000000000..f311f8a9e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/okhttp-3.0-library.gradle @@ -0,0 +1,8 @@ +apply plugin: "otel.library-instrumentation" +apply plugin: "net.ltgt.nullaway" + +dependencies { + library "com.squareup.okhttp3:okhttp:3.0.0" + + testImplementation project(':instrumentation:okhttp:okhttp-3.0:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpClientTracer.java new file mode 100644 index 000000000..34fb296f9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpClientTracer.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0; + +import static io.opentelemetry.instrumentation.okhttp.v3_0.RequestBuilderInjectAdapter.SETTER; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import okhttp3.Request; +import okhttp3.Response; + +final class OkHttpClientTracer extends HttpClientTracer { + + OkHttpClientTracer(OpenTelemetry openTelemetry) { + super(openTelemetry, NetPeerAttributes.INSTANCE); + } + + @Override + protected String method(Request httpRequest) { + return httpRequest.method(); + } + + @Override + protected URI url(Request httpRequest) { + return httpRequest.url().uri(); + } + + @Override + protected Integer status(Response httpResponse) { + return httpResponse.code(); + } + + @Override + protected String requestHeader(Request request, String name) { + return request.header(name); + } + + @Override + protected String responseHeader(Response response, String name) { + return response.header(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.okhttp-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTracing.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTracing.java new file mode 100644 index 000000000..2fe1a2f8c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTracing.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0; + +import io.opentelemetry.api.OpenTelemetry; +import java.util.concurrent.ExecutorService; +import okhttp3.Callback; +import okhttp3.Dispatcher; +import okhttp3.Interceptor; + +/** Entrypoint for tracing OkHttp clients. */ +public final class OkHttpTracing { + + /** Returns a new {@link OkHttpTracing} configured with the given {@link OpenTelemetry}. */ + public static OkHttpTracing create(OpenTelemetry openTelemetry) { + return new OkHttpTracing(openTelemetry); + } + + private final OkHttpClientTracer tracer; + + OkHttpTracing(OpenTelemetry openTelemetry) { + this.tracer = new OkHttpClientTracer(openTelemetry); + } + + /** + * Returns a new {@link Interceptor} that can be used with methods like {@link + * okhttp3.OkHttpClient.Builder#addInterceptor(Interceptor)}. Note that asynchronous calls using + * {@link okhttp3.Call.Factory#enqueue(Callback)} will not work correctly unless you also decorate + * the {@linkplain Dispatcher#executorService() dispatcher's executor service} with {@link + * io.opentelemetry.context.Context#taskWrapping(ExecutorService)}. For example, if using the + * default {@link Dispatcher}, you will need to configure {@link okhttp3.OkHttpClient.Builder} + * something like + * + *

{@code
+   * new OkHttpClient.Builder()
+   *   .dispatcher(new Dispatcher(Context.taskWrapping(new Dispatcher().executorService())))
+   *   .addInterceptor(OkHttpTracing.create(openTelemetry).newInterceptor())
+   *   ...
+   * }
+ */ + public Interceptor newInterceptor() { + return new TracingInterceptor(tracer); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/RequestBuilderInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/RequestBuilderInjectAdapter.java new file mode 100644 index 000000000..07a808e3b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/RequestBuilderInjectAdapter.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0; + +import io.opentelemetry.context.propagation.TextMapSetter; +import okhttp3.Request; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Helper class to inject span context into request headers. + * + * @author Pavol Loffay + */ +final class RequestBuilderInjectAdapter implements TextMapSetter { + + static final RequestBuilderInjectAdapter SETTER = new RequestBuilderInjectAdapter(); + + @Override + public void set(Request.@Nullable Builder carrier, String key, String value) { + if (carrier == null) { + return; + } + carrier.header(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/TracingInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/TracingInterceptor.java new file mode 100644 index 000000000..e05bbff89 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/TracingInterceptor.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +final class TracingInterceptor implements Interceptor { + + private final OkHttpClientTracer tracer; + + TracingInterceptor(OkHttpClientTracer tracer) { + this.tracer = tracer; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Context parentContext = Context.current(); + if (!tracer.shouldStartSpan(parentContext)) { + return chain.proceed(chain.request()); + } + + Request.Builder requestBuilder = chain.request().newBuilder(); + Context context = tracer.startSpan(parentContext, chain.request(), requestBuilder); + + Response response; + try (Scope ignored = context.makeCurrent()) { + response = chain.proceed(requestBuilder.build()); + } catch (Exception e) { + tracer.endExceptionally(context, e); + throw e; + } + tracer.end(context, response); + return response; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/test/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/test/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.groovy new file mode 100644 index 000000000..eae7e8caf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/library/src/test/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.groovy @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0 + +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import okhttp3.Dispatcher +import okhttp3.OkHttpClient + +class OkHttp3Test extends AbstractOkHttp3Test implements LibraryTestTrait { + @Override + OkHttpClient.Builder configureClient(OkHttpClient.Builder clientBuilder) { + return clientBuilder + // The double "new Dispatcher" style is the simplest way to decorate the default executor. + .dispatcher(new Dispatcher(Context.taskWrapping(new Dispatcher().executorService()))) + .addInterceptor(OkHttpTracing.create(getOpenTelemetry()).newInterceptor()) + } + + // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet + @Override + boolean testWithClientParent() { + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/testing/okhttp-3.0-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/testing/okhttp-3.0-testing.gradle new file mode 100644 index 000000000..372b3a055 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/testing/okhttp-3.0-testing.gradle @@ -0,0 +1,15 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api(project(':testing-common')) { + exclude module: 'okhttp' + } + + api "com.squareup.okhttp3:okhttp:3.0.0" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.groovy new file mode 100644 index 000000000..c82b5d504 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/okhttp/okhttp-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.groovy @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0 + +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.util.concurrent.TimeUnit +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Headers +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import okhttp3.internal.http.HttpMethod +import spock.lang.Shared + +abstract class AbstractOkHttp3Test extends HttpClientTest { + + abstract OkHttpClient.Builder configureClient(OkHttpClient.Builder clientBuilder) + + @Shared + def client = configureClient( + new OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .retryOnConnectionFailure(false)) + .build() + + @Override + Request buildRequest(String method, URI uri, Map headers) { + def body = HttpMethod.requiresRequestBody(method) ? RequestBody.create(MediaType.parse("text/plain"), "") : null + return new Request.Builder() + .url(uri.toURL()) + .method(method, body) + .headers(Headers.of(headers)).build() + } + + @Override + int sendRequest(Request request, String method, URI uri, Map headers) { + return client.newCall(request).execute().code() + } + + @Override + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + client.newCall(request).enqueue(new Callback() { + @Override + void onFailure(Call call, IOException e) { + requestResult.complete(e) + } + + @Override + void onResponse(Call call, Response response) throws IOException { + requestResult.complete(response.code()) + } + }) + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testCausality() { + false + } + + def "reused builder has one interceptor"() { + when: + def builder = configureClient(new OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .retryOnConnectionFailure(false)) + builder.build() + def newClient = builder.build() + + then: + newClient.interceptors().size() == 1 + } + + def "builder created from client has one interceptor"() { + when: + def newClient = client.newBuilder().build() + + then: + newClient.interceptors().size() == 1 + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/opentelemetry-annotations-1.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/opentelemetry-annotations-1.0-javaagent.gradle new file mode 100644 index 000000000..e5eb6ed70 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/opentelemetry-annotations-1.0-javaagent.gradle @@ -0,0 +1,18 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly project(':javaagent-tooling') + + // this instrumentation needs to do similar shading dance as opentelemetry-api-1.0 because + // the @WithSpan annotation references the OpenTelemetry API's SpanKind class + // + // see the comment in opentelemetry-api-1.0.gradle for more details + compileOnly project(path: ':opentelemetry-ext-annotations-shaded-for-instrumenting', configuration: 'shadow') + + testImplementation "run.mone:opentelemetry-extension-annotations" + testImplementation "net.bytebuddy:byte-buddy:${versions["net.bytebuddy"]}" +} + +test { + jvmArgs "-Dotel.instrumentation.opentelemetry-annotations.exclude-methods=io.opentelemetry.test.annotation.TracedWithSpan[ignored]" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanInstrumentation.java new file mode 100644 index 000000000..79cd5809c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanInstrumentation.java @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.otelannotations; + +import static io.opentelemetry.javaagent.instrumentation.otelannotations.WithSpanTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import application.io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.ByteCodeElement; +import net.bytebuddy.description.annotation.AnnotationSource; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +public class WithSpanInstrumentation implements TypeInstrumentation { + + private static final String TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG = + "otel.instrumentation.opentelemetry-annotations.exclude-methods"; + + private final ElementMatcher.Junction annotatedMethodMatcher; + // this matcher matches all methods that should be excluded from transformation + private final ElementMatcher.Junction excludedMethodsMatcher; + + WithSpanInstrumentation() { + annotatedMethodMatcher = + isAnnotatedWith(named("application.io.opentelemetry.extension.annotations.WithSpan")); + excludedMethodsMatcher = configureExcludedMethods(); + } + + @Override + public ElementMatcher typeMatcher() { + return declaresMethod(annotatedMethodMatcher); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + annotatedMethodMatcher.and(not(excludedMethodsMatcher)), + WithSpanInstrumentation.class.getName() + "$WithSpanAdvice"); + } + + /* + Returns a matcher for all methods that should be excluded from auto-instrumentation by + annotation-based advices. + */ + static ElementMatcher.Junction configureExcludedMethods() { + ElementMatcher.Junction result = none(); + + Map> excludedMethods = + MethodsConfigurationParser.parse( + Config.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); + for (Map.Entry> entry : excludedMethods.entrySet()) { + String className = entry.getKey(); + ElementMatcher.Junction classMather = + isDeclaredBy(ElementMatchers.named(className)); + + ElementMatcher.Junction excludedMethodsMatcher = none(); + for (String methodName : entry.getValue()) { + excludedMethodsMatcher = excludedMethodsMatcher.or(ElementMatchers.named(methodName)); + } + + result = result.or(classMather.and(excludedMethodsMatcher)); + } + + return result; + } + + @SuppressWarnings("unused") + public static class WithSpanAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + WithSpan applicationAnnotation = method.getAnnotation(WithSpan.class); + + SpanKind kind = tracer().extractSpanKind(applicationAnnotation); + Context current = Java8BytecodeBridge.currentContext(); + + // don't create a nested span if you're not supposed to. + if (tracer().shouldStartSpan(current, kind)) { + context = tracer().startSpan(current, applicationAnnotation, method, kind); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + returnValue = tracer().end(context, method.getReturnType(), returnValue); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanInstrumentationModule.java new file mode 100644 index 000000000..102af65bd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.otelannotations; + +import static java.util.Collections.singletonList; + +import application.io.opentelemetry.extension.annotations.WithSpan; +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +/** Instrumentation for methods annotated with {@link WithSpan} annotation. */ +@AutoService(InstrumentationModule.class) +public class WithSpanInstrumentationModule extends InstrumentationModule { + + public WithSpanInstrumentationModule() { + super("opentelemetry-annotations"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new WithSpanInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanTracer.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanTracer.java new file mode 100644 index 000000000..620f41487 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/WithSpanTracer.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.otelannotations; + +import application.io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy; +import java.lang.reflect.Method; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WithSpanTracer extends BaseTracer { + private static final WithSpanTracer TRACER = new WithSpanTracer(); + + public static WithSpanTracer tracer() { + return TRACER; + } + + private static final Logger log = LoggerFactory.getLogger(WithSpanTracer.class); + + private final AsyncSpanEndStrategies asyncSpanEndStrategies = + AsyncSpanEndStrategies.getInstance(); + + public Context startSpan( + Context parentContext, WithSpan applicationAnnotation, Method method, SpanKind kind) { + Span span = + spanBuilder( + parentContext, spanNameForMethodWithAnnotation(applicationAnnotation, method), kind) + .startSpan(); + if (kind == SpanKind.SERVER) { + return withServerSpan(parentContext, span); + } + if (kind == SpanKind.CLIENT) { + return withClientSpan(parentContext, span); + } + return parentContext.with(span); + } + + /** + * This method is used to generate an acceptable span (operation) name based on a given method + * reference. It first checks for existence of {@link WithSpan} annotation. If it is present, then + * tries to derive name from its {@code value} attribute. Otherwise delegates to {@link + * SpanNames#fromMethod(Method)}. + */ + public String spanNameForMethodWithAnnotation(WithSpan applicationAnnotation, Method method) { + if (applicationAnnotation != null && !applicationAnnotation.value().isEmpty()) { + return applicationAnnotation.value(); + } + return SpanNames.fromMethod(method); + } + + public SpanKind extractSpanKind(WithSpan applicationAnnotation) { + application.io.opentelemetry.api.trace.SpanKind applicationKind = + applicationAnnotation != null + ? applicationAnnotation.kind() + : application.io.opentelemetry.api.trace.SpanKind.INTERNAL; + return toAgentOrNull(applicationKind); + } + + public static SpanKind toAgentOrNull( + application.io.opentelemetry.api.trace.SpanKind applicationSpanKind) { + try { + return SpanKind.valueOf(applicationSpanKind.name()); + } catch (IllegalArgumentException e) { + log.debug("unexpected span kind: {}", applicationSpanKind.name()); + return SpanKind.INTERNAL; + } + } + + /** + * Denotes the end of the invocation of the traced method with a successful result which will end + * the span stored in the passed {@code context}. If the method returned a value representing an + * asynchronous operation then the span will not be finished until the asynchronous operation has + * completed. + * + * @param returnType Return type of the traced method. + * @param returnValue Return value from the traced method. + * @return Either {@code returnValue} or a value composing over {@code returnValue} for + * notification of completion. + * @throws ClassCastException if returnValue is not an instance of returnType + */ + public Object end(Context context, Class returnType, Object returnValue) { + if (returnType.isInstance(returnValue)) { + AsyncSpanEndStrategy asyncSpanEndStrategy = + asyncSpanEndStrategies.resolveStrategy(returnType); + if (asyncSpanEndStrategy != null) { + return asyncSpanEndStrategy.end(this, context, returnValue); + } + } + end(context); + return returnValue; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.opentelemetry-annotations-1.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy new file mode 100644 index 000000000..44d74838b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/test/groovy/WithSpanInstrumentationTest.groovy @@ -0,0 +1,422 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.PRODUCER +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.extension.annotations.WithSpan +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.TraceUtils +import io.opentelemetry.test.annotation.TracedWithSpan +import java.lang.reflect.Modifier +import java.util.concurrent.CompletableFuture +import net.bytebuddy.ByteBuddy +import net.bytebuddy.ClassFileVersion +import net.bytebuddy.asm.MemberAttributeExtension +import net.bytebuddy.description.annotation.AnnotationDescription +import net.bytebuddy.implementation.MethodDelegation +import net.bytebuddy.implementation.bind.annotation.RuntimeType +import net.bytebuddy.implementation.bind.annotation.This +import net.bytebuddy.matcher.ElementMatchers + +/** + * This test verifies that auto instrumentation supports {@link io.opentelemetry.extension.annotations.WithSpan} contrib annotation. + */ +class WithSpanInstrumentationTest extends AgentInstrumentationSpecification { + + def "should derive automatic name"() { + setup: + new TracedWithSpan().otel() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.otel" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should take span name from annotation"() { + setup: + new TracedWithSpan().namedOtel() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "manualName" + hasNoParent() + attributes { + } + } + } + } + } + + def "should take span kind from annotation"() { + setup: + new TracedWithSpan().oneOfAKind() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.oneOfAKind" + kind PRODUCER + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture multiple spans"() { + setup: + new TracedWithSpan().server() + + expect: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "TracedWithSpan.server" + kind SERVER + hasNoParent() + attributes { + } + } + span(1) { + name "TracedWithSpan.otel" + childOf span(0) + attributes { + } + } + } + } + } + + def "should not capture multiple server spans"() { + setup: + new TracedWithSpan().nestedServers() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.nestedServers" + kind SERVER + hasNoParent() + attributes { + } + } + } + } + } + + def "should not capture multiple client spans"() { + setup: + new TracedWithSpan().nestedClients() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.nestedClients" + kind CLIENT + hasNoParent() + attributes { + } + } + } + } + } + + def "should ignore method excluded by trace.annotated.methods.exclude configuration"() { + setup: + new TracedWithSpan().ignored() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + } + + def "should capture span for already completed CompletionStage"() { + setup: + def future = CompletableFuture.completedFuture("Done") + new TracedWithSpan().completionStage(future) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completionStage" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed CompletionStage"() { + setup: + def future = new CompletableFuture() + new TracedWithSpan().completionStage(future) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + future.complete("Done") + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completionStage" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already exceptionally completed CompletionStage"() { + setup: + def future = new CompletableFuture() + future.completeExceptionally(new IllegalArgumentException("Boom")) + new TracedWithSpan().completionStage(future) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completionStage" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually exceptionally completed CompletionStage"() { + setup: + def future = new CompletableFuture() + new TracedWithSpan().completionStage(future) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + future.completeExceptionally(new IllegalArgumentException("Boom")) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completionStage" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for null CompletionStage"() { + setup: + new TracedWithSpan().completionStage(null) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completionStage" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already completed CompletableFuture"() { + setup: + def future = CompletableFuture.completedFuture("Done") + new TracedWithSpan().completableFuture(future) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completableFuture" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed CompletableFuture"() { + setup: + def future = new CompletableFuture() + new TracedWithSpan().completableFuture(future) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + future.complete("Done") + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completableFuture" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already exceptionally completed CompletableFuture"() { + setup: + def future = new CompletableFuture() + future.completeExceptionally(new IllegalArgumentException("Boom")) + new TracedWithSpan().completableFuture(future) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completableFuture" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually exceptionally completed CompletableFuture"() { + setup: + def future = new CompletableFuture() + new TracedWithSpan().completableFuture(future) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + future.completeExceptionally(new IllegalArgumentException("Boom")) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completableFuture" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for null CompletableFuture"() { + setup: + new TracedWithSpan().completableFuture(null) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completableFuture" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "instrument java6 class"() { + setup: + /* + class GeneratedJava6TestClass implements Runnable { + @WithSpan + public void run() { + TraceUtils.runUnderTrace("intercept", {}) + } + } + */ + Class generatedClass = new ByteBuddy(ClassFileVersion.JAVA_V6) + .subclass(Object) + .name("GeneratedJava6TestClass") + .implement(Runnable) + .defineMethod("run", void.class, Modifier.PUBLIC).intercept(MethodDelegation.to(new Object() { + @RuntimeType + void intercept(@This Object o) { + TraceUtils.runUnderTrace("intercept", {}) + } + })) + .visit(new MemberAttributeExtension.ForMethod() + .annotateMethod(AnnotationDescription.Builder.ofType(WithSpan).build()) + .on(ElementMatchers.named("run"))) + .make() + .load(getClass().getClassLoader()) + .getLoaded() + + Runnable runnable = (Runnable) generatedClass.getConstructor().newInstance() + runnable.run() + + expect: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "GeneratedJava6TestClass.run" + kind INTERNAL + hasNoParent() + attributes { + } + } + span(1) { + name "intercept" + kind INTERNAL + childOf(span(0)) + attributes { + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/TracedWithSpan.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/TracedWithSpan.java new file mode 100644 index 000000000..8cfc2c598 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/TracedWithSpan.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test.annotation; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.extension.annotations.WithSpan; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public class TracedWithSpan { + + @WithSpan + public String otel() { + return "hello!"; + } + + @WithSpan("manualName") + public String namedOtel() { + return "hello!"; + } + + @WithSpan + public String ignored() { + return "hello!"; + } + + @WithSpan(kind = SpanKind.PRODUCER) + public String oneOfAKind() { + return "hello!"; + } + + @WithSpan(kind = SpanKind.SERVER) + public String server() { + return otel(); + } + + @WithSpan(kind = SpanKind.SERVER) + public String nestedServers() { + return innerServer(); + } + + @WithSpan(kind = SpanKind.SERVER) + public String innerServer() { + return "hello!"; + } + + @WithSpan(kind = SpanKind.CLIENT) + public String nestedClients() { + return innerClient(); + } + + @WithSpan(kind = SpanKind.CLIENT) + public String innerClient() { + return "hello!"; + } + + @WithSpan + public CompletionStage completionStage(CompletableFuture future) { + return future; + } + + @WithSpan + public CompletableFuture completableFuture(CompletableFuture future) { + return future; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/opentelemetry-api-1.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/opentelemetry-api-1.0-javaagent.gradle new file mode 100644 index 000000000..099c67d66 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/opentelemetry-api-1.0-javaagent.gradle @@ -0,0 +1,31 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + // this instrumentation needs to be able to reference both the OpenTelemetry API + // that is shaded in the bootstrap class loader (for sending telemetry to the agent), + // and the OpenTelemetry API that the user brings (in order to capture that telemetry) + // + // since (all) instrumentation already uses OpenTelemetry API for sending telemetry to the agent, + // this instrumentation uses a "temporarily shaded" OpenTelemetry API to represent the + // OpenTelemetry API that the user brings + // + // then later, after the OpenTelemetry API in the bootstrap class loader is shaded, + // the "temporarily shaded" OpenTelemetry API is unshaded, so that it will apply to the + // OpenTelemetry API that the user brings + // + // so in the code "application.io.opentelemetry.*" refers to the (unshaded) OpenTelemetry API that + // the application brings (as those references will be translated during the build to remove the + // "application." prefix) + // + // and in the code "io.opentelemetry.*" refers to the (shaded) OpenTelemetry API that is used by + // the agent (as those references will later be shaded) + compileOnly project(path: ':opentelemetry-api-shaded-for-instrumenting', configuration: 'shadow') + + // using OpenTelemetry SDK to make sure that instrumentation doesn't cause + // OpenTelemetrySdk.getTracerProvider() to throw ClassCastException + testImplementation "run.mone:opentelemetry-sdk" + + // @WithSpan annotation is used to generate spans in ContextBridgeTest + testImplementation "run.mone:opentelemetry-extension-annotations" + testInstrumentation project(':instrumentation:opentelemetry-annotations-1.0:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ApplicationOpenTelemetry.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ApplicationOpenTelemetry.java new file mode 100644 index 000000000..91952282b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ApplicationOpenTelemetry.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +import application.io.opentelemetry.api.OpenTelemetry; +import application.io.opentelemetry.api.trace.TracerProvider; +import application.io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.propagation.ApplicationContextPropagators; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.ApplicationTracerProvider; + +// Our convention for accessing agent package +@SuppressWarnings("UnnecessarilyFullyQualified") +public class ApplicationOpenTelemetry implements OpenTelemetry { + + public static final OpenTelemetry INSTANCE = new ApplicationOpenTelemetry(); + + private final TracerProvider applicationTracerProvider; + + private final ContextPropagators applicationContextPropagators; + + private ApplicationOpenTelemetry() { + io.opentelemetry.api.OpenTelemetry agentOpenTelemetry = + io.opentelemetry.api.GlobalOpenTelemetry.get(); + applicationTracerProvider = + new ApplicationTracerProvider(agentOpenTelemetry.getTracerProvider()); + applicationContextPropagators = + new ApplicationContextPropagators(agentOpenTelemetry.getPropagators()); + } + + @Override + public TracerProvider getTracerProvider() { + return applicationTracerProvider; + } + + @Override + public ContextPropagators getPropagators() { + return applicationContextPropagators; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextInstrumentation.java new file mode 100644 index 000000000..13cf7acd2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import application.io.opentelemetry.context.Context; +import application.io.opentelemetry.context.ContextStorage; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Returns {@link AgentContextStorage} as the implementation of {@link ContextStorage} in the + * application classpath. We do this instead of using the normal service loader mechanism to make + * sure there is no dependency on a system property or possibility of a user overriding this since + * it's required for instrumentation in the agent to work properly. + */ +public class ContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.context.ArrayBasedContext"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isStatic()).and(named("root")), + ContextInstrumentation.class.getName() + "$GetAdvice"); + } + + @SuppressWarnings("unused") + public static class GetAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit(@Advice.Return(readOnly = false) Context root) { + root = + new AgentContextStorage.AgentContextWrapper( + io.opentelemetry.context.Context.root(), root); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextStorageInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextStorageInstrumentation.java new file mode 100644 index 000000000..ae8bdfbd8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextStorageInstrumentation.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import application.io.opentelemetry.context.ContextStorage; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Returns {@link AgentContextStorage} as the implementation of {@link ContextStorage} in the + * application classpath. We do this instead of using the normal service loader mechanism to make + * sure there is no dependency on a system property or possibility of a user overriding this since + * it's required for instrumentation in the agent to work properly. + */ +public class ContextStorageInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.context.LazyStorage"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isStatic()).and(named("get")), + ContextStorageInstrumentation.class.getName() + "$GetAdvice"); + } + + @SuppressWarnings("unused") + public static class GetAdvice { + + @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) + public static Object onEnter() { + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit(@Advice.Return(readOnly = false) ContextStorage storage) { + storage = AgentContextStorage.INSTANCE; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryApiInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryApiInstrumentationModule.java new file mode 100644 index 000000000..8ee47b61c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryApiInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule { + public OpenTelemetryApiInstrumentationModule() { + super("opentelemetry-api"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ContextInstrumentation(), + new ContextStorageInstrumentation(), + new OpenTelemetryInstrumentation(), + new SpanInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryInstrumentation.java new file mode 100644 index 000000000..dff235834 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// Our convention for accessing agent package +@SuppressWarnings("UnnecessarilyFullyQualified") +public class OpenTelemetryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.GlobalOpenTelemetry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isStatic()).and(named("get")).and(takesArguments(0)), + OpenTelemetryInstrumentation.class.getName() + "$GetGlobalOpenTelemetryAdvice"); + } + + @SuppressWarnings("unused") + public static class GetGlobalOpenTelemetryAdvice { + + @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) + public static Object onEnter() { + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return(readOnly = false) + application.io.opentelemetry.api.OpenTelemetry openTelemetry) { + openTelemetry = ApplicationOpenTelemetry.INSTANCE; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/SpanInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/SpanInstrumentation.java new file mode 100644 index 000000000..90de611f1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/SpanInstrumentation.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import application.io.opentelemetry.api.trace.Span; +import application.io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class SpanInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.trace.PropagatedSpan"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isStatic()).and(named("create")), + SpanInstrumentation.class.getName() + "$CreateAdvice"); + } + + @SuppressWarnings("unused") + public static class CreateAdvice { + + // We replace the return value completely so don't need to call the method. + @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) + public static boolean methodEnter() { + return false; + } + + @Advice.OnMethodExit + public static void methodExit( + @Advice.Argument(0) SpanContext applicationSpanContext, + @Advice.Return(readOnly = false) Span applicationSpan) { + applicationSpan = + Bridging.toApplication( + io.opentelemetry.api.trace.Span.wrap(Bridging.toAgent(applicationSpanContext))); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/baggage/BaggageBridging.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/baggage/BaggageBridging.java new file mode 100644 index 000000000..076cea38a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/baggage/BaggageBridging.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.baggage; + +import application.io.opentelemetry.api.baggage.Baggage; +import application.io.opentelemetry.api.baggage.BaggageBuilder; +import application.io.opentelemetry.api.baggage.BaggageEntryMetadata; + +public final class BaggageBridging { + + public static Baggage toApplication(io.opentelemetry.api.baggage.Baggage agentBaggage) { + BaggageBuilder applicationBaggageBuilder = Baggage.builder(); + agentBaggage.forEach( + (key, entry) -> + applicationBaggageBuilder.put( + key, + entry.getValue(), + BaggageEntryMetadata.create(entry.getMetadata().getValue()))); + return applicationBaggageBuilder.build(); + } + + public static io.opentelemetry.api.baggage.Baggage toAgent(Baggage applicationBaggage) { + io.opentelemetry.api.baggage.BaggageBuilder agentBaggageBuilder = + io.opentelemetry.api.baggage.Baggage.builder(); + applicationBaggage.forEach( + (key, entry) -> + agentBaggageBuilder.put( + key, + entry.getValue(), + io.opentelemetry.api.baggage.BaggageEntryMetadata.create( + entry.getMetadata().getValue()))); + return agentBaggageBuilder.build(); + } + + private BaggageBridging() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/AgentContextStorage.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/AgentContextStorage.java new file mode 100644 index 000000000..1f9afb5ef --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/AgentContextStorage.java @@ -0,0 +1,229 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context; + +import application.io.opentelemetry.api.baggage.Baggage; +import application.io.opentelemetry.api.trace.Span; +import application.io.opentelemetry.context.Context; +import application.io.opentelemetry.context.ContextKey; +import application.io.opentelemetry.context.ContextStorage; +import application.io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.baggage.BaggageBridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import java.lang.reflect.Field; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link ContextStorage} which stores the {@link Context} in the user's application inside the + * {@link Context} in the agent. This allows for context interaction to be maintained between the + * app and agent. + * + *

This storage allows for implicit parenting of context to exist between the agent and + * application by storing the concrete application context in the agent context and returning a + * wrapper which accesses into this stored concrete context. + * + *

This storage also makes sure that OpenTelemetry objects are shared within the context. To do + * this, it recognizes the keys for OpenTelemetry objects (e.g, {@link Span}, {@link Baggage}) and + * always stores and retrieves them from the agent context, even when accessed from the application. + * All other accesses are to the concrete application context. + */ +// Annotation doesn't work on some fields due to fully qualified name (no clue why it matters...) +@SuppressWarnings("FieldMissingNullable") +public class AgentContextStorage implements ContextStorage, AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(AgentContextStorage.class); + + public static final AgentContextStorage INSTANCE = new AgentContextStorage(); + + public static io.opentelemetry.context.Context getAgentContext(Context applicationContext) { + if (applicationContext instanceof AgentContextWrapper) { + return ((AgentContextWrapper) applicationContext).toAgentContext(); + } + if (logger.isDebugEnabled()) { + logger.debug( + "unexpected context: {}", applicationContext, new Exception("unexpected context")); + } + return io.opentelemetry.context.Context.root(); + } + + static final io.opentelemetry.context.ContextKey APPLICATION_CONTEXT = + io.opentelemetry.context.ContextKey.named("otel-context"); + + static final io.opentelemetry.context.ContextKey + AGENT_SPAN_CONTEXT_KEY; + @Nullable static final ContextKey APPLICATION_SPAN_CONTEXT_KEY; + + static final io.opentelemetry.context.ContextKey + AGENT_BAGGAGE_CONTEXT_KEY; + @Nullable static final ContextKey APPLICATION_BAGGAGE_CONTEXT_KEY; + + static { + io.opentelemetry.context.ContextKey agentSpanContextKey; + try { + Class spanContextKey = Class.forName("io.opentelemetry.api.trace.SpanContextKey"); + Field spanContextKeyField = spanContextKey.getDeclaredField("KEY"); + spanContextKeyField.setAccessible(true); + agentSpanContextKey = + (io.opentelemetry.context.ContextKey) + spanContextKeyField.get(null); + } catch (Throwable t) { + agentSpanContextKey = null; + } + AGENT_SPAN_CONTEXT_KEY = agentSpanContextKey; + + ContextKey applicationSpanContextKey; + try { + Class spanContextKey = + Class.forName("application.io.opentelemetry.api.trace.SpanContextKey"); + Field spanContextKeyField = spanContextKey.getDeclaredField("KEY"); + spanContextKeyField.setAccessible(true); + applicationSpanContextKey = (ContextKey) spanContextKeyField.get(null); + } catch (Throwable t) { + applicationSpanContextKey = null; + } + APPLICATION_SPAN_CONTEXT_KEY = applicationSpanContextKey; + + io.opentelemetry.context.ContextKey + agentBaggageContextKey; + try { + Class baggageContextKey = Class.forName("io.opentelemetry.api.baggage.BaggageContextKey"); + Field baggageContextKeyField = baggageContextKey.getDeclaredField("KEY"); + baggageContextKeyField.setAccessible(true); + agentBaggageContextKey = + (io.opentelemetry.context.ContextKey) + baggageContextKeyField.get(null); + } catch (Throwable t) { + agentBaggageContextKey = null; + } + AGENT_BAGGAGE_CONTEXT_KEY = agentBaggageContextKey; + + ContextKey applicationBaggageContextKey; + try { + Class baggageContextKey = + Class.forName("application.io.opentelemetry.api.baggage.BaggageContextKey"); + Field baggageContextKeyField = baggageContextKey.getDeclaredField("KEY"); + baggageContextKeyField.setAccessible(true); + applicationBaggageContextKey = (ContextKey) baggageContextKeyField.get(null); + } catch (Throwable t) { + applicationBaggageContextKey = null; + } + APPLICATION_BAGGAGE_CONTEXT_KEY = applicationBaggageContextKey; + } + + @Override + public Scope attach(Context toAttach) { + io.opentelemetry.context.Context currentAgentContext = + io.opentelemetry.context.Context.current(); + Context currentApplicationContext = currentAgentContext.get(APPLICATION_CONTEXT); + if (currentApplicationContext == null) { + currentApplicationContext = Context.root(); + } + + if (currentApplicationContext == toAttach) { + return Scope.noop(); + } + + io.opentelemetry.context.Context newAgentContext; + if (toAttach instanceof AgentContextWrapper) { + newAgentContext = ((AgentContextWrapper) toAttach).toAgentContext(); + } else { + newAgentContext = currentAgentContext.with(APPLICATION_CONTEXT, toAttach); + } + + return newAgentContext.makeCurrent()::close; + } + + @Override + public Context current() { + io.opentelemetry.context.Context agentContext = io.opentelemetry.context.Context.current(); + Context applicationContext = agentContext.get(APPLICATION_CONTEXT); + if (applicationContext == null) { + applicationContext = Context.root(); + } + return new AgentContextWrapper(io.opentelemetry.context.Context.current(), applicationContext); + } + + @Override + public void close() throws Exception { + io.opentelemetry.context.ContextStorage agentStorage = + io.opentelemetry.context.ContextStorage.get(); + if (agentStorage instanceof AutoCloseable) { + ((AutoCloseable) agentStorage).close(); + } + } + + public static class AgentContextWrapper implements Context { + private final io.opentelemetry.context.Context agentContext; + private final Context applicationContext; + + public AgentContextWrapper( + io.opentelemetry.context.Context agentContext, Context applicationContext) { + this.agentContext = agentContext; + this.applicationContext = applicationContext; + } + + io.opentelemetry.context.Context toAgentContext() { + if (agentContext.get(APPLICATION_CONTEXT) == applicationContext) { + return agentContext; + } + return agentContext.with(APPLICATION_CONTEXT, applicationContext); + } + + @Override + public V get(ContextKey key) { + if (key == APPLICATION_SPAN_CONTEXT_KEY) { + io.opentelemetry.api.trace.Span agentSpan = agentContext.get(AGENT_SPAN_CONTEXT_KEY); + if (agentSpan == null) { + return null; + } + Span applicationSpan = Bridging.toApplication(agentSpan); + @SuppressWarnings("unchecked") + V value = (V) applicationSpan; + return value; + } + if (key == APPLICATION_BAGGAGE_CONTEXT_KEY) { + io.opentelemetry.api.baggage.Baggage agentBaggage = + agentContext.get(AGENT_BAGGAGE_CONTEXT_KEY); + if (agentBaggage == null) { + return null; + } + Baggage applicationBaggage = BaggageBridging.toApplication(agentBaggage); + @SuppressWarnings("unchecked") + V value = (V) applicationBaggage; + return value; + } + return applicationContext.get(key); + } + + @Override + public Context with(ContextKey k1, V v1) { + if (k1 == APPLICATION_SPAN_CONTEXT_KEY) { + Span applicationSpan = (Span) v1; + io.opentelemetry.api.trace.Span agentSpan = Bridging.toAgentOrNull(applicationSpan); + if (agentSpan == null) { + return this; + } + return new AgentContextWrapper( + agentContext.with(AGENT_SPAN_CONTEXT_KEY, agentSpan), applicationContext); + } + if (k1 == APPLICATION_BAGGAGE_CONTEXT_KEY) { + Baggage applicationBaggage = (Baggage) v1; + io.opentelemetry.api.baggage.Baggage agentBaggage = + BaggageBridging.toAgent(applicationBaggage); + return new AgentContextWrapper( + agentContext.with(AGENT_BAGGAGE_CONTEXT_KEY, agentBaggage), applicationContext); + } + return new AgentContextWrapper(agentContext, applicationContext.with(k1, v1)); + } + + @Override + public String toString() { + return "{agentContext=" + agentContext + ", applicationContext=" + applicationContext + "}"; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/propagation/ApplicationContextPropagators.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/propagation/ApplicationContextPropagators.java new file mode 100644 index 000000000..c2b1ad694 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/propagation/ApplicationContextPropagators.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.propagation; + +import application.io.opentelemetry.context.propagation.ContextPropagators; +import application.io.opentelemetry.context.propagation.TextMapPropagator; + +public class ApplicationContextPropagators implements ContextPropagators { + + private final ApplicationTextMapPropagator applicationTextMapPropagator; + + public ApplicationContextPropagators( + io.opentelemetry.context.propagation.ContextPropagators agentPropagators) { + applicationTextMapPropagator = + new ApplicationTextMapPropagator(agentPropagators.getTextMapPropagator()); + } + + @Override + public TextMapPropagator getTextMapPropagator() { + return applicationTextMapPropagator; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/propagation/ApplicationTextMapPropagator.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/propagation/ApplicationTextMapPropagator.java new file mode 100644 index 000000000..92369337c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/propagation/ApplicationTextMapPropagator.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.propagation; + +import application.io.opentelemetry.context.Context; +import application.io.opentelemetry.context.propagation.TextMapGetter; +import application.io.opentelemetry.context.propagation.TextMapPropagator; +import application.io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import java.util.Collection; + +class ApplicationTextMapPropagator implements TextMapPropagator { + + private final io.opentelemetry.context.propagation.TextMapPropagator agentTextMapPropagator; + + ApplicationTextMapPropagator( + io.opentelemetry.context.propagation.TextMapPropagator agentTextMapPropagator) { + this.agentTextMapPropagator = agentTextMapPropagator; + } + + @Override + public Collection fields() { + return agentTextMapPropagator.fields(); + } + + @Override + public Context extract( + Context applicationContext, C carrier, TextMapGetter applicationGetter) { + io.opentelemetry.context.Context agentContext = + AgentContextStorage.getAgentContext(applicationContext); + io.opentelemetry.context.Context agentUpdatedContext = + agentTextMapPropagator.extract(agentContext, carrier, new AgentGetter<>(applicationGetter)); + if (agentUpdatedContext == agentContext) { + return applicationContext; + } + return new AgentContextStorage.AgentContextWrapper(agentUpdatedContext, applicationContext); + } + + @Override + public void inject( + Context applicationContext, C carrier, TextMapSetter applicationSetter) { + io.opentelemetry.context.Context agentContext = + AgentContextStorage.getAgentContext(applicationContext); + agentTextMapPropagator.inject(agentContext, carrier, new AgentSetter<>(applicationSetter)); + } + + private static class AgentGetter + implements io.opentelemetry.context.propagation.TextMapGetter { + + private final TextMapGetter applicationGetter; + + AgentGetter(TextMapGetter applicationGetter) { + this.applicationGetter = applicationGetter; + } + + @Override + public Iterable keys(C c) { + return applicationGetter.keys(c); + } + + @Override + public String get(C carrier, String key) { + return applicationGetter.get(carrier, key); + } + } + + private static class AgentSetter + implements io.opentelemetry.context.propagation.TextMapSetter { + + private final TextMapSetter applicationSetter; + + AgentSetter(TextMapSetter applicationSetter) { + this.applicationSetter = applicationSetter; + } + + @Override + public void set(C carrier, String key, String value) { + applicationSetter.set(carrier, key, value); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationSpan.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationSpan.java new file mode 100644 index 000000000..9509f0e6e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationSpan.java @@ -0,0 +1,248 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace; + +import static io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging.toAgentOrNull; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.common.Attributes; +import application.io.opentelemetry.api.trace.Span; +import application.io.opentelemetry.api.trace.SpanBuilder; +import application.io.opentelemetry.api.trace.SpanContext; +import application.io.opentelemetry.api.trace.SpanKind; +import application.io.opentelemetry.api.trace.StatusCode; +import application.io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ApplicationSpan implements Span { + + private final io.opentelemetry.api.trace.Span agentSpan; + + ApplicationSpan(io.opentelemetry.api.trace.Span agentSpan) { + this.agentSpan = agentSpan; + } + + io.opentelemetry.api.trace.Span getAgentSpan() { + return agentSpan; + } + + @Override + public Span setAttribute(String key, String value) { + agentSpan.setAttribute(key, value); + return this; + } + + @Override + public Span setAttribute(String key, long value) { + agentSpan.setAttribute(key, value); + return this; + } + + @Override + public Span setAttribute(String key, double value) { + agentSpan.setAttribute(key, value); + return this; + } + + @Override + public Span setAttribute(String key, boolean value) { + agentSpan.setAttribute(key, value); + return this; + } + + @Override + public Span setAttribute(AttributeKey applicationKey, T value) { + io.opentelemetry.api.common.AttributeKey agentKey = Bridging.toAgent(applicationKey); + if (agentKey != null) { + agentSpan.setAttribute(agentKey, value); + } + return this; + } + + @Override + public Span addEvent(String name) { + agentSpan.addEvent(name); + return this; + } + + @Override + public Span addEvent(String name, long timestamp, TimeUnit unit) { + agentSpan.addEvent(name, timestamp, unit); + return this; + } + + @Override + public Span addEvent(String name, Attributes applicationAttributes) { + agentSpan.addEvent(name, Bridging.toAgent(applicationAttributes)); + return this; + } + + @Override + public Span addEvent( + String name, Attributes applicationAttributes, long timestamp, TimeUnit unit) { + agentSpan.addEvent(name, Bridging.toAgent(applicationAttributes), timestamp, unit); + return this; + } + + @Override + public Span setStatus(StatusCode status) { + agentSpan.setStatus(Bridging.toAgent(status)); + return this; + } + + @Override + public Span setStatus(StatusCode status, String description) { + agentSpan.setStatus(Bridging.toAgent(status), description); + return this; + } + + @Override + public Span recordException(Throwable throwable) { + agentSpan.recordException(throwable); + return this; + } + + @Override + public Span recordException(Throwable throwable, Attributes attributes) { + agentSpan.recordException(throwable, Bridging.toAgent(attributes)); + return this; + } + + @Override + public Span updateName(String name) { + agentSpan.updateName(name); + return this; + } + + @Override + public void end() { + agentSpan.end(); + } + + @Override + public void end(long timestamp, TimeUnit unit) { + agentSpan.end(timestamp, unit); + } + + @Override + public SpanContext getSpanContext() { + return Bridging.toApplication(agentSpan.getSpanContext()); + } + + @Override + public boolean isRecording() { + return agentSpan.isRecording(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ApplicationSpan)) { + return false; + } + ApplicationSpan other = (ApplicationSpan) obj; + return agentSpan.equals(other.agentSpan); + } + + @Override + public int hashCode() { + return agentSpan.hashCode(); + } + + static class Builder implements SpanBuilder { + + private static final Logger log = LoggerFactory.getLogger(Builder.class); + + private final io.opentelemetry.api.trace.SpanBuilder agentBuilder; + + Builder(io.opentelemetry.api.trace.SpanBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public SpanBuilder setParent(Context applicationContext) { + agentBuilder.setParent(AgentContextStorage.getAgentContext(applicationContext)); + return this; + } + + @Override + public SpanBuilder setNoParent() { + agentBuilder.setNoParent(); + return this; + } + + @Override + public SpanBuilder addLink(SpanContext applicationSpanContext) { + agentBuilder.addLink(Bridging.toAgent(applicationSpanContext)); + return this; + } + + @Override + public SpanBuilder addLink( + SpanContext applicationSpanContext, Attributes applicationAttributes) { + agentBuilder.addLink(Bridging.toAgent(applicationSpanContext)); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, String value) { + agentBuilder.setAttribute(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, long value) { + agentBuilder.setAttribute(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, double value) { + agentBuilder.setAttribute(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, boolean value) { + agentBuilder.setAttribute(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(AttributeKey applicationKey, T value) { + io.opentelemetry.api.common.AttributeKey agentKey = Bridging.toAgent(applicationKey); + if (agentKey != null) { + agentBuilder.setAttribute(agentKey, value); + } + return this; + } + + @Override + public SpanBuilder setSpanKind(SpanKind applicationSpanKind) { + io.opentelemetry.api.trace.SpanKind agentSpanKind = toAgentOrNull(applicationSpanKind); + if (agentSpanKind != null) { + agentBuilder.setSpanKind(agentSpanKind); + } + return this; + } + + @Override + public SpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { + agentBuilder.setStartTimestamp(startTimestamp, unit); + return this; + } + + @Override + public Span startSpan() { + return new ApplicationSpan(agentBuilder.startSpan()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationTracer.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationTracer.java new file mode 100644 index 000000000..04bb66da5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationTracer.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace; + +import application.io.opentelemetry.api.trace.SpanBuilder; +import application.io.opentelemetry.api.trace.Tracer; + +class ApplicationTracer implements Tracer { + + private final io.opentelemetry.api.trace.Tracer agentTracer; + + ApplicationTracer(io.opentelemetry.api.trace.Tracer agentTracer) { + this.agentTracer = agentTracer; + } + + @Override + public SpanBuilder spanBuilder(String spanName) { + return new ApplicationSpan.Builder(agentTracer.spanBuilder(spanName)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationTracerProvider.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationTracerProvider.java new file mode 100644 index 000000000..3c05153e2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationTracerProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace; + +import application.io.opentelemetry.api.trace.Tracer; +import application.io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.api.GlobalOpenTelemetry; + +public class ApplicationTracerProvider implements TracerProvider { + + private final io.opentelemetry.api.trace.TracerProvider agentTracerProvider; + + public ApplicationTracerProvider( + io.opentelemetry.api.trace.TracerProvider applicationOriginalTracerProvider) { + this.agentTracerProvider = applicationOriginalTracerProvider; + } + + @Override + public Tracer get(String instrumentationName) { + return new ApplicationTracer(agentTracerProvider.get(instrumentationName)); + } + + @Override + public Tracer get(String instrumentationName, String instrumentationVersion) { + return new ApplicationTracer( + GlobalOpenTelemetry.getTracerProvider().get(instrumentationName, instrumentationVersion)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/BridgedTraceFlags.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/BridgedTraceFlags.java new file mode 100644 index 000000000..6c7b545a8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/BridgedTraceFlags.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace; + +import application.io.opentelemetry.api.trace.TraceFlags; + +final class BridgedTraceFlags implements TraceFlags, io.opentelemetry.api.trace.TraceFlags { + + private static final BridgedTraceFlags[] INSTANCES = buildInstances(); + + static BridgedTraceFlags toAgent(TraceFlags applicationTraceFlags) { + if (applicationTraceFlags instanceof BridgedTraceFlags) { + return (BridgedTraceFlags) applicationTraceFlags; + } + return INSTANCES[applicationTraceFlags.asByte() & 255]; + } + + static BridgedTraceFlags fromAgent(io.opentelemetry.api.trace.TraceFlags agentTraceFlags) { + if (agentTraceFlags instanceof BridgedTraceFlags) { + return (BridgedTraceFlags) agentTraceFlags; + } + return INSTANCES[agentTraceFlags.asByte() & 255]; + } + + private final TraceFlags delegate; + + @Override + public boolean isSampled() { + return delegate.isSampled(); + } + + @Override + public String asHex() { + return delegate.asHex(); + } + + @Override + public byte asByte() { + return delegate.asByte(); + } + + private static BridgedTraceFlags[] buildInstances() { + BridgedTraceFlags[] instances = new BridgedTraceFlags[256]; + for (int i = 0; i < 256; i++) { + instances[i] = new BridgedTraceFlags(TraceFlags.fromByte((byte) i)); + } + return instances; + } + + private BridgedTraceFlags(TraceFlags delegate) { + this.delegate = delegate; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/Bridging.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/Bridging.java new file mode 100644 index 000000000..b1b104477 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/Bridging.java @@ -0,0 +1,162 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.common.Attributes; +import application.io.opentelemetry.api.trace.Span; +import application.io.opentelemetry.api.trace.SpanContext; +import application.io.opentelemetry.api.trace.SpanKind; +import application.io.opentelemetry.api.trace.StatusCode; +import application.io.opentelemetry.api.trace.TraceState; +import application.io.opentelemetry.api.trace.TraceStateBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class translates between the (unshaded) OpenTelemetry API that the application brings and + * the (shaded) OpenTelemetry API that is used by the agent. + * + *

"application.io.opentelemetry.*" refers to the (unshaded) OpenTelemetry API that the + * application brings (as those references will be translated during the build to remove the + * "application." prefix). + * + *

"io.opentelemetry.*" refers to the (shaded) OpenTelemetry API that is used by the agent (as + * those references will later be shaded). + * + *

Also see comments in this module's gradle file. + */ +// Our convention for accessing agent package +@SuppressWarnings("UnnecessarilyFullyQualified") +public class Bridging { + + private static final Logger log = LoggerFactory.getLogger(Bridging.class); + + public static Span toApplication(io.opentelemetry.api.trace.Span agentSpan) { + if (!agentSpan.getSpanContext().isValid()) { + // no need to wrap + return Span.getInvalid(); + } else { + return new ApplicationSpan(agentSpan); + } + } + + public static SpanContext toApplication(io.opentelemetry.api.trace.SpanContext agentContext) { + if (agentContext.isRemote()) { + return SpanContext.createFromRemoteParent( + agentContext.getTraceId(), + agentContext.getSpanId(), + BridgedTraceFlags.fromAgent(agentContext.getTraceFlags()), + toApplication(agentContext.getTraceState()),agentContext.getHeraContext()); + } else { + return SpanContext.create( + agentContext.getTraceId(), + agentContext.getSpanId(), + BridgedTraceFlags.fromAgent(agentContext.getTraceFlags()), + toApplication(agentContext.getTraceState()),agentContext.getHeraContext()); + } + } + + private static TraceState toApplication(io.opentelemetry.api.trace.TraceState agentTraceState) { + TraceStateBuilder applicationTraceState = TraceState.builder(); + agentTraceState.forEach(applicationTraceState::put); + return applicationTraceState.build(); + } + + public static io.opentelemetry.api.trace.Span toAgentOrNull(Span applicationSpan) { + if (!applicationSpan.getSpanContext().isValid()) { + // no need to wrap + return io.opentelemetry.api.trace.Span.getInvalid(); + } else if (applicationSpan instanceof ApplicationSpan) { + return ((ApplicationSpan) applicationSpan).getAgentSpan(); + } else { + return null; + } + } + + public static io.opentelemetry.api.trace.SpanKind toAgentOrNull(SpanKind applicationSpanKind) { + try { + return io.opentelemetry.api.trace.SpanKind.valueOf(applicationSpanKind.name()); + } catch (IllegalArgumentException e) { + log.debug("unexpected span kind: {}", applicationSpanKind.name()); + return null; + } + } + + public static io.opentelemetry.api.trace.SpanContext toAgent(SpanContext applicationContext) { + if (applicationContext.isRemote()) { + return io.opentelemetry.api.trace.SpanContext.createFromRemoteParent( + applicationContext.getTraceId(), + applicationContext.getSpanId(), + BridgedTraceFlags.toAgent(applicationContext.getTraceFlags()), + toAgent(applicationContext.getTraceState()),applicationContext.getHeraContext()); + } else { + return io.opentelemetry.api.trace.SpanContext.create( + applicationContext.getTraceId(), + applicationContext.getSpanId(), + BridgedTraceFlags.toAgent(applicationContext.getTraceFlags()), + toAgent(applicationContext.getTraceState()),applicationContext.getHeraContext()); + } + } + + public static io.opentelemetry.api.common.Attributes toAgent(Attributes applicationAttributes) { + io.opentelemetry.api.common.AttributesBuilder agentAttributes = + io.opentelemetry.api.common.Attributes.builder(); + applicationAttributes.forEach( + (key, value) -> { + @SuppressWarnings({"unchecked", "rawtypes"}) + io.opentelemetry.api.common.AttributeKey agentKey = toAgent(key); + if (agentKey != null) { + agentAttributes.put(agentKey, value); + } + }); + return agentAttributes.build(); + } + + // TODO optimize this by storing shaded AttributeKey inside of application AttributeKey instead of + // creating every time + @SuppressWarnings({"rawtypes"}) + public static io.opentelemetry.api.common.AttributeKey toAgent(AttributeKey applicationKey) { + switch (applicationKey.getType()) { + case STRING: + return io.opentelemetry.api.common.AttributeKey.stringKey(applicationKey.getKey()); + case BOOLEAN: + return io.opentelemetry.api.common.AttributeKey.booleanKey(applicationKey.getKey()); + case LONG: + return io.opentelemetry.api.common.AttributeKey.longKey(applicationKey.getKey()); + case DOUBLE: + return io.opentelemetry.api.common.AttributeKey.doubleKey(applicationKey.getKey()); + case STRING_ARRAY: + return io.opentelemetry.api.common.AttributeKey.stringArrayKey(applicationKey.getKey()); + case BOOLEAN_ARRAY: + return io.opentelemetry.api.common.AttributeKey.booleanArrayKey(applicationKey.getKey()); + case LONG_ARRAY: + return io.opentelemetry.api.common.AttributeKey.longArrayKey(applicationKey.getKey()); + case DOUBLE_ARRAY: + return io.opentelemetry.api.common.AttributeKey.doubleArrayKey(applicationKey.getKey()); + } + log.debug("unexpected attribute key type: {}", applicationKey.getType()); + return null; + } + + public static io.opentelemetry.api.trace.StatusCode toAgent(StatusCode applicationStatus) { + io.opentelemetry.api.trace.StatusCode agentCanonicalCode; + try { + agentCanonicalCode = io.opentelemetry.api.trace.StatusCode.valueOf(applicationStatus.name()); + } catch (IllegalArgumentException e) { + log.debug("unexpected status canonical code: {}", applicationStatus.name()); + return io.opentelemetry.api.trace.StatusCode.UNSET; + } + return agentCanonicalCode; + } + + private static io.opentelemetry.api.trace.TraceState toAgent(TraceState applicationTraceState) { + io.opentelemetry.api.trace.TraceStateBuilder agentTraceState = + io.opentelemetry.api.trace.TraceState.builder(); + applicationTraceState.forEach(agentTraceState::put); + return agentTraceState.build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy new file mode 100644 index 000000000..a609bfd04 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy @@ -0,0 +1,152 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.baggage.Baggage +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.context.ContextKey +import io.opentelemetry.extension.annotations.WithSpan +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicReference + +class ContextBridgeTest extends AgentInstrumentationSpecification { + + private static final ContextKey ANIMAL = ContextKey.named("animal") + + def "agent propagates application's context"() { + when: + def context = Context.current().with(ANIMAL, "cat") + def captured = new AtomicReference() + context.makeCurrent().withCloseable { + Executors.newSingleThreadExecutor().submit({ + captured.set(Context.current().get(ANIMAL)) + }).get() + } + + then: + captured.get() == "cat" + } + + def "application propagates agent's context"() { + when: + new Runnable() { + @WithSpan("test") + @Override + void run() { + // using @WithSpan above to make the agent generate a context + // and then using manual propagation below to verify that context can be propagated by user + def context = Context.current() + Context.root().makeCurrent().withCloseable { + Span.current().setAttribute("dog", "no") + context.makeCurrent().withCloseable { + Span.current().setAttribute("cat", "yes") + } + } + } + }.run() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + hasNoParent() + attributes { + "cat" "yes" + } + } + } + } + } + + def "agent propagates application's span"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + + def testSpan = tracer.spanBuilder("test").startSpan() + testSpan.makeCurrent().withCloseable { + Executors.newSingleThreadExecutor().submit({ + Span.current().setAttribute("cat", "yes") + }).get() + } + testSpan.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + hasNoParent() + attributes { + "cat" "yes" + } + } + } + } + } + + def "application propagates agent's span"() { + when: + new Runnable() { + @WithSpan("test") + @Override + void run() { + // using @WithSpan above to make the agent generate a span + // and then using manual propagation below to verify that span can be propagated by user + def span = Span.current() + Context.root().makeCurrent().withCloseable { + Span.current().setAttribute("dog", "no") + span.makeCurrent().withCloseable { + Span.current().setAttribute("cat", "yes") + } + } + } + }.run() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + hasNoParent() + attributes { + "cat" "yes" + } + } + } + } + } + + def "agent propagates application's baggage"() { + when: + def testBaggage = Baggage.builder().put("cat", "yes").build() + def ref = new AtomicReference() + def latch = new CountDownLatch(1) + testBaggage.makeCurrent().withCloseable { + Executors.newSingleThreadExecutor().submit({ + ref.set(Baggage.current()) + latch.countDown() + }).get() + } + + then: + latch.await() + ref.get().size() == 1 + ref.get().getEntryValue("cat") == "yes" + } + + // TODO (trask) + // more tests are needed here, not sure how to implement, probably need to write some test + // instrumentation to help test, similar to :testing-common:integration-tests + // + // * "application propagates agent's baggage" + // * "agent uses application's span" + // * "application uses agent's span" (this is covered above by "application propagates agent's span") + // * "agent uses application's baggage" + // * "application uses agent's baggage" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextTest.groovy b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextTest.groovy new file mode 100644 index 000000000..ba2d52a68 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextTest.groovy @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +class ContextTest extends AgentInstrumentationSpecification { + + def "Span.current() should return invalid"() { + when: + def span = Span.current() + + then: + !span.spanContext.valid + } + + def "Span.current() should return span"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").startSpan() + def scope = testSpan.makeCurrent() + def span = Span.current() + scope.close() + + then: + span == testSpan + } + + def "Span.fromContext should return invalid"() { + when: + def span = Span.fromContext(Context.current()) + + then: + !span.spanContext.valid + } + + def "getSpan should return span"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").startSpan() + def scope = testSpan.makeCurrent() + def span = Span.fromContext(Context.current()) + scope.close() + + then: + span == testSpan + } + + def "Span.fromContextOrNull should return null"() { + when: + def span = Span.fromContextOrNull(Context.current()) + + then: + span == null + } + + def "Span.fromContextOrNull should return span"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").startSpan() + def scope = testSpan.makeCurrent() + def span = Span.fromContextOrNull(Context.current()) + scope.close() + + then: + span == testSpan + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/TracerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/TracerTest.groovy new file mode 100644 index 000000000..907bf93f6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-1.0/javaagent/src/test/groovy/TracerTest.groovy @@ -0,0 +1,335 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.PRODUCER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.context.Scope +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class TracerTest extends AgentInstrumentationSpecification { + + def "capture span, kind, attributes, and status"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").setSpanKind(PRODUCER).startSpan() + testSpan.setAttribute("string", "1") + testSpan.setAttribute("long", 2) + testSpan.setAttribute("double", 3.0) + testSpan.setAttribute("boolean", true) + testSpan.setStatus(ERROR) + testSpan.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + kind PRODUCER + hasNoParent() + status ERROR + attributes { + "string" "1" + "long" 2 + "double" 3.0 + "boolean" true + } + } + } + } + } + + def "capture span with implicit parent using Tracer.withSpan()"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + Span parentSpan = tracer.spanBuilder("parent").startSpan() + Scope parentScope = Context.current().with(parentSpan).makeCurrent() + + def testSpan = tracer.spanBuilder("test").startSpan() + testSpan.end() + + parentSpan.end() + parentScope.close() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "test" + childOf span(0) + attributes { + } + } + } + } + } + + def "capture span with implicit parent using makeCurrent"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + Span parentSpan = tracer.spanBuilder("parent").startSpan() + Scope parentScope = parentSpan.makeCurrent() + + def testSpan = tracer.spanBuilder("test").startSpan() + testSpan.end() + + parentSpan.end() + parentScope.close() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "test" + childOf span(0) + attributes { + } + } + } + } + } + + def "capture span with implicit parent using TracingContextUtils.withSpan and makeCurrent"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + Span parentSpan = tracer.spanBuilder("parent").startSpan() + def parentContext = Context.current().with(parentSpan) + Scope parentScope = parentContext.makeCurrent() + + def testSpan = tracer.spanBuilder("test").startSpan() + testSpan.end() + + parentSpan.end() + parentScope.close() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "test" + childOf span(0) + attributes { + } + } + } + } + } + + def "capture span with explicit parent"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def parentSpan = tracer.spanBuilder("parent").startSpan() + def context = Context.root().with(parentSpan) + def testSpan = tracer.spanBuilder("test").setParent(context).startSpan() + testSpan.end() + parentSpan.end() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + span(1) { + name "test" + childOf span(0) + attributes { + } + } + } + } + } + + def "capture span with explicit no parent"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def parentSpan = tracer.spanBuilder("parent").startSpan() + def parentScope = parentSpan.makeCurrent() + def testSpan = tracer.spanBuilder("test").setNoParent().startSpan() + testSpan.end() + parentSpan.end() + parentScope.close() + + then: + assertTraces(2) { + traces.sort(orderByRootSpanName("parent", "test")) + trace(0, 1) { + span(0) { + name "parent" + hasNoParent() + attributes { + } + } + } + trace(1, 1) { + span(0) { + name "test" + hasNoParent() + attributes { + } + } + } + } + } + + def "capture name update"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").startSpan() + testSpan.updateName("test2") + testSpan.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test2" + hasNoParent() + attributes { + } + } + } + } + } + + def "capture exception()"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").startSpan() + testSpan.recordException(new IllegalStateException()) + testSpan.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + event(0) { + eventName("exception") + attributes { + "${SemanticAttributes.EXCEPTION_TYPE.key}" "java.lang.IllegalStateException" + "${SemanticAttributes.EXCEPTION_STACKTRACE.key}" String + } + } + attributes { + } + } + } + } + } + + def "capture exception with Attributes()"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").startSpan() + testSpan.recordException( + new IllegalStateException(), + Attributes.builder().put("dog", "bark").build()) + testSpan.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + event(0) { + eventName("exception") + attributes { + "${SemanticAttributes.EXCEPTION_TYPE.key}" "java.lang.IllegalStateException" + "${SemanticAttributes.EXCEPTION_STACKTRACE.key}" String + "dog" "bark" + } + } + attributes { + } + } + } + } + } + + def "capture name update using TracingContextUtils.getCurrentSpan()"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").startSpan() + def testScope = Context.current().with(testSpan).makeCurrent() + Span.current().updateName("test2") + testScope.close() + testSpan.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test2" + hasNoParent() + attributes { + } + } + } + } + } + + def "capture name update using TracingContextUtils.Span.fromContext(Context.current())"() { + when: + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").startSpan() + def testScope = Context.current().with(testSpan).makeCurrent() + Span.fromContext(Context.current()).updateName("test2") + testScope.close() + testSpan.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test2" + hasNoParent() + attributes { + } + } + } + } + } + + def "add wrapped span to context"() { + when: + // Lazy way to get a span context + def tracer = GlobalOpenTelemetry.getTracer("test") + def testSpan = tracer.spanBuilder("test").setSpanKind(PRODUCER).startSpan() + testSpan.end() + + def span = Span.wrap(testSpan.getSpanContext()) + def context = Context.current().with(span) + + then: + Span.fromContext(context).getSpanContext().getSpanId() == span.getSpanContext().getSpanId() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/opentelemetry-api-metrics-1.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/opentelemetry-api-metrics-1.0-javaagent.gradle new file mode 100644 index 000000000..ac7baec5e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/opentelemetry-api-metrics-1.0-javaagent.gradle @@ -0,0 +1,27 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + // this instrumentation needs to be able to reference both the OpenTelemetry API + // that is shaded in the bootstrap class loader (for sending telemetry to the agent), + // and the OpenTelemetry API that the user brings (in order to capture that telemetry) + // + // since (all) instrumentation already uses OpenTelemetry API for sending telemetry to the agent, + // this instrumentation uses a "temporarily shaded" OpenTelemetry API to represent the + // OpenTelemetry API that the user brings + // + // then later, after the OpenTelemetry API in the bootstrap class loader is shaded, + // the "temporarily shaded" OpenTelemetry API is unshaded, so that it will apply to the + // OpenTelemetry API that the user brings + // + // so in the code "application.io.opentelemetry.*" refers to the (unshaded) OpenTelemetry API that + // the application brings (as those references will be translated during the build to remove the + // "application." prefix) + // + // and in the code "io.opentelemetry.*" refers to the (shaded) OpenTelemetry API that is used by + // the agent (as those references will later be shaded) + compileOnly project(path: ':opentelemetry-api-shaded-for-instrumenting', configuration: 'shadow') + compileOnly "run.mone:opentelemetry-api-metrics" + + testImplementation "com.google.guava:guava" + testImplementation "run.mone:opentelemetry-sdk-metrics" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/OpenTelemetryApiMetricsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/OpenTelemetryApiMetricsInstrumentationModule.java new file mode 100644 index 000000000..48417bcb3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/OpenTelemetryApiMetricsInstrumentationModule.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class OpenTelemetryApiMetricsInstrumentationModule extends InstrumentationModule { + public OpenTelemetryApiMetricsInstrumentationModule() { + super("opentelemetry-metrics-api"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new OpenTelemetryMetricsInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/OpenTelemetryMetricsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/OpenTelemetryMetricsInstrumentation.java new file mode 100644 index 000000000..137663c12 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/OpenTelemetryMetricsInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge.ApplicationMeterProvider; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// Our convention for accessing agent package +@SuppressWarnings("UnnecessarilyFullyQualified") +public class OpenTelemetryMetricsInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.metrics.GlobalMeterProvider"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isStatic()).and(named("get")).and(takesArguments(0)), + OpenTelemetryMetricsInstrumentation.class.getName() + "$GetGlobalMetricsAdvice"); + } + + @SuppressWarnings("unused") + public static class GetGlobalMetricsAdvice { + + @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) + public static Object onEnter() { + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return(readOnly = false) + application.io.opentelemetry.api.metrics.MeterProvider metricsProvider) { + metricsProvider = ApplicationMeterProvider.INSTANCE; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationBatchRecorder.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationBatchRecorder.java new file mode 100644 index 000000000..beb0123ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationBatchRecorder.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.BatchRecorder; +import application.io.opentelemetry.api.metrics.DoubleCounter; +import application.io.opentelemetry.api.metrics.DoubleUpDownCounter; +import application.io.opentelemetry.api.metrics.DoubleValueRecorder; +import application.io.opentelemetry.api.metrics.LongCounter; +import application.io.opentelemetry.api.metrics.LongUpDownCounter; +import application.io.opentelemetry.api.metrics.LongValueRecorder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ApplicationBatchRecorder implements BatchRecorder { + + private static final Logger log = LoggerFactory.getLogger(ApplicationBatchRecorder.class); + + private final io.opentelemetry.api.metrics.BatchRecorder agentBatchRecorder; + + ApplicationBatchRecorder(io.opentelemetry.api.metrics.BatchRecorder agentBatchRecorder) { + this.agentBatchRecorder = agentBatchRecorder; + } + + @Override + public BatchRecorder put(LongValueRecorder measure, long value) { + if (measure instanceof ApplicationLongValueRecorder) { + agentBatchRecorder.put( + ((ApplicationLongValueRecorder) measure).getAgentLongValueRecorder(), value); + } else { + log.debug("unexpected measure: {}", measure); + } + return this; + } + + @Override + public BatchRecorder put(DoubleValueRecorder measure, double value) { + if (measure instanceof ApplicationDoubleValueRecorder) { + agentBatchRecorder.put( + ((ApplicationDoubleValueRecorder) measure).getAgentDoubleValueRecorder(), value); + } else { + log.debug("unexpected measure: {}", measure); + } + return this; + } + + @Override + public BatchRecorder put(LongCounter counter, long value) { + if (counter instanceof ApplicationLongCounter) { + agentBatchRecorder.put(((ApplicationLongCounter) counter).getAgentLongCounter(), value); + } else { + log.debug("unexpected counter: {}", counter); + } + return this; + } + + @Override + public BatchRecorder put(DoubleCounter counter, double value) { + if (counter instanceof ApplicationDoubleCounter) { + agentBatchRecorder.put(((ApplicationDoubleCounter) counter).getAgentDoubleCounter(), value); + } else { + log.debug("unexpected counter: {}", counter); + } + return this; + } + + @Override + public BatchRecorder put(LongUpDownCounter counter, long value) { + if (counter instanceof ApplicationLongUpDownCounter) { + agentBatchRecorder.put( + ((ApplicationLongUpDownCounter) counter).getAgentLongUpDownCounter(), value); + } else { + log.debug("unexpected counter: {}", counter); + } + return this; + } + + @Override + public BatchRecorder put(DoubleUpDownCounter counter, double value) { + if (counter instanceof ApplicationDoubleUpDownCounter) { + agentBatchRecorder.put( + ((ApplicationDoubleUpDownCounter) counter).getAgentDoubleUpDownCounter(), value); + } else { + log.debug("unexpected counter: {}", counter); + } + return this; + } + + @Override + public void record() { + agentBatchRecorder.record(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleCounter.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleCounter.java new file mode 100644 index 000000000..8a9e626b9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleCounter.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.BoundDoubleCounter; +import application.io.opentelemetry.api.metrics.DoubleCounter; +import application.io.opentelemetry.api.metrics.DoubleCounterBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; + +class ApplicationDoubleCounter implements DoubleCounter { + + private final io.opentelemetry.api.metrics.DoubleCounter agentDoubleCounter; + + ApplicationDoubleCounter(io.opentelemetry.api.metrics.DoubleCounter agentDoubleCounter) { + this.agentDoubleCounter = agentDoubleCounter; + } + + io.opentelemetry.api.metrics.DoubleCounter getAgentDoubleCounter() { + return agentDoubleCounter; + } + + @Override + public void add(double delta, Labels labels) { + agentDoubleCounter.add(delta, LabelBridging.toAgent(labels)); + } + + @Override + public void add(double v) { + agentDoubleCounter.add(v); + } + + @Override + public BoundDoubleCounter bind(Labels labels) { + return new BoundInstrument(agentDoubleCounter.bind(LabelBridging.toAgent(labels))); + } + + static class BoundInstrument implements BoundDoubleCounter { + + private final io.opentelemetry.api.metrics.BoundDoubleCounter agentBoundDoubleCounter; + + BoundInstrument(io.opentelemetry.api.metrics.BoundDoubleCounter agentBoundDoubleCounter) { + this.agentBoundDoubleCounter = agentBoundDoubleCounter; + } + + @Override + public void add(double delta) { + agentBoundDoubleCounter.add(delta); + } + + @Override + public void unbind() { + agentBoundDoubleCounter.unbind(); + } + } + + static class Builder implements DoubleCounterBuilder { + + private final io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder; + + Builder(io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public DoubleCounter build() { + return new ApplicationDoubleCounter(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleSumObserver.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleSumObserver.java new file mode 100644 index 000000000..4132ffac2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleSumObserver.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.DoubleSumObserver; +import application.io.opentelemetry.api.metrics.DoubleSumObserverBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; +import java.util.function.Consumer; + +// For observers, which have no API, there might be a better pattern than wrapping. +@SuppressWarnings("FieldCanBeLocal") +class ApplicationDoubleSumObserver implements DoubleSumObserver { + + private final io.opentelemetry.api.metrics.DoubleSumObserver agentDoubleSumObserver; + + protected ApplicationDoubleSumObserver( + io.opentelemetry.api.metrics.DoubleSumObserver agentDoubleSumObserver) { + this.agentDoubleSumObserver = agentDoubleSumObserver; + } + + static class AgentResultDoubleSumObserver + implements Consumer { + + private final Consumer metricUpdater; + + protected AgentResultDoubleSumObserver(Consumer metricUpdater) { + this.metricUpdater = metricUpdater; + } + + @Override + public void accept(io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult result) { + metricUpdater.accept(new ApplicationResultDoubleSumObserver(result)); + } + } + + static class ApplicationResultDoubleSumObserver implements DoubleResult { + + private final io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult + agentResultDoubleSumObserver; + + public ApplicationResultDoubleSumObserver( + io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult + agentResultDoubleSumObserver) { + this.agentResultDoubleSumObserver = agentResultDoubleSumObserver; + } + + @Override + public void observe(double value, Labels labels) { + agentResultDoubleSumObserver.observe(value, LabelBridging.toAgent(labels)); + } + } + + static class Builder implements DoubleSumObserverBuilder { + + private final io.opentelemetry.api.metrics.DoubleSumObserverBuilder agentBuilder; + + protected Builder(io.opentelemetry.api.metrics.DoubleSumObserverBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public Builder setUpdater(Consumer callback) { + agentBuilder.setUpdater( + result -> + callback.accept((sum, labels) -> result.observe(sum, LabelBridging.toAgent(labels)))); + return this; + } + + @Override + public DoubleSumObserver build() { + return new ApplicationDoubleSumObserver(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleUpDownCounter.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleUpDownCounter.java new file mode 100644 index 000000000..ef7d802ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleUpDownCounter.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.BoundDoubleUpDownCounter; +import application.io.opentelemetry.api.metrics.DoubleUpDownCounter; +import application.io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; + +class ApplicationDoubleUpDownCounter implements DoubleUpDownCounter { + + private final io.opentelemetry.api.metrics.DoubleUpDownCounter agentDoubleUpDownCounter; + + ApplicationDoubleUpDownCounter( + io.opentelemetry.api.metrics.DoubleUpDownCounter agentDoubleUpDownCounter) { + this.agentDoubleUpDownCounter = agentDoubleUpDownCounter; + } + + io.opentelemetry.api.metrics.DoubleUpDownCounter getAgentDoubleUpDownCounter() { + return agentDoubleUpDownCounter; + } + + @Override + public void add(double delta, Labels labels) { + agentDoubleUpDownCounter.add(delta, LabelBridging.toAgent(labels)); + } + + @Override + public void add(double v) { + agentDoubleUpDownCounter.add(v); + } + + @Override + public BoundDoubleUpDownCounter bind(Labels labels) { + return new BoundInstrument(agentDoubleUpDownCounter.bind(LabelBridging.toAgent(labels))); + } + + static class BoundInstrument implements BoundDoubleUpDownCounter { + + private final io.opentelemetry.api.metrics.BoundDoubleUpDownCounter + agentBoundDoubleUpDownCounter; + + BoundInstrument( + io.opentelemetry.api.metrics.BoundDoubleUpDownCounter agentBoundDoubleUpDownCounter) { + this.agentBoundDoubleUpDownCounter = agentBoundDoubleUpDownCounter; + } + + @Override + public void add(double delta) { + agentBoundDoubleUpDownCounter.add(delta); + } + + @Override + public void unbind() { + agentBoundDoubleUpDownCounter.unbind(); + } + } + + static class Builder implements DoubleUpDownCounterBuilder { + + private final io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder agentBuilder; + + Builder(io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public DoubleUpDownCounter build() { + return new ApplicationDoubleUpDownCounter(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleUpDownSumObserver.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleUpDownSumObserver.java new file mode 100644 index 000000000..79e56cd4e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleUpDownSumObserver.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.DoubleUpDownSumObserver; +import application.io.opentelemetry.api.metrics.DoubleUpDownSumObserverBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; +import java.util.function.Consumer; + +// For observers, which have no API, there might be a better pattern than wrapping. +@SuppressWarnings("FieldCanBeLocal") +class ApplicationDoubleUpDownSumObserver implements DoubleUpDownSumObserver { + + private final io.opentelemetry.api.metrics.DoubleUpDownSumObserver agentDoubleUpDownSumObserver; + + protected ApplicationDoubleUpDownSumObserver( + io.opentelemetry.api.metrics.DoubleUpDownSumObserver agentDoubleUpDownSumObserver) { + this.agentDoubleUpDownSumObserver = agentDoubleUpDownSumObserver; + } + + static class AgentResultDoubleUpDownSumObserver + implements Consumer { + + private final Consumer metricUpdater; + + protected AgentResultDoubleUpDownSumObserver(Consumer metricUpdater) { + this.metricUpdater = metricUpdater; + } + + @Override + public void accept(io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult result) { + metricUpdater.accept(new ApplicationResultDoubleUpDownSumObserver(result)); + } + } + + static class ApplicationResultDoubleUpDownSumObserver implements DoubleResult { + + private final io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult + agentResultDoubleUpDownSumObserver; + + public ApplicationResultDoubleUpDownSumObserver( + io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult + agentResultDoubleUpDownSumObserver) { + this.agentResultDoubleUpDownSumObserver = agentResultDoubleUpDownSumObserver; + } + + @Override + public void observe(double value, Labels labels) { + agentResultDoubleUpDownSumObserver.observe(value, LabelBridging.toAgent(labels)); + } + } + + static class Builder implements DoubleUpDownSumObserverBuilder { + + private final io.opentelemetry.api.metrics.DoubleUpDownSumObserverBuilder agentBuilder; + + protected Builder(io.opentelemetry.api.metrics.DoubleUpDownSumObserverBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public Builder setUpdater(Consumer callback) { + agentBuilder.setUpdater( + result -> + callback.accept((sum, labels) -> result.observe(sum, LabelBridging.toAgent(labels)))); + return this; + } + + @Override + public DoubleUpDownSumObserver build() { + return new ApplicationDoubleUpDownSumObserver(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleValueObserver.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleValueObserver.java new file mode 100644 index 000000000..3ad425d2b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleValueObserver.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.DoubleValueObserver; +import application.io.opentelemetry.api.metrics.DoubleValueObserverBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; +import java.util.function.Consumer; + +// For observers, which have no API, there might be a better pattern than wrapping. +@SuppressWarnings("FieldCanBeLocal") +class ApplicationDoubleValueObserver implements DoubleValueObserver { + + private final io.opentelemetry.api.metrics.DoubleValueObserver agentDoubleValueObserver; + + protected ApplicationDoubleValueObserver( + io.opentelemetry.api.metrics.DoubleValueObserver agentDoubleValueObserver) { + this.agentDoubleValueObserver = agentDoubleValueObserver; + } + + static class AgentResultDoubleValueObserver + implements Consumer { + + private final Consumer metricUpdater; + + protected AgentResultDoubleValueObserver(Consumer metricUpdater) { + this.metricUpdater = metricUpdater; + } + + @Override + public void accept(io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult result) { + metricUpdater.accept(new ApplicationResultDoubleValueObserver(result)); + } + } + + static class ApplicationResultDoubleValueObserver implements DoubleResult { + + private final io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult + agentResultDoubleValueObserver; + + public ApplicationResultDoubleValueObserver( + io.opentelemetry.api.metrics.AsynchronousInstrument.DoubleResult + agentResultDoubleValueObserver) { + this.agentResultDoubleValueObserver = agentResultDoubleValueObserver; + } + + @Override + public void observe(double value, Labels labels) { + agentResultDoubleValueObserver.observe(value, LabelBridging.toAgent(labels)); + } + } + + static class Builder implements DoubleValueObserverBuilder { + + private final io.opentelemetry.api.metrics.DoubleValueObserverBuilder agentBuilder; + + protected Builder(io.opentelemetry.api.metrics.DoubleValueObserverBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public Builder setUpdater(Consumer callback) { + agentBuilder.setUpdater( + result -> + callback.accept((sum, labels) -> result.observe(sum, LabelBridging.toAgent(labels)))); + return this; + } + + @Override + public DoubleValueObserver build() { + return new ApplicationDoubleValueObserver(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleValueRecorder.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleValueRecorder.java new file mode 100644 index 000000000..13397ecde --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationDoubleValueRecorder.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.BoundDoubleValueRecorder; +import application.io.opentelemetry.api.metrics.DoubleValueRecorder; +import application.io.opentelemetry.api.metrics.DoubleValueRecorderBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; + +class ApplicationDoubleValueRecorder implements DoubleValueRecorder { + + private final io.opentelemetry.api.metrics.DoubleValueRecorder agentDoubleValueRecorder; + + protected ApplicationDoubleValueRecorder( + io.opentelemetry.api.metrics.DoubleValueRecorder agentDoubleValueRecorder) { + this.agentDoubleValueRecorder = agentDoubleValueRecorder; + } + + protected io.opentelemetry.api.metrics.DoubleValueRecorder getAgentDoubleValueRecorder() { + return agentDoubleValueRecorder; + } + + @Override + public void record(double delta, Labels labels) { + agentDoubleValueRecorder.record(delta, LabelBridging.toAgent(labels)); + } + + @Override + public void record(double v) { + agentDoubleValueRecorder.record(v); + } + + @Override + public BoundDoubleValueRecorder bind(Labels labels) { + return new BoundInstrument(agentDoubleValueRecorder.bind(LabelBridging.toAgent(labels))); + } + + static class BoundInstrument implements BoundDoubleValueRecorder { + + private final io.opentelemetry.api.metrics.BoundDoubleValueRecorder agentBoundDoubleMeasure; + + public BoundInstrument( + io.opentelemetry.api.metrics.BoundDoubleValueRecorder agentBoundDoubleMeasure) { + this.agentBoundDoubleMeasure = agentBoundDoubleMeasure; + } + + @Override + public void record(double delta) { + agentBoundDoubleMeasure.record(delta); + } + + @Override + public void unbind() { + agentBoundDoubleMeasure.unbind(); + } + } + + static class Builder implements DoubleValueRecorderBuilder { + + private final io.opentelemetry.api.metrics.DoubleValueRecorderBuilder agentBuilder; + + public Builder(io.opentelemetry.api.metrics.DoubleValueRecorderBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public DoubleValueRecorder build() { + return new ApplicationDoubleValueRecorder(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongCounter.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongCounter.java new file mode 100644 index 000000000..a2a5abd18 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongCounter.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.BoundLongCounter; +import application.io.opentelemetry.api.metrics.LongCounter; +import application.io.opentelemetry.api.metrics.LongCounterBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; + +class ApplicationLongCounter implements LongCounter { + + private final io.opentelemetry.api.metrics.LongCounter agentLongCounter; + + ApplicationLongCounter(io.opentelemetry.api.metrics.LongCounter agentLongCounter) { + this.agentLongCounter = agentLongCounter; + } + + io.opentelemetry.api.metrics.LongCounter getAgentLongCounter() { + return agentLongCounter; + } + + @Override + public void add(long delta, Labels labels) { + agentLongCounter.add(delta, LabelBridging.toAgent(labels)); + } + + @Override + public void add(long l) { + agentLongCounter.add(l); + } + + @Override + public BoundLongCounter bind(Labels labels) { + return new BoundInstrument(agentLongCounter.bind(LabelBridging.toAgent(labels))); + } + + static class BoundInstrument implements BoundLongCounter { + + private final io.opentelemetry.api.metrics.BoundLongCounter agentBoundLongCounter; + + BoundInstrument(io.opentelemetry.api.metrics.BoundLongCounter agentBoundLongCounter) { + this.agentBoundLongCounter = agentBoundLongCounter; + } + + @Override + public void add(long delta) { + agentBoundLongCounter.add(delta); + } + + @Override + public void unbind() { + agentBoundLongCounter.unbind(); + } + } + + static class Builder implements LongCounterBuilder { + + private final io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder; + + Builder(io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public LongCounter build() { + return new ApplicationLongCounter(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongSumObserver.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongSumObserver.java new file mode 100644 index 000000000..18afb1dfc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongSumObserver.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.LongSumObserver; +import application.io.opentelemetry.api.metrics.LongSumObserverBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; +import java.util.function.Consumer; + +// For observers, which have no API, there might be a better pattern than wrapping. +@SuppressWarnings("FieldCanBeLocal") +class ApplicationLongSumObserver implements LongSumObserver { + + private final io.opentelemetry.api.metrics.LongSumObserver agentLongSumObserver; + + protected ApplicationLongSumObserver( + io.opentelemetry.api.metrics.LongSumObserver agentLongSumObserver) { + this.agentLongSumObserver = agentLongSumObserver; + } + + static class AgentResultLongSumObserver + implements Consumer { + + private final Consumer metricUpdater; + + protected AgentResultLongSumObserver(Consumer metricUpdater) { + this.metricUpdater = metricUpdater; + } + + @Override + public void accept(io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult result) { + metricUpdater.accept(new ApplicationResultLongSumObserver(result)); + } + } + + static class ApplicationResultLongSumObserver implements LongResult { + + private final io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult + agentResultLongSumObserver; + + public ApplicationResultLongSumObserver( + io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult agentResultLongSumObserver) { + this.agentResultLongSumObserver = agentResultLongSumObserver; + } + + @Override + public void observe(long value, Labels labels) { + agentResultLongSumObserver.observe(value, LabelBridging.toAgent(labels)); + } + } + + static class Builder implements LongSumObserverBuilder { + + private final io.opentelemetry.api.metrics.LongSumObserverBuilder agentBuilder; + + protected Builder(io.opentelemetry.api.metrics.LongSumObserverBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public LongSumObserverBuilder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public LongSumObserverBuilder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public LongSumObserverBuilder setUpdater(Consumer callback) { + agentBuilder.setUpdater( + result -> + callback.accept((sum, labels) -> result.observe(sum, LabelBridging.toAgent(labels)))); + return this; + } + + @Override + public LongSumObserver build() { + return new ApplicationLongSumObserver(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongUpDownCounter.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongUpDownCounter.java new file mode 100644 index 000000000..aba44c40c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongUpDownCounter.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.BoundLongUpDownCounter; +import application.io.opentelemetry.api.metrics.LongUpDownCounter; +import application.io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; + +class ApplicationLongUpDownCounter implements LongUpDownCounter { + + private final io.opentelemetry.api.metrics.LongUpDownCounter agentLongUpDownCounter; + + ApplicationLongUpDownCounter( + io.opentelemetry.api.metrics.LongUpDownCounter agentLongUpDownCounter) { + this.agentLongUpDownCounter = agentLongUpDownCounter; + } + + io.opentelemetry.api.metrics.LongUpDownCounter getAgentLongUpDownCounter() { + return agentLongUpDownCounter; + } + + @Override + public void add(long delta, Labels labels) { + agentLongUpDownCounter.add(delta, LabelBridging.toAgent(labels)); + } + + @Override + public void add(long l) { + agentLongUpDownCounter.add(l); + } + + @Override + public BoundLongUpDownCounter bind(Labels labels) { + return new BoundInstrument(agentLongUpDownCounter.bind(LabelBridging.toAgent(labels))); + } + + static class BoundInstrument implements BoundLongUpDownCounter { + + private final io.opentelemetry.api.metrics.BoundLongUpDownCounter agentBoundLongUpDownCounter; + + BoundInstrument( + io.opentelemetry.api.metrics.BoundLongUpDownCounter agentBoundLongUpDownCounter) { + this.agentBoundLongUpDownCounter = agentBoundLongUpDownCounter; + } + + @Override + public void add(long delta) { + agentBoundLongUpDownCounter.add(delta); + } + + @Override + public void unbind() { + agentBoundLongUpDownCounter.unbind(); + } + } + + static class Builder implements LongUpDownCounterBuilder { + + private final io.opentelemetry.api.metrics.LongUpDownCounterBuilder agentBuilder; + + Builder(io.opentelemetry.api.metrics.LongUpDownCounterBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public LongUpDownCounter build() { + return new ApplicationLongUpDownCounter(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongUpDownSumObserver.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongUpDownSumObserver.java new file mode 100644 index 000000000..f97fa4636 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongUpDownSumObserver.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.LongUpDownSumObserver; +import application.io.opentelemetry.api.metrics.LongUpDownSumObserverBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; +import java.util.function.Consumer; + +// For observers, which have no API, there might be a better pattern than wrapping. +@SuppressWarnings("FieldCanBeLocal") +class ApplicationLongUpDownSumObserver implements LongUpDownSumObserver { + + private final io.opentelemetry.api.metrics.LongUpDownSumObserver agentLongUpDownSumObserver; + + protected ApplicationLongUpDownSumObserver( + io.opentelemetry.api.metrics.LongUpDownSumObserver agentLongUpDownSumObserver) { + this.agentLongUpDownSumObserver = agentLongUpDownSumObserver; + } + + static class AgentResultLongUpDownSumObserver + implements Consumer { + + private final Consumer metricUpdater; + + protected AgentResultLongUpDownSumObserver(Consumer metricUpdater) { + this.metricUpdater = metricUpdater; + } + + @Override + public void accept(io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult result) { + metricUpdater.accept(new ApplicationResultLongUpDownSumObserver(result)); + } + } + + static class ApplicationResultLongUpDownSumObserver implements LongResult { + + private final io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult + agentResultLongUpDownSumObserver; + + public ApplicationResultLongUpDownSumObserver( + io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult + agentResultLongUpDownSumObserver) { + this.agentResultLongUpDownSumObserver = agentResultLongUpDownSumObserver; + } + + @Override + public void observe(long value, Labels labels) { + agentResultLongUpDownSumObserver.observe(value, LabelBridging.toAgent(labels)); + } + } + + static class Builder implements LongUpDownSumObserverBuilder { + + private final io.opentelemetry.api.metrics.LongUpDownSumObserverBuilder agentBuilder; + + protected Builder(io.opentelemetry.api.metrics.LongUpDownSumObserverBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public Builder setUpdater(Consumer callback) { + agentBuilder.setUpdater( + result -> + callback.accept((sum, labels) -> result.observe(sum, LabelBridging.toAgent(labels)))); + return this; + } + + @Override + public LongUpDownSumObserver build() { + return new ApplicationLongUpDownSumObserver(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongValueObserver.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongValueObserver.java new file mode 100644 index 000000000..a5a0f57b1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongValueObserver.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.LongValueObserver; +import application.io.opentelemetry.api.metrics.LongValueObserverBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; +import java.util.function.Consumer; + +// For observers, which have no API, there might be a better pattern than wrapping. +@SuppressWarnings("FieldCanBeLocal") +class ApplicationLongValueObserver implements LongValueObserver { + + private final io.opentelemetry.api.metrics.LongValueObserver agentLongValueObserver; + + public ApplicationLongValueObserver( + io.opentelemetry.api.metrics.LongValueObserver agentLongValueObserver) { + this.agentLongValueObserver = agentLongValueObserver; + } + + public static class AgentResultLongValueObserver + implements Consumer { + + private final Consumer metricUpdater; + + public AgentResultLongValueObserver(Consumer metricUpdater) { + this.metricUpdater = metricUpdater; + } + + @Override + public void accept(io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult result) { + metricUpdater.accept(new ApplicationResultLongValueObserver(result)); + } + } + + public static class ApplicationResultLongValueObserver implements LongResult { + + private final io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult + agentResultLongValueObserver; + + public ApplicationResultLongValueObserver( + io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult + agentResultLongValueObserver) { + this.agentResultLongValueObserver = agentResultLongValueObserver; + } + + @Override + public void observe(long value, Labels labels) { + agentResultLongValueObserver.observe(value, LabelBridging.toAgent(labels)); + } + } + + static class Builder implements LongValueObserverBuilder { + + private final io.opentelemetry.api.metrics.LongValueObserverBuilder agentBuilder; + + public Builder(io.opentelemetry.api.metrics.LongValueObserverBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public Builder setUpdater(Consumer callback) { + agentBuilder.setUpdater( + result -> + callback.accept((sum, labels) -> result.observe(sum, LabelBridging.toAgent(labels)))); + return this; + } + + @Override + public LongValueObserver build() { + return new ApplicationLongValueObserver(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongValueRecorder.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongValueRecorder.java new file mode 100644 index 000000000..411a5d60c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationLongValueRecorder.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.BoundLongValueRecorder; +import application.io.opentelemetry.api.metrics.LongValueRecorder; +import application.io.opentelemetry.api.metrics.LongValueRecorderBuilder; +import application.io.opentelemetry.api.metrics.common.Labels; + +class ApplicationLongValueRecorder implements LongValueRecorder { + + private final io.opentelemetry.api.metrics.LongValueRecorder agentLongValueRecorder; + + protected ApplicationLongValueRecorder( + io.opentelemetry.api.metrics.LongValueRecorder agentLongValueRecorder) { + this.agentLongValueRecorder = agentLongValueRecorder; + } + + public io.opentelemetry.api.metrics.LongValueRecorder getAgentLongValueRecorder() { + return agentLongValueRecorder; + } + + @Override + public void record(long delta, Labels labels) { + agentLongValueRecorder.record(delta, LabelBridging.toAgent(labels)); + } + + @Override + public void record(long l) { + agentLongValueRecorder.record(l); + } + + @Override + public BoundLongValueRecorder bind(Labels labels) { + return new BoundInstrument(agentLongValueRecorder.bind(LabelBridging.toAgent(labels))); + } + + static class BoundInstrument implements BoundLongValueRecorder { + + private final io.opentelemetry.api.metrics.BoundLongValueRecorder agentBoundLongValueRecorder; + + protected BoundInstrument( + io.opentelemetry.api.metrics.BoundLongValueRecorder agentBoundLongValueRecorder) { + this.agentBoundLongValueRecorder = agentBoundLongValueRecorder; + } + + @Override + public void record(long delta) { + agentBoundLongValueRecorder.record(delta); + } + + @Override + public void unbind() { + agentBoundLongValueRecorder.unbind(); + } + } + + static class Builder implements LongValueRecorderBuilder { + + private final io.opentelemetry.api.metrics.LongValueRecorderBuilder agentBuilder; + + protected Builder(io.opentelemetry.api.metrics.LongValueRecorderBuilder agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public Builder setDescription(String description) { + agentBuilder.setDescription(description); + return this; + } + + @Override + public Builder setUnit(String unit) { + agentBuilder.setUnit(unit); + return this; + } + + @Override + public LongValueRecorder build() { + return new ApplicationLongValueRecorder(agentBuilder.build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationMeter.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationMeter.java new file mode 100644 index 000000000..28e6391e5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationMeter.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.BatchRecorder; +import application.io.opentelemetry.api.metrics.DoubleCounterBuilder; +import application.io.opentelemetry.api.metrics.DoubleSumObserverBuilder; +import application.io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import application.io.opentelemetry.api.metrics.DoubleUpDownSumObserverBuilder; +import application.io.opentelemetry.api.metrics.DoubleValueObserverBuilder; +import application.io.opentelemetry.api.metrics.DoubleValueRecorderBuilder; +import application.io.opentelemetry.api.metrics.LongCounterBuilder; +import application.io.opentelemetry.api.metrics.LongSumObserverBuilder; +import application.io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import application.io.opentelemetry.api.metrics.LongUpDownSumObserverBuilder; +import application.io.opentelemetry.api.metrics.LongValueObserverBuilder; +import application.io.opentelemetry.api.metrics.LongValueRecorderBuilder; +import application.io.opentelemetry.api.metrics.Meter; + +class ApplicationMeter implements Meter { + + private final io.opentelemetry.api.metrics.Meter agentMeter; + + ApplicationMeter(io.opentelemetry.api.metrics.Meter agentMeter) { + this.agentMeter = agentMeter; + } + + @Override + public DoubleCounterBuilder doubleCounterBuilder(String name) { + return new ApplicationDoubleCounter.Builder(agentMeter.doubleCounterBuilder(name)); + } + + @Override + public LongCounterBuilder longCounterBuilder(String name) { + return new ApplicationLongCounter.Builder(agentMeter.longCounterBuilder(name)); + } + + @Override + public DoubleUpDownCounterBuilder doubleUpDownCounterBuilder(String name) { + return new ApplicationDoubleUpDownCounter.Builder(agentMeter.doubleUpDownCounterBuilder(name)); + } + + @Override + public LongUpDownCounterBuilder longUpDownCounterBuilder(String name) { + return new ApplicationLongUpDownCounter.Builder(agentMeter.longUpDownCounterBuilder(name)); + } + + @Override + public DoubleValueRecorderBuilder doubleValueRecorderBuilder(String name) { + return new ApplicationDoubleValueRecorder.Builder(agentMeter.doubleValueRecorderBuilder(name)); + } + + @Override + public LongValueRecorderBuilder longValueRecorderBuilder(String name) { + return new ApplicationLongValueRecorder.Builder(agentMeter.longValueRecorderBuilder(name)); + } + + @Override + public DoubleSumObserverBuilder doubleSumObserverBuilder(String name) { + return new ApplicationDoubleSumObserver.Builder(agentMeter.doubleSumObserverBuilder(name)); + } + + @Override + public LongSumObserverBuilder longSumObserverBuilder(String name) { + return new ApplicationLongSumObserver.Builder(agentMeter.longSumObserverBuilder(name)); + } + + @Override + public DoubleUpDownSumObserverBuilder doubleUpDownSumObserverBuilder(String name) { + return new ApplicationDoubleUpDownSumObserver.Builder( + agentMeter.doubleUpDownSumObserverBuilder(name)); + } + + @Override + public LongUpDownSumObserverBuilder longUpDownSumObserverBuilder(String name) { + return new ApplicationLongUpDownSumObserver.Builder( + agentMeter.longUpDownSumObserverBuilder(name)); + } + + @Override + public DoubleValueObserverBuilder doubleValueObserverBuilder(String name) { + return new ApplicationDoubleValueObserver.Builder(agentMeter.doubleValueObserverBuilder(name)); + } + + @Override + public LongValueObserverBuilder longValueObserverBuilder(String name) { + return new ApplicationLongValueObserver.Builder(agentMeter.longValueObserverBuilder(name)); + } + + @Override + public BatchRecorder newBatchRecorder(String... keyValuePairs) { + return new ApplicationBatchRecorder(agentMeter.newBatchRecorder(keyValuePairs)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationMeterProvider.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationMeterProvider.java new file mode 100644 index 000000000..761b99a49 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/ApplicationMeterProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import application.io.opentelemetry.api.metrics.Meter; +import application.io.opentelemetry.api.metrics.MeterProvider; + +// Our convention for accessing agent packages. +@SuppressWarnings("UnnecessarilyFullyQualified") +public class ApplicationMeterProvider implements MeterProvider { + + public static final MeterProvider INSTANCE = new ApplicationMeterProvider(); + + private final io.opentelemetry.api.metrics.MeterProvider agentMeterProvider; + + public ApplicationMeterProvider() { + this.agentMeterProvider = io.opentelemetry.api.metrics.GlobalMeterProvider.get(); + } + + @Override + public Meter get(String instrumentationName) { + return new ApplicationMeter(agentMeterProvider.get(instrumentationName)); + } + + @Override + public Meter get(String instrumentationName, String instrumentationVersion) { + return new ApplicationMeter( + agentMeterProvider.get(instrumentationName, instrumentationVersion)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/LabelBridging.java b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/LabelBridging.java new file mode 100644 index 000000000..d31fb2585 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/metrics/bridge/LabelBridging.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.metrics.bridge; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.common.LabelsBuilder; + +/** + * This class converts between Labels class that application brings and Labels class that agent + * uses. + * + *

TODO probably not the most performant solution... + */ +public class LabelBridging { + + public static Labels toAgent( + application.io.opentelemetry.api.metrics.common.Labels applicationLabels) { + LabelsBuilder builder = Labels.builder(); + applicationLabels.forEach(builder::put); + return builder.build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/test/groovy/MeterTest.groovy b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/test/groovy/MeterTest.groovy new file mode 100644 index 000000000..d126e4750 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/opentelemetry-api-metrics-1.0/javaagent/src/test/groovy/MeterTest.groovy @@ -0,0 +1,269 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.sdk.metrics.data.MetricDataType.DOUBLE_GAUGE +import static io.opentelemetry.sdk.metrics.data.MetricDataType.DOUBLE_SUM +import static io.opentelemetry.sdk.metrics.data.MetricDataType.SUMMARY +import static java.util.concurrent.TimeUnit.SECONDS + +import com.google.common.base.Stopwatch +import io.opentelemetry.api.metrics.AsynchronousInstrument +import io.opentelemetry.api.metrics.GlobalMeterProvider +import io.opentelemetry.api.metrics.common.Labels +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.sdk.metrics.data.MetricData +import io.opentelemetry.sdk.metrics.data.PointData +import java.util.function.Consumer + +class MeterTest extends AgentInstrumentationSpecification { + + def "test counter #builderMethod bound=#bind"() { + given: + // meters are global, and no way to unregister them, so tests use random name to avoid each other + def instrumentationName = "test" + new Random().nextLong() + + when: + def meter = GlobalMeterProvider.getMeter(instrumentationName, "1.2.3") + def instrument = meter."$builderMethod"("test") + .setDescription("d") + .setUnit("u") + .build() + if (bind) { + instrument = instrument.bind(Labels.empty()) + } + if (bind) { + instrument.add(value1) + instrument.add(value2) + } else { + instrument.add(value1, Labels.of("q", "r")) + instrument.add(value2, Labels.of("q", "r")) + } + + then: + def metricData = findMetric(instrumentationName, "test") + metricData != null + metricData.description == "d" + metricData.unit == "u" + metricData.type == expectedType + metricData.instrumentationLibraryInfo.name == instrumentationName + metricData.instrumentationLibraryInfo.version == "1.2.3" + points(metricData).size() == 1 + def point = points(metricData).iterator().next() + if (bind) { + point.labels == Labels.of("w", "x", "y", "z") + } else { + point.labels == Labels.of("q", "r") + } + point.value == expectedValue + + where: + builderMethod | bind | value1 | value2 | expectedValue | expectedType + "longCounterBuilder" | false | 5 | 6 | 11 | DOUBLE_SUM + "longCounterBuilder" | true | 5 | 6 | 11 | DOUBLE_SUM + "longUpDownCounterBuilder" | false | 5 | 6 | 11 | DOUBLE_SUM + "longUpDownCounterBuilder" | true | 5 | 6 | 11 | DOUBLE_SUM + "doubleCounterBuilder" | false | 5.5 | 6.6 | 12.1 | DOUBLE_SUM + "doubleCounterBuilder" | true | 5.5 | 6.6 | 12.1 | DOUBLE_SUM + "doubleUpDownCounterBuilder" | false | 5.5 | 6.6 | 12.1 | DOUBLE_SUM + "doubleUpDownCounterBuilder" | true | 5.5 | 6.6 | 12.1 | DOUBLE_SUM + } + + def "test recorder #builderMethod bound=#bind"() { + given: + // meters are global, and no way to unregister them, so tests use random name to avoid each other + def instrumentationName = "test" + new Random().nextLong() + + when: + def meter = GlobalMeterProvider.getMeter(instrumentationName, "1.2.3") + def instrument = meter."$builderMethod"("test") + .setDescription("d") + .setUnit("u") + .build() + if (bind) { + instrument = instrument.bind(Labels.empty()) + } + if (bind) { + instrument.record(value1) + instrument.record(value2) + } else { + instrument.record(value1, Labels.of("q", "r")) + instrument.record(value2, Labels.of("q", "r")) + } + + then: + def metricData = findMetric(instrumentationName, "test") + metricData != null + metricData.description == "d" + metricData.unit == "u" + metricData.type == SUMMARY + metricData.instrumentationLibraryInfo.name == instrumentationName + metricData.instrumentationLibraryInfo.version == "1.2.3" + points(metricData).size() == 1 + def point = points(metricData).iterator().next() + if (bind) { + point.labels == Labels.of("w", "x", "y", "z") + } else { + point.labels == Labels.of("q", "r") + } + + where: + builderMethod | bind | value1 | value2 | sum + "longValueRecorderBuilder" | false | 5 | 6 | 11 + "longValueRecorderBuilder" | true | 5 | 6 | 11 + "doubleValueRecorderBuilder" | false | 5.5 | 6.6 | 12.1 + "doubleValueRecorderBuilder" | true | 5.5 | 6.6 | 12.1 + } + + def "test observer #builderMethod"() { + given: + // meters are global, and no way to unregister them, so tests use random name to avoid each other + def instrumentationName = "test" + new Random().nextLong() + + when: + def meter = GlobalMeterProvider.getMeter(instrumentationName, "1.2.3") + def instrument = meter."$builderMethod"("test") + .setDescription("d") + .setUnit("u") + if (builderMethod == "longSumObserverBuilder") { + instrument.setUpdater(new Consumer() { + @Override + void accept(AsynchronousInstrument.LongResult resultLongSumObserver) { + resultLongSumObserver.observe(123, Labels.of("q", "r")) + } + }) + } else if (builderMethod == "longUpDownSumObserverBuilder") { + instrument.setUpdater(new Consumer() { + @Override + void accept(AsynchronousInstrument.LongResult resultLongUpDownSumObserver) { + resultLongUpDownSumObserver.observe(123, Labels.of("q", "r")) + } + }) + } else if (builderMethod == "longValueObserverBuilder") { + instrument.setUpdater(new Consumer() { + @Override + void accept(AsynchronousInstrument.LongResult resultLongObserver) { + resultLongObserver.observe(123, Labels.of("q", "r")) + } + }) + } else if (builderMethod == "doubleSumObserverBuilder") { + instrument.setUpdater(new Consumer() { + @Override + void accept(AsynchronousInstrument.DoubleResult resultDoubleSumObserver) { + resultDoubleSumObserver.observe(1.23, Labels.of("q", "r")) + } + }) + } else if (builderMethod == "doubleUpDownSumObserverBuilder") { + instrument.setUpdater(new Consumer() { + @Override + void accept(AsynchronousInstrument.DoubleResult resultDoubleUpDownSumObserver) { + resultDoubleUpDownSumObserver.observe(1.23, Labels.of("q", "r")) + } + }) + } else if (builderMethod == "doubleValueObserverBuilder") { + instrument.setUpdater(new Consumer() { + @Override + void accept(AsynchronousInstrument.DoubleResult resultDoubleObserver) { + resultDoubleObserver.observe(1.23, Labels.of("q", "r")) + } + }) + } + instrument.build() + + then: + def metricData = findMetric(instrumentationName, "test") + metricData != null + metricData.description == "d" + metricData.unit == "u" + metricData.type == expectedType + metricData.instrumentationLibraryInfo.name == instrumentationName + metricData.instrumentationLibraryInfo.version == "1.2.3" + points(metricData).size() == 1 + def point = points(metricData).iterator().next() + if (builderMethod.startsWith("long")) { + point.value == 123 + } else { + point.value == 1.23 + } + + where: + builderMethod | valueMethod | expectedType + "longSumObserverBuilder" | "value" | DOUBLE_SUM + "longUpDownSumObserverBuilder" | "value" | DOUBLE_SUM + "longValueObserverBuilder" | "sum" | DOUBLE_GAUGE + "doubleSumObserverBuilder" | "value" | DOUBLE_SUM + "doubleUpDownSumObserverBuilder" | "value" | DOUBLE_SUM + "doubleValueObserverBuilder" | "sum" | DOUBLE_GAUGE + } + + def "test batch recorder"() { + given: + // meters are global, and no way to unregister them, so tests use random name to avoid each other + def instrumentationName = "test" + new Random().nextLong() + + when: + def meter = GlobalMeterProvider.getMeter(instrumentationName, "1.2.3") + def longCounter = meter.longCounterBuilder("test") + .setDescription("d") + .setUnit("u") + .build() + def doubleMeasure = meter.doubleValueRecorderBuilder("test2") + .setDescription("d") + .setUnit("u") + .build() + + meter.newBatchRecorder("q", "r") + .put(longCounter, 5) + .put(longCounter, 6) + .put(doubleMeasure, 5.5) + .put(doubleMeasure, 6.6) + .record() + + then: + def metricData = findMetric(instrumentationName, "test") + metricData != null + metricData.description == "d" + metricData.unit == "u" + metricData.type == DOUBLE_SUM + metricData.instrumentationLibraryInfo.name == instrumentationName + metricData.instrumentationLibraryInfo.version == "1.2.3" + points(metricData).size() == 1 + def point = points(metricData).iterator().next() + point.value == 11 + + def metricData2 = findMetric(instrumentationName, "test2") + metricData2 != null + metricData2.description == "d" + metricData2.unit == "u" + metricData2.type == SUMMARY + metricData2.instrumentationLibraryInfo.name == instrumentationName + metricData2.instrumentationLibraryInfo.version == "1.2.3" + points(metricData2).size() == 1 + def point2 = points(metricData2).iterator().next() + point2.count == 2 + point2.sum == 12.1 + } + + def findMetric(instrumentationName, metricName) { + Stopwatch stopwatch = Stopwatch.createStarted() + while (stopwatch.elapsed(SECONDS) < 10) { + for (def metric : metrics) { + if (metric.instrumentationLibraryInfo.name == instrumentationName && metric.name == metricName) { + return metric + } + } + } + } + + List points(MetricData metricData) { + def points = [] + points.addAll(metricData.getDoubleGaugeData().getPoints()) + points.addAll(metricData.getDoubleHistogramData().getPoints()) + points.addAll(metricData.getDoubleSumData().getPoints()) + points.addAll(metricData.getDoubleSummaryData().getPoints()) + points.addAll(metricData.getLongGaugeData().getPoints()) + points.addAll(metricData.getLongSumData().getPoints()) + return points + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/oshi-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/oshi-javaagent.gradle new file mode 100644 index 000000000..ca7c8b6bb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/oshi-javaagent.gradle @@ -0,0 +1,17 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.github.oshi" + module = "oshi-core" + versions = "[5.3.1,)" + } +} + +dependencies { + implementation project(':instrumentation:oshi:library') + + library "com.github.oshi:oshi-core:5.3.1" + + testImplementation "com.google.guava:guava" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiInstrumentationModule.java new file mode 100644 index 000000000..c600460d9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.oshi; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class OshiInstrumentationModule extends InstrumentationModule { + + public OshiInstrumentationModule() { + super("oshi"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new SystemInfoInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiMetricsInstaller.java b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiMetricsInstaller.java new file mode 100644 index 000000000..83c706abd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiMetricsInstaller.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.oshi; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.AgentListener; +import java.lang.reflect.Method; +import java.util.Collections; + +/** + * An {@link AgentListener} that enables oshi metrics during agent startup if oshi is present on the + * system classpath. + */ +@AutoService(AgentListener.class) +public class OshiMetricsInstaller implements AgentListener { + @Override + public void afterAgent(Config config) { + if (config.isInstrumentationEnabled( + Collections.singleton("oshi"), /* defaultEnabled= */ true)) { + try { + // Call oshi.SystemInfo.getCurrentPlatformEnum() to activate SystemMetrics. + // Oshi instrumentation will intercept this call and enable SystemMetrics. + Class oshiSystemInfoClass = + ClassLoader.getSystemClassLoader().loadClass("oshi.SystemInfo"); + Method getCurrentPlatformEnumMethod = + oshiSystemInfoClass.getMethod("getCurrentPlatformEnum"); + getCurrentPlatformEnumMethod.invoke(null); + } catch (Throwable ex) { + // OK + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/SystemInfoInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/SystemInfoInstrumentation.java new file mode 100644 index 000000000..f8bcac9ee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/SystemInfoInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.oshi; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.instrumentation.oshi.SystemMetrics; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class SystemInfoInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("oshi.SystemInfo"); + } + + @Override + public ElementMatcher typeMatcher() { + return named("oshi.SystemInfo"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(isStatic()).and(named("getCurrentPlatformEnum")), + this.getClass().getName() + "$GetCurrentPlatformEnumAdvice"); + } + + @SuppressWarnings("unused") + public static class GetCurrentPlatformEnumAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + SystemMetrics.registerObservers(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/oshi/OshiTest.groovy b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/oshi/OshiTest.groovy new file mode 100644 index 000000000..208b31902 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/oshi/OshiTest.groovy @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.oshi + +import static java.util.concurrent.TimeUnit.SECONDS + +import com.google.common.base.Stopwatch +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +class OshiTest extends AgentInstrumentationSpecification { + + def "test system metrics is enabled"() { + expect: + // TODO (trask) is this the instrumentation library name we want? + findMetric("io.opentelemetry.javaagent.shaded.instrumentation.oshi", "system.disk.io") != null + findMetric("io.opentelemetry.javaagent.shaded.instrumentation.oshi", "system.disk.operations") != null + findMetric("io.opentelemetry.javaagent.shaded.instrumentation.oshi", "system.memory.usage") != null + findMetric("io.opentelemetry.javaagent.shaded.instrumentation.oshi", "system.memory.utilization") != null + findMetric("io.opentelemetry.javaagent.shaded.instrumentation.oshi", "system.network.errors") != null + findMetric("io.opentelemetry.javaagent.shaded.instrumentation.oshi", "system.network.io") != null + findMetric("io.opentelemetry.javaagent.shaded.instrumentation.oshi", "system.network.packets") != null + } + + def findMetric(instrumentationName, metricName) { + Stopwatch stopwatch = Stopwatch.createStarted() + while (stopwatch.elapsed(SECONDS) < 10) { + for (def metric : metrics) { + if (metric.instrumentationLibraryInfo.name == instrumentationName && metric.name == metricName) { + return metric + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/library/oshi-library.gradle b/opentelemetry-java-instrumentation/instrumentation/oshi/library/oshi-library.gradle new file mode 100644 index 000000000..66b642380 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/library/oshi-library.gradle @@ -0,0 +1,12 @@ +apply plugin: "otel.library-instrumentation" + + +dependencies { + implementation "io.opentelemetry:opentelemetry-api-metrics" + + library "com.github.oshi:oshi-core:5.3.1" + + testImplementation "io.opentelemetry:opentelemetry-sdk-metrics" + testImplementation project(':testing-common') + testImplementation "org.assertj:assertj-core" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/ProcessMetrics.java b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/ProcessMetrics.java new file mode 100644 index 000000000..d60c787c8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/ProcessMetrics.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.oshi; + +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import oshi.SystemInfo; +import oshi.software.os.OSProcess; +import oshi.software.os.OperatingSystem; + +/** Java Runtime Metrics Utility. */ +public class ProcessMetrics { + private static final String TYPE_LABEL_KEY = "type"; + + private ProcessMetrics() {} + + /** Register observers for java runtime metrics. */ + public static void registerObservers() { + Meter meter = GlobalMeterProvider.get().get(ProcessMetrics.class.getName()); + SystemInfo systemInfo = new SystemInfo(); + OperatingSystem osInfo = systemInfo.getOperatingSystem(); + OSProcess processInfo = osInfo.getProcess(osInfo.getProcessId()); + + meter + .longUpDownSumObserverBuilder("runtime.java.memory") + .setDescription("Runtime Java memory") + .setUnit("bytes") + .setUpdater( + r -> { + processInfo.updateAttributes(); + r.observe(processInfo.getResidentSetSize(), Labels.of(TYPE_LABEL_KEY, "rss")); + r.observe(processInfo.getVirtualSize(), Labels.of(TYPE_LABEL_KEY, "vms")); + }) + .build(); + + meter + .doubleValueObserverBuilder("runtime.java.cpu_time") + .setDescription("Runtime Java CPU time") + .setUnit("seconds") + .setUpdater( + r -> { + processInfo.updateAttributes(); + r.observe(processInfo.getUserTime() * 1000, Labels.of(TYPE_LABEL_KEY, "user")); + r.observe(processInfo.getKernelTime() * 1000, Labels.of(TYPE_LABEL_KEY, "system")); + }) + .build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/SystemMetrics.java b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/SystemMetrics.java new file mode 100644 index 000000000..1007dbaa8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/SystemMetrics.java @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.oshi; + +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import oshi.SystemInfo; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HWDiskStore; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.NetworkIF; + +/** System Metrics Utility. */ +public class SystemMetrics { + private static final String DEVICE_LABEL_KEY = "device"; + private static final String DIRECTION_LABEL_KEY = "direction"; + private static final Labels LABEL_STATE_USED = Labels.of("state", "used"); + private static final Labels LABEL_STATE_FREE = Labels.of("state", "free"); + + private SystemMetrics() {} + + /** Register observers for system metrics. */ + public static void registerObservers() { + Meter meter = GlobalMeterProvider.get().get("io.opentelemetry.instrumentation.oshi"); + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hal = systemInfo.getHardware(); + + meter + .longUpDownSumObserverBuilder("system.memory.usage") + .setDescription("System memory usage") + .setUnit("By") + .setUpdater( + r -> { + GlobalMemory mem = hal.getMemory(); + r.observe(mem.getTotal() - mem.getAvailable(), LABEL_STATE_USED); + r.observe(mem.getAvailable(), LABEL_STATE_FREE); + }) + .build(); + + meter + .doubleValueObserverBuilder("system.memory.utilization") + .setDescription("System memory utilization") + .setUnit("1") + .setUpdater( + r -> { + GlobalMemory mem = hal.getMemory(); + r.observe( + ((double) (mem.getTotal() - mem.getAvailable())) / mem.getTotal(), + LABEL_STATE_USED); + r.observe(((double) mem.getAvailable()) / mem.getTotal(), LABEL_STATE_FREE); + }) + .build(); + + meter + .longSumObserverBuilder("system.network.io") + .setDescription("System network IO") + .setUnit("By") + .setUpdater( + r -> { + for (NetworkIF networkIf : hal.getNetworkIFs()) { + networkIf.updateAttributes(); + long recv = networkIf.getBytesRecv(); + long sent = networkIf.getBytesSent(); + String device = networkIf.getName(); + r.observe( + recv, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "receive")); + r.observe( + sent, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "transmit")); + } + }) + .build(); + + meter + .longSumObserverBuilder("system.network.packets") + .setDescription("System network packets") + .setUnit("packets") + .setUpdater( + r -> { + for (NetworkIF networkIf : hal.getNetworkIFs()) { + networkIf.updateAttributes(); + long recv = networkIf.getPacketsRecv(); + long sent = networkIf.getPacketsSent(); + String device = networkIf.getName(); + r.observe( + recv, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "receive")); + r.observe( + sent, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "transmit")); + } + }) + .build(); + + meter + .longSumObserverBuilder("system.network.errors") + .setDescription("System network errors") + .setUnit("errors") + .setUpdater( + r -> { + for (NetworkIF networkIf : hal.getNetworkIFs()) { + networkIf.updateAttributes(); + long recv = networkIf.getInErrors(); + long sent = networkIf.getOutErrors(); + String device = networkIf.getName(); + r.observe( + recv, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "receive")); + r.observe( + sent, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "transmit")); + } + }) + .build(); + + meter + .longSumObserverBuilder("system.disk.io") + .setDescription("System disk IO") + .setUnit("By") + .setUpdater( + r -> { + for (HWDiskStore diskStore : hal.getDiskStores()) { + long read = diskStore.getReadBytes(); + long write = diskStore.getWriteBytes(); + String device = diskStore.getName(); + r.observe(read, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "read")); + r.observe(write, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "write")); + } + }) + .build(); + + meter + .longSumObserverBuilder("system.disk.operations") + .setDescription("System disk operations") + .setUnit("operations") + .setUpdater( + r -> { + for (HWDiskStore diskStore : hal.getDiskStores()) { + long read = diskStore.getReads(); + long write = diskStore.getWrites(); + String device = diskStore.getName(); + r.observe(read, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "read")); + r.observe(write, Labels.of(DEVICE_LABEL_KEY, device, DIRECTION_LABEL_KEY, "write")); + } + }) + .build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/AbstractMetricsTest.java b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/AbstractMetricsTest.java new file mode 100644 index 000000000..30b1ce294 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/AbstractMetricsTest.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.oshi; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +class AbstractMetricsTest { + TestMetricExporter testMetricExporter; + + static SdkMeterProvider meterProvider; + + @BeforeAll + static void initializeOpenTelemetry() { + meterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + } + + @BeforeEach + void beforeEach() { + testMetricExporter = new TestMetricExporter(); + } + + IntervalMetricReader createIntervalMetricReader() { + return IntervalMetricReader.builder() + .setExportIntervalMillis(100) + .setMetricExporter(testMetricExporter) + .setMetricProducers(Collections.singletonList(meterProvider)) + .buildAndStart(); + } + + public void verify( + String metricName, String unit, MetricDataType type, boolean checkNonZeroValue) { + List metricDataList = testMetricExporter.metricDataList; + for (MetricData metricData : metricDataList) { + if (metricData.getName().equals(metricName)) { + assertThat(metricData.getDescription()).isNotEmpty(); + assertThat(metricData.getUnit()).isEqualTo(unit); + List points = new ArrayList<>(); + points.addAll(metricData.getDoubleGaugeData().getPoints()); + points.addAll(metricData.getDoubleSumData().getPoints()); + points.addAll(metricData.getDoubleSummaryData().getPoints()); + points.addAll(metricData.getLongGaugeData().getPoints()); + points.addAll(metricData.getLongSumData().getPoints()); + + assertThat(points).isNotEmpty(); + assertThat(metricData.getType()).isEqualTo(type); + if (checkNonZeroValue) { + for (PointData point : points) { + if (point instanceof LongPointData) { + LongPointData longPoint = (LongPointData) point; + assertThat(longPoint.getValue()).isGreaterThan(0); + } else if (point instanceof DoublePointData) { + DoublePointData doublePoint = (DoublePointData) point; + assertThat(doublePoint.getValue()).isGreaterThan(0.0); + } else if (point instanceof DoubleSummaryPointData) { + DoubleSummaryPointData summaryPoint = (DoubleSummaryPointData) point; + assertThat(summaryPoint.getSum()).isGreaterThan(0.0); + } else { + Assertions.fail("unexpected type " + metricData.getType()); + } + } + } + return; + } + } + Assertions.fail("No metric for " + metricName); + } + + static class TestMetricExporter implements MetricExporter { + private final List metricDataList = new CopyOnWriteArrayList<>(); + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public CompletableResultCode export(Collection collection) { + metricDataList.addAll(collection); + latch.countDown(); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + public void waitForData() throws InterruptedException { + latch.await(1, TimeUnit.SECONDS); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/ProcessMetricsTest.java b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/ProcessMetricsTest.java new file mode 100644 index 000000000..7f5e19c53 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/ProcessMetricsTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.oshi; + +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import org.junit.jupiter.api.Test; + +public class ProcessMetricsTest extends AbstractMetricsTest { + + @Test + public void test() throws Exception { + ProcessMetrics.registerObservers(); + IntervalMetricReader intervalMetricReader = createIntervalMetricReader(); + + testMetricExporter.waitForData(); + intervalMetricReader.shutdown(); + + verify("runtime.java.memory", "bytes", MetricDataType.LONG_SUM, /* checkNonZeroValue= */ true); + verify( + "runtime.java.cpu_time", + "seconds", + MetricDataType.DOUBLE_GAUGE, + /* checkNonZeroValue= */ true); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/SystemMetricsTest.java b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/SystemMetricsTest.java new file mode 100644 index 000000000..4676222a3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/SystemMetricsTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.oshi; + +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import org.junit.jupiter.api.Test; + +public class SystemMetricsTest extends AbstractMetricsTest { + + @Test + public void test() throws Exception { + SystemMetrics.registerObservers(); + IntervalMetricReader intervalMetricReader = createIntervalMetricReader(); + + testMetricExporter.waitForData(); + intervalMetricReader.shutdown(); + + verify("system.memory.usage", "By", MetricDataType.LONG_SUM, /* checkNonZeroValue= */ true); + verify( + "system.memory.utilization", + "1", + MetricDataType.DOUBLE_GAUGE, + /* checkNonZeroValue= */ true); + + verify("system.network.io", "By", MetricDataType.LONG_SUM, /* checkNonZeroValue= */ false); + verify( + "system.network.packets", + "packets", + MetricDataType.LONG_SUM, + /* checkNonZeroValue= */ false); + verify( + "system.network.errors", "errors", MetricDataType.LONG_SUM, /* checkNonZeroValue= */ false); + + verify("system.disk.io", "By", MetricDataType.LONG_SUM, /* checkNonZeroValue= */ false); + verify( + "system.disk.operations", + "operations", + MetricDataType.LONG_SUM, + /* checkNonZeroValue= */ false); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/play-ws-1.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/play-ws-1.0-javaagent.gradle new file mode 100644 index 000000000..76388b8ee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/play-ws-1.0-javaagent.gradle @@ -0,0 +1,39 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.11' + versions = '[1.0.0,2.0.0)' + assertInverse = true + } + pass { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.12' + versions = '[1.0.0,2.0.0)' + assertInverse = true + } + fail { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.13' + versions = '[,]' + } +} + +def scalaVersion = '2.12' + +dependencies { + library group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '1.0.2' + + implementation project(':instrumentation:play-ws:play-ws-common:javaagent') + + testImplementation project(':instrumentation:play-ws:play-ws-testing') + + // These are to ensure cross compatibility + testInstrumentation project(':instrumentation:netty:netty-4.0:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:akka-http-10.0:javaagent') + testInstrumentation project(':instrumentation:akka-actor-2.5:javaagent') + + latestDepTestLibrary group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '1.+' +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/AsyncHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/AsyncHandlerWrapper.java new file mode 100644 index 000000000..a03c0c946 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/AsyncHandlerWrapper.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v1_0; + +import static io.opentelemetry.javaagent.instrumentation.playws.PlayWsClientTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import play.shaded.ahc.org.asynchttpclient.AsyncHandler; +import play.shaded.ahc.org.asynchttpclient.HttpResponseBodyPart; +import play.shaded.ahc.org.asynchttpclient.HttpResponseHeaders; +import play.shaded.ahc.org.asynchttpclient.HttpResponseStatus; +import play.shaded.ahc.org.asynchttpclient.Response; + +public class AsyncHandlerWrapper implements AsyncHandler { + private final AsyncHandler delegate; + private final Context context; + private final Context parentContext; + + private final Response.ResponseBuilder builder = new Response.ResponseBuilder(); + + public AsyncHandlerWrapper(AsyncHandler delegate, Context context, Context parentContext) { + this.delegate = delegate; + this.context = context; + this.parentContext = parentContext; + } + + public Context getParentContext() { + return parentContext; + } + + @Override + public State onBodyPartReceived(HttpResponseBodyPart content) throws Exception { + builder.accumulate(content); + return delegate.onBodyPartReceived(content); + } + + @Override + public State onStatusReceived(HttpResponseStatus status) throws Exception { + builder.reset(); + builder.accumulate(status); + return delegate.onStatusReceived(status); + } + + @Override + public State onHeadersReceived(HttpResponseHeaders httpHeaders) throws Exception { + builder.accumulate(httpHeaders); + return delegate.onHeadersReceived(httpHeaders); + } + + @Override + public Object onCompleted() throws Exception { + tracer().end(context, builder.build()); + + try (Scope ignored = parentContext.makeCurrent()) { + return delegate.onCompleted(); + } + } + + @Override + public void onThrowable(Throwable throwable) { + tracer().endExceptionally(context, throwable); + + try (Scope ignored = parentContext.makeCurrent()) { + delegate.onThrowable(throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/PlayWsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/PlayWsInstrumentationModule.java new file mode 100644 index 000000000..6f93ebdab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/PlayWsInstrumentationModule.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v1_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.playws.PlayWsClientTracer.tracer; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.playws.AsyncHttpClientInstrumentation; +import io.opentelemetry.javaagent.instrumentation.playws.HandlerPublisherInstrumentation; +import java.util.List; +import net.bytebuddy.asm.Advice; +import play.shaded.ahc.org.asynchttpclient.AsyncHandler; +import play.shaded.ahc.org.asynchttpclient.Request; +import play.shaded.ahc.org.asynchttpclient.handler.StreamedAsyncHandler; +import play.shaded.ahc.org.asynchttpclient.ws.WebSocketUpgradeHandler; + +@AutoService(InstrumentationModule.class) +public class PlayWsInstrumentationModule extends InstrumentationModule { + public PlayWsInstrumentationModule() { + super("play-ws", "play-ws-1.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new AsyncHttpClientInstrumentation( + PlayWsInstrumentationModule.class.getName() + "$ClientAdvice"), + new HandlerPublisherInstrumentation()); + } + + @SuppressWarnings("unused") + public static class ClientAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(value = 1, readOnly = false) AsyncHandler asyncHandler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, request, request.getHeaders()); + scope = context.makeCurrent(); + + if (asyncHandler instanceof StreamedAsyncHandler) { + asyncHandler = + new StreamedAsyncHandlerWrapper( + (StreamedAsyncHandler) asyncHandler, context, parentContext); + } else if (!(asyncHandler instanceof WebSocketUpgradeHandler)) { + // websocket upgrade handlers aren't supported + asyncHandler = new AsyncHandlerWrapper(asyncHandler, context, parentContext); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/StreamedAsyncHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/StreamedAsyncHandlerWrapper.java new file mode 100644 index 000000000..116b70a82 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v1_0/StreamedAsyncHandlerWrapper.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v1_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.reactivestreams.Publisher; +import play.shaded.ahc.org.asynchttpclient.handler.StreamedAsyncHandler; + +public class StreamedAsyncHandlerWrapper extends AsyncHandlerWrapper + implements StreamedAsyncHandler { + private final StreamedAsyncHandler streamedDelegate; + + public StreamedAsyncHandlerWrapper( + StreamedAsyncHandler delegate, Context context, Context parentContext) { + super(delegate, context, parentContext); + streamedDelegate = delegate; + } + + @Override + public State onStream(Publisher publisher) { + try (Scope ignored = getParentContext().makeCurrent()) { + return streamedDelegate.onStream(publisher); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/test/groovy/PlayWsClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/test/groovy/PlayWsClientTest.groovy new file mode 100644 index 000000000..0e20a872a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-1.0/javaagent/src/test/groovy/PlayWsClientTest.groovy @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class PlayJavaWsClientTest extends PlayJavaWsClientTestBase {} + +class PlayJavaStreamedWsClientTest extends PlayJavaStreamedWsClientTestBase {} + +class PlayScalaWsClientTest extends PlayScalaWsClientTestBase {} + +class PlayScalaStreamedWsClientTest extends PlayScalaStreamedWsClientTestBase {} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/play-ws-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/play-ws-2.0-javaagent.gradle new file mode 100644 index 000000000..e0af7a4a1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/play-ws-2.0-javaagent.gradle @@ -0,0 +1,45 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + + pass { + module = 'play-ahc-ws-standalone_2.11' + group = 'com.typesafe.play' + versions = '[2.0.0,]' + assertInverse = true + } + + pass { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.12' + versions = '[2.0.0,2.1.0)' + // 2.0.5 is missing play.shaded.ahc.org.asynchttpclient.AsyncHandler#onTlsHandshakeSuccess()V + skip('2.0.5') + assertInverse = true + } + + // No Scala 2.13 versions below 2.0.6 exist + pass { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.13' + versions = '[2.0.6,2.1.0)' + } +} + +def scalaVersion = '2.12' + +dependencies { + library group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '2.0.0' + + implementation project(':instrumentation:play-ws:play-ws-common:javaagent') + + testImplementation project(':instrumentation:play-ws:play-ws-testing') + + // These are to ensure cross compatibility + testInstrumentation project(':instrumentation:netty:netty-4.0:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:akka-http-10.0:javaagent') + testInstrumentation project(':instrumentation:akka-actor-2.5:javaagent') + + latestDepTestLibrary group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '2.0.+' +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/AsyncHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/AsyncHandlerWrapper.java new file mode 100644 index 000000000..7ef28dab8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/AsyncHandlerWrapper.java @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.playws.PlayWsClientTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.net.InetSocketAddress; +import java.util.List; +import play.shaded.ahc.io.netty.channel.Channel; +import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders; +import play.shaded.ahc.org.asynchttpclient.AsyncHandler; +import play.shaded.ahc.org.asynchttpclient.HttpResponseBodyPart; +import play.shaded.ahc.org.asynchttpclient.HttpResponseStatus; +import play.shaded.ahc.org.asynchttpclient.Response; +import play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequest; + +public class AsyncHandlerWrapper implements AsyncHandler { + private final AsyncHandler delegate; + private final Context context; + private final Context parentContext; + + private final Response.ResponseBuilder builder = new Response.ResponseBuilder(); + + public AsyncHandlerWrapper(AsyncHandler delegate, Context context, Context parentContext) { + this.delegate = delegate; + this.context = context; + this.parentContext = parentContext; + } + + public Context getParentContext() { + return parentContext; + } + + @Override + public State onBodyPartReceived(HttpResponseBodyPart content) throws Exception { + builder.accumulate(content); + return delegate.onBodyPartReceived(content); + } + + @Override + public State onStatusReceived(HttpResponseStatus status) throws Exception { + builder.reset(); + builder.accumulate(status); + return delegate.onStatusReceived(status); + } + + @Override + public State onHeadersReceived(HttpHeaders httpHeaders) throws Exception { + builder.accumulate(httpHeaders); + return delegate.onHeadersReceived(httpHeaders); + } + + @Override + public Object onCompleted() throws Exception { + Response response = builder.build(); + tracer().end(context, response); + + try (Scope ignored = parentContext.makeCurrent()) { + return delegate.onCompleted(); + } + } + + @Override + public void onThrowable(Throwable throwable) { + tracer().endExceptionally(context, throwable); + + try (Scope ignored = parentContext.makeCurrent()) { + delegate.onThrowable(throwable); + } + } + + @Override + public State onTrailingHeadersReceived(HttpHeaders headers) throws Exception { + return delegate.onTrailingHeadersReceived(headers); + } + + @Override + public void onHostnameResolutionAttempt(String name) { + delegate.onHostnameResolutionAttempt(name); + } + + @Override + public void onHostnameResolutionSuccess(String name, List list) { + delegate.onHostnameResolutionSuccess(name, list); + } + + @Override + public void onHostnameResolutionFailure(String name, Throwable cause) { + delegate.onHostnameResolutionFailure(name, cause); + } + + @Override + public void onTcpConnectAttempt(InetSocketAddress remoteAddress) { + delegate.onTcpConnectAttempt(remoteAddress); + } + + @Override + public void onTcpConnectSuccess(InetSocketAddress remoteAddress, Channel connection) { + delegate.onTcpConnectSuccess(remoteAddress, connection); + } + + @Override + public void onTcpConnectFailure(InetSocketAddress remoteAddress, Throwable cause) { + delegate.onTcpConnectFailure(remoteAddress, cause); + } + + @Override + public void onTlsHandshakeAttempt() { + delegate.onTlsHandshakeAttempt(); + } + + @Override + public void onTlsHandshakeSuccess() { + delegate.onTlsHandshakeSuccess(); + } + + @Override + public void onTlsHandshakeFailure(Throwable cause) { + delegate.onTlsHandshakeFailure(cause); + } + + @Override + public void onConnectionPoolAttempt() { + delegate.onConnectionPoolAttempt(); + } + + @Override + public void onConnectionPooled(Channel connection) { + delegate.onConnectionPooled(connection); + } + + @Override + public void onConnectionOffer(Channel connection) { + delegate.onConnectionOffer(connection); + } + + @Override + public void onRequestSend(NettyRequest request) { + delegate.onRequestSend(request); + } + + @Override + public void onRetry() { + delegate.onRetry(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/PlayWsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/PlayWsInstrumentationModule.java new file mode 100644 index 000000000..86ef50cfb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/PlayWsInstrumentationModule.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.playws.PlayWsClientTracer.tracer; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.playws.AsyncHttpClientInstrumentation; +import io.opentelemetry.javaagent.instrumentation.playws.HandlerPublisherInstrumentation; +import java.util.List; +import net.bytebuddy.asm.Advice; +import play.shaded.ahc.org.asynchttpclient.AsyncHandler; +import play.shaded.ahc.org.asynchttpclient.Request; +import play.shaded.ahc.org.asynchttpclient.handler.StreamedAsyncHandler; +import play.shaded.ahc.org.asynchttpclient.ws.WebSocketUpgradeHandler; + +@AutoService(InstrumentationModule.class) +public class PlayWsInstrumentationModule extends InstrumentationModule { + public PlayWsInstrumentationModule() { + super("play-ws", "play-ws-2.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new AsyncHttpClientInstrumentation(this.getClass().getName() + "$ClientAdvice"), + new HandlerPublisherInstrumentation()); + } + + @SuppressWarnings("unused") + public static class ClientAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(value = 1, readOnly = false) AsyncHandler asyncHandler, + @Advice.Local("otelContext") Context context) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, request, request.getHeaders()); + + if (asyncHandler instanceof StreamedAsyncHandler) { + asyncHandler = + new StreamedAsyncHandlerWrapper( + (StreamedAsyncHandler) asyncHandler, context, parentContext); + } else if (!(asyncHandler instanceof WebSocketUpgradeHandler)) { + // websocket upgrade handlers aren't supported + asyncHandler = new AsyncHandlerWrapper(asyncHandler, context, parentContext); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Thrown Throwable throwable, @Advice.Local("otelContext") Context context) { + if (context != null && throwable != null) { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/StreamedAsyncHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/StreamedAsyncHandlerWrapper.java new file mode 100644 index 000000000..9059628fc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_0/StreamedAsyncHandlerWrapper.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v2_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.reactivestreams.Publisher; +import play.shaded.ahc.org.asynchttpclient.handler.StreamedAsyncHandler; + +public class StreamedAsyncHandlerWrapper extends AsyncHandlerWrapper + implements StreamedAsyncHandler { + private final StreamedAsyncHandler streamedDelegate; + + public StreamedAsyncHandlerWrapper( + StreamedAsyncHandler delegate, Context context, Context parentContext) { + super(delegate, context, parentContext); + streamedDelegate = delegate; + } + + @Override + public State onStream(Publisher publisher) { + try (Scope ignored = getParentContext().makeCurrent()) { + return streamedDelegate.onStream(publisher); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/test/groovy/PlayWsClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/test/groovy/PlayWsClientTest.groovy new file mode 100644 index 000000000..0e20a872a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.0/javaagent/src/test/groovy/PlayWsClientTest.groovy @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class PlayJavaWsClientTest extends PlayJavaWsClientTestBase {} + +class PlayJavaStreamedWsClientTest extends PlayJavaStreamedWsClientTestBase {} + +class PlayScalaWsClientTest extends PlayScalaWsClientTestBase {} + +class PlayScalaStreamedWsClientTest extends PlayScalaStreamedWsClientTestBase {} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/play-ws-2.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/play-ws-2.1-javaagent.gradle new file mode 100644 index 000000000..9aade0d72 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/play-ws-2.1-javaagent.gradle @@ -0,0 +1,42 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + + fail { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.11' + versions = '[,]' + } + + pass { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.12' + versions = '[2.1.0,]' + skip('2.0.5') // muzzle passes but expecting failure, see play-ws-2.0-javaagent.gradle + assertInverse = true + } + + pass { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.13' + versions = '[2.1.0,]' + skip('2.0.5') // muzzle passes but expecting failure, see play-ws-2.0-javaagent.gradle + assertInverse = true + } +} + +def scalaVersion = '2.12' + +dependencies { + library group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '2.1.0' + + implementation project(':instrumentation:play-ws:play-ws-common:javaagent') + + testImplementation project(':instrumentation:play-ws:play-ws-testing') + + // These are to ensure cross compatibility + testInstrumentation project(':instrumentation:netty:netty-4.0:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:akka-http-10.0:javaagent') + testInstrumentation project(':instrumentation:akka-actor-2.5:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/AsyncHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/AsyncHandlerWrapper.java new file mode 100644 index 000000000..ab0b1acfd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/AsyncHandlerWrapper.java @@ -0,0 +1,152 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v2_1; + +import static io.opentelemetry.javaagent.instrumentation.playws.PlayWsClientTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.net.InetSocketAddress; +import java.util.List; +import javax.net.ssl.SSLSession; +import play.shaded.ahc.io.netty.channel.Channel; +import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders; +import play.shaded.ahc.org.asynchttpclient.AsyncHandler; +import play.shaded.ahc.org.asynchttpclient.HttpResponseBodyPart; +import play.shaded.ahc.org.asynchttpclient.HttpResponseStatus; +import play.shaded.ahc.org.asynchttpclient.Response; +import play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequest; + +public class AsyncHandlerWrapper implements AsyncHandler { + private final AsyncHandler delegate; + private final Context context; + private final Context parentContext; + + private final Response.ResponseBuilder builder = new Response.ResponseBuilder(); + + public AsyncHandlerWrapper(AsyncHandler delegate, Context context, Context parentContext) { + this.delegate = delegate; + this.context = context; + this.parentContext = parentContext; + } + + public Context getParentContext() { + return parentContext; + } + + @Override + public State onBodyPartReceived(HttpResponseBodyPart content) throws Exception { + builder.accumulate(content); + return delegate.onBodyPartReceived(content); + } + + @Override + public State onStatusReceived(HttpResponseStatus status) throws Exception { + builder.reset(); + builder.accumulate(status); + return delegate.onStatusReceived(status); + } + + @Override + public State onHeadersReceived(HttpHeaders httpHeaders) throws Exception { + builder.accumulate(httpHeaders); + return delegate.onHeadersReceived(httpHeaders); + } + + @Override + public Object onCompleted() throws Exception { + Response response = builder.build(); + tracer().end(context, response); + + try (Scope ignored = parentContext.makeCurrent()) { + return delegate.onCompleted(); + } + } + + @Override + public void onThrowable(Throwable throwable) { + tracer().endExceptionally(context, throwable); + + try (Scope ignored = parentContext.makeCurrent()) { + delegate.onThrowable(throwable); + } + } + + @Override + public State onTrailingHeadersReceived(HttpHeaders headers) throws Exception { + return delegate.onTrailingHeadersReceived(headers); + } + + @Override + public void onHostnameResolutionAttempt(String name) { + delegate.onHostnameResolutionAttempt(name); + } + + @Override + public void onHostnameResolutionSuccess(String name, List list) { + delegate.onHostnameResolutionSuccess(name, list); + } + + @Override + public void onHostnameResolutionFailure(String name, Throwable cause) { + delegate.onHostnameResolutionFailure(name, cause); + } + + @Override + public void onTcpConnectAttempt(InetSocketAddress remoteAddress) { + delegate.onTcpConnectAttempt(remoteAddress); + } + + @Override + public void onTcpConnectSuccess(InetSocketAddress remoteAddress, Channel connection) { + delegate.onTcpConnectSuccess(remoteAddress, connection); + } + + @Override + public void onTcpConnectFailure(InetSocketAddress remoteAddress, Throwable cause) { + delegate.onTcpConnectFailure(remoteAddress, cause); + } + + @Override + public void onTlsHandshakeAttempt() { + delegate.onTlsHandshakeAttempt(); + } + + @Override + public void onTlsHandshakeSuccess(SSLSession sslSession) { + delegate.onTlsHandshakeSuccess(sslSession); + } + + @Override + public void onTlsHandshakeFailure(Throwable cause) { + delegate.onTlsHandshakeFailure(cause); + } + + @Override + public void onConnectionPoolAttempt() { + delegate.onConnectionPoolAttempt(); + } + + @Override + public void onConnectionPooled(Channel connection) { + delegate.onConnectionPooled(connection); + } + + @Override + public void onConnectionOffer(Channel connection) { + delegate.onConnectionOffer(connection); + } + + @Override + public void onRequestSend(NettyRequest request) { + delegate.onRequestSend(request); + } + + @Override + public void onRetry() { + delegate.onRetry(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/PlayWsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/PlayWsInstrumentationModule.java new file mode 100644 index 000000000..9610793e9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/PlayWsInstrumentationModule.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v2_1; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.playws.PlayWsClientTracer.tracer; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.playws.AsyncHttpClientInstrumentation; +import io.opentelemetry.javaagent.instrumentation.playws.HandlerPublisherInstrumentation; +import java.util.List; +import net.bytebuddy.asm.Advice; +import play.shaded.ahc.org.asynchttpclient.AsyncHandler; +import play.shaded.ahc.org.asynchttpclient.Request; +import play.shaded.ahc.org.asynchttpclient.handler.StreamedAsyncHandler; +import play.shaded.ahc.org.asynchttpclient.ws.WebSocketUpgradeHandler; + +@AutoService(InstrumentationModule.class) +public class PlayWsInstrumentationModule extends InstrumentationModule { + public PlayWsInstrumentationModule() { + super("play-ws", "play-ws-2.1"); + } + + @Override + public List typeInstrumentations() { + return asList( + new AsyncHttpClientInstrumentation(this.getClass().getName() + "$ClientAdvice"), + new HandlerPublisherInstrumentation()); + } + + @SuppressWarnings("unused") + public static class ClientAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(value = 1, readOnly = false) AsyncHandler asyncHandler, + @Advice.Local("otelContext") Context context) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, request, request.getHeaders()); + + if (asyncHandler instanceof StreamedAsyncHandler) { + asyncHandler = + new StreamedAsyncHandlerWrapper( + (StreamedAsyncHandler) asyncHandler, context, parentContext); + } else if (!(asyncHandler instanceof WebSocketUpgradeHandler)) { + // websocket upgrade handlers aren't supported + asyncHandler = new AsyncHandlerWrapper(asyncHandler, context, parentContext); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Thrown Throwable throwable, @Advice.Local("otelContext") Context context) { + if (context != null && throwable != null) { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/StreamedAsyncHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/StreamedAsyncHandlerWrapper.java new file mode 100644 index 000000000..5fd2ec82b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/v2_1/StreamedAsyncHandlerWrapper.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws.v2_1; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.reactivestreams.Publisher; +import play.shaded.ahc.org.asynchttpclient.handler.StreamedAsyncHandler; + +public class StreamedAsyncHandlerWrapper extends AsyncHandlerWrapper + implements StreamedAsyncHandler { + private final StreamedAsyncHandler streamedDelegate; + + public StreamedAsyncHandlerWrapper( + StreamedAsyncHandler delegate, Context context, Context parentContext) { + super(delegate, context, parentContext); + streamedDelegate = delegate; + } + + @Override + public State onStream(Publisher publisher) { + try (Scope ignored = getParentContext().makeCurrent()) { + return streamedDelegate.onStream(publisher); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/test/groovy/PlayWsClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/test/groovy/PlayWsClientTest.groovy new file mode 100644 index 000000000..0e20a872a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-2.1/javaagent/src/test/groovy/PlayWsClientTest.groovy @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class PlayJavaWsClientTest extends PlayJavaWsClientTestBase {} + +class PlayJavaStreamedWsClientTest extends PlayJavaStreamedWsClientTestBase {} + +class PlayScalaWsClientTest extends PlayScalaWsClientTestBase {} + +class PlayScalaStreamedWsClientTest extends PlayScalaStreamedWsClientTestBase {} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/play-ws-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/play-ws-common-javaagent.gradle new file mode 100644 index 000000000..ae1380656 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/play-ws-common-javaagent.gradle @@ -0,0 +1,7 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +def scalaVersion = '2.12' + +dependencies { + compileOnly group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '1.0.2' +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/AsyncHttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/AsyncHttpClientInstrumentation.java new file mode 100644 index 000000000..55aa12ff4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/AsyncHttpClientInstrumentation.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AsyncHttpClientInstrumentation implements TypeInstrumentation { + private final String adviceName; + + public AsyncHttpClientInstrumentation(String adviceName) { + this.adviceName = adviceName; + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("play.shaded.ahc.org.asynchttpclient.AsyncHttpClient"); + } + + @Override + public ElementMatcher typeMatcher() { + // CachingAsyncHttpClient rejects overrides to AsyncHandler + // It also delegates to another AsyncHttpClient + return nameStartsWith("play.") + .and( + implementsInterface(named("play.shaded.ahc.org.asynchttpclient.AsyncHttpClient")) + .and(not(named("play.api.libs.ws.ahc.cache.CachingAsyncHttpClient")))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("execute")) + .and(takesArguments(2)) + .and(takesArgument(0, named("play.shaded.ahc.org.asynchttpclient.Request"))) + .and(takesArgument(1, named("play.shaded.ahc.org.asynchttpclient.AsyncHandler"))), + adviceName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/HandlerPublisherInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/HandlerPublisherInstrumentation.java new file mode 100644 index 000000000..c19867380 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/HandlerPublisherInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.reactivestreams.Subscriber; + +public class HandlerPublisherInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("play.shaded.ahc.com.typesafe.netty.HandlerPublisher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("subscribe"), + HandlerPublisherInstrumentation.class.getName() + "$WrapSubscriberAdvice"); + } + + @SuppressWarnings("unused") + public static class WrapSubscriberAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter( + @Advice.Argument(value = 0, readOnly = false) Subscriber subscriber) { + subscriber = new SubscriberWrapper<>(subscriber, Java8BytecodeBridge.currentContext()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/HeadersInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/HeadersInjectAdapter.java new file mode 100644 index 000000000..8273c518c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/HeadersInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws; + +import io.opentelemetry.context.propagation.TextMapSetter; +import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders; + +public class HeadersInjectAdapter implements TextMapSetter { + + public static final HeadersInjectAdapter SETTER = new HeadersInjectAdapter(); + + @Override + public void set(HttpHeaders carrier, String key, String value) { + carrier.set(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientTracer.java new file mode 100644 index 000000000..3fc06d33b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientTracer.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws; + +import static io.opentelemetry.javaagent.instrumentation.playws.HeadersInjectAdapter.SETTER; + +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.URI; +import java.net.URISyntaxException; +import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders; +import play.shaded.ahc.org.asynchttpclient.Request; +import play.shaded.ahc.org.asynchttpclient.Response; + +public class PlayWsClientTracer extends HttpClientTracer { + private static final PlayWsClientTracer TRACER = new PlayWsClientTracer(); + + private PlayWsClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static PlayWsClientTracer tracer() { + return TRACER; + } + + @Override + protected String method(Request request) { + return request.getMethod(); + } + + @Override + protected URI url(Request request) throws URISyntaxException { + return request.getUri().toJavaNetURI(); + } + + @Override + protected Integer status(Response response) { + return response.getStatusCode(); + } + + @Override + protected String requestHeader(Request request, String name) { + return request.getHeaders().get(name); + } + + @Override + protected String responseHeader(Response response, String name) { + return response.getHeaders().get(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.play-ws-common"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/SubscriberWrapper.java b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/SubscriberWrapper.java new file mode 100644 index 000000000..7b713e673 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/SubscriberWrapper.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.playws; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public class SubscriberWrapper implements Subscriber { + private final Subscriber delegate; + private final Context context; + + public SubscriberWrapper(Subscriber delegate, Context context) { + this.delegate = delegate; + this.context = context; + } + + @Override + public void onSubscribe(Subscription subscription) { + try (Scope ignored = context.makeCurrent()) { + delegate.onSubscribe(subscription); + } + } + + @Override + public void onNext(T o) { + try (Scope ignored = context.makeCurrent()) { + delegate.onNext(o); + } + } + + @Override + public void onError(Throwable throwable) { + try (Scope ignored = context.makeCurrent()) { + delegate.onError(throwable); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + delegate.onComplete(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/play-ws-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/play-ws-testing.gradle new file mode 100644 index 000000000..26a149d8e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/play-ws-testing.gradle @@ -0,0 +1,12 @@ +apply plugin: "otel.java-conventions" + +def scalaVersion = '2.12' + +dependencies { + api project(':testing-common') + api group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '1.0.2' + + implementation "org.codehaus.groovy:groovy-all" + implementation "io.opentelemetry:opentelemetry-api" + implementation "org.spockframework:spock-core" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBase.groovy b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBase.groovy new file mode 100644 index 000000000..5d44c11f2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBase.groovy @@ -0,0 +1,202 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.util.concurrent.CompletionStage +import java.util.concurrent.TimeUnit +import play.libs.ws.StandaloneWSClient +import play.libs.ws.StandaloneWSRequest +import play.libs.ws.StandaloneWSResponse +import play.libs.ws.ahc.StandaloneAhcWSClient +import scala.Function1 +import scala.collection.JavaConverters +import scala.concurrent.Await +import scala.concurrent.ExecutionContext +import scala.concurrent.Future +import scala.concurrent.duration.Duration +import scala.util.Try +import spock.lang.Shared + +class PlayJavaWsClientTestBase extends PlayWsClientTestBaseBase { + @Shared + StandaloneWSClient wsClient + + @Override + StandaloneWSRequest buildRequest(String method, URI uri, Map headers) { + def request = wsClient.url(uri.toURL().toString()).setFollowRedirects(true) + headers.entrySet().each { entry -> request.addHeader(entry.getKey(), entry.getValue()) } + return request.setMethod(method) + } + + @Override + int sendRequest(StandaloneWSRequest request, String method, URI uri, Map headers) { + return request.execute().toCompletableFuture().get().status + } + + @Override + void sendRequestWithCallback(StandaloneWSRequest request, String method, URI uri, Map headers, HttpClientTest.RequestResult requestResult) { + request.execute().whenComplete { response, throwable -> + requestResult.complete({ response.status }, throwable) + } + } + + def setupSpec() { + wsClient = new StandaloneAhcWSClient(asyncHttpClient, materializer) + } + + def cleanupSpec() { + wsClient?.close() + } +} + +class PlayJavaStreamedWsClientTestBase extends PlayWsClientTestBaseBase { + @Shared + StandaloneWSClient wsClient + + @Override + StandaloneWSRequest buildRequest(String method, URI uri, Map headers) { + def request = wsClient.url(uri.toURL().toString()).setFollowRedirects(true) + headers.entrySet().each { entry -> request.addHeader(entry.getKey(), entry.getValue()) } + request.setMethod(method) + return request + } + + @Override + int sendRequest(StandaloneWSRequest request, String method, URI uri, Map headers) { + return internalSendRequest(request).toCompletableFuture().get().status + } + + @Override + void sendRequestWithCallback(StandaloneWSRequest request, String method, URI uri, Map headers, HttpClientTest.RequestResult requestResult) { + internalSendRequest(request).whenComplete { response, throwable -> + requestResult.complete({ response.status }, throwable?.getCause()) + } + } + + private CompletionStage internalSendRequest(StandaloneWSRequest request) { + def stream = request.stream() + // The status can be ready before the body so explicitly call wait for body to be ready + return stream + .thenCompose { StandaloneWSResponse response -> + response.getBodyAsSource().runFold("", { acc, out -> "" }, materializer) + } + .thenCombine(stream) { String body, StandaloneWSResponse response -> + response + } + } + + def setupSpec() { + wsClient = new StandaloneAhcWSClient(asyncHttpClient, materializer) + } + + def cleanupSpec() { + wsClient?.close() + } +} + +class PlayScalaWsClientTestBase extends PlayWsClientTestBaseBase { + @Shared + play.api.libs.ws.StandaloneWSClient wsClient + + @Override + play.api.libs.ws.StandaloneWSRequest buildRequest(String method, URI uri, Map headers) { + return wsClient.url(uri.toURL().toString()) + .withMethod(method) + .withFollowRedirects(true) + .withHttpHeaders(JavaConverters.mapAsScalaMap(headers).toSeq()) + } + + @Override + int sendRequest(play.api.libs.ws.StandaloneWSRequest request, String method, URI uri, Map headers) { + def futureResponse = request.execute() + Await.ready(futureResponse, Duration.apply(10, TimeUnit.SECONDS)) + def value = futureResponse.value().get() + if (value.isSuccess()) { + return value.get().status() + } + throw value.failed().get() + } + + @Override + void sendRequestWithCallback(play.api.libs.ws.StandaloneWSRequest request, String method, URI uri, Map headers, HttpClientTest.RequestResult requestResult) { + request.execute().onComplete(new Function1, Void>() { + @Override + Void apply(Try response) { + if (response.isSuccess()) { + requestResult.complete(response.get().status()) + } else { + requestResult.complete(response.failed().get()) + } + return null + } + }, ExecutionContext.global()) + } + + def setupSpec() { + wsClient = new play.api.libs.ws.ahc.StandaloneAhcWSClient(asyncHttpClient, materializer) + } + + def cleanupSpec() { + wsClient?.close() + } +} + +class PlayScalaStreamedWsClientTestBase extends PlayWsClientTestBaseBase { + @Shared + play.api.libs.ws.StandaloneWSClient wsClient + + @Override + play.api.libs.ws.StandaloneWSRequest buildRequest(String method, URI uri, Map headers) { + return wsClient.url(uri.toURL().toString()) + .withMethod(method) + .withFollowRedirects(true) + .withHttpHeaders(JavaConverters.mapAsScalaMap(headers).toSeq()) + } + + @Override + int sendRequest(play.api.libs.ws.StandaloneWSRequest request, String method, URI uri, Map headers) { + Await.result(internalSendRequest(request), Duration.apply(10, TimeUnit.SECONDS)).status() + } + + @Override + void sendRequestWithCallback(play.api.libs.ws.StandaloneWSRequest request, String method, URI uri, Map headers, HttpClientTest.RequestResult requestResult) { + internalSendRequest(request).onComplete(new Function1, Void>() { + @Override + Void apply(Try response) { + if (response.isSuccess()) { + requestResult.complete(response.get().status()) + } else { + requestResult.complete(response.failed().get()) + } + return null + } + }, ExecutionContext.global()) + } + + private Future internalSendRequest(play.api.libs.ws.StandaloneWSRequest request) { + Future futureResponse = request.stream() + // The status can be ready before the body so explicitly call wait for body to be ready + Future bodyResponse = futureResponse.flatMap(new Function1>() { + @Override + Future apply(play.api.libs.ws.StandaloneWSResponse wsResponse) { + return wsResponse.bodyAsSource().runFold("", { acc, out -> "" }, materializer) + } + }, ExecutionContext.global()) + return bodyResponse.flatMap(new Function1>() { + @Override + Future apply(String v1) { + return futureResponse + } + }, ExecutionContext.global()) + } + + def setupSpec() { + wsClient = new play.api.libs.ws.ahc.StandaloneAhcWSClient(asyncHttpClient, materializer) + } + + def cleanupSpec() { + wsClient?.close() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBaseBase.groovy b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBaseBase.groovy new file mode 100644 index 000000000..0b7ba3247 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBaseBase.groovy @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +import akka.stream.ActorMaterializerSettings +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import play.shaded.ahc.io.netty.resolver.InetNameResolver +import play.shaded.ahc.io.netty.util.concurrent.EventExecutor +import play.shaded.ahc.io.netty.util.concurrent.ImmediateEventExecutor +import play.shaded.ahc.io.netty.util.concurrent.Promise +import play.shaded.ahc.org.asynchttpclient.AsyncHttpClient +import play.shaded.ahc.org.asynchttpclient.AsyncHttpClientConfig +import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient +import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClientConfig +import play.shaded.ahc.org.asynchttpclient.RequestBuilderBase +import spock.lang.Shared + +abstract class PlayWsClientTestBaseBase extends HttpClientTest implements AgentTestTrait { + @Shared + ActorSystem system + + @Shared + AsyncHttpClient asyncHttpClient + + @Shared + ActorMaterializer materializer + + def setupSpec() { + String name = "play-ws" + system = ActorSystem.create(name) + ActorMaterializerSettings settings = ActorMaterializerSettings.create(system) + materializer = ActorMaterializer.create(settings, system, name) + + // Replace dns name resolver with custom implementation that returns only once address for each + // host. This is needed for "connection error dropped request" because in case of connection + // failure ahc will try the next address which isn't necessary for this test. + RequestBuilderBase.DEFAULT_NAME_RESOLVER = new CustomNameResolver(ImmediateEventExecutor.INSTANCE) + + AsyncHttpClientConfig asyncHttpClientConfig = + new DefaultAsyncHttpClientConfig.Builder() + .setMaxRequestRetry(0) + .setShutdownQuietPeriod(0) + .setShutdownTimeout(0) + .setMaxRedirects(3) + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .build() + + asyncHttpClient = new DefaultAsyncHttpClient(asyncHttpClientConfig) + } + + def cleanupSpec() { + system?.terminate() + } + + @Override + int maxRedirects() { + 3 + } +} + +class CustomNameResolver extends InetNameResolver { + CustomNameResolver(EventExecutor executor) { + super(executor) + } + + protected void doResolve(String inetHost, Promise promise) throws Exception { + try { + promise.setSuccess(InetAddress.getByName(inetHost)) + } catch (UnknownHostException exception) { + promise.setFailure(exception) + } + } + + protected void doResolveAll(String inetHost, Promise> promise) throws Exception { + try { + // default implementation calls InetAddress.getAllByName + promise.setSuccess(Collections.singletonList(InetAddress.getByName(inetHost))) + } catch (UnknownHostException exception) { + promise.setFailure(exception) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/.gitignore b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/.gitignore new file mode 100644 index 000000000..5292519a2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/.gitignore @@ -0,0 +1 @@ +logs/ \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/play-2.4-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/play-2.4-javaagent.gradle new file mode 100644 index 000000000..54ac56901 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/play-2.4-javaagent.gradle @@ -0,0 +1,67 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'com.typesafe.play' + module = 'play_2.11' + versions = '[2.4.0,2.6)' + assertInverse = true + // versions 2.3.9 and 2.3.10 depends on com.typesafe.netty:netty-http-pipelining:1.1.2 + // which does not exist + skip('2.3.9', '2.3.10') + } + fail { + group = 'com.typesafe.play' + module = 'play_2.12' + versions = '[,]' + } + fail { + group = 'com.typesafe.play' + module = 'play_2.13' + versions = '[,]' + } +} + +otelJava { + // Play doesn't work with Java 9+ until 2.6.12 + maxJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +dependencies { + // TODO(anuraaga): Something about library configuration doesn't work well with scala compilation + // here. + compileOnly "com.typesafe.play:play_2.11:2.4.0" + + testInstrumentation project(':instrumentation:netty:netty-4.0:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:akka-http-10.0:javaagent') + testInstrumentation project(':instrumentation:async-http-client:async-http-client-2.0:javaagent') + + // Before 2.5, play used netty 3.x which isn't supported, so for better test consistency, we test with just 2.5 + testLibrary "com.typesafe.play:play-java_2.11:2.5.0" + testLibrary "com.typesafe.play:play-java-ws_2.11:2.5.0" + testLibrary("com.typesafe.play:play-test_2.11:2.5.0") { + exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' + } + + latestDepTestLibrary "com.typesafe.play:play-java_2.11:2.5.+" + latestDepTestLibrary "com.typesafe.play:play-java-ws_2.11:2.5.+" + latestDepTestLibrary("com.typesafe.play:play-test_2.11:2.5.+") { + exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' + } +} + +// async-http-client 2.0 does not work with Netty versions newer than this due to referencing an +// internal file. +if (!testLatestDeps) { + configurations.configureEach { + resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + //specifying a fixed version for all libraries with io.netty' group + if (details.requested.group == 'io.netty' && details.requested.name != "netty-bom" && details.requested.name != "netty") { + details.useVersion "4.0.34.Final" + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/ActionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/ActionInstrumentation.java new file mode 100644 index 000000000..22591f297 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/ActionInstrumentation.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.play.v2_4; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.play.v2_4.PlayTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import play.api.mvc.Action; +import play.api.mvc.Request; +import play.api.mvc.Result; +import scala.concurrent.Future; + +public class ActionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("play.api.mvc.Action"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("play.api.mvc.Action")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("apply") + .and(takesArgument(0, named("play.api.mvc.Request"))) + .and(returns(named("scala.concurrent.Future"))), + this.getClass().getName() + "$ApplyAdvice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request req, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startSpan("play.request", SpanKind.INTERNAL); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopTraceOnResponse( + @Advice.This Object thisAction, + @Advice.Thrown Throwable throwable, + @Advice.Argument(0) Request req, + @Advice.Return(readOnly = false) Future responseFuture, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + // Call onRequest on return after tags are populated. + tracer().updateSpanName(Java8BytecodeBridge.spanFromContext(context), req); + // set the span name on the upstream akka/netty span + tracer().updateSpanName(ServerSpan.fromContextOrNull(context), req); + + scope.close(); + // span finished in RequestCompleteCallback + if (throwable == null) { + responseFuture.onComplete( + new RequestCompleteCallback(context), ((Action) thisAction).executionContext()); + } else { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/PlayInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/PlayInstrumentationModule.java new file mode 100644 index 000000000..1182d842f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/PlayInstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.play.v2_4; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class PlayInstrumentationModule extends InstrumentationModule { + + public PlayInstrumentationModule() { + super("play", "play-2.4"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // play.GlobalSettings was removed in 2.6 + return hasClassesNamed("play.GlobalSettings"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ActionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/PlayTracer.java b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/PlayTracer.java new file mode 100644 index 000000000..a0229017d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/PlayTracer.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.play.v2_4; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import play.api.mvc.Request; +import scala.Option; + +public class PlayTracer extends BaseTracer { + private static final PlayTracer TRACER = new PlayTracer(); + + public static PlayTracer tracer() { + return TRACER; + } + + public void updateSpanName(Span span, Request request) { + if (request != null) { + Option pathOption = request.tags().get("ROUTE_PATTERN"); + if (!pathOption.isEmpty()) { + String path = pathOption.get(); + span.updateName(path); + } + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.play-2.4"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/RequestCompleteCallback.java b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/RequestCompleteCallback.java new file mode 100644 index 000000000..7a5f398ca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/RequestCompleteCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.play.v2_4; + +import static io.opentelemetry.javaagent.instrumentation.play.v2_4.PlayTracer.tracer; + +import io.opentelemetry.context.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import play.api.mvc.Result; +import scala.runtime.AbstractFunction1; +import scala.util.Try; + +public class RequestCompleteCallback extends AbstractFunction1, Object> { + + private static final Logger log = LoggerFactory.getLogger(RequestCompleteCallback.class); + + private final Context context; + + public RequestCompleteCallback(Context context) { + this.context = context; + } + + @Override + public Object apply(Try result) { + try { + if (result.isFailure()) { + tracer().endExceptionally(context, result.failed().get()); + } else { + tracer().end(context); + } + } catch (Throwable t) { + log.debug("error in play instrumentation", t); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy new file mode 100644 index 000000000..c7733116a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.CompletionStage +import play.libs.ws.WS +import play.libs.ws.WSRequest +import play.libs.ws.WSResponse +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Subject + +// Play 2.6+ uses a separately versioned client that shades the underlying dependency +// This means our built in instrumentation won't work. +class PlayWsClientTest extends HttpClientTest implements AgentTestTrait { + @Subject + @Shared + @AutoCleanup + def client = WS.newClient(-1) + + @Override + WSRequest buildRequest(String method, URI uri, Map headers) { + def request = client.url(uri.toString()) + headers.entrySet().each { + request.setHeader(it.key, it.value) + } + return request + } + + @Override + int sendRequest(WSRequest request, String method, URI uri, Map headers) { + return internalSendRequest(request, method).toCompletableFuture().get().status + } + + @Override + void sendRequestWithCallback(WSRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + internalSendRequest(request, method).whenComplete { response, throwable -> + requestResult.complete({ response.status }, throwable) + } + } + + private static CompletionStage internalSendRequest(WSRequest request, String method) { + return request.execute(method) + } + + //TODO see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2347 +// @Override +// String userAgent() { +// return "AHC" +// } + + @Override + boolean testRedirects() { + false + } + + @Override + Set> httpAttributes(URI uri) { + Set> extra = [ + SemanticAttributes.HTTP_SCHEME, + SemanticAttributes.HTTP_TARGET + ] + super.httpAttributes(uri) + extra + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + // Play HTTP client uses AsyncHttpClient internally which does not support HTTP 1.1 pipelining + // nor waiting for connection pool slots to free up. Therefore making a single connection test + // would require manually sequencing the connections, which is not meaningful for a high + // concurrency test. + return null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/server/PlayAsyncServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/server/PlayAsyncServerTest.groovy new file mode 100644 index 000000000..f29ae6ae2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/server/PlayAsyncServerTest.groovy @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static play.mvc.Http.Context.Implicit.request + +import java.util.concurrent.CompletableFuture +import java.util.function.Supplier +import play.libs.concurrent.HttpExecution +import play.mvc.Results +import play.routing.RoutingDsl +import play.server.Server + +class PlayAsyncServerTest extends PlayServerTest { + @Override + Server startServer(int port) { + def router = + new RoutingDsl() + .GET(SUCCESS.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(SUCCESS) { + Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) + } + }, HttpExecution.defaultContext()) + } as Supplier) + .GET(INDEXED_CHILD.getPath()).routeTo({ + CompletableFuture.supplyAsync({ + controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { request().getQueryString(it) } + Results.status(INDEXED_CHILD.getStatus()) + } + }, HttpExecution.defaultContext()) + } as Supplier) + .GET(QUERY_PARAM.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(QUERY_PARAM) { + Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) + } + }, HttpExecution.defaultContext()) + } as Supplier) + .GET(REDIRECT.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(REDIRECT) { + Results.found(REDIRECT.getBody()) + } + }, HttpExecution.defaultContext()) + } as Supplier) + .GET(ERROR.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(ERROR) { + Results.status(ERROR.getStatus(), ERROR.getBody()) + } + }, HttpExecution.defaultContext()) + } as Supplier) + .GET(EXCEPTION.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(EXCEPTION) { + throw new Exception(EXCEPTION.getBody()) + } + }, HttpExecution.defaultContext()) + } as Supplier) + + return Server.forRouter(router.build(), port) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/server/PlayServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/server/PlayServerTest.groovy new file mode 100644 index 000000000..15e343886 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.4/javaagent/src/test/groovy/server/PlayServerTest.groovy @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static play.mvc.Http.Context.Implicit.request + +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.function.Supplier +import play.mvc.Results +import play.routing.RoutingDsl +import play.server.Server + +class PlayServerTest extends HttpServerTest implements AgentTestTrait { + @Override + Server startServer(int port) { + def router = + new RoutingDsl() + .GET(SUCCESS.getPath()).routeTo({ + controller(SUCCESS) { + Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) + } + } as Supplier) + .GET(INDEXED_CHILD.getPath()).routeTo({ + controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { request().getQueryString(it) } + Results.status(INDEXED_CHILD.getStatus()) + } + } as Supplier) + .GET(QUERY_PARAM.getPath()).routeTo({ + controller(QUERY_PARAM) { + Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) + } + } as Supplier) + .GET(REDIRECT.getPath()).routeTo({ + controller(REDIRECT) { + Results.found(REDIRECT.getBody()) + } + } as Supplier) + .GET(ERROR.getPath()).routeTo({ + controller(ERROR) { + Results.status(ERROR.getStatus(), ERROR.getBody()) + } + } as Supplier) + .GET(EXCEPTION.getPath()).routeTo({ + controller(EXCEPTION) { + throw new Exception(EXCEPTION.getBody()) + } + } as Supplier) + + return Server.forRouter(router.build(), port) + } + + @Override + void stopServer(Server server) { + server.stop() + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + true + } + + @Override + boolean testConcurrency() { + return true + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name "play.request" + kind INTERNAL + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(Exception, EXCEPTION.body) + } + childOf((SpanData) parent) + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "HTTP GET" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/.gitignore b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/.gitignore new file mode 100644 index 000000000..5292519a2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/.gitignore @@ -0,0 +1 @@ +logs/ \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/play-2.6-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/play-2.6-javaagent.gradle new file mode 100644 index 000000000..a1c855713 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/play-2.6-javaagent.gradle @@ -0,0 +1,55 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +def scalaVersion = '2.11' +def playVersion = '2.6.0' + +muzzle { + pass { + group = 'com.typesafe.play' + module = "play_$scalaVersion" + versions = "[$playVersion,)" + assertInverse = true + // versions 2.3.9 and 2.3.10 depends on com.typesafe.netty:netty-http-pipelining:1.1.2 + // which does not exist + skip('2.3.9', '2.3.10') + } + pass { + group = 'com.typesafe.play' + module = 'play_2.12' + versions = "[$playVersion,)" + assertInverse = true + } + pass { + group = 'com.typesafe.play' + module = 'play_2.13' + versions = "[$playVersion,)" + assertInverse = true + } +} + +otelJava { + // Play doesn't work with Java 9+ until 2.6.12 + maxJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +dependencies { + // TODO(anuraaga): Something about library configuration doesn't work well with scala compilation + // here. + compileOnly group: 'com.typesafe.play', name: "play_$scalaVersion", version: playVersion + + testInstrumentation project(':instrumentation:netty:netty-4.0:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:akka-http-10.0:javaagent') + + testLibrary group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: playVersion + // TODO: Play WS is a separately versioned library starting with 2.6 and needs separate instrumentation. + testLibrary(group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: playVersion) { + exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' + } + + // TODO: This should be changed to the latest in scala 2.13 instead of 2.11 since its ahead + latestDepTestLibrary group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: '2.+' + latestDepTestLibrary(group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: '2.+') { + exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/ActionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/ActionInstrumentation.java new file mode 100644 index 000000000..4febcb7a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/ActionInstrumentation.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.play.v2_6; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.play.v2_6.PlayTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import play.api.mvc.Action; +import play.api.mvc.Request; +import play.api.mvc.Result; +import scala.concurrent.Future; + +public class ActionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("play.api.mvc.Action"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("play.api.mvc.Action")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("apply") + .and(takesArgument(0, named("play.api.mvc.Request"))) + .and(returns(named("scala.concurrent.Future"))), + this.getClass().getName() + "$ApplyAdvice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request req, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startSpan("play.request", SpanKind.INTERNAL); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopTraceOnResponse( + @Advice.This Object thisAction, + @Advice.Thrown Throwable throwable, + @Advice.Argument(0) Request req, + @Advice.Return(readOnly = false) Future responseFuture, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + // Call onRequest on return after tags are populated. + tracer().updateSpanName(Java8BytecodeBridge.spanFromContext(context), req); + // set the span name on the upstream akka/netty span + tracer().updateSpanName(ServerSpan.fromContextOrNull(context), req); + + scope.close(); + // span finished in RequestCompleteCallback + if (throwable == null) { + responseFuture.onComplete( + new RequestCompleteCallback(context), ((Action) thisAction).executionContext()); + } else { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/PlayInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/PlayInstrumentationModule.java new file mode 100644 index 000000000..2bbcbbb7d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/PlayInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.play.v2_6; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class PlayInstrumentationModule extends InstrumentationModule { + + public PlayInstrumentationModule() { + super("play", "play-2.6"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ActionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/PlayTracer.java b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/PlayTracer.java new file mode 100644 index 000000000..de02704d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/PlayTracer.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.play.v2_6; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.checkerframework.checker.nullness.qual.Nullable; +import play.api.mvc.Request; +import play.api.routing.HandlerDef; +import play.libs.typedmap.TypedKey; +import play.routing.Router; +import scala.Option; + +public class PlayTracer extends BaseTracer { + private static final PlayTracer TRACER = new PlayTracer(); + + public static PlayTracer tracer() { + return TRACER; + } + + @Nullable private static final Method typedKeyGetUnderlying; + + static { + Method typedKeyGetUnderlyingCheck = null; + try { + // This method was added in Play 2.6.8 + typedKeyGetUnderlyingCheck = TypedKey.class.getMethod("asScala"); + } catch (NoSuchMethodException ignored) { + // Ignore + } + // Fallback + if (typedKeyGetUnderlyingCheck == null) { + try { + typedKeyGetUnderlyingCheck = TypedKey.class.getMethod("underlying"); + } catch (NoSuchMethodException ignored) { + // Ignore + } + } + typedKeyGetUnderlying = typedKeyGetUnderlyingCheck; + } + + public void updateSpanName(Span span, Request request) { + if (request != null) { + // more about routes here: + // https://github.com/playframework/playframework/blob/master/documentation/manual/releases/release26/migration26/Migration26.md + Option defOption = null; + if (typedKeyGetUnderlying != null) { // Should always be non-null but just to make sure + try { + defOption = + request + .attrs() + .get( + (play.api.libs.typedmap.TypedKey) + typedKeyGetUnderlying.invoke(Router.Attrs.HANDLER_DEF)); + } catch (IllegalAccessException | InvocationTargetException ignored) { + // Ignore + } + } + if (defOption != null && !defOption.isEmpty()) { + String path = defOption.get().path(); + span.updateName(path); + } + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.play-2.6"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/RequestCompleteCallback.java b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/RequestCompleteCallback.java new file mode 100644 index 000000000..841b6b7f4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/RequestCompleteCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.play.v2_6; + +import static io.opentelemetry.javaagent.instrumentation.play.v2_6.PlayTracer.tracer; + +import io.opentelemetry.context.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import play.api.mvc.Result; +import scala.runtime.AbstractFunction1; +import scala.util.Try; + +public class RequestCompleteCallback extends AbstractFunction1, Object> { + + private static final Logger log = LoggerFactory.getLogger(RequestCompleteCallback.class); + + private final Context context; + + public RequestCompleteCallback(Context context) { + this.context = context; + } + + @Override + public Object apply(Try result) { + try { + if (result.isFailure()) { + tracer().endExceptionally(context, result.failed().get()); + } else { + tracer().end(context); + } + } catch (Throwable t) { + log.debug("error in play instrumentation", t); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/test/groovy/server/PlayAsyncServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/test/groovy/server/PlayAsyncServerTest.groovy new file mode 100644 index 000000000..b40b9cd07 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/test/groovy/server/PlayAsyncServerTest.groovy @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import java.util.function.Supplier +import play.BuiltInComponents +import play.Mode +import play.libs.concurrent.HttpExecution +import play.mvc.Results +import play.routing.RoutingDsl +import play.server.Server +import spock.lang.Shared + +class PlayAsyncServerTest extends PlayServerTest { + @Shared + def executor = Executors.newCachedThreadPool() + + def cleanupSpec() { + executor.shutdown() + } + + @Override + Server startServer(int port) { + def execContext = HttpExecution.fromThread(executor) + return Server.forRouter(Mode.TEST, port) { BuiltInComponents components -> + RoutingDsl.fromComponents(components) + .GET(SUCCESS.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(SUCCESS) { + Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) + } + }, execContext) + } as Supplier) + .GET(QUERY_PARAM.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(QUERY_PARAM) { + Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) + } + }, execContext) + } as Supplier) + .GET(REDIRECT.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(REDIRECT) { + Results.found(REDIRECT.getBody()) + } + }, execContext) + } as Supplier) + .GET(ERROR.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(ERROR) { + Results.status(ERROR.getStatus(), ERROR.getBody()) + } + }, execContext) + } as Supplier) + .GET(EXCEPTION.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(EXCEPTION) { + throw new Exception(EXCEPTION.getBody()) + } + }, execContext) + } as Supplier) + .build() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/test/groovy/server/PlayServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/test/groovy/server/PlayServerTest.groovy new file mode 100644 index 000000000..f9d973591 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/play/play-2.6/javaagent/src/test/groovy/server/PlayServerTest.groovy @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.function.Supplier +import play.BuiltInComponents +import play.Mode +import play.mvc.Results +import play.routing.RoutingDsl +import play.server.Server + +class PlayServerTest extends HttpServerTest implements AgentTestTrait { + @Override + Server startServer(int port) { + return Server.forRouter(Mode.TEST, port) { BuiltInComponents components -> + RoutingDsl.fromComponents(components) + .GET(SUCCESS.getPath()).routeTo({ + controller(SUCCESS) { + Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) + } + } as Supplier) + .GET(QUERY_PARAM.getPath()).routeTo({ + controller(QUERY_PARAM) { + Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) + } + } as Supplier) + .GET(REDIRECT.getPath()).routeTo({ + controller(REDIRECT) { + Results.found(REDIRECT.getBody()) + } + } as Supplier) + .GET(ERROR.getPath()).routeTo({ + controller(ERROR) { + Results.status(ERROR.getStatus(), ERROR.getBody()) + } + } as Supplier) + .GET(EXCEPTION.getPath()).routeTo({ + controller(EXCEPTION) { + throw new Exception(EXCEPTION.getBody()) + } + } as Supplier) + .build() + } + } + + @Override + void stopServer(Server server) { + server.stop() + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + true + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name "play.request" + kind INTERNAL + childOf((SpanData) parent) + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(Exception, EXCEPTION.body) + } + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "akka.request" + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/rabbitmq-2.7-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/rabbitmq-2.7-javaagent.gradle new file mode 100644 index 000000000..d513a8b4b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/rabbitmq-2.7-javaagent.gradle @@ -0,0 +1,27 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.rabbitmq" + module = 'amqp-client' + versions = "[2.7.0,)" + assertInverse = true + } +} + +dependencies { + library "com.rabbitmq:amqp-client:2.7.0" + + testLibrary ("org.springframework.amqp:spring-rabbit:1.1.0.RELEASE") { + exclude group: 'com.rabbitmq', module: 'amqp-client' + } + + testInstrumentation project(':instrumentation:reactor-3.1:javaagent') + + testLibrary 'io.projectreactor.rabbitmq:reactor-rabbitmq:1.0.0.RELEASE' +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.rabbitmq.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelInstrumentation.java new file mode 100644 index 000000000..d7fb3a315 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelInstrumentation.java @@ -0,0 +1,225 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.rabbitmq.RabbitCommandInstrumentation.SpanHolder.CURRENT_RABBIT_CONTEXT; +import static io.opentelemetry.javaagent.instrumentation.rabbitmq.RabbitTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.canThrow; +import static net.bytebuddy.matcher.ElementMatchers.isGetter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isSetter; +import static net.bytebuddy.matcher.ElementMatchers.nameEndsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.GetResponse; +import com.rabbitmq.client.MessageProperties; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RabbitChannelInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.rabbitmq.client.Channel"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("com.rabbitmq.client.Channel")) + // broken implementation that throws UnsupportedOperationException on getConnection() calls + .and(not(named("reactor.rabbitmq.ChannelProxy"))); + } + + @Override + public void transform(TypeTransformer transformer) { + // these transformations need to be applied in a specific order + transformer.applyAdviceToMethod( + isMethod() + .and( + not( + isGetter() + .or(isSetter()) + .or(nameEndsWith("Listener")) + .or(nameEndsWith("Listeners")) + .or(namedOneOf("processAsync", "open", "close", "abort", "basicGet")))) + .and(isPublic()) + .and(canThrow(IOException.class).or(canThrow(InterruptedException.class))), + RabbitChannelInstrumentation.class.getName() + "$ChannelMethodAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("basicPublish")).and(takesArguments(6)), + RabbitChannelInstrumentation.class.getName() + "$ChannelPublishAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("basicGet")).and(takesArgument(0, String.class)), + RabbitChannelInstrumentation.class.getName() + "$ChannelGetAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("basicConsume")) + .and(takesArgument(0, String.class)) + .and(takesArgument(6, named("com.rabbitmq.client.Consumer"))), + RabbitChannelInstrumentation.class.getName() + "$ChannelConsumeAdvice"); + } + + // TODO Why do we start span here and not in ChannelPublishAdvice below? + @SuppressWarnings("unused") + public static class ChannelMethodAdvice { + + @Advice.OnMethodEnter + public static void onEnter( + @Advice.This Channel channel, + @Advice.Origin("Channel.#m") String method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Channel.class); + if (callDepth > 0) { + return; + } + + context = tracer().startSpan(method, channel.getConnection()); + CURRENT_RABBIT_CONTEXT.set(context); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + CallDepthThreadLocalMap.reset(Channel.class); + + CURRENT_RABBIT_CONTEXT.remove(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } + + @SuppressWarnings("unused") + public static class ChannelPublishAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void setSpanNameAddHeaders( + @Advice.Argument(0) String exchange, + @Advice.Argument(1) String routingKey, + @Advice.Argument(value = 4, readOnly = false) AMQP.BasicProperties props, + @Advice.Argument(5) byte[] body) { + Context context = Java8BytecodeBridge.currentContext(); + Span span = Java8BytecodeBridge.spanFromContext(context); + + if (span.getSpanContext().isValid()) { + tracer().onPublish(span, exchange, routingKey); + if (body != null) { + span.setAttribute( + SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length); + } + + // This is the internal behavior when props are null. We're just doing it earlier now. + if (props == null) { + props = MessageProperties.MINIMAL_BASIC; + } + tracer().onProps(span, props); + + // We need to copy the BasicProperties and provide a header map we can modify + Map headers = props.getHeaders(); + headers = (headers == null) ? new HashMap<>() : new HashMap<>(headers); + + tracer().inject(context, headers, TextMapInjectAdapter.SETTER); + + props = + new AMQP.BasicProperties( + props.getContentType(), + props.getContentEncoding(), + headers, + props.getDeliveryMode(), + props.getPriority(), + props.getCorrelationId(), + props.getReplyTo(), + props.getExpiration(), + props.getMessageId(), + props.getTimestamp(), + props.getType(), + props.getUserId(), + props.getAppId(), + props.getClusterId()); + } + } + } + + @SuppressWarnings("unused") + public static class ChannelGetAdvice { + + @Advice.OnMethodEnter + public static long takeTimestamp(@Advice.Local("callDepth") int callDepth) { + + callDepth = CallDepthThreadLocalMap.incrementCallDepth(Channel.class); + return System.currentTimeMillis(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void extractAndStartSpan( + @Advice.This Channel channel, + @Advice.Argument(0) String queue, + @Advice.Enter long startTime, + @Advice.Local("callDepth") int callDepth, + @Advice.Return GetResponse response, + @Advice.Thrown Throwable throwable) { + if (callDepth > 0) { + return; + } + CallDepthThreadLocalMap.reset(Channel.class); + + // can't create span and put into scope in method enter above, because can't add parent after + // span creation + Context context = tracer().startGetSpan(queue, startTime, response, channel.getConnection()); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } + + @SuppressWarnings("unused") + public static class ChannelConsumeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapConsumer( + @Advice.Argument(0) String queue, + @Advice.Argument(value = 6, readOnly = false) Consumer consumer) { + // We have to save off the queue name here because it isn't available to the consumer later. + if (consumer != null && !(consumer instanceof TracedDelegatingConsumer)) { + consumer = new TracedDelegatingConsumer(queue, consumer); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitCommandInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitCommandInstrumentation.java new file mode 100644 index 000000000..3f853f894 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitCommandInstrumentation.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.rabbitmq.RabbitCommandInstrumentation.SpanHolder.CURRENT_RABBIT_CONTEXT; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.rabbitmq.client.Command; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RabbitCommandInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.rabbitmq.client.Command"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("com.rabbitmq.client.Command")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), + RabbitCommandInstrumentation.class.getName() + "$CommandConstructorAdvice"); + } + + public static class SpanHolder { + public static final ThreadLocal CURRENT_RABBIT_CONTEXT = new ThreadLocal<>(); + } + + @SuppressWarnings("unused") + public static class CommandConstructorAdvice { + + @Advice.OnMethodExit + public static void setSpanNameAddHeaders(@Advice.This Command command) { + + Context context = CURRENT_RABBIT_CONTEXT.get(); + if (context != null && command.getMethod() != null) { + RabbitTracer.onCommand(Java8BytecodeBridge.spanFromContext(context), command); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitMqInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitMqInstrumentationModule.java new file mode 100644 index 000000000..78d5c293c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitMqInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RabbitMqInstrumentationModule extends InstrumentationModule { + public RabbitMqInstrumentationModule() { + super("rabbitmq", "rabbitmq-2.7"); + } + + @Override + public List typeInstrumentations() { + return asList(new RabbitChannelInstrumentation(), new RabbitCommandInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitTracer.java b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitTracer.java new file mode 100644 index 000000000..0912f5cb6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitTracer.java @@ -0,0 +1,195 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.javaagent.instrumentation.rabbitmq.TextMapExtractAdapter.GETTER; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Command; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.GetResponse; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class RabbitTracer extends BaseTracer { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.rabbitmq.experimental-span-attributes", false); + + private static final RabbitTracer TRACER = new RabbitTracer(); + + public static RabbitTracer tracer() { + return TRACER; + } + + public Context startSpan(String method, Connection connection) { + Context parentContext = Context.current(); + SpanKind kind = method.equals("Channel.basicPublish") ? PRODUCER : CLIENT; + SpanBuilder span = + spanBuilder(parentContext, method, kind) + .setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "rabbitmq") + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue"); + + NetPeerAttributes.INSTANCE.setNetPeer(span, connection.getAddress(), connection.getPort()); + + return parentContext.with(span.startSpan()); + } + + public Context startGetSpan( + String queue, long startTime, GetResponse response, Connection connection) { + Context parentContext = Context.current(); + SpanBuilder spanBuilder = + spanBuilder(parentContext, spanNameOnGet(queue), CLIENT) + .setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "rabbitmq") + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue") + .setAttribute(SemanticAttributes.MESSAGING_OPERATION, "receive") + .setStartTimestamp(startTime, TimeUnit.MILLISECONDS); + + if (response != null) { + spanBuilder.setAttribute( + SemanticAttributes.MESSAGING_DESTINATION, + normalizeExchangeName(response.getEnvelope().getExchange())); + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + spanBuilder.setAttribute("rabbitmq.routing_key", response.getEnvelope().getRoutingKey()); + } + spanBuilder.setAttribute( + SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + (long) response.getBody().length); + } + NetPeerAttributes.INSTANCE.setNetPeer( + spanBuilder, connection.getAddress(), connection.getPort()); + onGet(spanBuilder, queue); + + // TODO: withClientSpan()? + return parentContext.with(spanBuilder.startSpan()); + } + + public Context startDeliverySpan( + String queue, Envelope envelope, AMQP.BasicProperties properties, byte[] body) { + Map headers = properties.getHeaders(); + Context parentContext = extract(headers, GETTER); + + long startTimeMillis = System.currentTimeMillis(); + Span span = + spanBuilder(parentContext, spanNameOnDeliver(queue), CONSUMER) + .setStartTimestamp(startTimeMillis, TimeUnit.MILLISECONDS) + .setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "rabbitmq") + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue") + .setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process") + .startSpan(); + onDeliver(span, envelope); + + if (body != null) { + span.setAttribute( + SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length); + } + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES && properties.getTimestamp() != null) { + // this will be set if the sender sets the timestamp, + // or if a plugin is installed on the rabbitmq broker + long produceTimeMillis = properties.getTimestamp().getTime(); + span.setAttribute( + "rabbitmq.record.queue_time_ms", Math.max(0L, startTimeMillis - produceTimeMillis)); + } + + return withConsumerSpan(parentContext, span); + } + + public void onPublish(Span span, String exchange, String routingKey) { + String exchangeName = normalizeExchangeName(exchange); + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION, exchangeName); + String routing = + routingKey == null || routingKey.isEmpty() + ? "" + : routingKey.startsWith("amq.gen-") ? "" : routingKey; + span.updateName(exchangeName + " -> " + routing + " send"); + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("rabbitmq.command", "basic.publish"); + if (routingKey != null && !routingKey.isEmpty()) { + span.setAttribute("rabbitmq.routing_key", routingKey); + } + } + } + + public void onProps(Span span, AMQP.BasicProperties props) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Integer deliveryMode = props.getDeliveryMode(); + if (deliveryMode != null) { + span.setAttribute("rabbitmq.delivery_mode", deliveryMode); + } + } + } + + public String spanNameOnGet(String queue) { + return (queue.startsWith("amq.gen-") ? "" : queue) + " receive"; + } + + public void onGet(SpanBuilder span, String queue) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("rabbitmq.command", "basic.get"); + span.setAttribute("rabbitmq.queue", queue); + } + } + + public String spanNameOnDeliver(String queue) { + if (queue == null || queue.isEmpty()) { + return " process"; + } else if (queue.startsWith("amq.gen-")) { + return " process"; + } else { + return queue + " process"; + } + } + + public void onDeliver(Span span, Envelope envelope) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("rabbitmq.command", "basic.deliver"); + } + + if (envelope != null) { + String exchange = envelope.getExchange(); + span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION, normalizeExchangeName(exchange)); + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + String routingKey = envelope.getRoutingKey(); + if (routingKey != null && !routingKey.isEmpty()) { + span.setAttribute("rabbitmq.routing_key", routingKey); + } + } + } + } + + private static String normalizeExchangeName(String exchange) { + return exchange == null || exchange.isEmpty() ? "" : exchange; + } + + public static void onCommand(Span span, Command command) { + String name = command.getMethod().protocolMethodName(); + + if (!name.equals("basic.publish")) { + span.updateName(name); + } + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("rabbitmq.command", name); + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.rabbitmq-2.7"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TextMapExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TextMapExtractAdapter.java new file mode 100644 index 000000000..75f0f90bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TextMapExtractAdapter.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Map; + +public class TextMapExtractAdapter implements TextMapGetter> { + + public static final TextMapExtractAdapter GETTER = new TextMapExtractAdapter(); + + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + Object obj = carrier.get(key); + return obj == null ? null : obj.toString(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TextMapInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TextMapInjectAdapter.java new file mode 100644 index 000000000..724f9edaa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TextMapInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Map; + +public class TextMapInjectAdapter implements TextMapSetter> { + + public static final TextMapInjectAdapter SETTER = new TextMapInjectAdapter(); + + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TracedDelegatingConsumer.java b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TracedDelegatingConsumer.java new file mode 100644 index 000000000..0caacb43d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TracedDelegatingConsumer.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import static io.opentelemetry.javaagent.instrumentation.rabbitmq.RabbitTracer.tracer; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownSignalException; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wrapping the consumer instead of instrumenting it directly because it doesn't get access to the + * queue name when the message is consumed. + */ +public class TracedDelegatingConsumer implements Consumer { + + private static final Logger log = LoggerFactory.getLogger(TracedDelegatingConsumer.class); + + private final String queue; + private final Consumer delegate; + + public TracedDelegatingConsumer(String queue, Consumer delegate) { + this.queue = queue; + this.delegate = delegate; + } + + @Override + public void handleConsumeOk(String consumerTag) { + delegate.handleConsumeOk(consumerTag); + } + + @Override + public void handleCancelOk(String consumerTag) { + delegate.handleCancelOk(consumerTag); + } + + @Override + public void handleCancel(String consumerTag) throws IOException { + delegate.handleCancel(consumerTag); + } + + @Override + public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) { + delegate.handleShutdownSignal(consumerTag, sig); + } + + @Override + public void handleRecoverOk(String consumerTag) { + delegate.handleRecoverOk(consumerTag); + } + + @Override + public void handleDelivery( + String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) + throws IOException { + Context context = tracer().startDeliverySpan(queue, envelope, properties, body); + + try (Scope ignored = context.makeCurrent()) { + // Call delegate. + delegate.handleDelivery(consumerTag, envelope, properties, body); + tracer().end(context); + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/RabbitMqTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/RabbitMqTest.groovy new file mode 100644 index 000000000..e5045617d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/RabbitMqTest.groovy @@ -0,0 +1,384 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.rabbitmq.client.AMQP +import com.rabbitmq.client.Channel +import com.rabbitmq.client.Connection +import com.rabbitmq.client.Consumer +import com.rabbitmq.client.DefaultConsumer +import com.rabbitmq.client.Envelope +import com.rabbitmq.client.GetResponse +import com.rabbitmq.client.ShutdownSignalException +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.springframework.amqp.core.AmqpAdmin +import org.springframework.amqp.core.AmqpTemplate +import org.springframework.amqp.core.Queue +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory +import org.springframework.amqp.rabbit.core.RabbitAdmin +import org.springframework.amqp.rabbit.core.RabbitTemplate + +class RabbitMqTest extends AgentInstrumentationSpecification implements WithRabbitMqTrait { + + Connection conn = connectionFactory.newConnection() + Channel channel = conn.createChannel() + + def setupSpec() { + startRabbit() + } + + def cleanupSpec() { + stopRabbit() + } + + def cleanup() { + try { + channel.close() + conn.close() + } catch (ShutdownSignalException ignored) { + } + } + + def "test rabbit publish/get"() { + setup: + GetResponse response = runUnderTrace("parent") { + channel.exchangeDeclare(exchangeName, "direct", false) + String queueName = channel.queueDeclare().getQueue() + channel.queueBind(queueName, exchangeName, routingKey) + channel.basicPublish(exchangeName, routingKey, null, "Hello, world!".getBytes()) + return channel.basicGet(queueName, true) + } + + expect: + new String(response.getBody()) == "Hello, world!" + + and: + assertTraces(1) { + trace(0, 6) { + span(0) { + name "parent" + attributes { + } + } + rabbitSpan(it, 1, null, null, null, "exchange.declare", span(0)) + rabbitSpan(it, 2, null, null, null, "queue.declare", span(0)) + rabbitSpan(it, 3, null, null, null, "queue.bind", span(0)) + rabbitSpan(it, 4, exchangeName, routingKey, "send", "$exchangeName -> $routingKey", span(0)) + rabbitSpan(it, 5, exchangeName, routingKey, "receive", "", span(0)) + } + } + + where: + exchangeName | routingKey + "some-exchange" | "some-routing-key" + } + + def "test rabbit publish/get default exchange"() { + setup: + String queueName = channel.queueDeclare().getQueue() + channel.basicPublish("", queueName, null, "Hello, world!".getBytes()) + GetResponse response = channel.basicGet(queueName, true) + + expect: + new String(response.getBody()) == "Hello, world!" + + and: + assertTraces(3) { + traces.subList(1, 3).sort(orderByRootSpanKind(PRODUCER, CLIENT)) + trace(0, 1) { + rabbitSpan(it, 0, null, null, null, "queue.declare") + } + trace(1, 1) { + rabbitSpan(it, 0, "", null, "send", " -> ") + } + trace(2, 1) { + rabbitSpan(it, 0, "", null, "receive", "", null) + } + } + } + + def "test rabbit consume #messageCount messages and setTimestamp=#setTimestamp"() { + setup: + channel.exchangeDeclare(exchangeName, "direct", false) + String queueName = (messageCount % 2 == 0) ? + channel.queueDeclare().getQueue() : + channel.queueDeclare("some-queue", false, true, true, null).getQueue() + channel.queueBind(queueName, exchangeName, "") + + def deliveries = [] + + Consumer callback = new DefaultConsumer(channel) { + @Override + void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + deliveries << new String(body) + } + } + + channel.basicConsume(queueName, callback) + + (1..messageCount).each { + if (setTimestamp) { + channel.basicPublish(exchangeName, "", + new AMQP.BasicProperties.Builder().timestamp(new Date()).build(), + "msg $it".getBytes()) + } else { + channel.basicPublish(exchangeName, "", null, "msg $it".getBytes()) + } + } + def resource = messageCount % 2 == 0 ? "" : queueName + + expect: + assertTraces(4 + messageCount) { + trace(0, 1) { + rabbitSpan(it, null, null, null, "exchange.declare") + } + trace(1, 1) { + rabbitSpan(it, null, null, null, "queue.declare") + } + trace(2, 1) { + rabbitSpan(it, null, null, null, "queue.bind") + } + trace(3, 1) { + rabbitSpan(it, null, null, null, "basic.consume") + } + (1..messageCount).each { + trace(3 + it, 2) { + rabbitSpan(it, 0, exchangeName, null, "send", "$exchangeName -> ") + rabbitSpan(it, 1, exchangeName, null, "process", resource, span(0), null, null, null, setTimestamp) + } + } + } + + deliveries == (1..messageCount).collect { "msg $it" } + + where: + exchangeName | messageCount | setTimestamp + "some-exchange" | 1 | false + "some-exchange" | 2 | false + "some-exchange" | 3 | false + "some-exchange" | 4 | false + "some-exchange" | 1 | true + "some-exchange" | 2 | true + "some-exchange" | 3 | true + "some-exchange" | 4 | true + } + + def "test rabbit consume error"() { + setup: + def error = new FileNotFoundException("Message Error") + channel.exchangeDeclare(exchangeName, "direct", false) + String queueName = channel.queueDeclare().getQueue() + channel.queueBind(queueName, exchangeName, "") + + Consumer callback = new DefaultConsumer(channel) { + @Override + void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + throw error + // Unfortunately this doesn't seem to be observable in the test outside of the span generated. + } + } + + channel.basicConsume(queueName, callback) + + channel.basicPublish(exchangeName, "", null, "msg".getBytes()) + + expect: + assertTraces(5) { + trace(0, 1) { + rabbitSpan(it, null, null, null, "exchange.declare") + } + trace(1, 1) { + rabbitSpan(it, null, null, null, "queue.declare") + } + trace(2, 1) { + rabbitSpan(it, null, null, null, "queue.bind") + } + trace(3, 1) { + rabbitSpan(it, null, null, null, "basic.consume") + } + trace(4, 2) { + rabbitSpan(it, 0, exchangeName, null, "send", "$exchangeName -> ") + rabbitSpan(it, 1, exchangeName, null, "process", "", span(0), null, error, error.message) + } + } + + where: + exchangeName = "some-error-exchange" + } + + def "test rabbit error (#command)"() { + when: + closure.call(channel) + + then: + def throwable = thrown(exception) + + and: + + assertTraces(1) { + trace(0, 1) { + rabbitSpan(it, null, null, operation, command, null, null, throwable, errorMsg) + } + } + + where: + command | exception | errorMsg | operation | closure + "exchange.declare" | IOException | null | null | { + it.exchangeDeclare("some-exchange", "invalid-type", true) + } + "Channel.basicConsume" | IllegalStateException | "Invalid configuration: 'queue' must be non-null." | null | { + it.basicConsume(null, null) + } + "" | IOException | null | "receive" | { + it.basicGet("amq.gen-invalid-channel", true) + } + } + + def "test spring rabbit"() { + setup: + def connectionFactory = new CachingConnectionFactory(connectionFactory) + AmqpAdmin admin = new RabbitAdmin(connectionFactory) + def queue = new Queue("some-routing-queue", false, true, true, null) + admin.declareQueue(queue) + AmqpTemplate template = new RabbitTemplate(connectionFactory) + template.convertAndSend(queue.name, "foo") + String message = (String) template.receiveAndConvert(queue.name) + + expect: + message == "foo" + + and: + assertTraces(3) { + traces.subList(1, 3).sort(orderByRootSpanKind(PRODUCER, CLIENT)) + trace(0, 1) { + rabbitSpan(it, null, null, null, "queue.declare") + } + trace(1, 1) { + rabbitSpan(it, 0, "", "some-routing-queue", "send", " -> some-routing-queue") + } + trace(2, 1) { + rabbitSpan(it, 0, "", "some-routing-queue", "receive", queue.name, null) + } + } + } + + def rabbitSpan( + TraceAssert trace, + String exchange, + String routingKey, + String operation, + String resource, + Object parentSpan = null, + Object linkSpan = null, + Throwable exception = null, + String errorMsg = null, + Boolean expectTimestamp = false + ) { + rabbitSpan(trace, 0, exchange, routingKey, operation, resource, parentSpan, linkSpan, exception, errorMsg, expectTimestamp) + } + + def rabbitSpan( + TraceAssert trace, + int index, + String exchange, + String routingKey, + String operation, + String resource, + Object parentSpan = null, + Object linkSpan = null, + Throwable exception = null, + String errorMsg = null, + Boolean expectTimestamp = false + ) { + + def spanName = resource + if (operation != null) { + spanName = spanName + " " + operation + } + + trace.span(index) { + name spanName + + switch (trace.span(index).attributes.get(AttributeKey.stringKey("rabbitmq.command"))) { + case "basic.publish": + kind PRODUCER + break + case "basic.get": + kind CLIENT + break + case "basic.deliver": + kind CONSUMER + break + default: + kind CLIENT + } + + if (parentSpan) { + childOf((SpanData) parentSpan) + } else { + hasNoParent() + } + + if (linkSpan) { + hasLink((SpanData) linkSpan) + } + + if (exception) { + status ERROR + errorEvent(exception.class, errorMsg) + } + + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" { it == null || it instanceof String } + "${SemanticAttributes.NET_PEER_IP.key}" { "127.0.0.1" } + "${SemanticAttributes.NET_PEER_PORT.key}" { it == null || it instanceof Long } + + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" exchange + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue" + //TODO add to SemanticAttributes + "rabbitmq.routing_key" { it == null || it == routingKey || it.startsWith("amq.gen-") } + if (operation != null && operation != "send") { + "${SemanticAttributes.MESSAGING_OPERATION.key}" operation + } + if (expectTimestamp) { + "rabbitmq.record.queue_time_ms" { it instanceof Long && it >= 0 } + } + + switch (trace.span(index).attributes.get(AttributeKey.stringKey("rabbitmq.command"))) { + case "basic.publish": + "rabbitmq.command" "basic.publish" + "rabbitmq.routing_key" { + it == null || it == "some-routing-key" || it == "some-routing-queue" || it.startsWith("amq.gen-") + } + "rabbitmq.delivery_mode" { it == null || it == 2 } + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + break + case "basic.get": + "rabbitmq.command" "basic.get" + //TODO why this queue name is not a destination for semantic convention + "rabbitmq.queue" { it == "some-queue" || it == "some-routing-queue" || it.startsWith("amq.gen-") } + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" { it == null || it instanceof Long } + break + case "basic.deliver": + "rabbitmq.command" "basic.deliver" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + break + default: + "rabbitmq.command" { it == null || it == resource } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/ReactorRabbitMqTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/ReactorRabbitMqTest.groovy new file mode 100644 index 000000000..9b070587d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/ReactorRabbitMqTest.groovy @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import reactor.rabbitmq.ExchangeSpecification +import reactor.rabbitmq.RabbitFlux +import reactor.rabbitmq.SenderOptions + +class ReactorRabbitMqTest extends AgentInstrumentationSpecification implements WithRabbitMqTrait { + + def setupSpec() { + startRabbit() + } + + def cleanupSpec() { + stopRabbit() + } + + def "should not fail declaring exchange"() { + given: + def sender = RabbitFlux.createSender(new SenderOptions().connectionFactory(connectionFactory)) + + when: + sender.declareExchange(ExchangeSpecification.exchange("testExchange")) + .block() + + then: + noExceptionThrown() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name 'exchange.declare' + kind SpanKind.CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" { it == null || it instanceof String } + "${SemanticAttributes.NET_PEER_IP.key}" String + "${SemanticAttributes.NET_PEER_PORT.key}" { it == null || it instanceof Long } + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq" + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue" + "rabbitmq.command" "exchange.declare" + } + } + } + } + + cleanup: + sender?.close() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/WithRabbitMqTrait.groovy b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/WithRabbitMqTrait.groovy new file mode 100644 index 000000000..73b775380 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/WithRabbitMqTrait.groovy @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.rabbitmq.client.ConnectionFactory +import java.time.Duration +import org.testcontainers.containers.GenericContainer + +trait WithRabbitMqTrait { + + static GenericContainer rabbitMqContainer + static ConnectionFactory connectionFactory + + def startRabbit() { + rabbitMqContainer = new GenericContainer('rabbitmq:latest') + .withExposedPorts(5672) + .withStartupTimeout(Duration.ofSeconds(120)) + rabbitMqContainer.start() + + connectionFactory = new ConnectionFactory( + host: rabbitMqContainer.containerIpAddress, + port: rabbitMqContainer.getMappedPort(5672) + ) + } + + def stopRabbit() { + rabbitMqContainer?.stop() + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/ratpack-1.4-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/ratpack-1.4-javaagent.gradle new file mode 100644 index 000000000..48d697ac7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/ratpack-1.4-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.ratpack" + module = 'ratpack-core' + versions = "[1.4.0,)" + } +} + +dependencies { + library "io.ratpack:ratpack-core:1.4.0" + + implementation project(':instrumentation:netty:netty-4.1:javaagent') + + testLibrary "io.ratpack:ratpack-test:1.4.0" + + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) { + testImplementation "com.sun.activation:jakarta.activation:1.2.2" + } +} + +// Requires old Guava. Can't use enforcedPlatform since predates BOM +configurations.testRuntimeClasspath.resolutionStrategy.force "com.google.guava:guava:19.0" diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ActionWrapper.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ActionWrapper.java new file mode 100644 index 000000000..76edd7d3b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ActionWrapper.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ratpack.func.Action; + +public class ActionWrapper implements Action { + + private static final Logger log = LoggerFactory.getLogger(ActionWrapper.class); + + private final Action delegate; + private final Context parentContext; + + private ActionWrapper(Action delegate, Context parentContext) { + assert parentContext != null; + this.delegate = delegate; + this.parentContext = parentContext; + } + + @Override + public void execute(T t) throws Exception { + try (Scope ignored = parentContext.makeCurrent()) { + delegate.execute(t); + } + } + + public static Action wrapIfNeeded(Action delegate) { + if (delegate instanceof ActionWrapper) { + return delegate; + } + log.debug("Wrapping action task {}", delegate); + return new ActionWrapper(delegate, Context.current()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/BlockWrapper.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/BlockWrapper.java new file mode 100644 index 000000000..af321b229 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/BlockWrapper.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ratpack.func.Block; + +public class BlockWrapper implements Block { + + private static final Logger log = LoggerFactory.getLogger(BlockWrapper.class); + + private final Block delegate; + private final Context parentContext; + + private BlockWrapper(Block delegate, Context parentContext) { + assert parentContext != null; + this.delegate = delegate; + this.parentContext = parentContext; + } + + @Override + public void execute() throws Exception { + try (Scope ignored = parentContext.makeCurrent()) { + delegate.execute(); + } + } + + public static Block wrapIfNeeded(Block delegate) { + if (delegate instanceof BlockWrapper) { + return delegate; + } + log.debug("Wrapping block {}", delegate); + return new BlockWrapper(delegate, Context.current()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ContinuationInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ContinuationInstrumentation.java new file mode 100644 index 000000000..74089da01 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ContinuationInstrumentation.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import ratpack.func.Block; + +public class ContinuationInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("ratpack.exec.internal.Continuation"); + } + + @Override + public ElementMatcher typeMatcher() { + return nameStartsWith("ratpack.exec.") + .and(implementsInterface(named("ratpack.exec.internal.Continuation"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("resume").and(takesArgument(0, named("ratpack.func.Block"))), + ContinuationInstrumentation.class.getName() + "$ResumeAdvice"); + } + + @SuppressWarnings("unused") + public static class ResumeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrap(@Advice.Argument(value = 0, readOnly = false) Block block) { + block = BlockWrapper.wrapIfNeeded(block); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/DefaultExecutionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/DefaultExecutionInstrumentation.java new file mode 100644 index 000000000..eeac6ba42 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/DefaultExecutionInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import ratpack.exec.internal.Continuation; +import ratpack.func.Action; + +public class DefaultExecutionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("ratpack.exec.internal.DefaultExecution"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + nameStartsWith("delimit") // include delimitStream + .and(takesArgument(0, named("ratpack.func.Action"))) + .and(takesArgument(1, named("ratpack.func.Action"))), + DefaultExecutionInstrumentation.class.getName() + "$DelimitAdvice"); + } + + @SuppressWarnings("unused") + public static class DelimitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrap( + @Advice.Argument(value = 0, readOnly = false) Action onError, + @Advice.Argument(value = 1, readOnly = false) Action segment) { + onError = ActionWrapper.wrapIfNeeded(onError); + segment = ActionWrapper.wrapIfNeeded(segment); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackInstrumentationModule.java new file mode 100644 index 000000000..68f6a0ffb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RatpackInstrumentationModule extends InstrumentationModule { + public RatpackInstrumentationModule() { + super("ratpack", "ratpack-1.4"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ContinuationInstrumentation(), + new DefaultExecutionInstrumentation(), + new ServerErrorHandlerInstrumentation(), + new ServerRegistryInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackTracer.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackTracer.java new file mode 100644 index 000000000..1008cc690 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackTracer.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import ratpack.handling.Context; + +public class RatpackTracer extends BaseTracer { + private static final RatpackTracer TRACER = new RatpackTracer(); + + public static RatpackTracer tracer() { + return TRACER; + } + + public void onContext(io.opentelemetry.context.Context otelContext, Context ctx) { + String description = ctx.getPathBinding().getDescription(); + if (description == null || description.isEmpty()) { + description = "/"; + } else if (!description.startsWith("/")) { + description = "/" + description; + } + + Span.fromContext(otelContext).updateName(description); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.ratpack-1.4"; + } + + @Override + protected Throwable unwrapThrowable(Throwable throwable) { + if (throwable.getCause() != null && throwable instanceof Error) { + throwable = throwable.getCause(); + } + return super.unwrapThrowable(throwable); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ServerErrorHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ServerErrorHandlerInstrumentation.java new file mode 100644 index 000000000..262d457c9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ServerErrorHandlerInstrumentation.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.ratpack.RatpackTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Optional; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import ratpack.handling.Context; + +public class ServerErrorHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("ratpack.error.ServerErrorHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isAbstract()).and(implementsInterface(named("ratpack.error.ServerErrorHandler"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("error") + .and(takesArgument(0, named("ratpack.handling.Context"))) + .and(takesArgument(1, Throwable.class)), + ServerErrorHandlerInstrumentation.class.getName() + "$ErrorAdvice"); + } + + @SuppressWarnings("unused") + public static class ErrorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void captureThrowable( + @Advice.Argument(0) Context ctx, @Advice.Argument(1) Throwable throwable) { + Optional otelContext = + ctx.maybeGet(io.opentelemetry.context.Context.class); + if (otelContext.isPresent()) { + tracer().onException(otelContext.get(), throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ServerRegistryInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ServerRegistryInstrumentation.java new file mode 100644 index 000000000..55334ea99 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/ServerRegistryInstrumentation.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import ratpack.handling.HandlerDecorator; +import ratpack.registry.Registry; + +public class ServerRegistryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("ratpack.server.internal.ServerRegistry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isStatic()).and(named("buildBaseRegistry")), + ServerRegistryInstrumentation.class.getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void injectTracing(@Advice.Return(readOnly = false) Registry registry) { + registry = + registry.join( + Registry.builder().add(HandlerDecorator.prepend(TracingHandler.INSTANCE)).build()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/TracingHandler.java b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/TracingHandler.java new file mode 100644 index 000000000..d52fec8db --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/TracingHandler.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ratpack; + +import static io.opentelemetry.javaagent.instrumentation.ratpack.RatpackTracer.tracer; + +import io.netty.util.Attribute; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import ratpack.handling.Context; +import ratpack.handling.Handler; + +public final class TracingHandler implements Handler { + public static final Handler INSTANCE = new TracingHandler(); + + @Override + public void handle(Context ctx) { + Attribute spanAttribute = + ctx.getDirectChannelAccess().getChannel().attr(AttributeKeys.SERVER_SPAN); + io.opentelemetry.context.Context serverSpanContext = spanAttribute.get(); + + // Must use context from channel, as executor instrumentation is not accurate - Ratpack + // internally queues events and then drains them in batches, causing executor instrumentation to + // attach the same context to a batch of events from different requests. + io.opentelemetry.context.Context parentContext = + serverSpanContext != null ? serverSpanContext : Java8BytecodeBridge.currentContext(); + + io.opentelemetry.context.Context ratpackContext = + tracer().startSpan(parentContext, "ratpack.handler", SpanKind.INTERNAL); + ctx.getExecution().add(ratpackContext); + + ctx.getResponse() + .beforeSend( + response -> { + if (serverSpanContext != null) { + // Rename the netty span name with the ratpack route. + tracer().onContext(serverSpanContext, ctx); + } + tracer().onContext(ratpackContext, ctx); + tracer().end(ratpackContext); + }); + + try (Scope ignored = ratpackContext.makeCurrent()) { + ctx.next(); + // exceptions are captured by ServerErrorHandlerInstrumentation + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/RatpackOtherTest.groovy b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/RatpackOtherTest.groovy new file mode 100644 index 000000000..2667e3f62 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/RatpackOtherTest.groovy @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.client.WebClient +import ratpack.path.PathBinding +import ratpack.server.RatpackServer +import spock.lang.Shared + +class RatpackOtherTest extends AgentInstrumentationSpecification { + + @Shared + RatpackServer app = RatpackServer.start { + it.handlers { + it.prefix("a") { + it.all {context -> + context.render(context.get(PathBinding).description) + } + } + it.prefix("b/::\\d+") { + it.all {context -> + context.render(context.get(PathBinding).description) + } + } + it.prefix("c/:val?") { + it.all {context -> + context.render(context.get(PathBinding).description) + } + } + it.prefix("d/:val") { + it.all {context -> + context.render(context.get(PathBinding).description) + } + } + it.prefix("e/:val?:\\d+") { + it.all {context -> + context.render(context.get(PathBinding).description) + } + } + it.prefix("f/:val:\\d+") { + it.all {context -> + context.render(context.get(PathBinding).description) + } + } + } + } + + // Force HTTP/1 with h1c to prevent tracing of upgrade request. + @Shared + WebClient client = WebClient.of("h1c://localhost:${app.bindPort}") + + def cleanupSpec() { + app.stop() + } + + def "test bindings for #path"() { + when: + def resp = client.get(path).aggregate().join() + + then: + resp.status().code() == 200 + resp.contentUtf8() == route + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "/$route" + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:${app.bindPort}/${path}" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + name "/$route" + kind INTERNAL + childOf span(0) + attributes { + } + } + } + } + + where: + path | route + "a" | "a" + "b/123" | "b/::\\d+" + "c" | "c/:val?" + "c/123" | "c/:val?" + "c/foo" | "c/:val?" + "d/123" | "d/:val" + "d/foo" | "d/:val" + "e" | "e/:val?:\\d+" + "e/123" | "e/:val?:\\d+" + "e/foo" | "e/:val?:\\d+" + "f/123" | "f/:val:\\d+" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackForkedHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackForkedHttpClientTest.groovy new file mode 100644 index 000000000..5e9d2733d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackForkedHttpClientTest.groovy @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import java.time.Duration +import ratpack.exec.Promise +import ratpack.http.client.HttpClient + +class RatpackForkedHttpClientTest extends RatpackHttpClientTest { + + @Override + Promise internalSendRequest(HttpClient client, String method, URI uri, Map headers) { + def resp = client.request(uri) { spec -> + // Connect timeout for the whole client was added in 1.5 so we need to add timeout for each request + spec.connectTimeout(Duration.ofSeconds(2)) + spec.method(method) + spec.headers { headersSpec -> + headers.entrySet().each { + headersSpec.add(it.key, it.value) + } + } + } + return resp.fork().map { + it.status.code + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackHttpClientTest.groovy new file mode 100644 index 000000000..e7cf9a36a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackHttpClientTest.groovy @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import io.netty.channel.ConnectTimeoutException +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.SpanAssert +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import java.time.Duration +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeoutException +import ratpack.exec.Operation +import ratpack.exec.Promise +import ratpack.func.Action +import ratpack.http.client.HttpClient +import ratpack.http.client.HttpClientSpec +import ratpack.test.exec.ExecHarness +import spock.lang.AutoCleanup +import spock.lang.Shared + +class RatpackHttpClientTest extends HttpClientTest implements AgentTestTrait { + + @AutoCleanup + @Shared + ExecHarness exec = ExecHarness.harness() + + @AutoCleanup + @Shared + def client = buildHttpClient() + + @AutoCleanup + @Shared + def singleConnectionClient = buildHttpClient({spec -> + spec.poolSize(1) + }) + + HttpClient buildHttpClient() { + return buildHttpClient(null) + } + + HttpClient buildHttpClient(Action action) { + HttpClient.of { + it.readTimeout(Duration.ofSeconds(2)) + // execController method added in 1.9 + if (HttpClientSpec.metaClass.getMetaMethod("execController") != null) { + it.execController(exec.getController()) + } + if (action != null) { + action.execute(it) + } + } + } + + @Override + Void buildRequest(String method, URI uri, Map headers) { + return null + } + + @Override + int sendRequest(Void request, String method, URI uri, Map headers) { + return exec.yield { + internalSendRequest(client, method, uri, headers) + }.valueOrThrow + } + + @Override + void sendRequestWithCallback(Void request, String method, URI uri, Map headers, RequestResult requestResult) { + exec.execute(Operation.of { + internalSendRequest(client, method, uri, headers).result {result -> + requestResult.complete({ result.value }, result.throwable) + } + }) + } + + // overridden in RatpackForkedHttpClientTest + Promise internalSendRequest(HttpClient client, String method, URI uri, Map headers) { + def resp = client.request(uri) { spec -> + // Connect timeout for the whole client was added in 1.5 so we need to add timeout for each request + spec.connectTimeout(Duration.ofSeconds(2)) + spec.method(method) + spec.headers { headersSpec -> + headers.entrySet().each { + headersSpec.add(it.key, it.value) + } + } + } + return resp.map { + it.status.code + } + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + return new SingleConnection() { + @Override + int doRequest(String path, Map headers) throws ExecutionException, InterruptedException, TimeoutException { + def uri = resolveAddress(path) + return exec.yield { + internalSendRequest(singleConnectionClient, "GET", uri, headers) + }.valueOrThrow + } + } + } + + @Override + String expectedClientSpanName(URI uri, String method) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return "CONNECT" + default: + return super.expectedClientSpanName(uri, method) + } + } + + @Override + void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + switch (uri.toString()) { + case "https://192.0.2.1/": // non routable address + spanAssert.errorEvent(ConnectTimeoutException, ~/connection timed out:/) + return + } + super.assertClientSpanErrorEvent(spanAssert, uri, exception) + } + + @Override + Set> httpAttributes(URI uri) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return [] + } + return super.httpAttributes(uri) + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testReusedRequest() { + // these tests will pass, but they don't really test anything since REQUEST is Void + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackPooledHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackPooledHttpClientTest.groovy new file mode 100644 index 000000000..4ee09f919 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackPooledHttpClientTest.groovy @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import io.opentelemetry.instrumentation.test.base.SingleConnection +import ratpack.http.client.HttpClient + +class RatpackPooledHttpClientTest extends RatpackHttpClientTest { + + @Override + HttpClient buildHttpClient() { + return buildHttpClient({spec -> + spec.poolSize(5) + }) + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + // this test is already run for RatpackHttpClientTest + // returning null here to avoid running the same test twice + return null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackAsyncHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackAsyncHttpServerTest.groovy new file mode 100644 index 000000000..017249e42 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackAsyncHttpServerTest.groovy @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import ratpack.error.ServerErrorHandler +import ratpack.exec.Promise +import ratpack.server.RatpackServer + +class RatpackAsyncHttpServerTest extends RatpackHttpServerTest { + + @Override + RatpackServer startServer(int bindPort) { + def ratpack = RatpackServer.start { + it.serverConfig { + it.port(bindPort) + it.address(InetAddress.getByName("localhost")) + } + it.handlers { + it.register { + it.add(ServerErrorHandler, new TestErrorHandler()) + } + it.prefix(SUCCESS.rawPath()) { + it.all {context -> + Promise.sync { + SUCCESS + } then { endpoint -> + controller(endpoint) { + context.response.status(endpoint.status).send(endpoint.body) + } + } + } + } + it.prefix(INDEXED_CHILD.rawPath()) { + it.all {context -> + Promise.sync { + INDEXED_CHILD + } then { + controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) } + context.response.status(INDEXED_CHILD.status).send() + } + } + } + } + it.prefix(QUERY_PARAM.rawPath()) { + it.all { context -> + Promise.sync { + QUERY_PARAM + } then { endpoint -> + controller(endpoint) { + context.response.status(endpoint.status).send(context.request.query) + } + } + } + } + it.prefix(REDIRECT.rawPath()) { + it.all {context -> + Promise.sync { + REDIRECT + } then { endpoint -> + controller(endpoint) { + context.redirect(endpoint.body) + } + } + } + } + it.prefix(ERROR.rawPath()) { + it.all {context -> + Promise.sync { + ERROR + } then { endpoint -> + controller(endpoint) { + context.response.status(endpoint.status).send(endpoint.body) + } + } + } + } + it.prefix(EXCEPTION.rawPath()) { + it.all { + Promise.sync { + EXCEPTION + } then { endpoint -> + controller(endpoint) { + throw new Exception(endpoint.body) + } + } + } + } + it.prefix("path/:id/param") { + it.all {context -> + Promise.sync { + PATH_PARAM + } then { endpoint -> + controller(endpoint) { + context.response.status(endpoint.status).send(context.pathTokens.id) + } + } + } + } + } + } + + assert ratpack.bindPort == bindPort + assert ratpack.bindHost == 'localhost' + return ratpack + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackForkedHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackForkedHttpServerTest.groovy new file mode 100644 index 000000000..2c04b684d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackForkedHttpServerTest.groovy @@ -0,0 +1,119 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import ratpack.error.ServerErrorHandler +import ratpack.exec.Promise +import ratpack.server.RatpackServer + +class RatpackForkedHttpServerTest extends RatpackHttpServerTest { + + @Override + RatpackServer startServer(int bindPort) { + + def ratpack = RatpackServer.start { + it.serverConfig { + it.port(bindPort) + it.address(InetAddress.getByName("localhost")) + } + it.handlers { + it.register { + it.add(ServerErrorHandler, new TestErrorHandler()) + } + it.prefix(SUCCESS.rawPath()) { + it.all {context -> + Promise.sync { + SUCCESS + }.fork().then { endpoint -> + controller(endpoint) { + context.response.status(endpoint.status).send(endpoint.body) + } + } + } + } + it.prefix(INDEXED_CHILD.rawPath()) { + it.all {context -> + Promise.sync { + INDEXED_CHILD + }.fork().then { + controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) } + context.response.status(INDEXED_CHILD.status).send() + } + } + } + } + it.prefix(QUERY_PARAM.rawPath()) { + it.all { context -> + Promise.sync { + QUERY_PARAM + }.fork().then { endpoint -> + controller(endpoint) { + context.response.status(endpoint.status).send(context.request.query) + } + } + } + } + it.prefix(REDIRECT.rawPath()) { + it.all {context -> + Promise.sync { + REDIRECT + }.fork().then { endpoint -> + controller(endpoint) { + context.redirect(endpoint.body) + } + } + } + } + it.prefix(ERROR.rawPath()) { + it.all {context -> + Promise.sync { + ERROR + }.fork().then { endpoint -> + controller(endpoint) { + context.response.status(endpoint.status).send(endpoint.body) + } + } + } + } + it.prefix(EXCEPTION.rawPath()) { + it.all { + Promise.sync { + EXCEPTION + }.fork().then { endpoint -> + controller(endpoint) { + throw new Exception(endpoint.body) + } + } + } + } + it.prefix("path/:id/param") { + it.all {context -> + Promise.sync { + PATH_PARAM + }.fork().then { endpoint -> + controller(endpoint) { + context.response.status(endpoint.status).send(context.pathTokens.id) + } + } + } + } + } + } + + assert ratpack.bindPort == bindPort + assert ratpack.bindHost == 'localhost' + return ratpack + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackHttpServerTest.groovy new file mode 100644 index 000000000..1cc774ace --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/ratpack-1.4/javaagent/src/test/groovy/server/RatpackHttpServerTest.groovy @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import ratpack.error.ServerErrorHandler +import ratpack.handling.Context +import ratpack.server.RatpackServer + +class RatpackHttpServerTest extends HttpServerTest implements AgentTestTrait { + + @Override + RatpackServer startServer(int bindPort) { + def ratpack = RatpackServer.start { + it.serverConfig { + it.port(bindPort) + it.address(InetAddress.getByName("localhost")) + } + it.handlers { + it.register { + it.add(ServerErrorHandler, new TestErrorHandler()) + } + it.prefix(SUCCESS.rawPath()) { + it.all {context -> + controller(SUCCESS) { + context.response.status(SUCCESS.status).send(SUCCESS.body) + } + } + } + it.prefix(INDEXED_CHILD.rawPath()) { + it.all {context -> + controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { context.request.queryParams.get(it) } + context.response.status(INDEXED_CHILD.status).send() + } + } + } + it.prefix(QUERY_PARAM.rawPath()) { + it.all { context -> + controller(QUERY_PARAM) { + context.response.status(QUERY_PARAM.status).send(context.request.query) + } + } + } + it.prefix(REDIRECT.rawPath()) { + it.all {context -> + controller(REDIRECT) { + context.redirect(REDIRECT.body) + } + } + } + it.prefix(ERROR.rawPath()) { + it.all {context -> + controller(ERROR) { + context.response.status(ERROR.status).send(ERROR.body) + } + } + } + it.prefix(EXCEPTION.rawPath()) { + it.all { + controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + } + it.prefix("path/:id/param") { + it.all {context -> + controller(PATH_PARAM) { + context.response.status(PATH_PARAM.status).send(context.pathTokens.id) + } + } + } + } + } + + assert ratpack.bindPort == bindPort + return ratpack + } + + static class TestErrorHandler implements ServerErrorHandler { + @Override + void error(Context context, Throwable throwable) throws Exception { + context.response.status(500).send(throwable.message) + } + } + + @Override + void stopServer(RatpackServer server) { + server.stop() + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + true + } + + @Override + boolean testPathParam() { + true + } + + @Override + boolean testConcurrency() { + true + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name endpoint.status == 404 ? "/" : endpoint == PATH_PARAM ? "/path/:id/param" : endpoint.path + kind INTERNAL + childOf((SpanData) parent) + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(Exception, EXCEPTION.body) + } + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return endpoint.status == 404 ? "/" : endpoint == PATH_PARAM ? "/path/:id/param" : endpoint.path + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/reactor-3.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/reactor-3.1-javaagent.gradle new file mode 100644 index 000000000..96fe78480 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/reactor-3.1-javaagent.gradle @@ -0,0 +1,30 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.projectreactor" + module = "reactor-core" + versions = "[3.1.0.RELEASE,)" + assertInverse = true + } +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.reactor.experimental-span-attributes=true" +} + +dependencies { + implementation project(':instrumentation:reactor-3.1:library') + + testLibrary "io.projectreactor:reactor-core:3.1.0.RELEASE" + testLibrary "io.projectreactor:reactor-test:3.1.0.RELEASE" + + testImplementation project(':instrumentation:reactor-3.1:testing') + testImplementation "run.mone:opentelemetry-extension-annotations" + + latestDepTestLibrary "io.projectreactor:reactor-core:3.+" + latestDepTestLibrary "io.projectreactor:reactor-test:3.+" + // Looks like later versions on reactor need this dependency for some reason even though it is marked as optional. + latestDepTestLibrary "io.micrometer:micrometer-core:1.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/HooksInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/HooksInstrumentation.java new file mode 100644 index 000000000..6b8f71f36 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/HooksInstrumentation.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactor; + +import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.reactor.TracingOperator; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HooksInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("reactor.core.publisher.Hooks"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isTypeInitializer().or(named("resetOnEachOperator")), + this.getClass().getName() + "$ResetOnEachOperatorAdvice"); + } + + @SuppressWarnings("unused") + public static class ResetOnEachOperatorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void postStaticInitializer() { + TracingOperator.newBuilder() + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBoolean( + "otel.instrumentation.reactor.experimental-span-attributes", false)) + .build() + .registerOnEachOperator(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/ReactorInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/ReactorInstrumentationModule.java new file mode 100644 index 000000000..2c39d1718 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/ReactorInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactor; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ReactorInstrumentationModule extends InstrumentationModule { + + public ReactorInstrumentationModule() { + super("reactor", "reactor-3.1"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HooksInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/ReactorCoreTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/ReactorCoreTest.groovy new file mode 100644 index 000000000..09136d0a9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/ReactorCoreTest.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.reactor.AbstractReactorCoreTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class ReactorCoreTest extends AbstractReactorCoreTest implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/ReactorWithSpanInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/ReactorWithSpanInstrumentationTest.groovy new file mode 100644 index 000000000..47a6fd3e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/ReactorWithSpanInstrumentationTest.groovy @@ -0,0 +1,305 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.reactor.TracedWithSpan +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.publisher.UnicastProcessor +import reactor.test.StepVerifier + +class ReactorWithSpanInstrumentationTest extends AgentInstrumentationSpecification { + + def "should capture span for already completed Mono"() { + setup: + def source = Mono.just("Value") + def result = new TracedWithSpan() + .mono(source) + + expect: + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.mono" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Mono"() { + setup: + def source = UnicastProcessor.create() + def mono = source.singleOrEmpty() + def result = new TracedWithSpan() + .mono(mono) + def verifier = StepVerifier.create(result) + .expectSubscription() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + source.onComplete() + + verifier.expectNext("Value") + .verifyComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.mono" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Mono"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = Mono.error(error) + def result = new TracedWithSpan() + .mono(source) + + expect: + StepVerifier.create(result) + .verifyErrorMatches({ it == error }) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.mono" + kind SpanKind.INTERNAL + hasNoParent() + status StatusCode.ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Mono"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastProcessor.create() + def mono = source.singleOrEmpty() + def result = new TracedWithSpan() + .mono(mono) + def verifier = StepVerifier.create(result) + .expectSubscription() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + + verifier + .verifyErrorMatches({ it == error }) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.mono" + kind SpanKind.INTERNAL + hasNoParent() + status StatusCode.ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Mono"() { + setup: + def source = UnicastProcessor.create() + def mono = source.singleOrEmpty() + def result = new TracedWithSpan() + .mono(mono) + def verifier = StepVerifier.create(result) + .expectSubscription() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + verifier.thenCancel().verify() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.mono" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + "reactor.canceled" true + } + } + } + } + } + + def "should capture span for already completed Flux"() { + setup: + def source = Flux.just("Value") + def result = new TracedWithSpan() + .flux(source) + + expect: + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flux" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Flux"() { + setup: + def source = UnicastProcessor.create() + def result = new TracedWithSpan() + .flux(source) + def verifier = StepVerifier.create(result) + .expectSubscription() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + source.onComplete() + + verifier.expectNext("Value") + .verifyComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flux" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Flux"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = Flux.error(error) + def result = new TracedWithSpan() + .flux(source) + + expect: + StepVerifier.create(result) + .verifyErrorMatches({ it == error }) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flux" + kind SpanKind.INTERNAL + hasNoParent() + status StatusCode.ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Flux"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastProcessor.create() + def result = new TracedWithSpan() + .flux(source) + def verifier = StepVerifier.create(result) + .expectSubscription() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + + verifier.verifyErrorMatches({ it == error }) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flux" + kind SpanKind.INTERNAL + hasNoParent() + status StatusCode.ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Flux"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastProcessor.create() + def result = new TracedWithSpan() + .flux(source) + def verifier = StepVerifier.create(result) + .expectSubscription() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + + verifier.thenCancel().verify() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flux" + kind SpanKind.INTERNAL + hasNoParent() + attributes { + "reactor.canceled" true + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/SubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/SubscriptionTest.groovy new file mode 100644 index 000000000..db1e22257 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/groovy/SubscriptionTest.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.reactor.AbstractSubscriptionTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class SubscriptionTest extends AbstractSubscriptionTest implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/java/io/opentelemetry/instrumentation/reactor/TracedWithSpan.java b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/java/io/opentelemetry/instrumentation/reactor/TracedWithSpan.java new file mode 100644 index 000000000..e9cdb7039 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/javaagent/src/test/java/io/opentelemetry/instrumentation/reactor/TracedWithSpan.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor; + +import io.opentelemetry.extension.annotations.WithSpan; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class TracedWithSpan { + @WithSpan + public Mono mono(Mono mono) { + return mono; + } + + @WithSpan + public Flux flux(Flux flux) { + return flux; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/reactor-3.1-library.gradle b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/reactor-3.1-library.gradle new file mode 100644 index 000000000..a169f352f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/reactor-3.1-library.gradle @@ -0,0 +1,12 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library "io.projectreactor:reactor-core:3.1.0.RELEASE" + testLibrary "io.projectreactor:reactor-test:3.1.0.RELEASE" + + testImplementation project(':instrumentation:reactor-3.1:testing') + latestDepTestLibrary "io.projectreactor:reactor-core:3.+" + latestDepTestLibrary "io.projectreactor:reactor-test:3.+" + // Looks like later versions on reactor need this dependency for some reason even though it is marked as optional. + latestDepTestLibrary "io.micrometer:micrometer-core:1.+" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategy.java b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategy.java new file mode 100644 index 000000000..cd3d1e1e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategy.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public final class ReactorAsyncSpanEndStrategy implements AsyncSpanEndStrategy { + private static final AttributeKey CANCELED_ATTRIBUTE_KEY = + AttributeKey.booleanKey("reactor.canceled"); + + public static ReactorAsyncSpanEndStrategy create() { + return newBuilder().build(); + } + + public static ReactorAsyncSpanEndStrategyBuilder newBuilder() { + return new ReactorAsyncSpanEndStrategyBuilder(); + } + + private final boolean captureExperimentalSpanAttributes; + + ReactorAsyncSpanEndStrategy(boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + @Override + public boolean supports(Class returnType) { + return returnType == Publisher.class || returnType == Mono.class || returnType == Flux.class; + } + + @Override + public Object end(BaseTracer tracer, Context context, Object returnValue) { + + EndOnFirstNotificationConsumer notificationConsumer = + new EndOnFirstNotificationConsumer(tracer, context); + if (returnValue instanceof Mono) { + Mono mono = (Mono) returnValue; + return mono.doOnError(notificationConsumer) + .doOnSuccess(notificationConsumer::onSuccess) + .doOnCancel(notificationConsumer::onCancel); + } else { + Flux flux = Flux.from((Publisher) returnValue); + return flux.doOnError(notificationConsumer) + .doOnComplete(notificationConsumer) + .doOnCancel(notificationConsumer::onCancel); + } + } + + /** + * Helper class to ensure that the span is ended exactly once regardless of how many OnComplete or + * OnError notifications are received. Multiple notifications can happen anytime multiple + * subscribers subscribe to the same publisher. + */ + private final class EndOnFirstNotificationConsumer extends AtomicBoolean + implements Runnable, Consumer { + + private final BaseTracer tracer; + private final Context context; + + public EndOnFirstNotificationConsumer(BaseTracer tracer, Context context) { + super(false); + this.tracer = tracer; + this.context = context; + } + + public void onSuccess(T ignored) { + accept(null); + } + + public void onCancel() { + if (compareAndSet(false, true)) { + if (captureExperimentalSpanAttributes) { + Span.fromContext(context).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + tracer.end(context); + } + } + + @Override + public void run() { + accept(null); + } + + @Override + public void accept(Throwable exception) { + if (compareAndSet(false, true)) { + if (exception != null) { + tracer.endExceptionally(context, exception); + } else { + tracer.end(context); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategyBuilder.java b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategyBuilder.java new file mode 100644 index 000000000..609e5b648 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategyBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor; + +public final class ReactorAsyncSpanEndStrategyBuilder { + private boolean captureExperimentalSpanAttributes; + + ReactorAsyncSpanEndStrategyBuilder() {} + + public ReactorAsyncSpanEndStrategyBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + public ReactorAsyncSpanEndStrategy build() { + return new ReactorAsyncSpanEndStrategy(captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingOperator.java b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingOperator.java new file mode 100644 index 000000000..532db89b7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingOperator.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.instrumentation.reactor; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Operators; + +/** Based on Spring Sleuth's Reactor instrumentation. */ +public final class TracingOperator { + + public static TracingOperator create() { + return newBuilder().build(); + } + + public static TracingOperatorBuilder newBuilder() { + return new TracingOperatorBuilder(); + } + + private final boolean captureExperimentalSpanAttributes; + + TracingOperator(boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + /** + * Registers a hook that applies to every operator, propagating {@link Context} to downstream + * callbacks to ensure spans in the {@link Context} are available throughout the lifetime of a + * reactive stream. This should generally be called in a static initializer block in your + * application. + */ + public void registerOnEachOperator() { + Hooks.onEachOperator(TracingSubscriber.class.getName(), tracingLift()); + AsyncSpanEndStrategies.getInstance() + .registerStrategy( + ReactorAsyncSpanEndStrategy.newBuilder() + .setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes) + .build()); + } + + /** Unregisters the hook registered by {@link #registerOnEachOperator()}. */ + public void resetOnEachOperator() { + Hooks.resetOnEachOperator(TracingSubscriber.class.getName()); + AsyncSpanEndStrategies.getInstance().unregisterStrategy(ReactorAsyncSpanEndStrategy.class); + } + + private static Function, ? extends Publisher> tracingLift() { + return Operators.lift(new Lifter<>()); + } + + public static class Lifter + implements BiFunction, CoreSubscriber> { + + @Override + public CoreSubscriber apply(Scannable publisher, CoreSubscriber sub) { + // if Flux/Mono #just, #empty, #error + if (publisher instanceof Fuseable.ScalarCallable) { + return sub; + } + return new TracingSubscriber<>(sub, sub.currentContext()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingOperatorBuilder.java b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingOperatorBuilder.java new file mode 100644 index 000000000..99889cfa3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingOperatorBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor; + +public final class TracingOperatorBuilder { + private boolean captureExperimentalSpanAttributes; + + TracingOperatorBuilder() {} + + public TracingOperatorBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + public TracingOperator build() { + return new TracingOperator(captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingSubscriber.java b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingSubscriber.java new file mode 100644 index 000000000..a851ce541 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/TracingSubscriber.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 The OpenTracing Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package io.opentelemetry.instrumentation.reactor; + +import io.opentelemetry.context.Scope; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.util.context.Context; + +/** + * Based on OpenTracing code. + * https://github.com/opentracing-contrib/java-reactor/blob/master/src/main/java/io/opentracing/contrib/reactor/TracedSubscriber.java + */ +public class TracingSubscriber implements CoreSubscriber { + private final io.opentelemetry.context.Context traceContext; + private final Subscriber subscriber; + private final Context context; + + public TracingSubscriber(Subscriber subscriber, Context ctx) { + this(subscriber, ctx, io.opentelemetry.context.Context.current()); + } + + public TracingSubscriber( + Subscriber subscriber, + Context ctx, + io.opentelemetry.context.Context contextToPropagate) { + this.subscriber = subscriber; + this.context = ctx; + this.traceContext = contextToPropagate; + } + + @Override + public void onSubscribe(Subscription subscription) { + subscriber.onSubscribe(subscription); + } + + @Override + public void onNext(T o) { + withActiveSpan(() -> subscriber.onNext(o)); + } + + @Override + public void onError(Throwable throwable) { + withActiveSpan(() -> subscriber.onError(throwable)); + } + + @Override + public void onComplete() { + withActiveSpan(subscriber::onComplete); + } + + @Override + public Context currentContext() { + return context; + } + + private void withActiveSpan(Runnable runnable) { + if (traceContext != null) { + try (Scope ignored = traceContext.makeCurrent()) { + runnable.run(); + } + } else { + runnable.run(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/HooksTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/HooksTest.groovy new file mode 100644 index 000000000..7760696be --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/HooksTest.groovy @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor + +import io.opentelemetry.instrumentation.test.LibraryInstrumentationSpecification +import java.util.concurrent.atomic.AtomicReference +import reactor.core.CoreSubscriber +import reactor.core.publisher.Mono + +class HooksTest extends LibraryInstrumentationSpecification { + + def "can reset out hooks"() { + setup: + def underTest = TracingOperator.create() + AtomicReference subscriber = new AtomicReference<>() + + when: "no hook registered" + new CapturingMono(subscriber).map { it + 1 }.subscribe() + + then: + !(subscriber.get() instanceof TracingSubscriber) + + when: "hook registered" + underTest.registerOnEachOperator() + new CapturingMono(subscriber).map { it + 1 }.subscribe() + + then: + subscriber.get() instanceof TracingSubscriber + + when: "hook reset" + underTest.resetOnEachOperator() + new CapturingMono(subscriber).map { it + 1 }.subscribe() + + then: + !(subscriber.get() instanceof TracingSubscriber) + } + + private static class CapturingMono extends Mono { + final AtomicReference subscriber + + CapturingMono(AtomicReference subscriber) { + this.subscriber = subscriber + } + + @Override + void subscribe(CoreSubscriber actual) { + subscriber.set(actual.actual) // debug showed this is the right way to do + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategyTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategyTest.groovy new file mode 100644 index 000000000..b6164fb00 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/ReactorAsyncSpanEndStrategyTest.groovy @@ -0,0 +1,370 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.api.tracer.BaseTracer +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.publisher.UnicastProcessor +import reactor.test.StepVerifier +import spock.lang.Specification + +class ReactorAsyncSpanEndStrategyTest extends Specification { + BaseTracer tracer + + Context context + + Span span + + def underTest = ReactorAsyncSpanEndStrategy.create() + + def underTestWithExperimentalAttributes = ReactorAsyncSpanEndStrategy.newBuilder() + .setCaptureExperimentalSpanAttributes(true) + .build() + + void setup() { + tracer = Mock() + context = Mock() + span = Mock() + span.storeInContext(_) >> { callRealMethod() } + } + + static class MonoTest extends ReactorAsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Mono) + } + + def "ends span on already completed"() { + when: + def result = (Mono) underTest.end(tracer, context, Mono.just("Value")) + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + + then: + 1 * tracer.end(context) + } + + def "ends span on already empty"() { + when: + def result = (Mono) underTest.end(tracer, context, Mono.empty()) + StepVerifier.create(result) + .verifyComplete() + + then: + 1 * tracer.end(context) + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + + when: + def result = (Mono) underTest.end(tracer, context, Mono.error(exception)) + StepVerifier.create(result) + .verifyErrorMatches({ it == exception }) + + then: + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span when completed"() { + given: + def source = UnicastProcessor.create() + def mono = source.singleOrEmpty() + + when: + def result = (Mono) underTest.end(tracer, context, mono) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + source.onNext("Value") + source.onComplete() + verifier.expectNext("Value") + .verifyComplete() + + then: + 1 * tracer.end(context) + } + + def "ends span when empty"() { + given: + def source = UnicastProcessor.create() + def mono = source.singleOrEmpty() + + when: + def result = (Mono) underTest.end(tracer, context, mono) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + source.onComplete() + verifier.verifyComplete() + + then: + 1 * tracer.end(context) + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = UnicastProcessor.create() + def mono = source.singleOrEmpty() + + when: + def result = (Mono) underTest.end(tracer, context, mono) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + source.onError(exception) + verifier.verifyErrorMatches({ it == exception }) + + then: + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span when cancelled"() { + given: + def source = UnicastProcessor.create() + def mono = source.singleOrEmpty() + def context = span.storeInContext(Context.root()) + + when: + def result = (Mono) underTest.end(tracer, context, mono) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + verifier.thenCancel().verify() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = UnicastProcessor.create() + def mono = source.singleOrEmpty() + def context = span.storeInContext(Context.root()) + + when: + def result = (Mono) underTestWithExperimentalAttributes.end(tracer, context, mono) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + verifier.thenCancel().verify() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "reactor.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + + when: + def result = (Mono) underTest.end(tracer, context, Mono.just("Value")) + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + + then: + 1 * tracer.end(context) + } + } + + static class FluxTest extends ReactorAsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Flux) + } + + def "ends span on already completed"() { + when: + def result = (Flux) underTest.end(tracer, context, Flux.just("Value")) + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + + then: + 1 * tracer.end(context) + } + + def "ends span on already empty"() { + when: + def result = (Flux) underTest.end(tracer, context, Flux.empty()) + StepVerifier.create(result) + .verifyComplete() + + then: + 1 * tracer.end(context) + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + + when: + def result = (Flux) underTest.end(tracer, context, Flux.error(exception)) + StepVerifier.create(result) + .verifyErrorMatches({ it == exception }) + + then: + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span when completed"() { + given: + def source = UnicastProcessor.create() + + when: + def result = (Flux) underTest.end(tracer, context, source) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + source.onNext("Value") + source.onComplete() + verifier.expectNext("Value") + .verifyComplete() + + then: + 1 * tracer.end(context) + } + + def "ends span when empty"() { + given: + def source = UnicastProcessor.create() + + when: + def result = (Flux) underTest.end(tracer, context, source) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + source.onComplete() + verifier.verifyComplete() + + then: + 1 * tracer.end(context) + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = UnicastProcessor.create() + + when: + def result = (Flux) underTest.end(tracer, context, source) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + source.onError(exception) + verifier.verifyErrorMatches({ it == exception }) + + then: + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span when cancelled"() { + given: + def source = UnicastProcessor.create() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flux) underTest.end(tracer, context, source) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + verifier.thenCancel() + .verify() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = UnicastProcessor.create() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flux) underTestWithExperimentalAttributes.end(tracer, context, source) + def verifier = StepVerifier.create(result) + .expectSubscription() + + then: + 0 * tracer._ + + when: + verifier.thenCancel() + .verify() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "reactor.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + when: + def result = (Flux) underTest.end(tracer, context, Flux.just("Value")) + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + StepVerifier.create(result) + .expectNext("Value") + .verifyComplete() + + then: + 1 * tracer.end(context) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/ReactorCoreTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/ReactorCoreTest.groovy new file mode 100644 index 000000000..9d7e65acb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/ReactorCoreTest.groovy @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor + +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import spock.lang.Shared + +class ReactorCoreTest extends AbstractReactorCoreTest implements LibraryTestTrait { + @Shared + TracingOperator tracingOperator = TracingOperator.create() + + def setupSpec() { + tracingOperator.registerOnEachOperator() + } + + def cleanupSpec() { + tracingOperator.resetOnEachOperator() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/SubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/SubscriptionTest.groovy new file mode 100644 index 000000000..9bfc89947 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/library/src/test/groovy/io/opentelemetry/instrumentation/reactor/SubscriptionTest.groovy @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor + +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import spock.lang.Shared + +class SubscriptionTest extends AbstractSubscriptionTest implements LibraryTestTrait { + @Shared + TracingOperator tracingOperator = TracingOperator.create() + + def setupSpec() { + tracingOperator.registerOnEachOperator() + } + + def cleanupSpec() { + tracingOperator.resetOnEachOperator() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/reactor-3.1-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/reactor-3.1-testing.gradle new file mode 100644 index 000000000..15cea29b7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/reactor-3.1-testing.gradle @@ -0,0 +1,11 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "io.projectreactor:reactor-core:3.1.0.RELEASE" + + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/reactor/AbstractReactorCoreTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/reactor/AbstractReactorCoreTest.groovy new file mode 100644 index 000000000..8a11e2a63 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/reactor/AbstractReactorCoreTest.groovy @@ -0,0 +1,474 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor + +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.TraceUtils +import java.time.Duration +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class AbstractReactorCoreTest extends InstrumentationSpecification { + + public static final String EXCEPTION_MESSAGE = "test exception" + + @Shared + def addOne = { i -> + addOneFunc(i) + } + + @Shared + def addTwo = { i -> + addTwoFunc(i) + } + + @Shared + def throwException = { + throw new IllegalStateException(EXCEPTION_MESSAGE) + } + + def "Publisher '#paramName' test"() { + when: + def result = runUnderTrace(publisherSupplier) + + then: + result == expected + and: + assertTraces(1) { + trace(0, workSpans + 2) { + basicSpan(it, 0, "trace-parent") + basicSpan(it, 1, "publisher-parent", span(0)) + + for (int i = 0; i < workSpans; i++) { + basicSpan(it, 2 + i, "add one", span(1)) + } + } + } + + where: + paramName | expected | workSpans | publisherSupplier + "basic mono" | 2 | 1 | { -> Mono.just(1).map(addOne) } + "two operations mono" | 4 | 2 | { -> Mono.just(2).map(addOne).map(addOne) } + "delayed mono" | 4 | 1 | { -> + Mono.just(3).delayElement(Duration.ofMillis(100)).map(addOne) + } + "delayed twice mono" | 6 | 2 | { -> + Mono.just(4).delayElement(Duration.ofMillis(100)).map(addOne).delayElement(Duration.ofMillis(100)).map(addOne) + } + "basic flux" | [6, 7] | 2 | { -> Flux.fromIterable([5, 6]).map(addOne) } + "two operations flux" | [8, 9] | 4 | { -> + Flux.fromIterable([6, 7]).map(addOne).map(addOne) + } + "delayed flux" | [8, 9] | 2 | { -> + Flux.fromIterable([7, 8]).delayElements(Duration.ofMillis(100)).map(addOne) + } + "delayed twice flux" | [10, 11] | 4 | { -> + Flux.fromIterable([8, 9]).delayElements(Duration.ofMillis(100)).map(addOne).delayElements(Duration.ofMillis(100)).map(addOne) + } + + "mono from callable" | 12 | 2 | { -> + Mono.fromCallable({ addOneFunc(10) }).map(addOne) + } + } + + def "Publisher error '#paramName' test"() { + when: + runUnderTrace(publisherSupplier) + + then: + def exception = thrown RuntimeException + exception.message == EXCEPTION_MESSAGE + and: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "trace-parent" + status ERROR + errorEvent(RuntimeException, EXCEPTION_MESSAGE) + hasNoParent() + } + + // It's important that we don't attach errors at the Reactor level so that we don't + // impact the spans on reactor instrumentations such as netty and lettuce, as reactor is + // more of a context propagation mechanism than something we would be tracking for + // errors this is ok. + basicSpan(it, 1, "publisher-parent", span(0)) + } + } + + where: + paramName | publisherSupplier + "mono" | { -> Mono.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "flux" | { -> Flux.error(new RuntimeException(EXCEPTION_MESSAGE)) } + } + + def "Publisher step '#paramName' test"() { + when: + runUnderTrace(publisherSupplier) + + then: + def exception = thrown IllegalStateException + exception.message == EXCEPTION_MESSAGE + and: + assertTraces(1) { + trace(0, workSpans + 2) { + span(0) { + name "trace-parent" + status ERROR + errorEvent(IllegalStateException, EXCEPTION_MESSAGE) + hasNoParent() + } + + // It's important that we don't attach errors at the Reactor level so that we don't + // impact the spans on reactor instrumentations such as netty and lettuce, as reactor is + // more of a context propagation mechanism than something we would be tracking for + // errors this is ok. + basicSpan(it, 1, "publisher-parent", span(0)) + + for (int i = 0; i < workSpans; i++) { + span(i + 2) { + name "add one" + childOf span(1) + attributes { + } + } + } + } + } + + where: + paramName | workSpans | publisherSupplier + "basic mono failure" | 1 | { -> Mono.just(1).map(addOne).map({ throwException() }) } + "basic flux failure" | 1 | { -> + Flux.fromIterable([5, 6]).map(addOne).map({ throwException() }) + } + } + + def "Publisher '#paramName' cancel"() { + when: + cancelUnderTrace(publisherSupplier) + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "trace-parent" + hasNoParent() + attributes { + } + } + + basicSpan(it, 1, "publisher-parent", span(0)) + } + } + + where: + paramName | publisherSupplier + "basic mono" | { -> Mono.just(1) } + "basic flux" | { -> Flux.fromIterable([5, 6]) } + } + + def "Publisher chain spans have the correct parent for '#paramName'"() { + when: + runUnderTrace(publisherSupplier) + + then: + assertTraces(1) { + trace(0, workSpans + 2) { + span(0) { + name "trace-parent" + hasNoParent() + attributes { + } + } + + basicSpan(it, 1, "publisher-parent", span(0)) + + for (int i = 0; i < workSpans; i++) { + span(i + 2) { + name "add one" + childOf span(1) + attributes { + } + } + } + } + } + + where: + paramName | workSpans | publisherSupplier + "basic mono" | 3 | { -> + Mono.just(1).map(addOne).map(addOne).then(Mono.just(1).map(addOne)) + } + "basic flux" | 5 | { -> + Flux.fromIterable([5, 6]).map(addOne).map(addOne).then(Mono.just(1).map(addOne)) + } + } + + def "Publisher chain spans have the correct parents from assembly time '#paramName'"() { + when: + runUnderTrace { + // The "add one" operations in the publisher created here should be children of the publisher-parent + Publisher publisher = publisherSupplier() + + def tracer = GlobalOpenTelemetry.getTracer("test") + def intermediate = tracer.spanBuilder("intermediate").startSpan() + // After this activation, the "add two" operations below should be children of this span + def scope = Context.current().with(intermediate).makeCurrent() + try { + if (publisher instanceof Mono) { + return ((Mono) publisher).map(addTwo) + } else if (publisher instanceof Flux) { + return ((Flux) publisher).map(addTwo) + } + throw new IllegalStateException("Unknown publisher type") + } finally { + intermediate.end() + scope.close() + } + } + + then: + assertTraces(1) { + trace(0, (workItems * 2) + 3) { + basicSpan(it, 0, "trace-parent") + basicSpan(it, 1, "publisher-parent", span(0)) + basicSpan(it, 2, "intermediate", span(1)) + + for (int i = 0; i < 2 * workItems; i = i + 2) { + basicSpan(it, 3 + i, "add one", span(1)) + basicSpan(it, 3 + i + 1, "add two", span(1)) + } + } + } + + where: + paramName | workItems | publisherSupplier + "basic mono" | 1 | { -> Mono.just(1).map(addOne) } + "basic flux" | 2 | { -> Flux.fromIterable([1, 2]).map(addOne) } + } + + def "Nested delayed mono with high concurrency"() { + setup: + def iterations = 100 + def remainingIterations = new HashSet<>((0L ..< iterations).toList()) + + when: + (0L ..< iterations).forEach { iteration -> + def outer = Mono.just("") + .map({ it }) + .delayElement(Duration.ofMillis(10)) + .map({ it }) + .delayElement(Duration.ofMillis(10)) + .doOnSuccess({ + def middle = Mono.just("") + .map({ it }) + .delayElement(Duration.ofMillis(10)) + .doOnSuccess({ + TraceUtils.runUnderTrace("inner") { + Span.current().setAttribute("iteration", iteration) + } + }) + + TraceUtils.runUnderTrace("middle") { + Span.current().setAttribute("iteration", iteration) + middle.subscribe() + } + }) + + // Context must propagate even if only subscribe is in root span scope + TraceUtils.runUnderTrace("outer") { + Span.current().setAttribute("iteration", iteration) + outer.subscribe() + } + } + + then: + assertTraces(iterations) { + for (int i = 0; i < iterations; i++) { + trace(i, 3) { + long iteration = -1 + span(0) { + name("outer") + iteration = span.getAttributes().get(AttributeKey.longKey("iteration")).toLong() + assert remainingIterations.remove(iteration) + } + span(1) { + name("middle") + childOf(span(0)) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + span(2) { + name("inner") + childOf(span(1)) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + } + } + } + + assert remainingIterations.isEmpty() + } + + def "Nested delayed flux with high concurrency"() { + setup: + def iterations = 100 + def remainingIterations = new HashSet<>((0L ..< iterations).toList()) + + when: + (0L ..< iterations).forEach { iteration -> + def outer = Flux.just("a", "b") + .map({ it }) + .delayElements(Duration.ofMillis(10)) + .map({ it }) + .delayElements(Duration.ofMillis(10)) + .doOnEach({ middleSignal -> + if (middleSignal.hasValue()) { + def value = middleSignal.get() + + def middle = Flux.just("c", "d") + .map({ it }) + .delayElements(Duration.ofMillis(10)) + .doOnEach({ innerSignal -> + if (innerSignal.hasValue()) { + TraceUtils.runUnderTrace("inner " + value + innerSignal.get()) { + Span.current().setAttribute("iteration", iteration) + } + } + }) + + TraceUtils.runUnderTrace("middle " + value) { + Span.current().setAttribute("iteration", iteration) + middle.subscribe() + } + } + }) + + // Context must propagate even if only subscribe is in root span scope + TraceUtils.runUnderTrace("outer") { + Span.current().setAttribute("iteration", iteration) + outer.subscribe() + } + } + + then: + assertTraces(iterations) { + for (int i = 0; i < iterations; i++) { + trace(i, 7) { + long iteration = -1 + String middleA = null + String middleB = null + span(0) { + name("outer") + iteration = span.getAttributes().get(AttributeKey.longKey("iteration")).toLong() + assert remainingIterations.remove(iteration) + } + span("middle a") { + middleA = span.spanId + childOf(span(0)) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + span("middle b") { + middleB = span.spanId + childOf(span(0)) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + span("inner ac") { + parentSpanId(middleA) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + span("inner ad") { + parentSpanId(middleA) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + span("inner bc") { + parentSpanId(middleB) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + span("inner bd") { + parentSpanId(middleB) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + } + } + } + + assert remainingIterations.isEmpty() + } + + def runUnderTrace(def publisherSupplier) { + TraceUtils.runUnderTrace("trace-parent") { + def tracer = GlobalOpenTelemetry.getTracer("test") + def span = tracer.spanBuilder("publisher-parent").startSpan() + def scope = Context.current().with(span).makeCurrent() + try { + def publisher = publisherSupplier() + // Read all data from publisher + if (publisher instanceof Mono) { + return publisher.block() + } else if (publisher instanceof Flux) { + return publisher.toStream().toArray({ size -> new Integer[size] }) + } + + throw new IllegalStateException("Unknown publisher: " + publisher) + } finally { + span.end() + scope.close() + } + } + } + + def cancelUnderTrace(def publisherSupplier) { + TraceUtils.runUnderTrace("trace-parent") { + def tracer = GlobalOpenTelemetry.getTracer("test") + def span = tracer.spanBuilder("publisher-parent").startSpan() + def scope = Context.current().with(span).makeCurrent() + + def publisher = publisherSupplier() + publisher.subscribe(new Subscriber() { + void onSubscribe(Subscription subscription) { + subscription.cancel() + } + + void onNext(Integer t) { + } + + void onError(Throwable error) { + } + + void onComplete() { + } + }) + + span.end() + scope.close() + } + } + + static addOneFunc(int i) { + runInternalSpan("add one") + return i + 1 + } + + static addTwoFunc(int i) { + runInternalSpan("add two") + return i + 2 + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/reactor/AbstractSubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/reactor/AbstractSubscriptionTest.groovy new file mode 100644 index 000000000..cd20282ec --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/reactor/AbstractSubscriptionTest.groovy @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.reactor + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import java.util.concurrent.CountDownLatch +import reactor.core.publisher.Mono + +abstract class AbstractSubscriptionTest extends InstrumentationSpecification { + + def "subscription test"() { + when: + CountDownLatch latch = new CountDownLatch(1) + runUnderTrace("parent") { + Mono connection = Mono.create { + it.success(new Connection()) + } + connection.subscribe { + it.query() + latch.countDown() + } + } + latch.await() + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "Connection.query", span(0)) + } + } + + } + + static class Connection { + static int query() { + def span = GlobalOpenTelemetry.getTracer("test").spanBuilder("Connection.query").startSpan() + span.end() + return new Random().nextInt() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/reactor-netty-0.9-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/reactor-netty-0.9-javaagent.gradle new file mode 100644 index 000000000..dc42c767a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/reactor-netty-0.9-javaagent.gradle @@ -0,0 +1,25 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.projectreactor.netty" + module = "reactor-netty" + versions = "[0.9.0.RELEASE,1.0.0)" + } + fail { + group = "io.projectreactor.netty" + module = "reactor-netty-http" + versions = "[1.0.0,)" + } +} + +dependencies { + implementation project(':instrumentation:netty:netty-4.1:library') + library "io.projectreactor.netty:reactor-netty:0.9.0.RELEASE" + + testInstrumentation project(':instrumentation:reactor-netty:reactor-netty-1.0:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:reactor-3.1:javaagent') + + latestDepTestLibrary "io.projectreactor.netty:reactor-netty:(,1.0.0)" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/DecoratorFunctions.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/DecoratorFunctions.java new file mode 100644 index 000000000..1f92e26fb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/DecoratorFunctions.java @@ -0,0 +1,143 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v0_9; + +import io.netty.channel.Channel; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import java.util.function.BiConsumer; +import org.checkerframework.checker.nullness.qual.Nullable; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; + +public final class DecoratorFunctions { + + // ignore our own callbacks - or already decorated functions + public static boolean shouldDecorate(Class callbackClass) { + return !callbackClass.getName().startsWith("io.opentelemetry.javaagent"); + } + + private abstract static class OnMessageDecorator implements BiConsumer { + private final BiConsumer delegate; + private final boolean forceParentContext; + + protected OnMessageDecorator( + BiConsumer delegate, boolean forceParentContext) { + this.delegate = delegate; + this.forceParentContext = forceParentContext; + } + + @Override + public final void accept(M message, Connection connection) { + Channel channel = connection.channel(); + // don't try to get the client span from the netty channel when forceParentSpan is true + // this way the parent context will always be propagated + if (forceParentContext) { + channel = null; + } + Context context = getChannelContext(currentContext(message), channel); + if (context == null) { + delegate.accept(message, connection); + } else { + try (Scope ignored = context.makeCurrent()) { + delegate.accept(message, connection); + } + } + } + + abstract reactor.util.context.Context currentContext(M message); + } + + public static final class OnRequestDecorator extends OnMessageDecorator { + public OnRequestDecorator(BiConsumer delegate) { + super(delegate, /* forceParentContext= */ false); + } + + @Override + reactor.util.context.Context currentContext(HttpClientRequest message) { + return message.currentContext(); + } + } + + public static final class OnResponseDecorator extends OnMessageDecorator { + public OnResponseDecorator( + BiConsumer delegate, + boolean forceParentContext) { + super(delegate, forceParentContext); + } + + @Override + reactor.util.context.Context currentContext(HttpClientResponse message) { + return message.currentContext(); + } + } + + private abstract static class OnMessageErrorDecorator implements BiConsumer { + private final BiConsumer delegate; + + protected OnMessageErrorDecorator(BiConsumer delegate) { + this.delegate = delegate; + } + + @Override + public final void accept(M message, Throwable throwable) { + Context context = getChannelContext(currentContext(message), null); + if (context == null) { + delegate.accept(message, throwable); + } else { + try (Scope ignored = context.makeCurrent()) { + delegate.accept(message, throwable); + } + } + } + + abstract reactor.util.context.Context currentContext(M message); + } + + public static final class OnRequestErrorDecorator + extends OnMessageErrorDecorator { + public OnRequestErrorDecorator( + BiConsumer delegate) { + super(delegate); + } + + @Override + reactor.util.context.Context currentContext(HttpClientRequest message) { + return message.currentContext(); + } + } + + public static final class OnResponseErrorDecorator + extends OnMessageErrorDecorator { + public OnResponseErrorDecorator( + BiConsumer delegate) { + super(delegate); + } + + @Override + reactor.util.context.Context currentContext(HttpClientResponse message) { + return message.currentContext(); + } + } + + @Nullable + private static Context getChannelContext( + reactor.util.context.Context reactorContext, @Nullable Channel channel) { + // try to get the client span context from the channel if it's available + if (channel != null) { + Context context = channel.attr(AttributeKeys.CLIENT_CONTEXT).get(); + if (context != null) { + return context; + } + } + // otherwise use the parent span context + return reactorContext.getOrDefault(MapConnect.CONTEXT_ATTRIBUTE, null); + } + + private DecoratorFunctions() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/HttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/HttpClientInstrumentation.java new file mode 100644 index 000000000..6d38ead12 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/HttpClientInstrumentation.java @@ -0,0 +1,162 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v0_9; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.util.function.BiConsumer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; + +public class HttpClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("reactor.netty.http.client.HttpClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isStatic().and(namedOneOf("create", "newConnection", "from")), + this.getClass().getName() + "$CreateAdvice"); + + // advice classes below expose current context in doOn*/doAfter* callbacks + transformer.applyAdviceToMethod( + isPublic() + .and(namedOneOf("doOnRequest", "doAfterRequest")) + .and(takesArguments(1)) + .and(takesArgument(0, BiConsumer.class)), + this.getClass().getName() + "$OnRequestAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(named("doOnRequestError")) + .and(takesArguments(1)) + .and(takesArgument(0, BiConsumer.class)), + this.getClass().getName() + "$OnRequestErrorAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(namedOneOf("doOnResponse", "doAfterResponse")) + .and(takesArguments(1)) + .and(takesArgument(0, BiConsumer.class)), + this.getClass().getName() + "$OnResponseAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(named("doOnResponseError")) + .and(takesArguments(1)) + .and(takesArgument(0, BiConsumer.class)), + this.getClass().getName() + "$OnResponseErrorAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(named("doOnError")) + .and(takesArguments(2)) + .and(takesArgument(0, BiConsumer.class)) + .and(takesArgument(1, BiConsumer.class)), + this.getClass().getName() + "$OnErrorAdvice"); + } + + @SuppressWarnings("unused") + public static class CreateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, @Advice.Return(readOnly = false) HttpClient client) { + + if (CallDepthThreadLocalMap.decrementCallDepth(HttpClient.class) == 0 && throwable == null) { + client = client.doOnRequest(new OnRequest()).mapConnect(new MapConnect()); + } + } + } + + @SuppressWarnings("unused") + public static class OnRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer callback) { + if (DecoratorFunctions.shouldDecorate(callback.getClass())) { + callback = new DecoratorFunctions.OnRequestDecorator(callback); + } + } + } + + @SuppressWarnings("unused") + public static class OnRequestErrorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer callback) { + if (DecoratorFunctions.shouldDecorate(callback.getClass())) { + callback = new DecoratorFunctions.OnRequestErrorDecorator(callback); + } + } + } + + @SuppressWarnings("unused") + public static class OnResponseAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer callback, + @Advice.Origin("#m") String methodName) { + if (DecoratorFunctions.shouldDecorate(callback.getClass())) { + boolean forceParentContext = methodName.equals("doAfterResponse"); + callback = new DecoratorFunctions.OnResponseDecorator(callback, forceParentContext); + } + } + } + + @SuppressWarnings("unused") + public static class OnResponseErrorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer callback) { + if (DecoratorFunctions.shouldDecorate(callback.getClass())) { + callback = new DecoratorFunctions.OnResponseErrorDecorator(callback); + } + } + } + + @SuppressWarnings("unused") + public static class OnErrorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer requestCallback, + @Advice.Argument(value = 1, readOnly = false) + BiConsumer responseCallback) { + if (DecoratorFunctions.shouldDecorate(requestCallback.getClass())) { + requestCallback = new DecoratorFunctions.OnRequestErrorDecorator(requestCallback); + } + if (DecoratorFunctions.shouldDecorate(responseCallback.getClass())) { + responseCallback = new DecoratorFunctions.OnResponseErrorDecorator(responseCallback); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/MapConnect.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/MapConnect.java new file mode 100644 index 000000000..f8e2de8cf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/MapConnect.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v0_9; + +import io.netty.bootstrap.Bootstrap; +import io.opentelemetry.context.Context; +import java.util.function.BiFunction; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; + +public class MapConnect + implements BiFunction, Bootstrap, Mono> { + + static final String CONTEXT_ATTRIBUTE = MapConnect.class.getName() + ".Context"; + + @Override + public Mono apply(Mono m, Bootstrap b) { + return m.subscriberContext(s -> s.put(CONTEXT_ATTRIBUTE, Context.current())); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/OnRequest.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/OnRequest.java new file mode 100644 index 000000000..706f01be6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/OnRequest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v0_9; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import java.util.function.BiConsumer; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClientRequest; + +public class OnRequest implements BiConsumer { + @Override + public void accept(HttpClientRequest r, Connection c) { + Context context = r.currentContext().get(MapConnect.CONTEXT_ATTRIBUTE); + c.channel().attr(AttributeKeys.WRITE_CONTEXT).set(context); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyInstrumentationModule.java new file mode 100644 index 000000000..f6c6f5619 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyInstrumentationModule.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v0_9; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import net.bytebuddy.matcher.ElementMatcher; +import reactor.netty.http.client.HttpClient; + +/** + * This instrumentation solves the problem of the correct context propagation through the roller + * coaster of Project Reactor and Netty thread hopping. It uses two public hooks of {@link + * HttpClient}: {@link HttpClient#mapConnect(BiFunction)} and {@link + * HttpClient#doOnRequest(BiConsumer)} to pass context from the caller to Reactor to Netty. + */ +@AutoService(InstrumentationModule.class) +public class ReactorNettyInstrumentationModule extends InstrumentationModule { + + public ReactorNettyInstrumentationModule() { + super("reactor-netty", "reactor-netty-0.9"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // Removed in 1.0.0 + return hasClassesNamed("reactor.netty.tcp.InetSocketAddressUtil"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.groovy new file mode 100644 index 000000000..ff28f319f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.groovy @@ -0,0 +1,191 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v0_9 + +import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.SpanAssert +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.atomic.AtomicReference +import reactor.netty.http.client.HttpClient + +abstract class AbstractReactorNettyHttpClientTest extends HttpClientTest implements AgentTestTrait { + + @Override + boolean testRedirects() { + false + } + + @Override + String userAgent() { + return "ReactorNetty" + } + + @Override + HttpClient.ResponseReceiver buildRequest(String method, URI uri, Map headers) { + return createHttpClient() + .followRedirect(true) + .headers({ h -> headers.each { k, v -> h.add(k, v) } }) + .baseUrl(resolveAddress("").toString()) + ."${method.toLowerCase()}"() + .uri(uri.toString()) + } + + @Override + int sendRequest(HttpClient.ResponseReceiver request, String method, URI uri, Map headers) { + return request.responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { + resp + } + }.block().status().code() + } + + @Override + void sendRequestWithCallback(HttpClient.ResponseReceiver request, String method, URI uri, Map headers, RequestResult requestResult) { + request.responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { resp } + }.subscribe({ + requestResult.complete(it.status().code()) + }, { throwable -> + requestResult.complete(throwable) + }) + } + + @Override + String expectedClientSpanName(URI uri, String method) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return "CONNECT" + default: + return super.expectedClientSpanName(uri, method) + } + } + + @Override + void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + if (exception.class.getName().endsWith("ReactiveException")) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + exception = exception.getCause() + } + } + super.assertClientSpanErrorEvent(spanAssert, uri, exception) + } + + @Override + Set> httpAttributes(URI uri) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return [] + } + return super.httpAttributes(uri) + } + + abstract HttpClient createHttpClient() + + def "should expose context to http client callbacks"() { + given: + def onRequestSpan = new AtomicReference() + def afterRequestSpan = new AtomicReference() + def onResponseSpan = new AtomicReference() + def afterResponseSpan = new AtomicReference() + + def httpClient = createHttpClient() + .doOnRequest({ rq, con -> onRequestSpan.set(Span.current()) }) + .doAfterRequest({ rq, con -> afterRequestSpan.set(Span.current()) }) + .doOnResponse({ rs, con -> onResponseSpan.set(Span.current()) }) + .doAfterResponse({ rs, con -> afterResponseSpan.set(Span.current()) }) + + when: + runUnderTrace("parent") { + httpClient.baseUrl(resolveAddress("").toString()) + .get() + .uri("/success") + .responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { resp } + } + .block() + } + + then: + assertTraces(1) { + trace(0, 3) { + def parentSpan = span(0) + def nettyClientSpan = span(1) + + basicSpan(it, 0, "parent") + clientSpan(it, 1, parentSpan, "GET", resolveAddress("/success")) + serverSpan(it, 2, nettyClientSpan) + + assertSameSpan(parentSpan, onRequestSpan) + assertSameSpan(nettyClientSpan, afterRequestSpan) + assertSameSpan(nettyClientSpan, onResponseSpan) + assertSameSpan(parentSpan, afterResponseSpan) + } + } + } + + def "should expose context to http request error callback"() { + given: + def onRequestErrorSpan = new AtomicReference() + + def httpClient = createHttpClient() + .doOnRequestError({ rq, err -> onRequestErrorSpan.set(Span.current()) }) + + when: + runUnderTrace("parent") { + httpClient.get() + .uri("http://localhost:$UNUSABLE_PORT/") + .responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { resp } + } + .block() + } + + then: + def ex = thrown(Exception) + + assertTraces(1) { + trace(0, 2) { + def parentSpan = span(0) + + basicSpan(it, 0, "parent", null, ex) + span(1) { + def actualException = ex.cause + kind SpanKind.CLIENT + childOf parentSpan + status StatusCode.ERROR + errorEvent(actualException.class, actualException.message) + } + + assertSameSpan(parentSpan, onRequestErrorSpan) + } + } + } + + + private static void assertSameSpan(SpanData expected, AtomicReference actual) { + def expectedSpanContext = expected.spanContext + def actualSpanContext = actual.get().spanContext + assert expectedSpanContext.traceId == actualSpanContext.traceId + assert expectedSpanContext.spanId == actualSpanContext.spanId + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyHttpClientTest.groovy new file mode 100644 index 000000000..b1bb99b68 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyHttpClientTest.groovy @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v0_9 + +import io.netty.channel.ChannelOption +import io.opentelemetry.instrumentation.test.base.SingleConnection +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeoutException +import reactor.netty.http.client.HttpClient + +class ReactorNettyHttpClientTest extends AbstractReactorNettyHttpClientTest { + + HttpClient createHttpClient() { + return HttpClient.create().tcpConfiguration({ tcpClient -> + tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + }) + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + String url + try { + url = new URL("http", host, port, "").toString() + } catch (MalformedURLException e) { + throw new ExecutionException(e) + } + + def httpClient = HttpClient + .newConnection() + .baseUrl(url) + + return new SingleConnection() { + + @Override + int doRequest(String path, Map headers) throws ExecutionException, InterruptedException, TimeoutException { + return httpClient + .headers({ h -> headers.each { k, v -> h.add(k, v) } }) + .get() + .uri(path) + .responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { resp } + } + .block().status().code() + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyHttpClientUsingFromTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyHttpClientUsingFromTest.groovy new file mode 100644 index 000000000..488e9cd33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyHttpClientUsingFromTest.groovy @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v0_9 + +import io.netty.channel.ChannelOption +import reactor.netty.http.client.HttpClient +import reactor.netty.tcp.TcpClient + +class ReactorNettyHttpClientUsingFromTest extends AbstractReactorNettyHttpClientTest { + + HttpClient createHttpClient() { + return HttpClient.from(TcpClient.create()).tcpConfiguration({ tcpClient -> + tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + }) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/reactor-netty-1.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/reactor-netty-1.0-javaagent.gradle new file mode 100644 index 000000000..495f7ac0f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/reactor-netty-1.0-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + fail { + group = "io.projectreactor.netty" + module = "reactor-netty" + versions = "[,1.0.0)" + } + pass { + group = "io.projectreactor.netty" + module = "reactor-netty-http" + versions = "[1.0.0,)" + assertInverse = true + } +} + +dependencies { + implementation project(':instrumentation:netty:netty-4.1:library') + library "io.projectreactor.netty:reactor-netty-http:1.0.0" + + testInstrumentation project(':instrumentation:reactor-netty:reactor-netty-0.9:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:reactor-3.1:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/DecoratorFunctions.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/DecoratorFunctions.java new file mode 100644 index 000000000..0236b676d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/DecoratorFunctions.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +import io.netty.channel.Channel; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import java.util.function.BiConsumer; +import org.checkerframework.checker.nullness.qual.Nullable; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClientInfos; +import reactor.util.context.ContextView; + +public final class DecoratorFunctions { + + // ignore our own callbacks - or already decorated functions + public static boolean shouldDecorate(Class callbackClass) { + return !callbackClass.getName().startsWith("io.opentelemetry.javaagent"); + } + + public static final class OnMessageDecorator + implements BiConsumer { + private final BiConsumer delegate; + + public OnMessageDecorator(BiConsumer delegate) { + this.delegate = delegate; + } + + @Override + public void accept(M message, Connection connection) { + Context context = getChannelContext(message.currentContextView(), connection.channel()); + if (context == null) { + delegate.accept(message, connection); + } else { + try (Scope ignored = context.makeCurrent()) { + delegate.accept(message, connection); + } + } + } + } + + public static final class OnMessageErrorDecorator + implements BiConsumer { + private final BiConsumer delegate; + + public OnMessageErrorDecorator(BiConsumer delegate) { + this.delegate = delegate; + } + + @Override + public void accept(M message, Throwable throwable) { + Context context = getChannelContext(message.currentContextView(), null); + if (context == null) { + delegate.accept(message, throwable); + } else { + try (Scope ignored = context.makeCurrent()) { + delegate.accept(message, throwable); + } + } + } + } + + @Nullable + private static Context getChannelContext(ContextView contextView, @Nullable Channel channel) { + // try to get the client span context from the channel if it's available + if (channel != null) { + Context context = channel.attr(AttributeKeys.CLIENT_CONTEXT).get(); + if (context != null) { + return context; + } + } + // otherwise use the parent span context + return contextView.getOrDefault(MapConnect.CONTEXT_ATTRIBUTE, null); + } + + private DecoratorFunctions() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpClientInstrumentation.java new file mode 100644 index 000000000..4aeec7c91 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpClientInstrumentation.java @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.util.function.BiConsumer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; + +public class HttpClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("reactor.netty.http.client.HttpClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isStatic().and(namedOneOf("create", "newConnection", "from")), + this.getClass().getName() + "$CreateAdvice"); + + // advice classes below expose current context in doOn*/doAfter* callbacks + transformer.applyAdviceToMethod( + isPublic() + .and(namedOneOf("doOnRequest", "doAfterRequest")) + .and(takesArguments(1)) + .and(takesArgument(0, BiConsumer.class)), + this.getClass().getName() + "$OnRequestAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(named("doOnRequestError")) + .and(takesArguments(1)) + .and(takesArgument(0, BiConsumer.class)), + this.getClass().getName() + "$OnRequestErrorAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(namedOneOf("doOnResponse", "doAfterResponseSuccess", "doOnRedirect")) + .and(takesArguments(1)) + .and(takesArgument(0, BiConsumer.class)), + this.getClass().getName() + "$OnResponseAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(named("doOnResponseError")) + .and(takesArguments(1)) + .and(takesArgument(0, BiConsumer.class)), + this.getClass().getName() + "$OnResponseErrorAdvice"); + transformer.applyAdviceToMethod( + isPublic() + .and(named("doOnError")) + .and(takesArguments(2)) + .and(takesArgument(0, BiConsumer.class)) + .and(takesArgument(1, BiConsumer.class)), + this.getClass().getName() + "$OnErrorAdvice"); + } + + @SuppressWarnings("unused") + public static class CreateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, @Advice.Return(readOnly = false) HttpClient client) { + + if (CallDepthThreadLocalMap.decrementCallDepth(HttpClient.class) == 0 && throwable == null) { + client = client.doOnRequest(new OnRequest()).mapConnect(new MapConnect()); + } + } + } + + @SuppressWarnings("unused") + public static class OnRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer callback) { + if (DecoratorFunctions.shouldDecorate(callback.getClass())) { + callback = new DecoratorFunctions.OnMessageDecorator<>(callback); + } + } + } + + @SuppressWarnings("unused") + public static class OnRequestErrorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer callback) { + if (DecoratorFunctions.shouldDecorate(callback.getClass())) { + callback = new DecoratorFunctions.OnMessageErrorDecorator<>(callback); + } + } + } + + @SuppressWarnings("unused") + public static class OnResponseAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer callback) { + if (DecoratorFunctions.shouldDecorate(callback.getClass())) { + callback = new DecoratorFunctions.OnMessageDecorator<>(callback); + } + } + } + + @SuppressWarnings("unused") + public static class OnResponseErrorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer callback) { + if (DecoratorFunctions.shouldDecorate(callback.getClass())) { + callback = new DecoratorFunctions.OnMessageErrorDecorator<>(callback); + } + } + } + + @SuppressWarnings("unused") + public static class OnErrorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) + BiConsumer requestCallback, + @Advice.Argument(value = 1, readOnly = false) + BiConsumer responseCallback) { + if (DecoratorFunctions.shouldDecorate(requestCallback.getClass())) { + requestCallback = new DecoratorFunctions.OnMessageErrorDecorator<>(requestCallback); + } + if (DecoratorFunctions.shouldDecorate(responseCallback.getClass())) { + responseCallback = new DecoratorFunctions.OnMessageErrorDecorator<>(responseCallback); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/MapConnect.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/MapConnect.java new file mode 100644 index 000000000..10031575a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/MapConnect.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +import io.opentelemetry.context.Context; +import java.util.function.Function; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; + +public class MapConnect + implements Function, Mono> { + + static final String CONTEXT_ATTRIBUTE = MapConnect.class.getName() + ".Context"; + + @Override + public Mono apply(Mono m) { + return m.contextWrite(s -> s.put(CONTEXT_ATTRIBUTE, Context.current())); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/OnRequest.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/OnRequest.java new file mode 100644 index 000000000..adff05db7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/OnRequest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.netty.v4_1.AttributeKeys; +import java.util.function.BiConsumer; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClientRequest; + +public class OnRequest implements BiConsumer { + @Override + public void accept(HttpClientRequest r, Connection c) { + Context context = r.currentContextView().get(MapConnect.CONTEXT_ATTRIBUTE); + c.channel().attr(AttributeKeys.WRITE_CONTEXT).set(context); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyInstrumentationModule.java new file mode 100644 index 000000000..d758e23d9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyInstrumentationModule.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import net.bytebuddy.matcher.ElementMatcher; +import reactor.netty.http.client.HttpClient; + +/** + * This instrumentation solves the problem of the correct context propagation through the roller + * coaster of Project Reactor and Netty thread hopping. It uses two public hooks of {@link + * HttpClient}: {@link HttpClient#mapConnect(Function)} and {@link + * HttpClient#doOnRequest(BiConsumer)} to pass context from the caller to Reactor to Netty. + */ +@AutoService(InstrumentationModule.class) +public class ReactorNettyInstrumentationModule extends InstrumentationModule { + + public ReactorNettyInstrumentationModule() { + super("reactor-netty", "reactor-netty-1.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // Introduced in 1.0.0 + return hasClassesNamed("reactor.netty.transport.AddressUtils"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/AbstractReactorNettyHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/AbstractReactorNettyHttpClientTest.groovy new file mode 100644 index 000000000..2ff481e1b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/AbstractReactorNettyHttpClientTest.groovy @@ -0,0 +1,232 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0 + +import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.netty.resolver.AddressResolver +import io.netty.resolver.AddressResolverGroup +import io.netty.resolver.InetNameResolver +import io.netty.util.concurrent.EventExecutor +import io.netty.util.concurrent.Promise +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.SpanAssert +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.atomic.AtomicReference +import reactor.netty.http.client.HttpClient + +abstract class AbstractReactorNettyHttpClientTest extends HttpClientTest implements AgentTestTrait { + + @Override + boolean testRedirects() { + false + } + + @Override + String userAgent() { + return "ReactorNetty" + } + + @Override + HttpClient.ResponseReceiver buildRequest(String method, URI uri, Map headers) { + return createHttpClient() + .followRedirect(true) + .headers({ h -> headers.each { k, v -> h.add(k, v) } }) + .baseUrl(resolveAddress("").toString()) + ."${method.toLowerCase()}"() + .uri(uri.toString()) + } + + @Override + int sendRequest(HttpClient.ResponseReceiver request, String method, URI uri, Map headers) { + return request.responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { + resp + } + }.block().status().code() + } + + @Override + void sendRequestWithCallback(HttpClient.ResponseReceiver request, String method, URI uri, Map headers, RequestResult requestResult) { + request.responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { resp } + }.subscribe({ + requestResult.complete(it.status().code()) + }, { throwable -> + requestResult.complete(throwable) + }) + } + + @Override + String expectedClientSpanName(URI uri, String method) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return "CONNECT" + default: + return super.expectedClientSpanName(uri, method) + } + } + + @Override + void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + if (exception.class.getName().endsWith("ReactiveException")) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + exception = exception.getCause() + } + } + super.assertClientSpanErrorEvent(spanAssert, uri, exception) + } + + @Override + Set> httpAttributes(URI uri) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + return [] + } + return super.httpAttributes(uri) + } + + abstract HttpClient createHttpClient() + + AddressResolverGroup getAddressResolverGroup() { + return CustomNameResolverGroup.INSTANCE + } + + def "should expose context to http client callbacks"() { + given: + def onRequestSpan = new AtomicReference() + def afterRequestSpan = new AtomicReference() + def onResponseSpan = new AtomicReference() + def afterResponseSpan = new AtomicReference() + + def httpClient = createHttpClient() + .doOnRequest({ rq, con -> onRequestSpan.set(Span.current()) }) + .doAfterRequest({ rq, con -> afterRequestSpan.set(Span.current()) }) + .doOnResponse({ rs, con -> onResponseSpan.set(Span.current()) }) + .doAfterResponseSuccess({ rs, con -> afterResponseSpan.set(Span.current()) }) + + when: + runUnderTrace("parent") { + httpClient.baseUrl(resolveAddress("").toString()) + .get() + .uri("/success") + .responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { resp } + } + .block() + } + + then: + assertTraces(1) { + trace(0, 3) { + def parentSpan = span(0) + def nettyClientSpan = span(1) + + basicSpan(it, 0, "parent") + clientSpan(it, 1, parentSpan, "GET", resolveAddress("/success")) + serverSpan(it, 2, nettyClientSpan) + + assertSameSpan(parentSpan, onRequestSpan) + assertSameSpan(nettyClientSpan, afterRequestSpan) + assertSameSpan(nettyClientSpan, onResponseSpan) + assertSameSpan(parentSpan, afterResponseSpan) + } + } + } + + def "should expose context to http request error callback"() { + given: + def onRequestErrorSpan = new AtomicReference() + + def httpClient = createHttpClient() + .doOnRequestError({ rq, err -> onRequestErrorSpan.set(Span.current()) }) + + when: + runUnderTrace("parent") { + httpClient.get() + .uri("http://localhost:$UNUSABLE_PORT/") + .response() + .block() + } + + then: + def ex = thrown(Exception) + + assertTraces(1) { + trace(0, 2) { + def parentSpan = span(0) + + basicSpan(it, 0, "parent", null, ex) + span(1) { + def actualException = ex.cause + kind SpanKind.CLIENT + childOf parentSpan + status StatusCode.ERROR + errorEvent(actualException.class, actualException.message) + } + + assertSameSpan(parentSpan, onRequestErrorSpan) + } + } + } + + private static void assertSameSpan(SpanData expected, AtomicReference actual) { + def expectedSpanContext = expected.spanContext + def actualSpanContext = actual.get().spanContext + assert expectedSpanContext.traceId == actualSpanContext.traceId + assert expectedSpanContext.spanId == actualSpanContext.spanId + } + + // custom address resolver that returns at most one address for each host + // adapted from io.netty.resolver.DefaultAddressResolverGroup + static class CustomNameResolverGroup extends AddressResolverGroup { + public static final CustomNameResolverGroup INSTANCE = new CustomNameResolverGroup() + + private CustomNameResolverGroup() { + } + + protected AddressResolver newResolver(EventExecutor executor) throws Exception { + return (new CustomNameResolver(executor)).asAddressResolver() + } + } + + static class CustomNameResolver extends InetNameResolver { + CustomNameResolver(EventExecutor executor) { + super(executor) + } + + protected void doResolve(String inetHost, Promise promise) throws Exception { + try { + promise.setSuccess(InetAddress.getByName(inetHost)) + } catch (UnknownHostException exception) { + promise.setFailure(exception) + } + } + + protected void doResolveAll(String inetHost, Promise> promise) throws Exception { + try { + // default implementation calls InetAddress.getAllByName + promise.setSuccess(Collections.singletonList(InetAddress.getByName(inetHost))) + } catch (UnknownHostException exception) { + promise.setFailure(exception) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientTest.groovy new file mode 100644 index 000000000..73d24900c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0 + +import io.netty.channel.ChannelOption +import io.opentelemetry.instrumentation.test.base.SingleConnection +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeoutException +import reactor.netty.http.client.HttpClient + +class ReactorNettyHttpClientTest extends AbstractReactorNettyHttpClientTest { + + HttpClient createHttpClient() { + return HttpClient.create().tcpConfiguration({ tcpClient -> + tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + }).resolver(getAddressResolverGroup()) + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + def httpClient = HttpClient + .newConnection() + .host(host) + .port(port) + + return new SingleConnection() { + + @Override + int doRequest(String path, Map headers) throws ExecutionException, InterruptedException, TimeoutException { + return httpClient + .headers({ h -> headers.each { k, v -> h.add(k, v) } }) + .get() + .uri(path) + .responseSingle {resp, content -> + // Make sure to consume content since that's when we close the span. + content.map { resp } + } + .block() + .status().code() + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientUsingFromTest.groovy b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientUsingFromTest.groovy new file mode 100644 index 000000000..098c96312 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/reactor-netty/reactor-netty-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientUsingFromTest.groovy @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0 + +import io.netty.channel.ChannelOption +import reactor.netty.http.client.HttpClient +import reactor.netty.tcp.TcpClient + +class ReactorNettyHttpClientUsingFromTest extends AbstractReactorNettyHttpClientTest { + + HttpClient createHttpClient() { + return HttpClient.from(TcpClient.create()).tcpConfiguration({ tcpClient -> + tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + }).resolver(getAddressResolverGroup()) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/rediscala-1.8-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/rediscala-1.8-javaagent.gradle new file mode 100644 index 000000000..5c3a2a4b5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/rediscala-1.8-javaagent.gradle @@ -0,0 +1,49 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "com.github.etaty" + module = "rediscala_2.11" + versions = "[1.5.0,)" + assertInverse = true + } + + pass { + group = "com.github.etaty" + module = "rediscala_2.12" + versions = "[1.8.0,)" + assertInverse = true + } + + pass { + group = "com.github.etaty" + module = "rediscala_2.13" + versions = "[1.9.0,)" + assertInverse = true + } + + pass { + group = "com.github.Ma27" + module = "rediscala_2.11" + versions = "[1.8.1,)" + assertInverse = true + } + + pass { + group = "com.github.Ma27" + module = "rediscala_2.12" + versions = "[1.8.1,)" + assertInverse = true + } + + pass { + group = "com.github.Ma27" + module = "rediscala_2.13" + versions = "[1.9.0,)" + assertInverse = true + } +} + +dependencies { + library "com.github.etaty:rediscala_2.11:1.8.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/OnCompleteHandler.java b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/OnCompleteHandler.java new file mode 100644 index 000000000..a43c8e230 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/OnCompleteHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rediscala; + +import static io.opentelemetry.javaagent.instrumentation.rediscala.RediscalaClientTracer.tracer; + +import io.opentelemetry.context.Context; +import scala.runtime.AbstractFunction1; +import scala.util.Try; + +public class OnCompleteHandler extends AbstractFunction1, Void> { + private final Context context; + + public OnCompleteHandler(Context context) { + this.context = context; + } + + @Override + public Void apply(Try result) { + if (result.isFailure()) { + tracer().endExceptionally(context, result.failed().get()); + } else { + tracer().end(context); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaClientTracer.java new file mode 100644 index 000000000..0fe07e4ce --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaClientTracer.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rediscala; + +import io.opentelemetry.instrumentation.api.tracer.ClassNames; +import io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; +import java.net.InetSocketAddress; +import redis.RedisCommand; + +public class RediscalaClientTracer + extends DatabaseClientTracer, RedisCommand, String> { + + private static final RediscalaClientTracer TRACER = new RediscalaClientTracer(); + + private RediscalaClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static RediscalaClientTracer tracer() { + return TRACER; + } + + @Override + protected String sanitizeStatement(RedisCommand redisCommand) { + return ClassNames.simpleName(redisCommand.getClass()); + } + + @Override + protected String spanName( + RedisCommand connection, RedisCommand statement, String operation) { + return operation; + } + + @Override + protected String dbSystem(RedisCommand redisCommand) { + return DbSystemValues.REDIS; + } + + @Override + protected InetSocketAddress peerAddress(RedisCommand redisCommand) { + return null; + } + + @Override + protected String dbStatement( + RedisCommand connection, RedisCommand command, String operation) { + return operation; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.rediscala-1.8"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaInstrumentationModule.java new file mode 100644 index 000000000..fec134a08 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rediscala; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RediscalaInstrumentationModule extends InstrumentationModule { + + public RediscalaInstrumentationModule() { + super("rediscala", "rediscala-1.8"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RequestInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RequestInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RequestInstrumentation.java new file mode 100644 index 000000000..e173fc42e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RequestInstrumentation.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rediscala; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.rediscala.RediscalaClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.RedisCommand; +import scala.concurrent.ExecutionContext; +import scala.concurrent.Future; + +public class RequestInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("redis.Request"); + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType( + namedOneOf( + "redis.ActorRequest", + "redis.Request", + "redis.BufferedRequest", + "redis.RoundRobinPoolRequest")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("send")) + .and(takesArgument(0, named("redis.RedisCommand"))) + .and(returns(named("scala.concurrent.Future"))), + this.getClass().getName() + "$SendAdvice"); + } + + @SuppressWarnings("unused") + public static class SendAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) RedisCommand cmd, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startSpan(currentContext(), cmd, cmd); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable, + @Advice.FieldValue("executionContext") ExecutionContext ctx, + @Advice.Return(readOnly = false) Future responseFuture) { + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + responseFuture.onComplete(new OnCompleteHandler(context), ctx); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/test/groovy/RediscalaClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/test/groovy/RediscalaClientTest.groovy new file mode 100644 index 000000000..89750f9c6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rediscala-1.8/javaagent/src/test/groovy/RediscalaClientTest.groovy @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import akka.actor.ActorSystem +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.testcontainers.containers.GenericContainer +import redis.ByteStringDeserializerDefault +import redis.ByteStringSerializerLowPriority +import redis.RedisClient +import redis.RedisDispatcher +import scala.Option +import scala.concurrent.Await +import scala.concurrent.duration.Duration +import spock.lang.Shared + +class RediscalaClientTest extends AgentInstrumentationSpecification { + + private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) + + @Shared + int port + + @Shared + ActorSystem system + + @Shared + RedisClient redisClient + + def setupSpec() { + redisServer.start() + port = redisServer.getMappedPort(6379) + system = ActorSystem.create() + redisClient = new RedisClient("localhost", + port, + Option.apply(null), + Option.apply(null), + "RedisClient", + Option.apply(null), + system, + new RedisDispatcher("rediscala.rediscala-client-worker-dispatcher")) + } + + def cleanupSpec() { + redisServer.stop() + system?.terminate() + } + + def "set command"() { + when: + def value = redisClient.set("foo", + "bar", + Option.apply(null), + Option.apply(null), + false, + false, + new ByteStringSerializerLowPriority.String$()) + + + then: + Await.result(value, Duration.apply("3 second")) == true + assertTraces(1) { + trace(0, 1) { + span(0) { + name "Set" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "Set" + } + } + } + } + } + + def "get command"() { + when: + def write = redisClient.set("bar", + "baz", + Option.apply(null), + Option.apply(null), + false, + false, + new ByteStringSerializerLowPriority.String$()) + def value = redisClient.get("bar", new ByteStringDeserializerDefault.String$()) + + then: + Await.result(write, Duration.apply("3 second")) == true + Await.result(value, Duration.apply("3 second")) == Option.apply("baz") + assertTraces(2) { + trace(0, 1) { + span(0) { + name "Set" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "Set" + } + } + } + trace(1, 1) { + span(0) { + name "Get" + kind CLIENT + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "redis" + "${SemanticAttributes.DB_STATEMENT.key}" "Get" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/redisson-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/redisson-3.0-javaagent.gradle new file mode 100644 index 000000000..6ea2df305 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/redisson-3.0-javaagent.gradle @@ -0,0 +1,20 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.redisson" + module = "redisson" + versions = "[3.0.0,)" + } +} + +dependencies { + library "org.redisson:redisson:3.0.0" + + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" +} + +test { + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedisConnectionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedisConnectionInstrumentation.java new file mode 100644 index 000000000..5ec045206 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedisConnectionInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.redisson.RedissonSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.net.InetSocketAddress; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.redisson.client.RedisConnection; + +public class RedisConnectionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.redisson.client.RedisConnection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("send")), this.getClass().getName() + "$SendAdvice"); + } + + @SuppressWarnings("unused") + public static class SendAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This RedisConnection connection, + @Advice.Argument(0) Object arg, + @Advice.Local("otelRedissonRequest") RedissonRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + InetSocketAddress remoteAddress = (InetSocketAddress) connection.getChannel().remoteAddress(); + request = RedissonRequest.create(remoteAddress, arg); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRedissonRequest") RedissonRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + instrumenter().end(context, request, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonDbAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonDbAttributesExtractor.java new file mode 100644 index 000000000..5c46ac2f1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonDbAttributesExtractor.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson; + +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class RedissonDbAttributesExtractor extends DbAttributesExtractor { + + @Override + protected String system(RedissonRequest request) { + return SemanticAttributes.DbSystemValues.REDIS; + } + + @Nullable + @Override + protected String user(RedissonRequest request) { + return null; + } + + @Nullable + @Override + protected String name(RedissonRequest request) { + return null; + } + + @Override + protected String connectionString(RedissonRequest request) { + return null; + } + + @Override + protected String statement(RedissonRequest request) { + return request.getStatement(); + } + + @Nullable + @Override + protected String operation(RedissonRequest request) { + return request.getOperation(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonInstrumentationModule.java new file mode 100644 index 000000000..0e9fe1fa9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RedissonInstrumentationModule extends InstrumentationModule { + + public RedissonInstrumentationModule() { + super("redisson", "redisson-3.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RedisConnectionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonNetAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonNetAttributesExtractor.java new file mode 100644 index 000000000..4faf9ade5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonNetAttributesExtractor.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson; + +import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor; +import java.net.InetSocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class RedissonNetAttributesExtractor + extends InetSocketAddressNetAttributesExtractor { + + @Override + public InetSocketAddress getAddress(RedissonRequest request, @Nullable Void unused) { + return request.getAddress(); + } + + @Nullable + @Override + public String transport(RedissonRequest request) { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonRequest.java b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonRequest.java new file mode 100644 index 000000000..bc92ab250 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonRequest.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +import com.google.auto.value.AutoValue; +import io.netty.buffer.ByteBuf; +import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.redisson.client.protocol.CommandData; +import org.redisson.client.protocol.CommandsData; + +@AutoValue +public abstract class RedissonRequest { + + public static RedissonRequest create(InetSocketAddress address, Object command) { + return new AutoValue_RedissonRequest(address, command); + } + + public abstract InetSocketAddress getAddress(); + + public abstract Object getCommand(); + + public String getOperation() { + Object command = getCommand(); + if (command instanceof CommandData) { + return ((CommandData) command).getCommand().getName(); + } else if (command instanceof CommandsData) { + CommandsData commandsData = (CommandsData) command; + if (commandsData.getCommands().size() == 1) { + return commandsData.getCommands().get(0).getCommand().getName(); + } + } + return null; + } + + public String getStatement() { + List sanitizedStatements = sanitizeStatement(); + switch (sanitizedStatements.size()) { + case 0: + return null; + // optimize for the most common case + case 1: + return sanitizedStatements.get(0); + default: + return String.join(";", sanitizedStatements); + } + } + + private List sanitizeStatement() { + Object command = getCommand(); + // get command + if (command instanceof CommandsData) { + List> commands = ((CommandsData) command).getCommands(); + return commands.stream() + .map(RedissonRequest::normalizeSingleCommand) + .collect(Collectors.toList()); + } else if (command instanceof CommandData) { + return singletonList(normalizeSingleCommand((CommandData) command)); + } + return emptyList(); + } + + private static String normalizeSingleCommand(CommandData command) { + Object[] commandParams = command.getParams(); + List args = new ArrayList<>(commandParams.length + 1); + if (command.getCommand().getSubName() != null) { + args.add(command.getCommand().getSubName()); + } + for (Object param : commandParams) { + if (param instanceof ByteBuf) { + try { + // slice() does not copy the actual byte buffer, it only returns a readable/writable + // "view" of the original buffer (i.e. read and write marks are not shared) + ByteBuf buf = ((ByteBuf) param).slice(); + // state can be null here: no Decoders used by Codecs use it + args.add(command.getCodec().getValueDecoder().decode(buf, null)); + } catch (Exception ignored) { + args.add("?"); + } + } else { + args.add(param); + } + } + return RedisCommandSanitizer.sanitize(command.getCommand().getName(), args); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonSingletons.java b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonSingletons.java new file mode 100644 index 000000000..aa3d11a24 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonSingletons.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor; + +public final class RedissonSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.redisson-3.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + DbAttributesExtractor dbAttributesExtractor = + new RedissonDbAttributesExtractor(); + RedissonNetAttributesExtractor netAttributesExtractor = new RedissonNetAttributesExtractor(); + SpanNameExtractor spanName = DbSpanNameExtractor.create(dbAttributesExtractor); + + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(dbAttributesExtractor) + .addAttributesExtractor(netAttributesExtractor) + .newInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private RedissonSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy new file mode 100644 index 000000000..2df5f53f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.TimeUnit +import org.redisson.Redisson +import org.redisson.api.RBucket +import org.redisson.api.RFuture +import org.redisson.api.RList +import org.redisson.api.RSet +import org.redisson.api.RedissonClient +import org.redisson.config.Config +import org.redisson.config.SingleServerConfig +import org.testcontainers.containers.GenericContainer +import spock.lang.Shared + +class RedissonAsyncClientTest extends AgentInstrumentationSpecification { + + private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) + @Shared + int port + + @Shared + RedissonClient redisson + @Shared + String address + + def setupSpec() { + redisServer.start() + port = redisServer.getMappedPort(6379) + address = "localhost:" + port + if (Boolean.getBoolean("testLatestDeps")) { + // Newer versions of redisson require scheme, older versions forbid it + address = "redis://" + address + } + } + + def cleanupSpec() { + redisson.shutdown() + redisServer.stop() + } + + def setup() { + Config config = new Config() + SingleServerConfig singleServerConfig = config.useSingleServer() + singleServerConfig.setAddress(address) + // disable connection ping if it exists + singleServerConfig.metaClass.getMetaMethod("setPingConnectionInterval", int)?.invoke(singleServerConfig, 0) + redisson = Redisson.create(config) + clearExportedData() + } + + def "test future set"() { + when: + RBucket keyObject = redisson.getBucket("foo") + RFuture future = keyObject.setAsync("bar") + future.get(3, TimeUnit.SECONDS) + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + } + } + } + } + } + + def "test future whenComplete"() { + when: + RSet rSet = redisson.getSet("set1") + RFuture result = rSet.addAsync("s1") + result.whenComplete({ res, throwable -> + RList strings = redisson.getList("list1") + strings.add("a") + }) + + then: + result.get(3, TimeUnit.SECONDS) + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SADD" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "SADD set1 ?" + "$SemanticAttributes.DB_OPERATION.key" "SADD" + } + } + } + trace(1, 1) { + span(0) { + name "RPUSH" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "RPUSH list1 ?" + "$SemanticAttributes.DB_OPERATION.key" "RPUSH" + } + } + } + } + } + +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/test/groovy/RedissonClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/test/groovy/RedissonClientTest.groovy new file mode 100644 index 000000000..b385a7883 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/redisson-3.0/javaagent/src/test/groovy/RedissonClientTest.groovy @@ -0,0 +1,314 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static java.util.regex.Pattern.compile +import static java.util.regex.Pattern.quote + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.redisson.Redisson +import org.redisson.api.RAtomicLong +import org.redisson.api.RBatch +import org.redisson.api.RBucket +import org.redisson.api.RList +import org.redisson.api.RLock +import org.redisson.api.RMap +import org.redisson.api.RScoredSortedSet +import org.redisson.api.RSet +import org.redisson.api.RedissonClient +import org.redisson.config.Config +import org.redisson.config.SingleServerConfig +import org.testcontainers.containers.GenericContainer +import spock.lang.Shared + +class RedissonClientTest extends AgentInstrumentationSpecification { + + private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) + @Shared + int port + + @Shared + RedissonClient redisson + @Shared + String address + + def setupSpec() { + redisServer.start() + port = redisServer.getMappedPort(6379) + address = "localhost:" + port + if (Boolean.getBoolean("testLatestDeps")) { + // Newer versions of redisson require scheme, older versions forbid it + address = "redis://" + address + } + } + + def cleanupSpec() { + redisson.shutdown() + redisServer.stop() + } + + def setup() { + Config config = new Config() + SingleServerConfig singleServerConfig = config.useSingleServer() + singleServerConfig.setAddress(address) + // disable connection ping if it exists + singleServerConfig.metaClass.getMetaMethod("setPingConnectionInterval", int)?.invoke(singleServerConfig, 0) + redisson = Redisson.create(config) + clearExportedData() + } + + def "test string command"() { + when: + RBucket keyObject = redisson.getBucket("foo") + keyObject.set("bar") + keyObject.get() + + then: + assertTraces(2) { + trace(0, 1) { + span(0) { + name "SET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "SET foo ?" + "$SemanticAttributes.DB_OPERATION.key" "SET" + } + } + } + trace(1, 1) { + span(0) { + name "GET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "GET foo" + "$SemanticAttributes.DB_OPERATION.key" "GET" + } + } + } + } + } + + def "test batch command"() { + when: + RBatch batch = redisson.createBatch() + batch.getBucket("batch1").setAsync("v1") + batch.getBucket("batch2").setAsync("v2") + batch.execute() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "DB Query" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "SET batch1 ?;SET batch2 ?" + "$SemanticAttributes.DB_OPERATION.key" null + } + } + } + } + } + + def "test list command"() { + when: + RList strings = redisson.getList("list1") + strings.add("a") + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "RPUSH" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "RPUSH list1 ?" + "$SemanticAttributes.DB_OPERATION.key" "RPUSH" + } + } + } + } + } + + def "test hash command"() { + when: + RMap rMap = redisson.getMap("map1") + rMap.put("key1", "value1") + rMap.get("key1") + + then: + assertTraces(2) { + trace(0, 1) { + span(0) { + def script = "local v = redis.call('hget', KEYS[1], ARGV[1]); redis.call('hset', KEYS[1], ARGV[1], ARGV[2]); return v" + + name "EVAL" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "EVAL $script 1 map1 ? ?" + "$SemanticAttributes.DB_OPERATION.key" "EVAL" + } + } + } + trace(1, 1) { + span(0) { + name "HGET" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "HGET map1 key1" + "$SemanticAttributes.DB_OPERATION.key" "HGET" + } + } + } + } + } + + def "test set command"() { + when: + RSet rSet = redisson.getSet("set1") + rSet.add("s1") + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "SADD" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "SADD set1 ?" + "$SemanticAttributes.DB_OPERATION.key" "SADD" + } + } + } + } + } + + def "test sorted set command"() { + when: + Map scores = new HashMap<>() + scores.put("u1", 1.0d) + scores.put("u2", 3.0d) + scores.put("u3", 0.0d) + RScoredSortedSet sortSet = redisson.getScoredSortedSet("sort_set1") + sortSet.addAll(scores) + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "ZADD" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "ZADD sort_set1 ? ? ? ? ? ?" + "$SemanticAttributes.DB_OPERATION.key" "ZADD" + } + } + } + } + } + + def "test AtomicLong command"() { + when: + RAtomicLong atomicLong = redisson.getAtomicLong("AtomicLong") + atomicLong.incrementAndGet() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "INCR" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" "INCR AtomicLong" + "$SemanticAttributes.DB_OPERATION.key" "INCR" + } + } + } + } + } + + def "test lock command"() { + when: + RLock lock = redisson.getLock("lock") + lock.lock() + lock.unlock() + + then: + assertTraces(2) { + trace(0, 1) { + span(0) { + // Use .* to match the actual script, since it changes between redisson versions + // everything that does not change is quoted so that it's matched literally + def lockScriptPattern = compile("^" + quote("EVAL ") + ".*" + quote(" 1 lock ? ?") + "\$") + + name "EVAL" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" { lockScriptPattern.matcher(it).matches() } + "$SemanticAttributes.DB_OPERATION.key" "EVAL" + } + } + } + trace(1, 1) { + span(0) { + def lockScriptPattern = compile("^" + quote("EVAL ") + ".*" + quote(" 2 lock ") + "\\S+" + quote(" ? ? ?") + "\$") + + name "EVAL" + kind CLIENT + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "redis" + "$SemanticAttributes.NET_PEER_IP.key" "127.0.0.1" + "$SemanticAttributes.NET_PEER_NAME.key" "localhost" + "$SemanticAttributes.NET_PEER_PORT.key" port + "$SemanticAttributes.DB_STATEMENT.key" { lockScriptPattern.matcher(it).matches() } + "$SemanticAttributes.DB_OPERATION.key" "EVAL" + } + } + } + } + } +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/rmi-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/rmi-javaagent.gradle new file mode 100644 index 000000000..aab1e6166 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/rmi-javaagent.gradle @@ -0,0 +1,45 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + coreJdk() + } +} + +def rmic = tasks.register('rmic') { + dependsOn(testClasses) + + def clazz = 'rmi.app.ServerLegacy' + + // Try one level up too in case java.home refers to jre directory inside jdk directory + def rmicBinaryPath = ['/bin/rmic', '/../bin/rmic'].findResult { + def path = new File(System.getProperty("java.home"), it).getAbsoluteFile() + path.isFile() ? path.toString() : null + } ?: "rmic" + + String command = """$rmicBinaryPath -g -keep -classpath ${sourceSets.test.output.classesDirs.asPath} -d ${buildDir}/classes/java/test ${clazz}""" + command.execute().text +} + +test.dependsOn rmic + +// We cannot use "--release" javac option here because that will forbid importing "sun.rmi" package. +// We also can't seem to use the toolchain without the "--release" option. So disable everything. + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + toolchain { + languageVersion = null + } +} + +tasks.withType(JavaCompile).configureEach { + options.release = null +} +tasks.withType(GroovyCompile).configureEach { + options.release = null +} +tasks.withType(Test).configureEach { + jvmArgs "-Djava.rmi.server.hostname=127.0.0.1" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientInstrumentationModule.java new file mode 100644 index 000000000..11da208fa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.client; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RmiClientInstrumentationModule extends InstrumentationModule { + + public RmiClientInstrumentationModule() { + super("rmi", "rmi-client"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new UnicastRefInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientTracer.java new file mode 100644 index 000000000..3b40d5f9a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientTracer.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.client; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.RpcClientTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.reflect.Method; + +public class RmiClientTracer extends RpcClientTracer { + private static final RmiClientTracer TRACER = new RmiClientTracer(); + + public static RmiClientTracer tracer() { + return TRACER; + } + + public Context startSpan(Method method) { + Context parentContext = Context.current(); + String serviceName = method.getDeclaringClass().getName(); + String methodName = method.getName(); + + Span span = + spanBuilder(parentContext, serviceName + "/" + methodName, CLIENT) + .setAttribute(SemanticAttributes.RPC_SYSTEM, getRpcSystem()) + .setAttribute(SemanticAttributes.RPC_SERVICE, serviceName) + .setAttribute(SemanticAttributes.RPC_METHOD, methodName) + .startSpan(); + + return parentContext.with(span); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.rmi"; + } + + @Override + protected String getRpcSystem() { + return "java_rmi"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/UnicastRefInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/UnicastRefInstrumentation.java new file mode 100644 index 000000000..bedcfc98a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/UnicastRefInstrumentation.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.client; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.instrumentation.rmi.client.RmiClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class UnicastRefInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("sun.rmi.server.UnicastRef")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("invoke")) + .and(takesArgument(0, named("java.rmi.Remote"))) + .and(takesArgument(1, Method.class)), + this.getClass().getName() + "$InvokeAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 1) Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + // TODO replace with client span check + if (!Java8BytecodeBridge.currentSpan().getSpanContext().isValid()) { + return; + } + context = tracer().startSpan(method); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/ContextPayload.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/ContextPayload.java new file mode 100644 index 000000000..39c3d6b72 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/ContextPayload.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.context; + +import static io.opentelemetry.javaagent.instrumentation.rmi.client.RmiClientTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** ContextPayload wraps context information shared between client and server. */ +public class ContextPayload { + + private static final Logger log = LoggerFactory.getLogger(ContextPayload.class); + + private final Map context; + public static final ExtractAdapter GETTER = new ExtractAdapter(); + public static final InjectAdapter SETTER = new InjectAdapter(); + + public ContextPayload() { + context = new HashMap<>(); + } + + public ContextPayload(Map context) { + this.context = context; + } + + public static ContextPayload from(Context context) { + ContextPayload payload = new ContextPayload(); + tracer().inject(context, payload, SETTER); + return payload; + } + + public static ContextPayload read(ObjectInput oi) throws IOException { + try { + Object object = oi.readObject(); + if (object instanceof Map) { + return new ContextPayload((Map) object); + } + } catch (ClassCastException | ClassNotFoundException ex) { + log.debug("Error reading object", ex); + } + + return null; + } + + public Map getSpanContext() { + return context; + } + + public void write(ObjectOutput out) throws IOException { + out.writeObject(context); + } + + public static class ExtractAdapter implements TextMapGetter { + @Override + public Iterable keys(ContextPayload contextPayload) { + return contextPayload.getSpanContext().keySet(); + } + + @Override + public String get(ContextPayload carrier, String key) { + return carrier.getSpanContext().get(key); + } + } + + public static class InjectAdapter implements TextMapSetter { + @Override + public void set(ContextPayload carrier, String key, String value) { + carrier.getSpanContext().put(key, value); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/ContextPropagator.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/ContextPropagator.java new file mode 100644 index 000000000..5eeaecadc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/ContextPropagator.java @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.context; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import java.io.IOException; +import java.io.ObjectOutput; +import java.rmi.NoSuchObjectException; +import java.rmi.server.ObjID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sun.rmi.transport.Connection; +import sun.rmi.transport.StreamRemoteCall; +import sun.rmi.transport.TransportConstants; + +public class ContextPropagator { + + private static final Logger log = LoggerFactory.getLogger(ContextPropagator.class); + + // Internal RMI object ids that we don't want to trace + private static final ObjID ACTIVATOR_ID = new ObjID(ObjID.ACTIVATOR_ID); + private static final ObjID DGC_ID = new ObjID(ObjID.DGC_ID); + private static final ObjID REGISTRY_ID = new ObjID(ObjID.REGISTRY_ID); + + // RMI object id used to identify agent instrumentation + public static final ObjID CONTEXT_CALL_ID = + new ObjID("io.opentelemetry.javaagent.context-call".hashCode()); + + // Operation id used for checking context propagation is possible + // RMI expects these operations to have negative identifier, as positive ones mean legacy + // precompiled Stubs would be used instead + private static final int CONTEXT_CHECK_CALL_OPERATION_ID = -1; + // Seconds step of context propagation which contains actual payload + private static final int CONTEXT_PAYLOAD_OPERATION_ID = -2; + + public static final ContextPropagator PROPAGATOR = new ContextPropagator(); + + public boolean isRmiInternalObject(ObjID id) { + return ACTIVATOR_ID.equals(id) || DGC_ID.equals(id) || REGISTRY_ID.equals(id); + } + + public boolean isOperationWithPayload(int operationId) { + return operationId == CONTEXT_PAYLOAD_OPERATION_ID; + } + + public void attemptToPropagateContext( + ContextStore knownConnections, Connection c, Context context) { + if (checkIfContextCanBePassed(knownConnections, c)) { + if (!syntheticCall(c, ContextPayload.from(context), CONTEXT_PAYLOAD_OPERATION_ID)) { + log.debug("Couldn't send context payload"); + } + } + } + + private static boolean checkIfContextCanBePassed( + ContextStore knownConnections, Connection c) { + Boolean storedResult = knownConnections.get(c); + if (storedResult != null) { + return storedResult; + } + + boolean result = syntheticCall(c, null, CONTEXT_CHECK_CALL_OPERATION_ID); + knownConnections.put(c, result); + return result; + } + + /** Returns true when no error happened during call. */ + private static boolean syntheticCall(Connection c, ContextPayload payload, int operationId) { + StreamRemoteCall shareContextCall = new StreamRemoteCall(c); + try { + c.getOutputStream().write(TransportConstants.Call); + + ObjectOutput out = shareContextCall.getOutputStream(); + + CONTEXT_CALL_ID.write(out); + + // call header, part 2 (read by Dispatcher) + out.writeInt(operationId); // in normal call this is method number (operation index) + out.writeLong(operationId); // in normal RMI call this holds stub/skeleton hash + + // Payload should be sent only after we make sure we're connected to instrumented server + // + // if method is not found by un-instrumented code then writing payload will cause an exception + // in RMI server - as the payload will be interpreted as another call + // but it will not be parsed correctly - closing connection + if (payload != null) { + payload.write(out); + } + + try { + shareContextCall.executeCall(); + } catch (Exception e) { + Exception ex = shareContextCall.getServerException(); + if (ex != null) { + if (ex instanceof NoSuchObjectException) { + return false; + } else { + log.debug("Server error when executing synthetic call", ex); + } + } else { + log.debug("Error executing synthetic call", e); + } + return false; + } finally { + shareContextCall.done(); + } + + } catch (IOException e) { + log.debug("Communication error executing synthetic call", e); + return false; + } + return true; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/RmiContextPropagationInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/RmiContextPropagationInstrumentationModule.java new file mode 100644 index 000000000..bd0917833 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/RmiContextPropagationInstrumentationModule.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.context; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.rmi.context.client.RmiClientContextInstrumentation; +import io.opentelemetry.javaagent.instrumentation.rmi.context.server.RmiServerContextInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RmiContextPropagationInstrumentationModule extends InstrumentationModule { + public RmiContextPropagationInstrumentationModule() { + super("rmi", "rmi-context-propagation"); + } + + @Override + public List typeInstrumentations() { + return asList(new RmiClientContextInstrumentation(), new RmiServerContextInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/client/RmiClientContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/client/RmiClientContextInstrumentation.java new file mode 100644 index 000000000..202fb9fdb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/client/RmiClientContextInstrumentation.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.context.client; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.instrumentation.rmi.context.ContextPropagator.PROPAGATOR; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import java.rmi.server.ObjID; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import sun.rmi.transport.Connection; + +/** + * Main entry point for transferring context between RMI service. + * + *

It injects into StreamRemoteCall constructor used for invoking remote tasks and performs a + * backwards compatible check to ensure if the other side is prepared to receive context propagation + * messages then if successful sends a context propagation message + * + *

Context propagation consist of a Serialized HashMap with all data set by usual context + * injection, which includes things like sampling priority, trace and parent id + * + *

As well as optional baggage items + * + *

On the other side of the communication a special Dispatcher is created when a message with + * CONTEXT_CALL_ID is received. + * + *

If the server is not instrumented first call will gracefully fail just like any other unknown + * call. With small caveat that this first call needs to *not* have any parameters, since those will + * not be read from connection and instead will be interpreted as another remote instruction, but + * that instruction will essentially be garbage data and will cause the parsing loop to throw + * exception and shutdown the connection which we do not want + */ +public class RmiClientContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("sun.rmi.transport.StreamRemoteCall")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor() + .and(takesArgument(0, named("sun.rmi.transport.Connection"))) + .and(takesArgument(1, named("java.rmi.server.ObjID"))), + getClass().getName() + "$StreamRemoteCallConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class StreamRemoteCallConstructorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) Connection c, @Advice.Argument(1) ObjID id) { + if (!c.isReusable()) { + return; + } + if (PROPAGATOR.isRmiInternalObject(id)) { + return; + } + Context currentContext = Java8BytecodeBridge.currentContext(); + Span activeSpan = Java8BytecodeBridge.spanFromContext(currentContext); + if (!activeSpan.getSpanContext().isValid()) { + return; + } + + // caching if a connection can support enhanced format + ContextStore knownConnections = + InstrumentationContext.get(Connection.class, Boolean.class); + + PROPAGATOR.attemptToPropagateContext(knownConnections, c, currentContext); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/server/ContextDispatcher.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/server/ContextDispatcher.java new file mode 100644 index 000000000..78eee03ac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/server/ContextDispatcher.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.context.server; + +import static io.opentelemetry.javaagent.instrumentation.api.rmi.ThreadLocalContext.THREAD_LOCAL_CONTEXT; +import static io.opentelemetry.javaagent.instrumentation.rmi.context.ContextPayload.GETTER; +import static io.opentelemetry.javaagent.instrumentation.rmi.context.ContextPropagator.CONTEXT_CALL_ID; +import static io.opentelemetry.javaagent.instrumentation.rmi.context.ContextPropagator.PROPAGATOR; +import static io.opentelemetry.javaagent.instrumentation.rmi.server.RmiServerTracer.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.rmi.context.ContextPayload; +import java.io.IOException; +import java.io.ObjectInput; +import java.rmi.Remote; +import java.rmi.server.RemoteCall; +import sun.rmi.server.Dispatcher; +import sun.rmi.transport.Target; + +/** + * ContextDispatcher is responsible for handling both initial context propagation check call and + * following call which carries payload + * + *

Context propagation check is only expected not to throw any exception, hinting to the client + * that its communicating with an instrumented server. Non instrumented server would've thrown + * UnknownObjectException + * + *

Because caching of the result after first call on a connection, only payload calls are + * expected + */ +public class ContextDispatcher implements Dispatcher { + private static final ContextDispatcher CONTEXT_DISPATCHER = new ContextDispatcher(); + private static final NoopRemote NOOP_REMOTE = new NoopRemote(); + + public static Target newDispatcherTarget() { + return new Target( + NOOP_REMOTE, CONTEXT_DISPATCHER, NOOP_REMOTE, CONTEXT_CALL_ID, /* permanent= */ false); + } + + @Override + public void dispatch(Remote obj, RemoteCall call) throws IOException { + ObjectInput in = call.getInputStream(); + int operationId = in.readInt(); + in.readLong(); // skip 8 bytes + + if (PROPAGATOR.isOperationWithPayload(operationId)) { + ContextPayload payload = ContextPayload.read(in); + if (payload != null) { + Context context = tracer().extract(payload, GETTER); + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + if (spanContext.isValid()) { + THREAD_LOCAL_CONTEXT.set(context); + } else { + THREAD_LOCAL_CONTEXT.set(null); + } + } + } + + // send result stream the client is expecting + call.getResultStream(true); + + // release held streams to allow next call to continue + call.releaseInputStream(); + call.releaseOutputStream(); + call.done(); + } + + public static class NoopRemote implements Remote {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/server/RmiServerContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/server/RmiServerContextInstrumentation.java new file mode 100644 index 000000000..eaf8e91fd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/server/RmiServerContextInstrumentation.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.context.server; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.instrumentation.rmi.context.ContextPropagator.CONTEXT_CALL_ID; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import sun.rmi.transport.Target; + +public class RmiServerContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("sun.rmi.transport.ObjectTable")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isStatic()) + .and(named("getTarget")) + .and(takesArgument(0, named("sun.rmi.transport.ObjectEndpoint"))), + getClass().getName() + "$ObjectTableAdvice"); + } + + @SuppressWarnings("unused") + public static class ObjectTableAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) Object oe, @Advice.Return(readOnly = false) Target result) { + // comparing toString() output allows us to avoid using reflection to be able to compare + // ObjID and ObjectEndpoint objects + // ObjectEndpoint#toString() only returns this.objId.toString() value which is exactly + // what we're interested in here. + if (!CONTEXT_CALL_ID.toString().equals(oe.toString())) { + return; + } + result = ContextDispatcher.newDispatcherTarget(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RemoteServerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RemoteServerInstrumentation.java new file mode 100644 index 000000000..27be497d2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RemoteServerInstrumentation.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.server; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.instrumentation.api.rmi.ThreadLocalContext.THREAD_LOCAL_CONTEXT; +import static io.opentelemetry.javaagent.instrumentation.rmi.server.RmiServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.lang.reflect.Method; +import java.rmi.server.RemoteServer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RemoteServerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("java.rmi.server.RemoteServer")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(not(isStatic())), + this.getClass().getName() + "$PublicMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class PublicMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(RemoteServer.class); + if (callDepth > 0) { + return; + } + + // TODO review and unify with all other SERVER instrumentation + Context parentContext = THREAD_LOCAL_CONTEXT.getAndResetContext(); + + context = tracer().startSpan(parentContext, method); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + + CallDepthThreadLocalMap.reset(RemoteServer.class); + if (throwable != null) { + RmiServerTracer.tracer().endExceptionally(context, throwable); + } else { + RmiServerTracer.tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerInstrumentation.java new file mode 100644 index 000000000..e21e8a534 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerInstrumentation.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.server; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RmiServerInstrumentation extends InstrumentationModule { + + public RmiServerInstrumentation() { + super("rmi", "rmi-server"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RemoteServerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerTracer.java new file mode 100644 index 000000000..a5ab4ef26 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerTracer.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rmi.server; + +import static io.opentelemetry.api.trace.SpanKind.SERVER; + +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.RpcServerTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.reflect.Method; + +public class RmiServerTracer extends RpcServerTracer { + private static final RmiServerTracer TRACER = new RmiServerTracer(); + + public static RmiServerTracer tracer() { + return TRACER; + } + + public Context startSpan(Context parentContext, Method method) { + String serviceName = method.getDeclaringClass().getName(); + String methodName = method.getName(); + + SpanBuilder spanBuilder = + spanBuilder(parentContext, serviceName + "/" + methodName, SERVER) + .setAttribute(SemanticAttributes.RPC_SYSTEM, "java_rmi") + .setAttribute(SemanticAttributes.RPC_SERVICE, serviceName) + .setAttribute(SemanticAttributes.RPC_METHOD, methodName); + return parentContext.with(spanBuilder.startSpan()); + } + + @Override + protected TextMapGetter getGetter() { + return null; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.rmi"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/groovy/RmiTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/groovy/RmiTest.groovy new file mode 100644 index 000000000..af7929665 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/groovy/RmiTest.groovy @@ -0,0 +1,180 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.rmi.registry.LocateRegistry +import java.rmi.server.UnicastRemoteObject +import rmi.app.Greeter +import rmi.app.Server +import rmi.app.ServerLegacy + +class RmiTest extends AgentInstrumentationSpecification { + def registryPort = PortUtils.findOpenPort() + def serverRegistry = LocateRegistry.createRegistry(registryPort) + def clientRegistry = LocateRegistry.getRegistry("localhost", registryPort) + + def cleanup() { + UnicastRemoteObject.unexportObject(serverRegistry, true) + } + + def "Client call creates spans"() { + setup: + def server = new Server() + serverRegistry.rebind(Server.RMI_ID, server) + + when: + def response = runUnderTrace("parent") { + def client = (Greeter) clientRegistry.lookup(Server.RMI_ID) + return client.hello("you") + } + + then: + response.contains("Hello you") + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + name "rmi.app.Greeter/hello" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "java_rmi" + "${SemanticAttributes.RPC_SERVICE.key}" "rmi.app.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "hello" + } + } + span(2) { + name "rmi.app.Server/hello" + kind SERVER + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "java_rmi" + "${SemanticAttributes.RPC_SERVICE.key}" "rmi.app.Server" + "${SemanticAttributes.RPC_METHOD.key}" "hello" + } + } + } + } + + cleanup: + serverRegistry.unbind("Server") + } + + def "Calling server builtin methods doesn't create server spans"() { + setup: + def server = new Server() + serverRegistry.rebind(Server.RMI_ID, server) + + when: + server.equals(new Server()) + server.getRef() + server.hashCode() + server.toString() + server.getClass() + + then: + assertTraces(0) {} + + cleanup: + serverRegistry.unbind("Server") + } + + def "Service throws exception and its propagated to spans"() { + setup: + def server = new Server() + serverRegistry.rebind(Server.RMI_ID, server) + + when: + runUnderTrace("parent") { + def client = (Greeter) clientRegistry.lookup(Server.RMI_ID) + client.exceptional() + } + + then: + def thrownException = thrown(IllegalStateException) + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent", null, thrownException) + span(1) { + name "rmi.app.Greeter/exceptional" + kind CLIENT + childOf span(0) + status ERROR + errorEvent(IllegalStateException, String) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "java_rmi" + "${SemanticAttributes.RPC_SERVICE.key}" "rmi.app.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "exceptional" + + } + } + span(2) { + name "rmi.app.Server/exceptional" + kind SERVER + status ERROR + errorEvent(IllegalStateException, String) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "java_rmi" + "${SemanticAttributes.RPC_SERVICE.key}" "rmi.app.Server" + "${SemanticAttributes.RPC_METHOD.key}" "exceptional" + } + } + } + } + + cleanup: + serverRegistry.unbind("Server") + } + + def "Client call using ServerLegacy_stub creates spans"() { + setup: + def server = new ServerLegacy() + serverRegistry.rebind(ServerLegacy.RMI_ID, server) + + when: + def response = runUnderTrace("parent") { + def client = (Greeter) clientRegistry.lookup(ServerLegacy.RMI_ID) + return client.hello("you") + } + + then: + response.contains("Hello you") + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + name "rmi.app.Greeter/hello" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "java_rmi" + "${SemanticAttributes.RPC_SERVICE.key}" "rmi.app.Greeter" + "${SemanticAttributes.RPC_METHOD.key}" "hello" + } + } + span(2) { + childOf span(1) + name "rmi.app.ServerLegacy/hello" + kind SERVER + attributes { + "${SemanticAttributes.RPC_SYSTEM.key}" "java_rmi" + "${SemanticAttributes.RPC_SERVICE.key}" "rmi.app.ServerLegacy" + "${SemanticAttributes.RPC_METHOD.key}" "hello" + } + } + } + } + + cleanup: + serverRegistry.unbind(ServerLegacy.RMI_ID) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/Greeter.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/Greeter.java new file mode 100644 index 000000000..70bc3f747 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/Greeter.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package rmi.app; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +public interface Greeter extends Remote { + String hello(String name) throws RemoteException; + + void exceptional() throws RemoteException; +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/Server.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/Server.java new file mode 100644 index 000000000..b0ea1f543 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/Server.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package rmi.app; + +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +public class Server extends UnicastRemoteObject implements Greeter { + public static final String RMI_ID = Server.class.getSimpleName(); + + private static final long serialVersionUID = 1L; + + public Server() throws RemoteException { + super(); + } + + @Override + public String hello(String name) { + return someMethod(name); + } + + public String someMethod(String name) { + return "Hello " + name; + } + + @Override + public void exceptional() { + throw new IllegalStateException("expected"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/ServerLegacy.java b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/ServerLegacy.java new file mode 100644 index 000000000..3f8b0e3d0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rmi/javaagent/src/test/java/rmi/app/ServerLegacy.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package rmi.app; + +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +public class ServerLegacy extends UnicastRemoteObject implements Greeter { + static final String RMI_ID = ServerLegacy.class.getSimpleName(); + + private static final long serialVersionUID = 1L; + + public ServerLegacy() throws RemoteException { + super(); + } + + @Override + public String hello(String name) { + return "Hello " + name; + } + + @Override + public void exceptional() { + throw new IllegalStateException("expected"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/rocketmq-client-4.8-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/rocketmq-client-4.8-javaagent.gradle new file mode 100644 index 000000000..753fe3202 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/rocketmq-client-4.8-javaagent.gradle @@ -0,0 +1,21 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.rocketmq" + module = 'rocketmq-client' + versions = "[4.0.0,)" + assertInverse = true + } +} + +dependencies { + library "org.apache.rocketmq:rocketmq-client:4.8.0" + implementation project(':instrumentation:rocketmq-client-4.8:library') + testImplementation project(':instrumentation:rocketmq-client-4.8:testing') + testLibrary "org.apache.rocketmq:rocketmq-test:4.8.0" +} + +tasks.withType(Test).configureEach { + jvmArgs "-Dotel.instrumentation.rocketmq-client.experimental-span-attributes=true" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqClientHooks.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqClientHooks.java new file mode 100644 index 000000000..a90833b4e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqClientHooks.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rocketmq; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.rocketmq.RocketMqTracing; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.client.hook.SendMessageHook; + +public final class RocketMqClientHooks { + private static final RocketMqTracing TRACING = + RocketMqTracing.newBuilder(GlobalOpenTelemetry.get()) + .setPropagationEnabled( + Config.get().getBoolean("otel.instrumentation.rocketmq-client.propagation", true)) + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBoolean( + "otel.instrumentation.rocketmq-client.experimental-span-attributes", false)) + .build(); + + public static final ConsumeMessageHook CONSUME_MESSAGE_HOOK = + TRACING.newTracingConsumeMessageHook(); + + public static final SendMessageHook SEND_MESSAGE_HOOK = TRACING.newTracingSendMessageHook(); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqConsumerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqConsumerInstrumentation.java new file mode 100644 index 000000000..904f0ae16 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqConsumerInstrumentation.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rocketmq; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; + +public class RocketMqConsumerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.rocketmq.client.consumer.DefaultMQPushConsumer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("start")).and(takesArguments(0)), + RocketMqConsumerInstrumentation.class.getName() + "$AdviceStart"); + } + + public static class AdviceStart { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue( + value = "defaultMQPushConsumerImpl", + declaringType = DefaultMQPushConsumer.class) + DefaultMQPushConsumerImpl defaultMqPushConsumerImpl) { + defaultMqPushConsumerImpl.registerConsumeMessageHook( + RocketMqClientHooks.CONSUME_MESSAGE_HOOK); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqInstrumentationModule.java new file mode 100644 index 000000000..40ba9a0c3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rocketmq; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RocketMqInstrumentationModule extends InstrumentationModule { + public RocketMqInstrumentationModule() { + super("rocketmq-client", "rocketmq-client-4.8"); + } + + @Override + public List typeInstrumentations() { + return asList(new RocketMqProducerInstrumentation(), new RocketMqConsumerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqProducerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqProducerInstrumentation.java new file mode 100644 index 000000000..dc9b836e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmq/RocketMqProducerInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rocketmq; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.producer.DefaultMQProducer; + +public class RocketMqProducerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.rocketmq.client.producer.DefaultMQProducer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("start")).and(takesArguments(0)), + RocketMqProducerInstrumentation.class.getName() + "$AdviceStart"); + } + + public static class AdviceStart { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue(value = "defaultMQProducerImpl", declaringType = DefaultMQProducer.class) + DefaultMQProducerImpl defaultMqProducerImpl) { + defaultMqProducerImpl.registerSendMessageHook(RocketMqClientHooks.SEND_MESSAGE_HOOK); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/test/groovy/io/opentelemetry/instrumentation/rocketmq/RocketMqClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/test/groovy/io/opentelemetry/instrumentation/rocketmq/RocketMqClientTest.groovy new file mode 100644 index 000000000..61167aaf4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/javaagent/src/test/groovy/io/opentelemetry/instrumentation/rocketmq/RocketMqClientTest.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer +import org.apache.rocketmq.client.producer.DefaultMQProducer + +class RocketMqClientTest extends AbstractRocketMqClientTest implements AgentTestTrait { + + @Override + void configureMQProducer(DefaultMQProducer producer) { + } + + @Override + void configureMQPushConsumer(DefaultMQPushConsumer consumer) { + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/rocketmq-client-4.8-library.gradle b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/rocketmq-client-4.8-library.gradle new file mode 100644 index 000000000..2384c535a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/rocketmq-client-4.8-library.gradle @@ -0,0 +1,9 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library "org.apache.rocketmq:rocketmq-client:4.8.0" + testImplementation project(':instrumentation:rocketmq-client-4.8:testing') + testLibrary "org.apache.rocketmq:rocketmq-test:4.8.0" + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/ContextAndScope.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/ContextAndScope.java new file mode 100644 index 000000000..3c10b73ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/ContextAndScope.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.apache.rocketmq.client.trace.TraceBean; + +@AutoValue +abstract class ContextAndScope extends TraceBean { + + static ContextAndScope create(Context context, Scope scope) { + return new AutoValue_ContextAndScope(context, scope); + } + + abstract Context getContext(); + + abstract Scope getScope(); + + void close() { + getScope().close(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqConsumerTracer.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqConsumerTracer.java new file mode 100644 index 000000000..0844447b7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqConsumerTracer.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.instrumentation.rocketmq.TextMapExtractAdapter.GETTER; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class RocketMqConsumerTracer extends BaseTracer { + + private final boolean captureExperimentalSpanAttributes; + private final boolean propagationEnabled; + + RocketMqConsumerTracer( + OpenTelemetry openTelemetry, + boolean captureExperimentalSpanAttributes, + boolean propagationEnabled) { + super(openTelemetry); + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + this.propagationEnabled = propagationEnabled; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.rocketmq-client-4.8"; + } + + Context startSpan(Context parentContext, List msgs) { + if (msgs.size() == 1) { + SpanBuilder spanBuilder = startSpanBuilder(extractParent(msgs.get(0)), msgs.get(0)); + return withConsumerSpan(parentContext, spanBuilder.startSpan()); + } else { + SpanBuilder spanBuilder = + spanBuilder(parentContext, "multiple_sources receive", CONSUMER) + .setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "rocketmq") + .setAttribute(SemanticAttributes.MESSAGING_OPERATION, "receive"); + Context rootContext = withConsumerSpan(parentContext, spanBuilder.startSpan()); + for (MessageExt message : msgs) { + createChildSpan(rootContext, message); + } + return rootContext; + } + } + + private void createChildSpan(Context parentContext, MessageExt msg) { + SpanBuilder childSpanBuilder = + startSpanBuilder(parentContext, msg) + .addLink(Span.fromContext(extractParent(msg)).getSpanContext()); + end(parentContext.with(childSpanBuilder.startSpan())); + } + + private SpanBuilder startSpanBuilder(Context parentContext, MessageExt msg) { + SpanBuilder spanBuilder = + spanBuilder(parentContext, spanNameOnConsume(msg), CONSUMER) + .setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "rocketmq") + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION, msg.getTopic()) + .setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic") + .setAttribute(SemanticAttributes.MESSAGING_OPERATION, "process") + .setAttribute(SemanticAttributes.MESSAGING_MESSAGE_ID, msg.getMsgId()) + .setAttribute( + SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + (long) msg.getBody().length); + onConsume(spanBuilder, msg); + return spanBuilder; + } + + private Context extractParent(MessageExt msg) { + if (propagationEnabled) { + return extract(msg.getProperties(), GETTER); + } else { + return Context.current(); + } + } + + private void onConsume(SpanBuilder spanBuilder, MessageExt msg) { + if (captureExperimentalSpanAttributes) { + spanBuilder.setAttribute("messaging.rocketmq.tags", msg.getTags()); + spanBuilder.setAttribute("messaging.rocketmq.queue_id", msg.getQueueId()); + spanBuilder.setAttribute("messaging.rocketmq.queue_offset", msg.getQueueOffset()); + spanBuilder.setAttribute("messaging.rocketmq.broker_address", getBrokerHost(msg)); + } + } + + private static String spanNameOnConsume(MessageExt msg) { + return msg.getTopic() + " process"; + } + + @Nullable + private static String getBrokerHost(MessageExt msg) { + if (msg.getStoreHost() != null) { + return msg.getStoreHost().toString().replace("/", ""); + } else { + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqProducerTracer.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqProducerTracer.java new file mode 100644 index 000000000..11a482515 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqProducerTracer.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; + +final class RocketMqProducerTracer extends BaseTracer { + + private final boolean captureExperimentalSpanAttributes; + + RocketMqProducerTracer(OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { + super(openTelemetry); + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.rocketmq-client-4.8"; + } + + Context startProducerSpan(Context parentContext, String addr, Message msg) { + SpanBuilder spanBuilder = spanBuilder(parentContext, spanNameOnProduce(msg), PRODUCER); + onProduce(spanBuilder, msg, addr); + return parentContext.with(spanBuilder.startSpan()); + } + + private void onProduce(SpanBuilder spanBuilder, Message msg, String addr) { + spanBuilder.setAttribute(SemanticAttributes.MESSAGING_SYSTEM, "rocketmq"); + spanBuilder.setAttribute(SemanticAttributes.MESSAGING_DESTINATION_KIND, "topic"); + spanBuilder.setAttribute(SemanticAttributes.MESSAGING_DESTINATION, msg.getTopic()); + if (captureExperimentalSpanAttributes) { + spanBuilder.setAttribute("messaging.rocketmq.tags", msg.getTags()); + spanBuilder.setAttribute("messaging.rocketmq.broker_address", addr); + } + } + + public void afterProduce(Context context, SendResult sendResult) { + Span span = Span.fromContext(context); + span.setAttribute(SemanticAttributes.MESSAGING_MESSAGE_ID, sendResult.getMsgId()); + if (captureExperimentalSpanAttributes) { + span.setAttribute("messaging.rocketmq.send_result", sendResult.getSendStatus().name()); + } + } + + private static String spanNameOnProduce(Message msg) { + return msg.getTopic() + " send"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqTracing.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqTracing.java new file mode 100644 index 000000000..7026859db --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqTracing.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import io.opentelemetry.api.OpenTelemetry; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.client.hook.SendMessageHook; + +/** Entrypoint for tracing RocketMq producers or consumers. */ +public final class RocketMqTracing { + + /** Returns a new {@link RocketMqTracing} configured with the given {@link OpenTelemetry}. */ + public static RocketMqTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + /** + * Returns a new {@link RocketMqTracingBuilder} configured with the given {@link OpenTelemetry}. + */ + public static RocketMqTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new RocketMqTracingBuilder(openTelemetry); + } + + private final boolean propagationEnabled; + + private final RocketMqConsumerTracer rocketMqConsumerTracer; + private final RocketMqProducerTracer rocketMqProducerTracer; + + RocketMqTracing( + OpenTelemetry openTelemetry, + boolean captureExperimentalSpanAttributes, + boolean propagationEnabled) { + this.propagationEnabled = propagationEnabled; + rocketMqConsumerTracer = + new RocketMqConsumerTracer( + openTelemetry, captureExperimentalSpanAttributes, propagationEnabled); + rocketMqProducerTracer = + new RocketMqProducerTracer(openTelemetry, captureExperimentalSpanAttributes); + } + + /** + * Returns a new {@link ConsumeMessageHook} for use with methods like {@link + * org.apache.rocketmq.client.impl.consumer.DefaultMQPullConsumerImpl#registerConsumeMessageHook(ConsumeMessageHook)}. + */ + public ConsumeMessageHook newTracingConsumeMessageHook() { + return new TracingConsumeMessageHookImpl(rocketMqConsumerTracer); + } + + /** + * Returns a new {@link SendMessageHook} for use with methods like {@link + * org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#registerSendMessageHook(SendMessageHook)}. + */ + public SendMessageHook newTracingSendMessageHook() { + return new TracingSendMessageHookImpl(rocketMqProducerTracer, propagationEnabled); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqTracingBuilder.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqTracingBuilder.java new file mode 100644 index 000000000..faaa2407d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/RocketMqTracingBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import io.opentelemetry.api.OpenTelemetry; + +/** A builder of {@link RocketMqTracing}. */ +public final class RocketMqTracingBuilder { + + private final OpenTelemetry openTelemetry; + + private boolean captureExperimentalSpanAttributes; + private boolean propagationEnabled = true; + + RocketMqTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Sets whether experimental attributes should be set to spans. These attributes may be changed or + * removed in the future, so only enable this if you know you do not require attributes filled by + * this instrumentation to be stable across versions + */ + public RocketMqTracingBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + /** + * Sets whether the trace context should be written from producers / read from consumers for + * propagating through messaging. + */ + public RocketMqTracingBuilder setPropagationEnabled(boolean propagationEnabled) { + this.propagationEnabled = propagationEnabled; + return this; + } + + /** + * Returns a new {@link RocketMqTracing} with the settings of this {@link RocketMqTracingBuilder}. + */ + public RocketMqTracing build() { + return new RocketMqTracing( + openTelemetry, captureExperimentalSpanAttributes, propagationEnabled); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TextMapExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TextMapExtractAdapter.java new file mode 100644 index 000000000..c7fd9ed0a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TextMapExtractAdapter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Map; + +final class TextMapExtractAdapter implements TextMapGetter> { + + public static final TextMapExtractAdapter GETTER = new TextMapExtractAdapter(); + + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TextMapInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TextMapInjectAdapter.java new file mode 100644 index 000000000..556f60f32 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TextMapInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Map; + +final class TextMapInjectAdapter implements TextMapSetter> { + + public static final TextMapInjectAdapter SETTER = new TextMapInjectAdapter(); + + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TracingConsumeMessageHookImpl.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TracingConsumeMessageHookImpl.java new file mode 100644 index 000000000..ddf64ab16 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TracingConsumeMessageHookImpl.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import io.opentelemetry.context.Context; +import org.apache.rocketmq.client.hook.ConsumeMessageContext; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.client.trace.TraceBean; +import org.apache.rocketmq.client.trace.TraceContext; + +import java.util.ArrayList; +import java.util.List; + +final class TracingConsumeMessageHookImpl implements ConsumeMessageHook { + + private final RocketMqConsumerTracer tracer; + + TracingConsumeMessageHookImpl(RocketMqConsumerTracer tracer) { + this.tracer = tracer; + } + + @Override + public String hookName() { + return "OpenTelemetryConsumeMessageTraceHook"; + } + + @Override + public void consumeMessageBefore(ConsumeMessageContext context) { + if (context == null || context.getMsgList() == null || context.getMsgList().isEmpty()) { + return; + } + Context otelContext = tracer.startSpan(Context.current(), context.getMsgList()); + + // it's safe to store the scope in the rocketMq trace context, both before() and after() methods + // are always called from the same thread; see: + // - ConsumeMessageConcurrentlyService$ConsumeRequest#run() + // - ConsumeMessageOrderlyService$ConsumeRequest#run() + // 兼容自带的rocketmq + Object mqTraceContextObj = context.getMqTraceContext(); + if(mqTraceContextObj == null){ + mqTraceContextObj = new TraceContext(); + context.setMqTraceContext(mqTraceContextObj); + } + TraceContext mqTraceContext = (TraceContext)mqTraceContextObj; + List traceBeans = mqTraceContext.getTraceBeans(); + if(traceBeans == null){ + traceBeans = new ArrayList<>(1); + mqTraceContext.setTraceBeans(traceBeans); + } + traceBeans.add(ContextAndScope.create(otelContext, otelContext.makeCurrent())); + } + + @Override + public void consumeMessageAfter(ConsumeMessageContext context) { + if (context == null || context.getMsgList() == null || context.getMsgList().isEmpty() || context.getMqTraceContext() == null) { + return; + } + TraceContext mqTraceContext = (TraceContext) context.getMqTraceContext(); + List traceBeans = mqTraceContext.getTraceBeans(); + if(traceBeans != null){ + for(TraceBean traceBean : traceBeans){ + if(traceBean instanceof ContextAndScope){ + ContextAndScope contextAndScope = (ContextAndScope) traceBean; + contextAndScope.close(); + tracer.end(contextAndScope.getContext()); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TracingSendMessageHookImpl.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TracingSendMessageHookImpl.java new file mode 100644 index 000000000..c4b03ffe0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmq/TracingSendMessageHookImpl.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq; + +import static io.opentelemetry.instrumentation.rocketmq.TextMapInjectAdapter.SETTER; + +import io.opentelemetry.context.Context; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.hook.SendMessageHook; +import org.apache.rocketmq.client.trace.TraceBean; +import org.apache.rocketmq.client.trace.TraceContext; + +import java.util.ArrayList; +import java.util.List; + +final class TracingSendMessageHookImpl implements SendMessageHook { + + private final RocketMqProducerTracer tracer; + private final boolean propagationEnabled; + + TracingSendMessageHookImpl(RocketMqProducerTracer tracer, boolean propagationEnabled) { + this.tracer = tracer; + this.propagationEnabled = propagationEnabled; + } + + @Override + public String hookName() { + return "OpenTelemetrySendMessageTraceHook"; + } + + @Override + public void sendMessageBefore(SendMessageContext context) { + if (context == null) { + return; + } + Context otelContext = + tracer.startProducerSpan(Context.current(), context.getBrokerAddr(), context.getMessage()); + if (propagationEnabled) { + tracer.inject(otelContext, context.getMessage().getProperties(), SETTER); + } + // 兼容rocketmq自带的Hook + Object mqTraceContextObj = context.getMqTraceContext(); + if (mqTraceContextObj == null) { + mqTraceContextObj = new TraceContext(); + context.setMqTraceContext(mqTraceContextObj); + } + TraceContext mqTraceContext = (TraceContext) mqTraceContextObj; + List traceBeans = mqTraceContext.getTraceBeans(); + if (traceBeans == null) { + traceBeans = new ArrayList<>(1); + mqTraceContext.setTraceBeans(traceBeans); + } + traceBeans.add(ContextAndScope.create(otelContext, otelContext.makeCurrent())); + } + + @Override + public void sendMessageAfter(SendMessageContext context) { + if (context == null || context.getMqTraceContext() == null || context.getSendResult() == null || context.getMqTraceContext() == null) { + return; + } + TraceContext mqTraceContext = (TraceContext) context.getMqTraceContext(); + List traceBeans = mqTraceContext.getTraceBeans(); + if (traceBeans != null) { + for (TraceBean traceBean : traceBeans) { + if (traceBean instanceof ContextAndScope) { + ContextAndScope otelContext = (ContextAndScope) traceBean; + tracer.afterProduce(otelContext.getContext(), context.getSendResult()); + otelContext.close(); + tracer.end(otelContext.getContext()); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/test/groovy/io/opentelemetry/instrumentation/rocketmq/RocketMqClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/test/groovy/io/opentelemetry/instrumentation/rocketmq/RocketMqClientTest.groovy new file mode 100644 index 000000000..004222c28 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/library/src/test/groovy/io/opentelemetry/instrumentation/rocketmq/RocketMqClientTest.groovy @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq + +import io.opentelemetry.instrumentation.api.config.Config +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer +import org.apache.rocketmq.client.producer.DefaultMQProducer + +class RocketMqClientTest extends AbstractRocketMqClientTest implements LibraryTestTrait { + + @Override + void configureMQProducer(DefaultMQProducer producer) { + producer.getDefaultMQProducerImpl().registerSendMessageHook(RocketMqTracing.newBuilder(openTelemetry) + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBoolean( + "otel.instrumentation.rocketmq-client.experimental-span-attributes", true)) + .build().newTracingSendMessageHook()) + } + + @Override + void configureMQPushConsumer(DefaultMQPushConsumer consumer) { + consumer.getDefaultMQPushConsumerImpl().registerConsumeMessageHook(RocketMqTracing.newBuilder(openTelemetry) + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBoolean( + "otel.instrumentation.rocketmq-client.experimental-span-attributes", true)) + .build().newTracingConsumeMessageHook()) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/build.gradle.kts b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/build.gradle.kts new file mode 100644 index 000000000..9102d8713 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":testing-common")) + implementation("org.apache.rocketmq:rocketmq-test:4.8.0") + + implementation("com.google.guava:guava") + implementation("org.codehaus.groovy:groovy-all") + implementation("run.mone:opentelemetry-api") + implementation("org.spockframework:spock-core") +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/rocketmq-client-4.8-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/rocketmq-client-4.8-testing.gradle new file mode 100644 index 000000000..0ced9e8ff --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/rocketmq-client-4.8-testing.gradle @@ -0,0 +1,11 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + implementation "org.apache.rocketmq:rocketmq-test:4.8.0" + + implementation "com.google.guava:guava" + implementation "org.codehaus.groovy:groovy-all" + implementation "run.mone:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmq/AbstractRocketMqClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmq/AbstractRocketMqClientTest.groovy new file mode 100644 index 000000000..6b417684e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmq/AbstractRocketMqClientTest.groovy @@ -0,0 +1,260 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq + +import base.BaseConf +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer +import org.apache.rocketmq.client.producer.DefaultMQProducer +import org.apache.rocketmq.client.producer.SendCallback +import org.apache.rocketmq.client.producer.SendResult +import org.apache.rocketmq.common.message.Message +import org.apache.rocketmq.remoting.common.RemotingHelper +import spock.lang.Shared +import spock.lang.Unroll + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +@Unroll +abstract class AbstractRocketMqClientTest extends InstrumentationSpecification { + + @Shared + DefaultMQProducer producer + + @Shared + DefaultMQPushConsumer consumer + + @Shared + String sharedTopic + + @Shared + Message msg + + @Shared + def msgs = new ArrayList() + + abstract void configureMQProducer(DefaultMQProducer producer) + + abstract void configureMQPushConsumer(DefaultMQPushConsumer consumer) + + def setupSpec() { + sharedTopic = BaseConf.initTopic() + msg = new Message(sharedTopic, "TagA", ("Hello RocketMQ").getBytes(RemotingHelper.DEFAULT_CHARSET)) + Message msg1 = new Message(sharedTopic, "TagA", ("hello world a").getBytes()) + Message msg2 = new Message(sharedTopic, "TagB", ("hello world b").getBytes()) + msgs.add(msg1) + msgs.add(msg2) + producer = BaseConf.getProducer(BaseConf.nsAddr) + configureMQProducer(producer) + consumer = BaseConf.getConsumer(BaseConf.nsAddr, sharedTopic, "*", new TracingMessageListener()) + configureMQPushConsumer(consumer) + } + + def cleanupSpec() { + producer?.shutdown() + consumer?.shutdown() + BaseConf.deleteTempDir() + } + + def "test rocketmq produce callback"() { + when: + producer.send(msg, new SendCallback() { + @Override + void onSuccess(SendResult sendResult) { + } + + @Override + void onException(Throwable throwable) { + } + }) + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name sharedTopic + " send" + kind PRODUCER + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rocketmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" sharedTopic + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + "messaging.rocketmq.tags" "TagA" + "messaging.rocketmq.broker_address" String + "messaging.rocketmq.send_result" "SEND_OK" + } + } + span(1) { + name sharedTopic + " process" + kind CONSUMER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rocketmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" sharedTopic + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + "messaging.rocketmq.tags" "TagA" + "messaging.rocketmq.broker_address" String + "messaging.rocketmq.queue_id" Long + "messaging.rocketmq.queue_offset" Long + } + } + span(2) { + name "messageListener" + kind INTERNAL + childOf span(1) + } + } + } + } + + def "test rocketmq produce and consume"() { + when: + runWithSpan("parent") { + producer.send(msg) + } + + then: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "parent" + kind INTERNAL + } + span(1) { + name sharedTopic + " send" + kind PRODUCER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rocketmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" sharedTopic + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + "messaging.rocketmq.tags" "TagA" + "messaging.rocketmq.broker_address" String + "messaging.rocketmq.send_result" "SEND_OK" + } + } + span(2) { + name sharedTopic + " process" + kind CONSUMER + childOf span(1) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rocketmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" sharedTopic + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + "messaging.rocketmq.tags" "TagA" + "messaging.rocketmq.broker_address" String + "messaging.rocketmq.queue_id" Long + "messaging.rocketmq.queue_offset" Long + } + } + span(3) { + name "messageListener" + kind INTERNAL + childOf span(2) + } + } + } + } + + def "test rocketmq produce and batch consume"() { + setup: + consumer.setConsumeMessageBatchMaxSize(2) + + when: + runWithSpan("parent") { + producer.send(msgs) + } + + then: + assertTraces(2) { + def producerSpan = null + + trace(0, 2) { + producerSpan = span(1) + + span(0) { + name "parent" + kind INTERNAL + } + span(1) { + name sharedTopic + " send" + kind PRODUCER + childOf span(0) + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rocketmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" sharedTopic + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + "messaging.rocketmq.broker_address" String + "messaging.rocketmq.send_result" "SEND_OK" + } + } + } + + trace(1, 4) { + span(0) { + name "multiple_sources receive" + kind CONSUMER + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rocketmq" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "receive" + } + } + span(1) { + name sharedTopic + " process" + kind CONSUMER + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rocketmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" sharedTopic + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + "messaging.rocketmq.tags" "TagA" + "messaging.rocketmq.broker_address" String + "messaging.rocketmq.queue_id" Long + "messaging.rocketmq.queue_offset" Long + } + childOf span(0) + hasLink producerSpan + } + span(2) { + name sharedTopic + " process" + kind CONSUMER + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rocketmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" sharedTopic + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "topic" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + "${SemanticAttributes.MESSAGING_MESSAGE_ID.key}" String + "messaging.rocketmq.tags" "TagB" + "messaging.rocketmq.broker_address" String + "messaging.rocketmq.queue_id" Long + "messaging.rocketmq.queue_offset" Long + } + childOf span(0) + hasLink producerSpan + } + span(3) { + name "messageListener" + kind INTERNAL + childOf span(0) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmq/TracingMessageListener.groovy b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmq/TracingMessageListener.groovy new file mode 100644 index 000000000..a43aa4374 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmq/TracingMessageListener.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmq + +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly +import org.apache.rocketmq.common.message.MessageExt + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan + +class TracingMessageListener implements MessageListenerOrderly { + @Override + ConsumeOrderlyStatus consumeMessage(List list, ConsumeOrderlyContext consumeOrderlyContext) { + runInternalSpan("messageListener") + return ConsumeOrderlyStatus.SUCCESS + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/java/base/BaseConf.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/java/base/BaseConf.java new file mode 100644 index 000000000..0230ab698 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/java/base/BaseConf.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package base; + +import java.util.UUID; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.test.util.MQRandomUtils; +import org.apache.rocketmq.test.util.RandomUtil; + +public final class BaseConf { + public static final String nsAddr; + public static final String broker1Addr; + static final String broker1Name; + static final String clusterName; + static final NamesrvController namesrvController; + static final BrokerController brokerController; + + static { + System.setProperty( + RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); + namesrvController = IntegrationTestBase.createAndStartNamesrv(); + nsAddr = "localhost:" + namesrvController.getNettyServerConfig().getListenPort(); + brokerController = IntegrationTestBase.createAndStartBroker(nsAddr); + clusterName = brokerController.getBrokerConfig().getBrokerClusterName(); + broker1Name = brokerController.getBrokerConfig().getBrokerName(); + broker1Addr = "localhost:" + brokerController.getNettyServerConfig().getListenPort(); + } + + private BaseConf() {} + + public static String initTopic() { + String topic = MQRandomUtils.getRandomTopic(); + IntegrationTestBase.initTopic(topic, nsAddr, clusterName); + return topic; + } + + public static DefaultMQPushConsumer getConsumer( + String nsAddr, String topic, String subExpression, MessageListener listener) + throws MQClientException { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup"); + consumer.setInstanceName(RandomUtil.getStringByUUID()); + consumer.setNamesrvAddr(nsAddr); + consumer.subscribe(topic, subExpression); + consumer.setMessageListener(listener); + consumer.start(); + return consumer; + } + + public static DefaultMQProducer getProducer(String ns) throws MQClientException { + DefaultMQProducer producer = new DefaultMQProducer(RandomUtil.getStringByUUID()); + producer.setInstanceName(UUID.randomUUID().toString()); + producer.setNamesrvAddr(ns); + producer.start(); + return producer; + } + + public static void deleteTempDir() { + namesrvController.shutdown(); + IntegrationTestBase.deleteTempDir(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/java/base/IntegrationTestBase.java b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/java/base/IntegrationTestBase.java new file mode 100644 index 000000000..53107a397 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rocketmq-client-4.8/testing/src/main/java/base/IntegrationTestBase.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package base; + +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.logging.InternalLogger; +import org.apache.rocketmq.logging.InternalLoggerFactory; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.test.util.MQAdmin; +import org.junit.Assert; + +public final class IntegrationTestBase { + public static final InternalLogger logger = + InternalLoggerFactory.getLogger(IntegrationTestBase.class); + + static final String BROKER_NAME_PREFIX = "TestBrokerName_"; + static final AtomicInteger BROKER_INDEX = new AtomicInteger(0); + static final List TMPE_FILES = new ArrayList<>(); + static final List BROKER_CONTROLLERS = new ArrayList<>(); + static final List NAMESRV_CONTROLLERS = new ArrayList<>(); + static final int COMMIT_LOG_SIZE = 1024 * 1024 * 100; + static final int INDEX_NUM = 1000; + + private static String createTempDir() { + String path = null; + try { + File file = Files.createTempDirectory("opentelemetry-rocketmq-client-temp").toFile(); + TMPE_FILES.add(file); + path = file.getCanonicalPath(); + } catch (IOException e) { + logger.warn("Error creating temporary directory.", e); + } + return path; + } + + public static void deleteTempDir() { + for (File file : TMPE_FILES) { + boolean deleted = file.delete(); + if (!deleted) { + file.deleteOnExit(); + } + } + } + + public static NamesrvController createAndStartNamesrv() { + String baseDir = createTempDir(); + Path kvConfigPath = Paths.get(baseDir, "namesrv", "kvConfig.json"); + Path namesrvPath = Paths.get(baseDir, "namesrv", "namesrv.properties"); + + NamesrvConfig namesrvConfig = new NamesrvConfig(); + NettyServerConfig nameServerNettyServerConfig = new NettyServerConfig(); + + namesrvConfig.setKvConfigPath(kvConfigPath.toString()); + namesrvConfig.setConfigStorePath(namesrvPath.toString()); + + // find 3 consecutive open ports and use the last one of them + // rocketmq will also bind to given port - 2 + nameServerNettyServerConfig.setListenPort(PortUtils.findOpenPorts(3) + 2); + NamesrvController namesrvController = + new NamesrvController(namesrvConfig, nameServerNettyServerConfig); + try { + Assert.assertTrue(namesrvController.initialize()); + logger.info("Name Server Start:{}", nameServerNettyServerConfig.getListenPort()); + namesrvController.start(); + } catch (Exception e) { + logger.info("Name Server start failed", e); + } + NAMESRV_CONTROLLERS.add(namesrvController); + return namesrvController; + } + + public static BrokerController createAndStartBroker(String nsAddr) { + String baseDir = createTempDir(); + Path commitLogPath = Paths.get(baseDir, "commitlog"); + + BrokerConfig brokerConfig = new BrokerConfig(); + MessageStoreConfig storeConfig = new MessageStoreConfig(); + brokerConfig.setBrokerName(BROKER_NAME_PREFIX + BROKER_INDEX.getAndIncrement()); + brokerConfig.setBrokerIP1("127.0.0.1"); + brokerConfig.setNamesrvAddr(nsAddr); + brokerConfig.setEnablePropertyFilter(true); + storeConfig.setStorePathRootDir(baseDir); + storeConfig.setStorePathCommitLog(commitLogPath.toString()); + storeConfig.setMappedFileSizeCommitLog(COMMIT_LOG_SIZE); + storeConfig.setMaxIndexNum(INDEX_NUM); + storeConfig.setMaxHashSlotNum(INDEX_NUM * 4); + return createAndStartBroker(storeConfig, brokerConfig); + } + + public static BrokerController createAndStartBroker( + MessageStoreConfig storeConfig, BrokerConfig brokerConfig) { + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + nettyServerConfig.setListenPort(PortUtils.findOpenPort()); + storeConfig.setHaListenPort(PortUtils.findOpenPort()); + BrokerController brokerController = + new BrokerController(brokerConfig, nettyServerConfig, nettyClientConfig, storeConfig); + try { + Assert.assertTrue(brokerController.initialize()); + logger.info( + "Broker Start name:{} addr:{}", + brokerConfig.getBrokerName(), + brokerController.getBrokerAddr()); + brokerController.start(); + } catch (Throwable t) { + logger.error("Broker start failed", t); + throw new IllegalStateException("Broker start failed", t); + } + BROKER_CONTROLLERS.add(brokerController); + return brokerController; + } + + public static void initTopic(String topic, String nsAddr, String clusterName) { + MQAdmin.createTopic(nsAddr, clusterName, topic, 20); + } + + private IntegrationTestBase() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/runtime-metrics-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/runtime-metrics-javaagent.gradle new file mode 100644 index 000000000..45a19b814 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/runtime-metrics-javaagent.gradle @@ -0,0 +1,5 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + implementation project(':instrumentation:runtime-metrics:library') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsInstaller.java b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsInstaller.java new file mode 100644 index 000000000..df64f7507 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsInstaller.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.runtimemetrics.GarbageCollector; +import io.opentelemetry.instrumentation.runtimemetrics.MemoryPools; +import io.opentelemetry.javaagent.extension.AgentListener; +import java.util.Collections; + +/** An {@link AgentListener} that enables runtime metrics during agent startup. */ +@AutoService(AgentListener.class) +public class RuntimeMetricsInstaller implements AgentListener { + @Override + public void afterAgent(Config config) { + if (config.isInstrumentationEnabled( + Collections.singleton("runtime-metrics"), /* defaultEnabled= */ true)) { + GarbageCollector.registerObservers(); + MemoryPools.registerObservers(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/src/test/groovy/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/src/test/groovy/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsTest.groovy new file mode 100644 index 000000000..a3aa6c676 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/javaagent/src/test/groovy/io/opentelemetry/instrumentation/javaagent/runtimemetrics/RuntimeMetricsTest.groovy @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import spock.util.concurrent.PollingConditions + +class RuntimeMetricsTest extends AgentInstrumentationSpecification { + + def "test runtime metrics is enabled"() { + when: + def conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) + + then: + conditions.eventually { + assert getMetrics().any { it.name == "runtime.jvm.gc.time" } + assert getMetrics().any { it.name == "runtime.jvm.gc.count" } + assert getMetrics().any { it.name == "runtime.jvm.memory.area" } + assert getMetrics().any { it.name == "runtime.jvm.memory.pool" } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/runtime-metrics-library.gradle b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/runtime-metrics-library.gradle new file mode 100644 index 000000000..f68c35ee5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/runtime-metrics-library.gradle @@ -0,0 +1,9 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + implementation "run.mone:opentelemetry-api-metrics" + + testImplementation "run.mone:opentelemetry-sdk-metrics" + testImplementation project(':testing-common') + testImplementation "org.mockito:mockito-core" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/GarbageCollector.java b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/GarbageCollector.java new file mode 100644 index 000000000..2109eb540 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/GarbageCollector.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics; + +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.List; + +/** + * Registers observers that generate metrics about JVM garbage collectors. + * + *

Example usage: + * + *

{@code
+ * GarbageCollector.registerObservers();
+ * }
+ * + *

Example metrics being exported: + * + *

+ *   runtime.jvm.gc.time{gc="PS1"} 6.7
+ *   runtime.jvm.gc.count{gc="PS1"} 1
+ * 
+ */ +public final class GarbageCollector { + private static final String GC_LABEL_KEY = "gc"; + + /** Register all observers provided by this module. */ + public static void registerObservers() { + List garbageCollectors = ManagementFactory.getGarbageCollectorMXBeans(); + Meter meter = GlobalMeterProvider.getMeter(GarbageCollector.class.getName()); + List labelSets = new ArrayList<>(garbageCollectors.size()); + for (GarbageCollectorMXBean gc : garbageCollectors) { + labelSets.add(Labels.of(GC_LABEL_KEY, gc.getName())); + } + meter + .longSumObserverBuilder("runtime.jvm.gc.time") + .setDescription("Time spent in a given JVM garbage collector in milliseconds.") + .setUnit("ms") + .setUpdater( + resultLongObserver -> { + for (int i = 0; i < garbageCollectors.size(); i++) { + resultLongObserver.observe( + garbageCollectors.get(i).getCollectionTime(), labelSets.get(i)); + } + }) + .build(); + meter + .longSumObserverBuilder("runtime.jvm.gc.count") + .setDescription( + "The number of collections that have occurred for a given JVM garbage collector.") + .setUnit("collections") + .setUpdater( + resultLongObserver -> { + for (int i = 0; i < garbageCollectors.size(); i++) { + resultLongObserver.observe( + garbageCollectors.get(i).getCollectionCount(), labelSets.get(i)); + } + }) + .build(); + } + + private GarbageCollector() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/MemoryPools.java b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/MemoryPools.java new file mode 100644 index 000000000..86070dea5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/MemoryPools.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics; + +import io.opentelemetry.api.metrics.AsynchronousInstrument.LongResult; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.util.ArrayList; +import java.util.List; + +/** + * Registers observers that generate metrics about JVM memory areas. + * + *

Example usage: + * + *

{@code
+ * MemoryPools.registerObservers();
+ * }
+ * + *

Example metrics being exported: Component + * + *

+ *   runtime.jvm.memory.area{type="used",area="heap"} 2000000
+ *   runtime.jvm.memory.area{type="committed",area="non_heap"} 200000
+ *   runtime.jvm.memory.area{type="used",pool="PS Eden Space"} 2000
+ * 
+ */ +public final class MemoryPools { + private static final String TYPE_LABEL_KEY = "type"; + private static final String AREA_LABEL_KEY = "area"; + private static final String POOL_LABEL_KEY = "pool"; + private static final String USED = "used"; + private static final String COMMITTED = "committed"; + private static final String MAX = "max"; + private static final String HEAP = "heap"; + private static final String NON_HEAP = "non_heap"; + + private static final Labels COMMITTED_HEAP = + Labels.of(TYPE_LABEL_KEY, COMMITTED, AREA_LABEL_KEY, HEAP); + private static final Labels USED_HEAP = Labels.of(TYPE_LABEL_KEY, USED, AREA_LABEL_KEY, HEAP); + private static final Labels MAX_HEAP = Labels.of(TYPE_LABEL_KEY, MAX, AREA_LABEL_KEY, HEAP); + + private static final Labels COMMITTED_NON_HEAP = + Labels.of(TYPE_LABEL_KEY, COMMITTED, AREA_LABEL_KEY, NON_HEAP); + private static final Labels USED_NON_HEAP = + Labels.of(TYPE_LABEL_KEY, USED, AREA_LABEL_KEY, NON_HEAP); + private static final Labels MAX_NON_HEAP = + Labels.of(TYPE_LABEL_KEY, MAX, AREA_LABEL_KEY, NON_HEAP); + + /** Register only the "area" observers. */ + public static void registerMemoryAreaObservers() { + MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + Meter meter = GlobalMeterProvider.getMeter(MemoryPools.class.getName()); + meter + .longUpDownSumObserverBuilder("runtime.jvm.memory.area") + .setDescription("Bytes of a given JVM memory area.") + .setUnit("By") + .setUpdater( + resultLongObserver -> { + observeHeap(resultLongObserver, memoryBean.getHeapMemoryUsage()); + observeNonHeap(resultLongObserver, memoryBean.getNonHeapMemoryUsage()); + }) + .build(); + } + + /** Register only the "pool" observers. */ + public static void registerMemoryPoolObservers() { + List poolBeans = ManagementFactory.getMemoryPoolMXBeans(); + Meter meter = GlobalMeterProvider.getMeter(MemoryPools.class.getName()); + List usedLabelSets = new ArrayList<>(poolBeans.size()); + List committedLabelSets = new ArrayList<>(poolBeans.size()); + List maxLabelSets = new ArrayList<>(poolBeans.size()); + for (MemoryPoolMXBean pool : poolBeans) { + usedLabelSets.add(Labels.of(TYPE_LABEL_KEY, USED, POOL_LABEL_KEY, pool.getName())); + committedLabelSets.add(Labels.of(TYPE_LABEL_KEY, COMMITTED, POOL_LABEL_KEY, pool.getName())); + maxLabelSets.add(Labels.of(TYPE_LABEL_KEY, MAX, POOL_LABEL_KEY, pool.getName())); + } + meter + .longUpDownSumObserverBuilder("runtime.jvm.memory.pool") + .setDescription("Bytes of a given JVM memory pool.") + .setUnit("By") + .setUpdater( + resultLongObserver -> { + for (int i = 0; i < poolBeans.size(); i++) { + MemoryUsage poolUsage = poolBeans.get(i).getUsage(); + if (poolUsage != null) { + observe( + resultLongObserver, + poolUsage, + usedLabelSets.get(i), + committedLabelSets.get(i), + maxLabelSets.get(i)); + } + } + }) + .build(); + } + + /** Register all observers provided by this module. */ + public static void registerObservers() { + registerMemoryAreaObservers(); + registerMemoryPoolObservers(); + } + + static void observeHeap(LongResult observer, MemoryUsage usage) { + observe(observer, usage, USED_HEAP, COMMITTED_HEAP, MAX_HEAP); + } + + static void observeNonHeap(LongResult observer, MemoryUsage usage) { + observe(observer, usage, USED_NON_HEAP, COMMITTED_NON_HEAP, MAX_NON_HEAP); + } + + private static void observe( + LongResult observer, + MemoryUsage usage, + Labels usedLabels, + Labels committedLabels, + Labels maxLabels) { + // TODO: Decide if init is needed or not. It is a constant that can be queried once on startup. + // if (usage.getInit() != -1) { + // observer.observe(usage.getInit(), ...); + // } + observer.observe(usage.getUsed(), usedLabels); + observer.observe(usage.getCommitted(), committedLabels); + // TODO: Decide if max is needed or not. It is a constant that can be queried once on startup. + if (usage.getMax() != -1) { + observer.observe(usage.getMax(), maxLabels); + } + } + + private MemoryPools() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/MemoryPoolsTest.java b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/MemoryPoolsTest.java new file mode 100644 index 000000000..8557fefd9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/runtime-metrics/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/MemoryPoolsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import io.opentelemetry.api.metrics.AsynchronousInstrument; +import io.opentelemetry.api.metrics.common.Labels; +import java.lang.management.MemoryUsage; +import org.junit.jupiter.api.Test; + +class MemoryPoolsTest { + + @Test + void observeHeap() { + AsynchronousInstrument.LongResult observer = mock(AsynchronousInstrument.LongResult.class); + MemoryPools.observeHeap(observer, new MemoryUsage(-1, 1, 2, 3)); + verify(observer).observe(1, Labels.of("type", "used", "area", "heap")); + verify(observer).observe(2, Labels.of("type", "committed", "area", "heap")); + verify(observer).observe(3, Labels.of("type", "max", "area", "heap")); + verifyNoMoreInteractions(observer); + } + + @Test + void observeHeapNoMax() { + AsynchronousInstrument.LongResult observer = mock(AsynchronousInstrument.LongResult.class); + MemoryPools.observeHeap(observer, new MemoryUsage(-1, 1, 2, -1)); + verify(observer).observe(1, Labels.of("type", "used", "area", "heap")); + verify(observer).observe(2, Labels.of("type", "committed", "area", "heap")); + verifyNoMoreInteractions(observer); + } + + @Test + void observeNonHeap() { + AsynchronousInstrument.LongResult observer = mock(AsynchronousInstrument.LongResult.class); + MemoryPools.observeNonHeap(observer, new MemoryUsage(-1, 4, 5, 6)); + verify(observer).observe(4, Labels.of("type", "used", "area", "non_heap")); + verify(observer).observe(5, Labels.of("type", "committed", "area", "non_heap")); + verify(observer).observe(6, Labels.of("type", "max", "area", "non_heap")); + verifyNoMoreInteractions(observer); + } + + @Test + void observeNonHeapNoMax() { + AsynchronousInstrument.LongResult observer = mock(AsynchronousInstrument.LongResult.class); + MemoryPools.observeNonHeap(observer, new MemoryUsage(-1, 4, 5, -1)); + verify(observer).observe(4, Labels.of("type", "used", "area", "non_heap")); + verify(observer).observe(5, Labels.of("type", "committed", "area", "non_heap")); + verifyNoMoreInteractions(observer); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/rxjava-1.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/rxjava-1.0-library.gradle new file mode 100644 index 000000000..99ec0063f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/rxjava-1.0-library.gradle @@ -0,0 +1,5 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library "io.reactivex:rxjava:1.0.7" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/SpanFinishingSubscription.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/SpanFinishingSubscription.java new file mode 100644 index 000000000..0a9af4333 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/SpanFinishingSubscription.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import java.util.concurrent.atomic.AtomicReference; +import rx.Subscription; + +public class SpanFinishingSubscription implements Subscription { + private final BaseTracer tracer; + private final AtomicReference contextRef; + + public SpanFinishingSubscription(BaseTracer tracer, AtomicReference contextRef) { + this.tracer = tracer; + this.contextRef = contextRef; + } + + @Override + public void unsubscribe() { + Context context = contextRef.getAndSet(null); + if (context != null) { + tracer.end(context); + } + } + + @Override + public boolean isUnsubscribed() { + return contextRef.get() == null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/TracedOnSubscribe.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/TracedOnSubscribe.java new file mode 100644 index 000000000..0c6d0e364 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/TracedOnSubscribe.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import rx.Observable; +import rx.Subscriber; +import rx.__OpenTelemetryTracingUtil; + +public class TracedOnSubscribe implements Observable.OnSubscribe { + private final Observable.OnSubscribe delegate; + private final String spanName; + private final Context parentContext; + private final BaseTracer tracer; + private final SpanKind spanKind; + + public TracedOnSubscribe( + Observable originalObservable, String spanName, BaseTracer tracer, SpanKind spanKind) { + delegate = __OpenTelemetryTracingUtil.extractOnSubscribe(originalObservable); + this.spanName = spanName; + this.tracer = tracer; + this.spanKind = spanKind; + + parentContext = Context.current(); + } + + @Override + public void call(Subscriber subscriber) { + Context context = tracer.startSpan(parentContext, spanName, spanKind); + decorateSpan(Span.fromContext(context)); + try (Scope ignored = context.makeCurrent()) { + delegate.call(new TracedSubscriber<>(context, subscriber, tracer)); + } + } + + protected void decorateSpan(Span span) { + // Subclasses can use it to provide addition attributes to the span + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/TracedSubscriber.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/TracedSubscriber.java new file mode 100644 index 000000000..d8c9e38f1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava/TracedSubscriber.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import java.util.concurrent.atomic.AtomicReference; +import rx.Subscriber; + +public class TracedSubscriber extends Subscriber { + + private final AtomicReference contextRef; + private final Subscriber delegate; + private final BaseTracer tracer; + + public TracedSubscriber(Context context, Subscriber delegate, BaseTracer tracer) { + contextRef = new AtomicReference<>(context); + this.delegate = delegate; + this.tracer = tracer; + SpanFinishingSubscription subscription = new SpanFinishingSubscription(tracer, contextRef); + delegate.add(subscription); + } + + @Override + public void onStart() { + Context context = contextRef.get(); + if (context != null) { + try (Scope ignored = context.makeCurrent()) { + delegate.onStart(); + } + } else { + delegate.onStart(); + } + } + + @Override + public void onNext(T value) { + Context context = contextRef.get(); + if (context != null) { + try (Scope ignored = context.makeCurrent()) { + delegate.onNext(value); + } + } else { + delegate.onNext(value); + } + } + + @Override + public void onCompleted() { + Context context = contextRef.getAndSet(null); + if (context != null) { + Throwable error = null; + try (Scope ignored = context.makeCurrent()) { + delegate.onCompleted(); + } catch (Throwable t) { + error = t; + throw t; + } finally { + if (error != null) { + tracer.endExceptionally(context, error); + } else { + tracer.end(context); + } + } + } else { + delegate.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + Context context = contextRef.getAndSet(null); + if (context != null) { + tracer.endExceptionally(context, e); + } + // TODO (trask) should this be wrapped in parent of context(?) + delegate.onError(e); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/rx/__OpenTelemetryTracingUtil.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/rx/__OpenTelemetryTracingUtil.java new file mode 100644 index 000000000..647d6d9e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-1.0/library/src/main/java/rx/__OpenTelemetryTracingUtil.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package rx; + +/** + * This class must be in the rx package in order to access the package accessible onSubscribe field. + */ +@SuppressWarnings("checkstyle:TypeName") +public final class __OpenTelemetryTracingUtil { + public static Observable.OnSubscribe extractOnSubscribe(Observable observable) { + return observable.onSubscribe; + } + + private __OpenTelemetryTracingUtil() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/rxjava-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/rxjava-2.0-javaagent.gradle new file mode 100644 index 000000000..72d76dfbc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/rxjava-2.0-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.reactivex.rxjava2" + module = "rxjava" + versions = "[2.0.6,)" + assertInverse = true + } +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.rxjava.experimental-span-attributes=true" +} + +dependencies { + library "io.reactivex.rxjava2:rxjava:2.0.6" + + implementation project(":instrumentation:rxjava:rxjava-2.0:library") + + testImplementation "io.opentelemetry:opentelemetry-extension-annotations" + testImplementation project(':instrumentation:rxjava:rxjava-2.0:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/RxJava2InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/RxJava2InstrumentationModule.java new file mode 100644 index 000000000..b36ce0e9f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/RxJava2InstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rxjava2; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RxJava2InstrumentationModule extends InstrumentationModule { + + public RxJava2InstrumentationModule() { + super("rxjava2"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new RxJavaPluginsInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/RxJavaPluginsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/RxJavaPluginsInstrumentation.java new file mode 100644 index 000000000..1be2e5c28 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/RxJavaPluginsInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rxjava2; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.reactivex.plugins.RxJavaPlugins; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RxJavaPluginsInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.reactivex.plugins.RxJavaPlugins"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(isMethod(), this.getClass().getName() + "$MethodAdvice"); + } + + @SuppressWarnings("unused") + public static class MethodAdvice { + + // TODO(anuraaga): Replace with adding a type initializer to RxJavaPlugins + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2685 + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void activateOncePerClassloader() { + TracingAssemblyActivation.activate(RxJavaPlugins.class); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/TracingAssemblyActivation.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/TracingAssemblyActivation.java new file mode 100644 index 000000000..124651724 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava2/TracingAssemblyActivation.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rxjava2; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.rxjava2.TracingAssembly; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class TracingAssemblyActivation { + + private static final ClassValue activated = + new ClassValue() { + @Override + protected AtomicBoolean computeValue(Class type) { + return new AtomicBoolean(); + } + }; + + public static void activate(Class clz) { + if (activated.get(clz).compareAndSet(false, true)) { + TracingAssembly.newBuilder() + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBooleanProperty( + "otel.instrumentation.rxjava.experimental-span-attributes", false)) + .build() + .enable(); + } + } + + private TracingAssemblyActivation() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2SubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2SubscriptionTest.groovy new file mode 100644 index 000000000..99ac04a93 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2SubscriptionTest.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava2.AbstractRxJava2SubscriptionTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class RxJava2SubscriptionTest extends AbstractRxJava2SubscriptionTest implements AgentTestTrait { + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2Test.groovy new file mode 100644 index 000000000..d93d90636 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2Test.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava2.AbstractRxJava2Test +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class RxJava2Test extends AbstractRxJava2Test implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2WithSpanInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2WithSpanInstrumentationTest.groovy new file mode 100644 index 000000000..3f24b3309 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/groovy/RxJava2WithSpanInstrumentationTest.groovy @@ -0,0 +1,1065 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.instrumentation.rxjava2.TracedWithSpan +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.observers.TestObserver +import io.reactivex.processors.UnicastProcessor +import io.reactivex.subjects.CompletableSubject +import io.reactivex.subjects.MaybeSubject +import io.reactivex.subjects.SingleSubject +import io.reactivex.subjects.UnicastSubject +import io.reactivex.subscribers.TestSubscriber +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription + +class RxJava2WithSpanInstrumentationTest extends AgentInstrumentationSpecification { + + def "should capture span for already completed Completable"() { + setup: + def observer = new TestObserver() + def source = Completable.complete() + new TracedWithSpan() + .completable(source) + .subscribe(observer) + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Completable"() { + setup: + def source = CompletableSubject.create() + def observer = new TestObserver() + new TracedWithSpan() + .completable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Completable"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestObserver() + def source = Completable.error(error) + new TracedWithSpan() + .completable(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Completable"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = CompletableSubject.create() + def observer = new TestObserver() + new TracedWithSpan() + .completable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Completable"() { + setup: + def source = CompletableSubject.create() + def observer = new TestObserver() + new TracedWithSpan() + .completable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed Maybe"() { + setup: + def observer = new TestObserver() + def source = Maybe.just("Value") + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already empty Maybe"() { + setup: + def observer = new TestObserver() + def source = Maybe. empty() + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Maybe"() { + setup: + def source = MaybeSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onSuccess("Value") + observer.assertValue("Value") + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Maybe"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestObserver() + def source = Maybe. error(error) + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Maybe"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = MaybeSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Maybe"() { + setup: + def source = MaybeSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed Single"() { + setup: + def observer = new TestObserver() + def source = Single.just("Value") + new TracedWithSpan() + .single(source) + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Single"() { + setup: + def source = SingleSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .single(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onSuccess("Value") + observer.assertValue("Value") + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Single"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestObserver() + def source = Single. error(error) + new TracedWithSpan() + .single(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Single"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = SingleSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .single(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Single"() { + setup: + def source = SingleSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .single(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed Observable"() { + setup: + def observer = new TestObserver() + def source = Observable. just("Value") + new TracedWithSpan() + .observable(source) + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Observable"() { + setup: + def source = UnicastSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .observable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Observable"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestObserver() + def source = Observable. error(error) + new TracedWithSpan() + .observable(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Observable"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .observable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Observable"() { + setup: + def source = UnicastSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .observable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed Flowable"() { + setup: + def observer = new TestSubscriber() + def source = Flowable. just("Value") + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Flowable"() { + setup: + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Flowable"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestSubscriber() + def source = Flowable. error(error) + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Flowable"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Flowable"() { + setup: + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.dispose() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed ParallelFlowable"() { + setup: + def observer = new TestSubscriber() + def source = Flowable. just("Value") + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed ParallelFlowable"() { + setup: + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored ParallelFlowable"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestSubscriber() + def source = Flowable. error(error) + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored ParallelFlowable"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled ParallelFlowable"() { + setup: + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for eventually completed Publisher"() { + setup: + def source = new CustomPublisher() + def observer = new TestSubscriber() + new TracedWithSpan() + .publisher(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.publisher" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Publisher"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = new CustomPublisher() + def observer = new TestSubscriber() + new TracedWithSpan() + .publisher(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.publisher" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Publisher"() { + setup: + def source = new CustomPublisher() + def observer = new TestSubscriber() + new TracedWithSpan() + .publisher(source) + .subscribe(observer) + observer.assertSubscribed() + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.publisher" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + static class CustomPublisher implements Publisher, Subscription { + Subscriber subscriber + + @Override + void subscribe(Subscriber subscriber) { + this.subscriber = subscriber + subscriber.onSubscribe(this) + } + + void onComplete() { + this.subscriber.onComplete() + } + + void onError(Throwable exception) { + this.subscriber.onError(exception) + } + + @Override + void request(long l) {} + + @Override + void cancel() {} + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/rxjava2/TracedWithSpan.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/rxjava2/TracedWithSpan.java new file mode 100644 index 000000000..6c84a2643 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/rxjava2/TracedWithSpan.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.extension.annotations.WithSpan; +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.parallel.ParallelFlowable; +import org.reactivestreams.Publisher; + +public class TracedWithSpan { + + @WithSpan + public Completable completable(Completable source) { + return source; + } + + @WithSpan + public Maybe maybe(Maybe source) { + return source; + } + + @WithSpan + public Single single(Single source) { + return source; + } + + @WithSpan + public Observable observable(Observable source) { + return source; + } + + @WithSpan + public Flowable flowable(Flowable source) { + return source; + } + + @WithSpan + public ParallelFlowable parallelFlowable(ParallelFlowable source) { + return source; + } + + @WithSpan + public Publisher publisher(Publisher source) { + return source; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/rxjava-2.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/rxjava-2.0-library.gradle new file mode 100644 index 000000000..1866b7331 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/rxjava-2.0-library.gradle @@ -0,0 +1,7 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library "io.reactivex.rxjava2:rxjava:2.1.3" + + testImplementation project(':instrumentation:rxjava:rxjava-2.0:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/RxJava2AsyncSpanEndStrategy.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/RxJava2AsyncSpanEndStrategy.java new file mode 100644 index 000000000..38701b540 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/RxJava2AsyncSpanEndStrategy.java @@ -0,0 +1,168 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy; +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.functions.Action; +import io.reactivex.functions.BiConsumer; +import io.reactivex.functions.Consumer; +import io.reactivex.parallel.ParallelFlowable; +import java.util.concurrent.atomic.AtomicBoolean; +import org.reactivestreams.Publisher; + +public final class RxJava2AsyncSpanEndStrategy implements AsyncSpanEndStrategy { + private static final AttributeKey CANCELED_ATTRIBUTE_KEY = + AttributeKey.booleanKey("rxjava.canceled"); + + public static RxJava2AsyncSpanEndStrategy create() { + return newBuilder().build(); + } + + public static RxJava2AsyncSpanEndStrategyBuilder newBuilder() { + return new RxJava2AsyncSpanEndStrategyBuilder(); + } + + private final boolean captureExperimentalSpanAttributes; + + RxJava2AsyncSpanEndStrategy(boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + @Override + public boolean supports(Class returnType) { + return returnType == Publisher.class + || returnType == Completable.class + || returnType == Maybe.class + || returnType == Single.class + || returnType == Observable.class + || returnType == Flowable.class + || returnType == ParallelFlowable.class; + } + + @Override + public Object end(BaseTracer tracer, Context context, Object returnValue) { + + EndOnFirstNotificationConsumer notificationConsumer = + new EndOnFirstNotificationConsumer<>(tracer, context); + if (returnValue instanceof Completable) { + return endWhenComplete((Completable) returnValue, notificationConsumer); + } else if (returnValue instanceof Maybe) { + return endWhenMaybeComplete((Maybe) returnValue, notificationConsumer); + } else if (returnValue instanceof Single) { + return endWhenSingleComplete((Single) returnValue, notificationConsumer); + } else if (returnValue instanceof Observable) { + return endWhenObservableComplete((Observable) returnValue, notificationConsumer); + } else if (returnValue instanceof ParallelFlowable) { + return endWhenFirstComplete((ParallelFlowable) returnValue, notificationConsumer); + } + return endWhenPublisherComplete((Publisher) returnValue, notificationConsumer); + } + + private static Completable endWhenComplete( + Completable completable, EndOnFirstNotificationConsumer notificationConsumer) { + return completable + .doOnEvent(notificationConsumer) + .doOnDispose(notificationConsumer::onCancelOrDispose); + } + + private static Maybe endWhenMaybeComplete( + Maybe maybe, EndOnFirstNotificationConsumer notificationConsumer) { + @SuppressWarnings("unchecked") + EndOnFirstNotificationConsumer typedConsumer = + (EndOnFirstNotificationConsumer) notificationConsumer; + return maybe.doOnEvent(typedConsumer).doOnDispose(notificationConsumer::onCancelOrDispose); + } + + private static Single endWhenSingleComplete( + Single single, EndOnFirstNotificationConsumer notificationConsumer) { + @SuppressWarnings("unchecked") + EndOnFirstNotificationConsumer typedConsumer = + (EndOnFirstNotificationConsumer) notificationConsumer; + return single.doOnEvent(typedConsumer).doOnDispose(notificationConsumer::onCancelOrDispose); + } + + private static Observable endWhenObservableComplete( + Observable observable, EndOnFirstNotificationConsumer notificationConsumer) { + return observable + .doOnComplete(notificationConsumer) + .doOnError(notificationConsumer) + .doOnDispose(notificationConsumer::onCancelOrDispose); + } + + private static ParallelFlowable endWhenFirstComplete( + ParallelFlowable parallelFlowable, + EndOnFirstNotificationConsumer notificationConsumer) { + return parallelFlowable + .doOnComplete(notificationConsumer) + .doOnError(notificationConsumer) + .doOnCancel(notificationConsumer::onCancelOrDispose); + } + + private static Flowable endWhenPublisherComplete( + Publisher publisher, EndOnFirstNotificationConsumer notificationConsumer) { + return Flowable.fromPublisher(publisher) + .doOnComplete(notificationConsumer) + .doOnError(notificationConsumer) + .doOnCancel(notificationConsumer::onCancelOrDispose); + } + + /** + * Helper class to ensure that the span is ended exactly once regardless of how many OnComplete or + * OnError notifications are received. Multiple notifications can happen anytime multiple + * subscribers subscribe to the same publisher. + */ + private final class EndOnFirstNotificationConsumer extends AtomicBoolean + implements Action, Consumer, BiConsumer { + + private final BaseTracer tracer; + private final Context context; + + public EndOnFirstNotificationConsumer(BaseTracer tracer, Context context) { + super(false); + this.tracer = tracer; + this.context = context; + } + + @Override + public void run() { + accept(null); + } + + public void onCancelOrDispose() { + if (compareAndSet(false, true)) { + if (captureExperimentalSpanAttributes) { + Span.fromContext(context).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + tracer.end(context); + } + } + + @Override + public void accept(Throwable exception) { + if (compareAndSet(false, true)) { + if (exception != null) { + tracer.endExceptionally(context, exception); + } else { + tracer.end(context); + } + } + } + + @Override + public void accept(T value, Throwable exception) { + accept(exception); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/RxJava2AsyncSpanEndStrategyBuilder.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/RxJava2AsyncSpanEndStrategyBuilder.java new file mode 100644 index 000000000..b049e6dc5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/RxJava2AsyncSpanEndStrategyBuilder.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava2; + +public final class RxJava2AsyncSpanEndStrategyBuilder { + + private boolean captureExperimentalSpanAttributes; + + RxJava2AsyncSpanEndStrategyBuilder() {} + + public RxJava2AsyncSpanEndStrategyBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + public RxJava2AsyncSpanEndStrategy build() { + return new RxJava2AsyncSpanEndStrategy(captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingAssembly.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingAssembly.java new file mode 100644 index 000000000..169d42a56 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingAssembly.java @@ -0,0 +1,308 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies; +import io.reactivex.Completable; +import io.reactivex.CompletableObserver; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.MaybeObserver; +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.functions.BiFunction; +import io.reactivex.functions.Function; +import io.reactivex.internal.fuseable.ConditionalSubscriber; +import io.reactivex.parallel.ParallelFlowable; +import io.reactivex.plugins.RxJavaPlugins; +import org.checkerframework.checker.lock.qual.GuardedBy; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.reactivestreams.Subscriber; + +/** + * RxJava2 library instrumentation. + * + *

In order to enable RxJava2 instrumentation one has to call the {@link + * TracingAssembly#enable()} method. + * + *

Instrumentation uses on*Assembly and on*Subscribe RxJavaPlugin hooks + * to wrap RxJava2 classes in their tracing equivalents. + * + *

Instrumentation can be disabled by calling the {@link TracingAssembly#disable()} method. + */ +public final class TracingAssembly { + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction + oldOnObservableSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction< + ? super Completable, ? super CompletableObserver, ? extends CompletableObserver> + oldOnCompletableSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction + oldOnSingleSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction + oldOnMaybeSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction + oldOnFlowableSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static Function + oldOnParallelAssembly; + + @GuardedBy("TracingAssembly.class") + private static boolean enabled; + + public static TracingAssembly create() { + return newBuilder().build(); + } + + public static TracingAssemblyBuilder newBuilder() { + return new TracingAssemblyBuilder(); + } + + private final boolean captureExperimentalSpanAttributes; + + TracingAssembly(boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + public void enable() { + synchronized (TracingAssembly.class) { + if (enabled) { + return; + } + + enableObservable(); + + enableCompletable(); + + enableSingle(); + + enableMaybe(); + + enableFlowable(); + + enableParallel(); + + enableWithSpanStrategy(captureExperimentalSpanAttributes); + + enabled = true; + } + } + + public void disable() { + synchronized (TracingAssembly.class) { + if (!enabled) { + return; + } + + disableObservable(); + + disableCompletable(); + + disableSingle(); + + disableMaybe(); + + disableFlowable(); + + disableParallel(); + + disableWithSpanStrategy(); + + enabled = false; + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableParallel() { + oldOnParallelAssembly = RxJavaPlugins.getOnParallelAssembly(); + RxJavaPlugins.setOnParallelAssembly( + compose( + oldOnParallelAssembly, + parallelFlowable -> new TracingParallelFlowable(parallelFlowable, Context.current()))); + } + + private static void enableCompletable() { + oldOnCompletableSubscribe = RxJavaPlugins.getOnCompletableSubscribe(); + RxJavaPlugins.setOnCompletableSubscribe( + biCompose( + oldOnCompletableSubscribe, + (completable, observer) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + return new TracingCompletableObserver(observer, context); + } + })); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableFlowable() { + oldOnFlowableSubscribe = RxJavaPlugins.getOnFlowableSubscribe(); + RxJavaPlugins.setOnFlowableSubscribe( + biCompose( + oldOnFlowableSubscribe, + (flowable, subscriber) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + if (subscriber instanceof ConditionalSubscriber) { + return new TracingConditionalSubscriber<>( + (ConditionalSubscriber) subscriber, context); + } else { + return new TracingSubscriber<>(subscriber, context); + } + } + })); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableObservable() { + if (TracingObserver.canEnable()) { + oldOnObservableSubscribe = RxJavaPlugins.getOnObservableSubscribe(); + RxJavaPlugins.setOnObservableSubscribe( + biCompose( + oldOnObservableSubscribe, + (observable, observer) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + return new TracingObserver(observer, context); + } + })); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableSingle() { + oldOnSingleSubscribe = RxJavaPlugins.getOnSingleSubscribe(); + RxJavaPlugins.setOnSingleSubscribe( + biCompose( + oldOnSingleSubscribe, + (single, singleObserver) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + return new TracingSingleObserver(singleObserver, context); + } + })); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableMaybe() { + oldOnMaybeSubscribe = RxJavaPlugins.getOnMaybeSubscribe(); + RxJavaPlugins.setOnMaybeSubscribe( + (BiFunction) + biCompose( + oldOnMaybeSubscribe, + (BiFunction) + (maybe, maybeObserver) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + return new TracingMaybeObserver(maybeObserver, context); + } + })); + } + + private static void enableWithSpanStrategy(boolean captureExperimentalSpanAttributes) { + AsyncSpanEndStrategies.getInstance() + .registerStrategy( + RxJava2AsyncSpanEndStrategy.newBuilder() + .setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes) + .build()); + } + + private static void disableParallel() { + RxJavaPlugins.setOnParallelAssembly(oldOnParallelAssembly); + oldOnParallelAssembly = null; + } + + private static void disableObservable() { + RxJavaPlugins.setOnObservableSubscribe(oldOnObservableSubscribe); + oldOnObservableSubscribe = null; + } + + private static void disableCompletable() { + RxJavaPlugins.setOnCompletableSubscribe(oldOnCompletableSubscribe); + oldOnCompletableSubscribe = null; + } + + private static void disableFlowable() { + RxJavaPlugins.setOnFlowableSubscribe(oldOnFlowableSubscribe); + oldOnFlowableSubscribe = null; + } + + private static void disableSingle() { + RxJavaPlugins.setOnSingleSubscribe(oldOnSingleSubscribe); + oldOnSingleSubscribe = null; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void disableMaybe() { + RxJavaPlugins.setOnMaybeSubscribe( + (BiFunction) oldOnMaybeSubscribe); + oldOnMaybeSubscribe = null; + } + + private static void disableWithSpanStrategy() { + AsyncSpanEndStrategies.getInstance().unregisterStrategy(RxJava2AsyncSpanEndStrategy.class); + } + + private static Function compose( + Function before, Function after) { + if (before == null) { + return after; + } + return (T v) -> after.apply(before.apply(v)); + } + + private static BiFunction biCompose( + BiFunction before, + BiFunction after) { + if (before == null) { + return after; + } + return (T v, U u) -> after.apply(v, before.apply(v, u)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingAssemblyBuilder.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingAssemblyBuilder.java new file mode 100644 index 000000000..050d039ff --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingAssemblyBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava2; + +public final class TracingAssemblyBuilder { + private boolean captureExperimentalSpanAttributes; + + TracingAssemblyBuilder() {} + + public TracingAssemblyBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + public TracingAssembly build() { + return new TracingAssembly(captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingCompletableObserver.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingCompletableObserver.java new file mode 100644 index 000000000..f9d8af384 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingCompletableObserver.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.CompletableObserver; +import io.reactivex.disposables.Disposable; +import io.reactivex.internal.disposables.DisposableHelper; + +class TracingCompletableObserver implements CompletableObserver, Disposable { + + private final CompletableObserver actual; + private final Context context; + private Disposable disposable; + + TracingCompletableObserver(CompletableObserver actual, Context context) { + this.actual = actual; + this.context = context; + } + + @Override + public void onSubscribe(Disposable d) { + if (!DisposableHelper.validate(disposable, d)) { + return; + } + disposable = d; + actual.onSubscribe(this); + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + actual.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + try (Scope ignored = context.makeCurrent()) { + actual.onError(e); + } + } + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingConditionalSubscriber.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingConditionalSubscriber.java new file mode 100644 index 000000000..78171e920 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingConditionalSubscriber.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.internal.fuseable.ConditionalSubscriber; +import io.reactivex.internal.fuseable.QueueSubscription; +import io.reactivex.internal.subscribers.BasicFuseableConditionalSubscriber; + +class TracingConditionalSubscriber extends BasicFuseableConditionalSubscriber { + + // BasicFuseableConditionalSubscriber#actual has been renamed to downstream in newer versions, we + // can't use it in this class + private final ConditionalSubscriber wrappedSubscriber; + private final Context context; + + TracingConditionalSubscriber(ConditionalSubscriber actual, Context context) { + super(actual); + this.wrappedSubscriber = actual; + this.context = context; + } + + @Override + public boolean tryOnNext(T t) { + try (Scope ignored = context.makeCurrent()) { + return wrappedSubscriber.tryOnNext(t); + } + } + + @Override + public void onNext(T t) { + try (Scope ignored = context.makeCurrent()) { + wrappedSubscriber.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + try (Scope ignored = context.makeCurrent()) { + wrappedSubscriber.onError(t); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + wrappedSubscriber.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + QueueSubscription qs = this.qs; + if (qs != null) { + int m = qs.requestFusion(mode); + sourceMode = m; + return m; + } + return NONE; + } + + @Override + public T poll() throws Exception { + return qs.poll(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingMaybeObserver.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingMaybeObserver.java new file mode 100644 index 000000000..22098edd1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingMaybeObserver.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.MaybeObserver; +import io.reactivex.disposables.Disposable; +import io.reactivex.internal.disposables.DisposableHelper; + +class TracingMaybeObserver implements MaybeObserver, Disposable { + + private final MaybeObserver actual; + private final Context context; + private Disposable disposable; + + TracingMaybeObserver(MaybeObserver actual, Context context) { + this.actual = actual; + this.context = context; + } + + @Override + public void onSubscribe(Disposable d) { + if (!DisposableHelper.validate(disposable, d)) { + return; + } + disposable = d; + actual.onSubscribe(this); + } + + @Override + public void onSuccess(T t) { + try (Scope ignored = context.makeCurrent()) { + actual.onSuccess(t); + } + } + + @Override + public void onError(Throwable e) { + try (Scope ignored = context.makeCurrent()) { + actual.onError(e); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + actual.onComplete(); + } + } + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingObserver.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingObserver.java new file mode 100644 index 000000000..82d6bd363 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingObserver.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.Observer; +import io.reactivex.internal.fuseable.QueueDisposable; +import io.reactivex.internal.observers.BasicFuseableObserver; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; + +class TracingObserver extends BasicFuseableObserver { + private static final MethodHandle queueDisposableGetter = getQueueDisposableGetter(); + + // BasicFuseableObserver#actual has been renamed to downstream in newer versions, we can't use it + // in this class + private final Observer wrappedObserver; + private final Context context; + + TracingObserver(Observer actual, Context context) { + super(actual); + this.wrappedObserver = actual; + this.context = context; + } + + @Override + public void onNext(T t) { + try (Scope ignored = context.makeCurrent()) { + wrappedObserver.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + try (Scope ignored = context.makeCurrent()) { + wrappedObserver.onError(t); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + wrappedObserver.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + QueueDisposable qd = getQueueDisposable(); + if (qd != null) { + int m = qd.requestFusion(mode); + sourceMode = m; + return m; + } + return NONE; + } + + @Override + public T poll() throws Exception { + return getQueueDisposable().poll(); + } + + private QueueDisposable getQueueDisposable() { + try { + return (QueueDisposable) queueDisposableGetter.invoke(this); + } catch (Throwable throwable) { + throw new IllegalStateException(throwable); + } + } + + private static MethodHandle getGetterHandle(String fieldName) { + try { + return MethodHandles.lookup() + .findGetter(BasicFuseableObserver.class, fieldName, QueueDisposable.class); + } catch (NoSuchFieldException | IllegalAccessException ignored) { + // Ignore + } + return null; + } + + private static MethodHandle getQueueDisposableGetter() { + MethodHandle getter = getGetterHandle("qd"); + if (getter == null) { + // in versions before 2.2.1 field was named "qs" + getter = getGetterHandle("qs"); + } + return getter; + } + + public static boolean canEnable() { + return queueDisposableGetter != null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingParallelFlowable.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingParallelFlowable.java new file mode 100644 index 000000000..de279ee18 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingParallelFlowable.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.internal.fuseable.ConditionalSubscriber; +import io.reactivex.parallel.ParallelFlowable; +import org.reactivestreams.Subscriber; + +class TracingParallelFlowable extends ParallelFlowable { + + private final ParallelFlowable source; + private final Context context; + + TracingParallelFlowable(ParallelFlowable source, Context context) { + this.source = source; + this.context = context; + } + + @SuppressWarnings("unchecked") + @Override + public void subscribe(Subscriber[] subscribers) { + if (!validate(subscribers)) { + return; + } + int n = subscribers.length; + Subscriber[] parents = new Subscriber[n]; + for (int i = 0; i < n; i++) { + Subscriber z = subscribers[i]; + if (z instanceof ConditionalSubscriber) { + parents[i] = + new TracingConditionalSubscriber<>((ConditionalSubscriber) z, context); + } else { + parents[i] = new TracingSubscriber<>(z, context); + } + } + try (Scope ignored = context.makeCurrent()) { + source.subscribe(parents); + } + } + + @Override + public int parallelism() { + return source.parallelism(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingSingleObserver.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingSingleObserver.java new file mode 100644 index 000000000..97e9326d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingSingleObserver.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.SingleObserver; +import io.reactivex.disposables.Disposable; +import io.reactivex.internal.disposables.DisposableHelper; + +class TracingSingleObserver implements SingleObserver, Disposable { + + private final SingleObserver actual; + private final Context context; + private Disposable disposable; + + TracingSingleObserver(SingleObserver actual, Context context) { + this.actual = actual; + this.context = context; + } + + @Override + public void onSubscribe(Disposable d) { + if (!DisposableHelper.validate(disposable, d)) { + return; + } + this.disposable = d; + actual.onSubscribe(this); + } + + @Override + public void onSuccess(T t) { + try (Scope ignored = context.makeCurrent()) { + actual.onSuccess(t); + } + } + + @Override + public void onError(Throwable throwable) { + try (Scope ignored = context.makeCurrent()) { + actual.onError(throwable); + } + } + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingSubscriber.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingSubscriber.java new file mode 100644 index 000000000..2dfe46364 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava2/TracingSubscriber.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.internal.fuseable.QueueSubscription; +import io.reactivex.internal.subscribers.BasicFuseableSubscriber; +import org.reactivestreams.Subscriber; + +class TracingSubscriber extends BasicFuseableSubscriber { + + // BasicFuseableSubscriber#actual has been renamed to downstream in newer versions, we can't use + // it in this class + private final Subscriber wrappedSubscriber; + private final Context context; + + TracingSubscriber(Subscriber actual, Context context) { + super(actual); + this.wrappedSubscriber = actual; + this.context = context; + } + + @Override + public void onNext(T t) { + try (Scope ignored = context.makeCurrent()) { + wrappedSubscriber.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + try (Scope ignored = context.makeCurrent()) { + wrappedSubscriber.onError(t); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + wrappedSubscriber.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + QueueSubscription qs = this.qs; + if (qs != null) { + int m = qs.requestFusion(mode); + sourceMode = m; + return m; + } + return NONE; + } + + @Override + public T poll() throws Exception { + return qs.poll(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2AsyncSpanEndStrategyTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2AsyncSpanEndStrategyTest.groovy new file mode 100644 index 000000000..711d75805 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2AsyncSpanEndStrategyTest.groovy @@ -0,0 +1,1042 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.api.tracer.BaseTracer +import io.opentelemetry.instrumentation.rxjava2.RxJava2AsyncSpanEndStrategy +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.observers.TestObserver +import io.reactivex.parallel.ParallelFlowable +import io.reactivex.processors.ReplayProcessor +import io.reactivex.processors.UnicastProcessor +import io.reactivex.subjects.CompletableSubject +import io.reactivex.subjects.MaybeSubject +import io.reactivex.subjects.ReplaySubject +import io.reactivex.subjects.SingleSubject +import io.reactivex.subjects.UnicastSubject +import io.reactivex.subscribers.TestSubscriber +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import spock.lang.Specification + +class RxJava2AsyncSpanEndStrategyTest extends Specification { + BaseTracer tracer + + Context context + + Span span + + def underTest = RxJava2AsyncSpanEndStrategy.create() + + def underTestWithExperimentalAttributes = RxJava2AsyncSpanEndStrategy.newBuilder() + .setCaptureExperimentalSpanAttributes(true) + .build() + + void setup() { + tracer = Mock() + context = Mock() + span = Mock() + span.storeInContext(_) >> { callRealMethod() } + } + + static class CompletableTest extends RxJava2AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Completable) + } + + def "ends span on already completed"() { + given: + def observer = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, Completable.complete()) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, Completable.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = CompletableSubject.create() + def observer = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = CompletableSubject.create() + def observer = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = CompletableSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Completable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = CompletableSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Completable) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = CompletableSubject.create() + def observer1 = new TestObserver() + def observer2 = new TestObserver() + def observer3 = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer1.assertComplete() + observer2.assertComplete() + observer3.assertComplete() + } + } + + static class MaybeTest extends RxJava2AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Maybe) + } + + def "ends span on already completed"() { + given: + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, Maybe.just("Value")) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already empty"() { + given: + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, Maybe.empty()) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, Maybe.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = MaybeSubject.create() + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onSuccess("Value") + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when empty"() { + given: + def source = MaybeSubject.create() + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = MaybeSubject.create() + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = MaybeSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = MaybeSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Maybe) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = MaybeSubject.create() + def observer1 = new TestObserver() + def observer2 = new TestObserver() + def observer3 = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onSuccess("Value") + + then: + 1 * tracer.end(context) + observer1.assertValue("Value") + observer1.assertComplete() + observer2.assertValue("Value") + observer2.assertComplete() + observer3.assertValue("Value") + observer3.assertComplete() + } + } + + static class SingleTest extends RxJava2AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Single) + } + + def "ends span on already completed"() { + given: + def observer = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, Single.just("Value")) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, Single.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = SingleSubject.create() + def observer = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onSuccess("Value") + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = SingleSubject.create() + def observer = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = SingleSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Single) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = SingleSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Single) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = SingleSubject.create() + def observer1 = new TestObserver() + def observer2 = new TestObserver() + def observer3 = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onSuccess("Value") + + then: + 1 * tracer.end(context) + observer1.assertValue("Value") + observer1.assertComplete() + observer2.assertValue("Value") + observer2.assertComplete() + observer3.assertValue("Value") + observer3.assertComplete() + } + } + + static class ObservableTest extends RxJava2AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Observable) + } + + def "ends span on already completed"() { + given: + def observer = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, Observable.just("Value")) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, Observable.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = UnicastSubject.create() + def observer = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = UnicastSubject.create() + def observer = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = UnicastSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Observable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = UnicastSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Observable) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = ReplaySubject.create() + def observer1 = new TestObserver() + def observer2 = new TestObserver() + def observer3 = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer1.assertComplete() + observer2.assertComplete() + observer3.assertComplete() + } + } + + static class FlowableTest extends RxJava2AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Flowable) + } + + def "ends span on already completed"() { + given: + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, Flowable.just("Value")) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, Flowable.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flowable) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = ReplayProcessor.create() + def observer1 = new TestSubscriber() + def observer2 = new TestSubscriber() + def observer3 = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer1.assertComplete() + observer2.assertComplete() + observer3.assertComplete() + } + } + + static class ParallelFlowableTest extends RxJava2AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(ParallelFlowable) + } + + def "ends span on already completed"() { + given: + def observer = new TestSubscriber() + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, Flowable.just("Value").parallel()) + result.sequential().subscribe(observer) + + then: + observer.assertComplete() + 1 * tracer.end(context) + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestSubscriber() + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, Flowable.error(exception).parallel()) + result.sequential().subscribe(observer) + + then: + observer.assertError(exception) + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span when completed"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, source.parallel()) + result.sequential().subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + observer.assertComplete() + 1 * tracer.end(context) + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, source.parallel()) + result.sequential().subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + observer.assertError(exception) + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span when cancelled"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, source.parallel()) + result.sequential().subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (ParallelFlowable) underTestWithExperimentalAttributes.end(tracer, context, source.parallel()) + result.sequential().subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + } + + static class PublisherTest extends RxJava2AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Publisher) + } + + def "ends span when completed"() { + given: + def source = new CustomPublisher() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = new CustomPublisher() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = new CustomPublisher() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = new CustomPublisher() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flowable) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + } + + static class CustomPublisher implements Publisher, Subscription { + Subscriber subscriber + + @Override + void subscribe(Subscriber subscriber) { + this.subscriber = subscriber + subscriber.onSubscribe(this) + } + + def onComplete() { + this.subscriber.onComplete() + } + + def onError(Throwable exception) { + this.subscriber.onError(exception) + } + + @Override + void request(long l) { } + + @Override + void cancel() { } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2SubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2SubscriptionTest.groovy new file mode 100644 index 000000000..935e49de1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2SubscriptionTest.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava2.AbstractRxJava2SubscriptionTest +import io.opentelemetry.instrumentation.rxjava2.TracingAssembly +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import spock.lang.Shared + +class RxJava2SubscriptionTest extends AbstractRxJava2SubscriptionTest implements LibraryTestTrait { + @Shared + TracingAssembly tracingAssembly = TracingAssembly.create() + + def setupSpec() { + tracingAssembly.enable() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2Test.groovy new file mode 100644 index 000000000..8b9ca49ea --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/library/src/test/groovy/RxJava2Test.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava2.AbstractRxJava2Test +import io.opentelemetry.instrumentation.rxjava2.TracingAssembly +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import spock.lang.Shared + +class RxJava2Test extends AbstractRxJava2Test implements LibraryTestTrait { + @Shared + TracingAssembly tracingAssembly = TracingAssembly.create() + + def setupSpec() { + tracingAssembly.enable() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/rxjava-2.0-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/rxjava-2.0-testing.gradle new file mode 100644 index 000000000..74aaba1de --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/rxjava-2.0-testing.gradle @@ -0,0 +1,13 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "io.reactivex.rxjava2:rxjava:2.1.3" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "io.opentelemetry:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/AbstractRxJava2SubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/AbstractRxJava2SubscriptionTest.groovy new file mode 100644 index 000000000..752472677 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/AbstractRxJava2SubscriptionTest.groovy @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava2 + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.functions.Consumer + +import java.util.concurrent.CountDownLatch + +abstract class AbstractRxJava2SubscriptionTest extends InstrumentationSpecification { + + def "subscribe single test"() { + when: + CountDownLatch latch = new CountDownLatch(1) + runUnderTrace("parent") { + Single connection = Single.create { + it.onSuccess(new Connection()) + } + connection.subscribe(new Consumer() { + @Override + void accept(Connection t) { + t.query() + latch.countDown() + } + }) + } + latch.await() + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "Connection.query", span(0)) + } + } + } + + def "test observable fusion"() { + when: + CountDownLatch latch = new CountDownLatch(1) + runUnderTrace("parent") { + Observable integerObservable = Observable.just(1, 2, 3, 4) + integerObservable.concatMap({ + return Observable.just(it) + }).count().subscribe(new Consumer() { + @Override + void accept(Long count) { + runInternalSpan("child") + latch.countDown() + } + }) + } + latch.await() + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "child", span(0)) + } + } + } + + static class Connection { + static int query() { + def span = GlobalOpenTelemetry.getTracer("test").spanBuilder("Connection.query").startSpan() + span.end() + return new Random().nextInt() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/AbstractRxJava2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/AbstractRxJava2Test.groovy new file mode 100644 index 000000000..d2f9ca54c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/AbstractRxJava2Test.groovy @@ -0,0 +1,412 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava2 + +import io.opentelemetry.api.common.AttributeKey + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTraceWithoutExceptionCatch +import static java.util.concurrent.TimeUnit.MILLISECONDS + +import com.google.common.collect.Lists +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.reactivex.BackpressureStrategy +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.internal.operators.flowable.FlowablePublish +import io.reactivex.internal.operators.observable.ObservablePublish +import io.reactivex.schedulers.Schedulers +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import spock.lang.Shared +import spock.lang.Unroll + +/** + *

Tests in this class may seem not exhaustive due to the fact that some classes are converted + * into others, ie. {@link Completable#toMaybe()}. Fortunately, RxJava2 uses helper classes like + * {@link io.reactivex.internal.operators.maybe.MaybeFromCompletable} and as a result we + * can test subscriptions and cancellations correctly. + */ +@Unroll +abstract class AbstractRxJava2Test extends InstrumentationSpecification { + + public static final String EXCEPTION_MESSAGE = "test exception" + + @Shared + def addOne = { i -> + addOneFunc(i) + } + + @Shared + def addTwo = { i -> + addTwoFunc(i) + } + + @Shared + def throwException = { + throw new IllegalStateException(EXCEPTION_MESSAGE) + } + + static addOneFunc(int i) { + runUnderTrace("addOne") { + return i + 1 + } + } + + static addTwoFunc(int i) { + runUnderTrace("addTwo") { + return i + 2 + } + } + + def "Publisher '#name' test"() { + when: + def result = assemblePublisherUnderTrace(publisherSupplier) + + then: + result == expected + and: + assertTraces(1) { + sortSpansByStartTime() + trace(0, workSpans + 1) { + + basicSpan(it, 0, "publisher-parent") + for (int i = 1; i < workSpans + 1; ++i) { + basicSpan(it, i, "addOne", span(0)) + } + } + } + + where: + name | expected | workSpans | publisherSupplier + "basic maybe" | 2 | 1 | { -> Maybe.just(1).map(addOne) } + "two operations maybe" | 4 | 2 | { -> Maybe.just(2).map(addOne).map(addOne) } + "delayed maybe" | 4 | 1 | { -> + Maybe.just(3).delay(100, MILLISECONDS).map(addOne) + } + "delayed twice maybe" | 6 | 2 | { -> + Maybe.just(4).delay(100, MILLISECONDS).map(addOne).delay(100, MILLISECONDS).map(addOne) + } + "basic flowable" | [6, 7] | 2 | { -> + Flowable.fromIterable([5, 6]).map(addOne) + } + "two operations flowable" | [8, 9] | 4 | { -> + Flowable.fromIterable([6, 7]).map(addOne).map(addOne) + } + "delayed flowable" | [8, 9] | 2 | { -> + Flowable.fromIterable([7, 8]).delay(100, MILLISECONDS).map(addOne) + } + "delayed twice flowable" | [10, 11] | 4 | { -> + Flowable.fromIterable([8, 9]).delay(100, MILLISECONDS).map(addOne).delay(100, MILLISECONDS).map(addOne) + } + "maybe from callable" | 12 | 2 | { -> + Maybe.fromCallable({ addOneFunc(10) }).map(addOne) + } + "basic single" | 1 | 1 | { -> Single.just(0).map(addOne) } + "basic observable" | [1] | 1 | { -> Observable.just(0).map(addOne) } + "connectable flowable" | [1] | 1 | { -> + FlowablePublish.just(0).delay(100, MILLISECONDS).map(addOne) + } + "connectable observable" | [1] | 1 | { -> + ObservablePublish.just(0).delay(100, MILLISECONDS).map(addOne) + } + } + + def "Publisher error '#name' test"() { + when: + assemblePublisherUnderTrace(publisherSupplier) + + then: + def thrownException = thrown RuntimeException + thrownException.message == EXCEPTION_MESSAGE + and: + assertTraces(1) { + sortSpansByStartTime() + trace(0, 1) { + // It's important that we don't attach errors at the Reactor level so that we don't + // impact the spans on reactor integrations such as netty and lettuce, as reactor is + // more of a context propagation mechanism than something we would be tracking for + // errors this is ok. + basicSpan(it, 0, "publisher-parent") + } + } + + where: + name | publisherSupplier + "maybe" | { -> Maybe.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "flowable" | { -> Flowable.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "single" | { -> Single.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "observable" | { -> Observable.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "completable" | { -> Completable.error(new RuntimeException(EXCEPTION_MESSAGE)) } + } + + def "Publisher step '#name' test"() { + when: + assemblePublisherUnderTrace(publisherSupplier) + + then: + def exception = thrown RuntimeException + exception.message == EXCEPTION_MESSAGE + and: + assertTraces(1) { + sortSpansByStartTime() + trace(0, workSpans + 1) { + // It's important that we don't attach errors at the Reactor level so that we don't + // impact the spans on reactor integrations such as netty and lettuce, as reactor is + // more of a context propagation mechanism than something we would be tracking for + // errors this is ok. + basicSpan(it, 0, "publisher-parent") + + for (int i = 1; i < workSpans + 1; i++) { + basicSpan(it, i, "addOne", span(0)) + } + } + } + + where: + name | workSpans | publisherSupplier + "basic maybe failure" | 1 | { -> + Maybe.just(1).map(addOne).map({ throwException() }) + } + "basic flowable failure" | 1 | { -> + Flowable.fromIterable([5, 6]).map(addOne).map({ throwException() }) + } + } + + def "Publisher '#name' cancel"() { + when: + cancelUnderTrace(publisherSupplier) + + then: + assertTraces(1) { + trace(0, 1) { + basicSpan(it, 0, "publisher-parent") + } + } + + where: + name | publisherSupplier + "basic maybe" | { -> Maybe.just(1) } + "basic flowable" | { -> Flowable.fromIterable([5, 6]) } + "basic single" | { -> Single.just(1) } + "basic completable" | { -> Completable.fromCallable({ -> 1 }) } + "basic observable" | { -> Observable.just(1) } + } + + def "Publisher chain spans have the correct parent for '#name'"() { + when: + assemblePublisherUnderTrace(publisherSupplier) + + then: + assertTraces(1) { + trace(0, workSpans + 1) { + basicSpan(it, 0, "publisher-parent") + + for (int i = 1; i < workSpans + 1; i++) { + basicSpan(it, i, "addOne", span(0)) + } + } + } + + where: + name | workSpans | publisherSupplier + "basic maybe" | 3 | { -> + Maybe.just(1).map(addOne).map(addOne).concatWith(Maybe.just(1).map(addOne)) + } + "basic flowable" | 5 | { -> + Flowable.fromIterable([5, 6]).map(addOne).map(addOne).concatWith(Maybe.just(1).map(addOne).toFlowable()) + } + } + + def "Publisher chain spans have the correct parents from subscription time"() { + when: + def maybe = Maybe.just(42) + .map(addOne) + .map(addTwo) + + runUnderTrace("trace-parent") { + maybe.blockingGet() + } + + then: + assertTraces(1) { + trace(0, 3) { + sortSpansByStartTime() + basicSpan(it, 0, "trace-parent") + basicSpan(it, 1, "addOne", span(0)) + basicSpan(it, 2, "addTwo", span(0)) + } + } + } + + def "Publisher chain spans have the correct parents from subscription time '#name'"() { + when: + assemblePublisherUnderTrace { + // The "add one" operations in the publisher created here should be children of the publisher-parent + def publisher = publisherSupplier() + + runUnderTrace("intermediate") { + if (publisher instanceof Maybe) { + return ((Maybe) publisher).map(addTwo) + } else if (publisher instanceof Flowable) { + return ((Flowable) publisher).map(addTwo) + } else if (publisher instanceof Single) { + return ((Single) publisher).map(addTwo) + } else if (publisher instanceof Observable) { + return ((Observable) publisher).map(addTwo) + } else if (publisher instanceof Completable) { + return ((Completable) publisher).toMaybe().map(addTwo) + } + throw new IllegalStateException("Unknown publisher type") + } + } + + then: + assertTraces(1) { + trace(0, 2 + 2 * workItems) { + sortSpansByStartTime() + basicSpan(it, 0, "publisher-parent") + basicSpan(it, 1, "intermediate", span(0)) + + for (int i = 2; i < 2 + 2 * workItems; i = i + 2) { + basicSpan(it, i, "addOne", span(0)) + basicSpan(it, i + 1, "addTwo", span(0)) + } + } + } + + where: + name | workItems | publisherSupplier + "basic maybe" | 1 | { -> Maybe.just(1).map(addOne) } + "basic flowable" | 2 | { -> Flowable.fromIterable([1, 2]).map(addOne) } + "basic single" | 1 | { -> Single.just(1).map(addOne) } + "basic observable" | 1 | { -> Observable.just(1).map(addOne) } + } + + def "Flowables produce the right number of results '#scheduler'"() { + when: + List values = runUnderTrace("flowable root") { + Flowable.fromIterable([1, 2, 3, 4]) + .parallel() + .runOn(scheduler) + .flatMap({ num -> + Maybe.just(num).map(addOne).toFlowable() + }) + .sequential() + .toList() + .blockingGet() + } + + then: + values.size() == 4 + assertTraces(1) { + trace(0, 5) { + basicSpan(it, 0, "flowable root") + for (int i = 1; i < values.size() + 1; i++) { + basicSpan(it, i, "addOne", span(0)) + } + } + } + + where: + scheduler << [Schedulers.newThread(), Schedulers.computation(), Schedulers.single(), Schedulers.trampoline()] + } + + def "test many ongoing trace chains on '#scheduler'"() { + setup: + int iterations = 100 + Set remainingIterations = new HashSet<>((0L..(iterations - 1)).toList()) + + when: + RxJava2ConcurrencyTestHelper.launchAndWait(scheduler, iterations, 60000) + + then: + assertTraces(iterations) { + for (int i = 0; i < iterations; i++) { + trace(i, 3) { + long iteration = -1 + span(0) { + name("outer") + iteration = span.getAttributes().get(AttributeKey.longKey("iteration")).toLong() + assert remainingIterations.remove(iteration) + } + span(1) { + name("middle") + childOf(span(0)) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + span(2) { + name("inner") + childOf(span(1)) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + } + } + } + + assert remainingIterations.isEmpty() + + where: + scheduler << [Schedulers.newThread(), Schedulers.computation(), Schedulers.single(), Schedulers.trampoline()] + } + + def cancelUnderTrace(def publisherSupplier) { + runUnderTraceWithoutExceptionCatch("publisher-parent") { + def publisher = publisherSupplier() + if (publisher instanceof Maybe) { + publisher = publisher.toFlowable() + } else if (publisher instanceof Single) { + publisher = publisher.toFlowable() + } else if (publisher instanceof Completable) { + publisher = publisher.toFlowable() + } else if (publisher instanceof Observable) { + publisher = publisher.toFlowable(BackpressureStrategy.LATEST) + } + + publisher.subscribe(new Subscriber() { + void onSubscribe(Subscription subscription) { + subscription.cancel() + } + + void onNext(Integer t) { + } + + void onError(Throwable error) { + } + + void onComplete() { + } + }) + } + } + + @SuppressWarnings("unchecked") + def assemblePublisherUnderTrace(def publisherSupplier) { + // The "add two" operations below should be children of this span + runUnderTraceWithoutExceptionCatch("publisher-parent") { + def publisher = publisherSupplier() + + // Read all data from publisher + if (publisher instanceof Maybe) { + return ((Maybe) publisher).blockingGet() + } else if (publisher instanceof Flowable) { + return Lists.newArrayList(((Flowable) publisher).blockingIterable()) + } else if (publisher instanceof Single) { + return ((Single) publisher).blockingGet() + } else if (publisher instanceof Observable) { + return Lists.newArrayList(((Observable) publisher).blockingIterable()) + } else if (publisher instanceof Completable) { + return ((Completable) publisher).toMaybe().blockingGet() + } + + throw new IllegalStateException("Unknown publisher: " + publisher) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/RxJava2ConcurrencyTestHelper.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/RxJava2ConcurrencyTestHelper.java new file mode 100644 index 000000000..8884ecbca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava2/RxJava2ConcurrencyTestHelper.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava2; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.test.utils.TraceUtils; +import io.reactivex.Scheduler; +import io.reactivex.Single; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * This test creates the specified number of traces with three spans: 1) Outer (root) span 2) Middle + * span, child of outer, created in success handler of the chain subscribed to in the context of the + * outer span (with some delay and map thrown in for good measure) 3) Inner span, child of middle, + * created in the success handler of a new chain started and subscribed to in the the middle span + * + *

The varying delays between the stages where each span is created should guarantee that + * scheduler threads handling various stages of the chain will have to alternate between contexts + * from different traces. + */ +public class RxJava2ConcurrencyTestHelper { + public static void launchAndWait(Scheduler scheduler, int iterations, long timeoutMillis) { + CountDownLatch latch = new CountDownLatch(iterations); + + for (int i = 0; i < iterations; i++) { + launchOuter(new Iteration(scheduler, latch, i)); + } + + try { + // Continue even on timeout so the test assertions can show what is missing + //noinspection ResultOfMethodCallIgnored + latch.await(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private static void launchOuter(Iteration iteration) { + TraceUtils.runUnderTrace( + "outer", + () -> { + Span.current().setAttribute("iteration", iteration.index); + + Single.fromCallable(() -> iteration) + .subscribeOn(iteration.scheduler) + .observeOn(iteration.scheduler) + // Use varying delay so that different stages of the chain would alternate. + .delay(iteration.index % 10, TimeUnit.MILLISECONDS, iteration.scheduler) + .map((it) -> it) + .delay(iteration.index % 10, TimeUnit.MILLISECONDS, iteration.scheduler) + .doOnSuccess(RxJava2ConcurrencyTestHelper::launchInner) + .subscribe(); + + return null; + }); + } + + private static void launchInner(Iteration iteration) { + TraceUtils.runUnderTrace( + "middle", + () -> { + Span.current().setAttribute("iteration", iteration.index); + + Single.fromCallable(() -> iteration) + .subscribeOn(iteration.scheduler) + .observeOn(iteration.scheduler) + .delay(iteration.index % 10, TimeUnit.MILLISECONDS, iteration.scheduler) + .doOnSuccess( + (it) -> { + TraceUtils.runUnderTrace( + "inner", + () -> { + Span.current().setAttribute("iteration", it.index); + return null; + }); + it.countDown.countDown(); + }) + .subscribe(); + + return null; + }); + } + + private static class Iteration { + public final Scheduler scheduler; + public final CountDownLatch countDown; + public final int index; + + private Iteration(Scheduler scheduler, CountDownLatch countDown, int index) { + this.scheduler = scheduler; + this.countDown = countDown; + this.index = index; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/rxjava-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/rxjava-3.0-javaagent.gradle new file mode 100644 index 000000000..f6027d3f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/rxjava-3.0-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.reactivex.rxjava3" + module = "rxjava" + versions = "[3.0.0,)" + assertInverse = true + } +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.rxjava.experimental-span-attributes=true" +} + +dependencies { + library "io.reactivex.rxjava3:rxjava:3.0.0" + + implementation project(":instrumentation:rxjava:rxjava-3.0:library") + + testImplementation "io.opentelemetry:opentelemetry-extension-annotations" + testImplementation project(':instrumentation:rxjava:rxjava-3.0:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/RxJava3InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/RxJava3InstrumentationModule.java new file mode 100644 index 000000000..8f2848abd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/RxJava3InstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rxjava3; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class RxJava3InstrumentationModule extends InstrumentationModule { + + public RxJava3InstrumentationModule() { + super("rxjava3"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new RxJavaPluginsInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/RxJavaPluginsInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/RxJavaPluginsInstrumentation.java new file mode 100644 index 000000000..fef2852cb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/RxJavaPluginsInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rxjava3; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RxJavaPluginsInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.reactivex.rxjava3.plugins.RxJavaPlugins"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(isMethod(), this.getClass().getName() + "$MethodAdvice"); + } + + @SuppressWarnings("unused") + public static class MethodAdvice { + + // TODO(anuraaga): Replace with adding a type initializer to RxJavaPlugins + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2685 + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void activateOncePerClassloader() { + TracingAssemblyActivation.activate(RxJavaPlugins.class); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/TracingAssemblyActivation.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/TracingAssemblyActivation.java new file mode 100644 index 000000000..52699d9a4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava3/TracingAssemblyActivation.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rxjava3; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.rxjava3.TracingAssembly; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class TracingAssemblyActivation { + + private static final ClassValue activated = + new ClassValue() { + @Override + protected AtomicBoolean computeValue(Class type) { + return new AtomicBoolean(); + } + }; + + public static void activate(Class clz) { + if (activated.get(clz).compareAndSet(false, true)) { + TracingAssembly.newBuilder() + .setCaptureExperimentalSpanAttributes( + Config.get() + .getBooleanProperty( + "otel.instrumentation.rxjava.experimental-span-attributes", false)) + .build() + .enable(); + } + } + + private TracingAssemblyActivation() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy new file mode 100644 index 000000000..32b5b607f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava3.AbstractRxJava3SubscriptionTest +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest implements AgentTestTrait { + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3Test.groovy new file mode 100644 index 000000000..1cd151d9f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3Test.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava3.AbstractRxJava3Test +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class RxJava3Test extends AbstractRxJava3Test implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3WithSpanInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3WithSpanInstrumentationTest.groovy new file mode 100644 index 000000000..c196cbb98 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3WithSpanInstrumentationTest.groovy @@ -0,0 +1,1044 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.instrumentation.rxjava3.TracedWithSpan +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.observers.TestObserver +import io.reactivex.rxjava3.processors.UnicastProcessor +import io.reactivex.rxjava3.subjects.CompletableSubject +import io.reactivex.rxjava3.subjects.MaybeSubject +import io.reactivex.rxjava3.subjects.SingleSubject +import io.reactivex.rxjava3.subjects.UnicastSubject +import io.reactivex.rxjava3.subscribers.TestSubscriber +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription + +class RxJava3WithSpanInstrumentationTest extends AgentInstrumentationSpecification { + + def "should capture span for already completed Completable"() { + setup: + def observer = new TestObserver() + def source = Completable.complete() + new TracedWithSpan() + .completable(source) + .subscribe(observer) + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Completable"() { + setup: + def source = CompletableSubject.create() + def observer = new TestObserver() + new TracedWithSpan() + .completable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Completable"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestObserver() + def source = Completable.error(error) + new TracedWithSpan() + .completable(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Completable"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = CompletableSubject.create() + def observer = new TestObserver() + new TracedWithSpan() + .completable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Completable"() { + setup: + def source = CompletableSubject.create() + def observer = new TestObserver() + new TracedWithSpan() + .completable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.dispose() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.completable" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed Maybe"() { + setup: + def observer = new TestObserver() + def source = Maybe.just("Value") + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already empty Maybe"() { + setup: + def observer = new TestObserver() + def source = Maybe. empty() + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Maybe"() { + setup: + def source = MaybeSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onSuccess("Value") + observer.assertValue("Value") + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Maybe"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestObserver() + def source = Maybe. error(error) + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Maybe"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = MaybeSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Maybe"() { + setup: + def source = MaybeSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .maybe(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.dispose() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.maybe" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed Single"() { + setup: + def observer = new TestObserver() + def source = Single.just("Value") + new TracedWithSpan() + .single(source) + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Single"() { + setup: + def source = SingleSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .single(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onSuccess("Value") + observer.assertValue("Value") + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Single"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestObserver() + def source = Single. error(error) + new TracedWithSpan() + .single(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Single"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = SingleSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .single(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Single"() { + setup: + def source = SingleSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .single(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.dispose() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.single" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed Observable"() { + setup: + def observer = new TestObserver() + def source = Observable. just("Value") + new TracedWithSpan() + .observable(source) + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Observable"() { + setup: + def source = UnicastSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .observable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Observable"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestObserver() + def source = Observable. error(error) + new TracedWithSpan() + .observable(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Observable"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .observable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Observable"() { + setup: + def source = UnicastSubject. create() + def observer = new TestObserver() + new TracedWithSpan() + .observable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.dispose() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.observable" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed Flowable"() { + setup: + def observer = new TestSubscriber() + def source = Flowable. just("Value") + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed Flowable"() { + setup: + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored Flowable"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestSubscriber() + def source = Flowable. error(error) + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Flowable"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Flowable"() { + setup: + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .flowable(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.flowable" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for already completed ParallelFlowable"() { + setup: + def observer = new TestSubscriber() + def source = Flowable. just("Value") + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + observer.assertValue("Value") + observer.assertComplete() + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually completed ParallelFlowable"() { + setup: + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for already errored ParallelFlowable"() { + setup: + def error = new IllegalArgumentException("Boom") + def observer = new TestSubscriber() + def source = Flowable. error(error) + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + observer.assertError(error) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for eventually errored ParallelFlowable"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled ParallelFlowable"() { + setup: + def source = UnicastProcessor. create() + def observer = new TestSubscriber() + new TracedWithSpan() + .parallelFlowable(source.parallel()) + .sequential() + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onNext("Value") + observer.assertValue("Value") + + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.parallelFlowable" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + def "should capture span for eventually completed Publisher"() { + setup: + def source = new CustomPublisher() + def observer = new TestSubscriber() + new TracedWithSpan() + .publisher(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onComplete() + observer.assertComplete() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.publisher" + kind INTERNAL + hasNoParent() + attributes { + } + } + } + } + } + + def "should capture span for eventually errored Publisher"() { + setup: + def error = new IllegalArgumentException("Boom") + def source = new CustomPublisher() + def observer = new TestSubscriber() + new TracedWithSpan() + .publisher(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + source.onError(error) + observer.assertError(error) + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.publisher" + kind INTERNAL + hasNoParent() + status ERROR + errorEvent(IllegalArgumentException, "Boom") + attributes { + } + } + } + } + } + + def "should capture span for canceled Publisher"() { + setup: + def source = new CustomPublisher() + def observer = new TestSubscriber() + new TracedWithSpan() + .publisher(source) + .subscribe(observer) + + expect: + Thread.sleep(500) // sleep a bit just to make sure no span is captured + assertTraces(0) {} + + observer.cancel() + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TracedWithSpan.publisher" + kind INTERNAL + hasNoParent() + attributes { + "rxjava.canceled" true + } + } + } + } + } + + static class CustomPublisher implements Publisher, Subscription { + Subscriber subscriber + + @Override + void subscribe(Subscriber subscriber) { + this.subscriber = subscriber + subscriber.onSubscribe(this) + } + + void onComplete() { + this.subscriber.onComplete() + } + + void onError(Throwable exception) { + this.subscriber.onError(exception) + } + + @Override + void request(long l) {} + + @Override + void cancel() {} + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/rxjava3/TracedWithSpan.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/rxjava3/TracedWithSpan.java new file mode 100644 index 000000000..65785fb5b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/io/opentelemetry/instrumentation/rxjava3/TracedWithSpan.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.extension.annotations.WithSpan; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import org.reactivestreams.Publisher; + +public class TracedWithSpan { + + @WithSpan + public Completable completable(Completable source) { + return source; + } + + @WithSpan + public Maybe maybe(Maybe source) { + return source; + } + + @WithSpan + public Single single(Single source) { + return source; + } + + @WithSpan + public Observable observable(Observable source) { + return source; + } + + @WithSpan + public Flowable flowable(Flowable source) { + return source; + } + + @WithSpan + public ParallelFlowable parallelFlowable(ParallelFlowable source) { + return source; + } + + @WithSpan + public Publisher publisher(Publisher source) { + return source; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/rxjava-3.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/rxjava-3.0-library.gradle new file mode 100644 index 000000000..a69d0ad90 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/rxjava-3.0-library.gradle @@ -0,0 +1,7 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + library "io.reactivex.rxjava3:rxjava:3.0.12" + + testImplementation project(':instrumentation:rxjava:rxjava-3.0:testing') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/RxJava3AsyncSpanEndStrategy.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/RxJava3AsyncSpanEndStrategy.java new file mode 100644 index 000000000..1b901dc54 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/RxJava3AsyncSpanEndStrategy.java @@ -0,0 +1,168 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.functions.BiConsumer; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import java.util.concurrent.atomic.AtomicBoolean; +import org.reactivestreams.Publisher; + +public final class RxJava3AsyncSpanEndStrategy implements AsyncSpanEndStrategy { + private static final AttributeKey CANCELED_ATTRIBUTE_KEY = + AttributeKey.booleanKey("rxjava.canceled"); + + public static RxJava3AsyncSpanEndStrategy create() { + return newBuilder().build(); + } + + public static RxJava3AsyncSpanEndStrategyBuilder newBuilder() { + return new RxJava3AsyncSpanEndStrategyBuilder(); + } + + private final boolean captureExperimentalSpanAttributes; + + RxJava3AsyncSpanEndStrategy(boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + @Override + public boolean supports(Class returnType) { + return returnType == Publisher.class + || returnType == Completable.class + || returnType == Maybe.class + || returnType == Single.class + || returnType == Observable.class + || returnType == Flowable.class + || returnType == ParallelFlowable.class; + } + + @Override + public Object end(BaseTracer tracer, Context context, Object returnValue) { + + EndOnFirstNotificationConsumer notificationConsumer = + new EndOnFirstNotificationConsumer<>(tracer, context); + if (returnValue instanceof Completable) { + return endWhenComplete((Completable) returnValue, notificationConsumer); + } else if (returnValue instanceof Maybe) { + return endWhenMaybeComplete((Maybe) returnValue, notificationConsumer); + } else if (returnValue instanceof Single) { + return endWhenSingleComplete((Single) returnValue, notificationConsumer); + } else if (returnValue instanceof Observable) { + return endWhenObservableComplete((Observable) returnValue, notificationConsumer); + } else if (returnValue instanceof ParallelFlowable) { + return endWhenFirstComplete((ParallelFlowable) returnValue, notificationConsumer); + } + return endWhenPublisherComplete((Publisher) returnValue, notificationConsumer); + } + + private static Completable endWhenComplete( + Completable completable, EndOnFirstNotificationConsumer notificationConsumer) { + return completable + .doOnEvent(notificationConsumer) + .doOnDispose(notificationConsumer::onCancelOrDispose); + } + + private static Maybe endWhenMaybeComplete( + Maybe maybe, EndOnFirstNotificationConsumer notificationConsumer) { + @SuppressWarnings("unchecked") + EndOnFirstNotificationConsumer typedConsumer = + (EndOnFirstNotificationConsumer) notificationConsumer; + return maybe.doOnEvent(typedConsumer).doOnDispose(notificationConsumer::onCancelOrDispose); + } + + private static Single endWhenSingleComplete( + Single single, EndOnFirstNotificationConsumer notificationConsumer) { + @SuppressWarnings("unchecked") + EndOnFirstNotificationConsumer typedConsumer = + (EndOnFirstNotificationConsumer) notificationConsumer; + return single.doOnEvent(typedConsumer).doOnDispose(notificationConsumer::onCancelOrDispose); + } + + private static Observable endWhenObservableComplete( + Observable observable, EndOnFirstNotificationConsumer notificationConsumer) { + return observable + .doOnComplete(notificationConsumer) + .doOnError(notificationConsumer) + .doOnDispose(notificationConsumer::onCancelOrDispose); + } + + private static ParallelFlowable endWhenFirstComplete( + ParallelFlowable parallelFlowable, + EndOnFirstNotificationConsumer notificationConsumer) { + return parallelFlowable + .doOnComplete(notificationConsumer) + .doOnError(notificationConsumer) + .doOnCancel(notificationConsumer::onCancelOrDispose); + } + + private static Flowable endWhenPublisherComplete( + Publisher publisher, EndOnFirstNotificationConsumer notificationConsumer) { + return Flowable.fromPublisher(publisher) + .doOnComplete(notificationConsumer) + .doOnError(notificationConsumer) + .doOnCancel(notificationConsumer::onCancelOrDispose); + } + + /** + * Helper class to ensure that the span is ended exactly once regardless of how many OnComplete or + * OnError notifications are received. Multiple notifications can happen anytime multiple + * subscribers subscribe to the same publisher. + */ + private final class EndOnFirstNotificationConsumer extends AtomicBoolean + implements Action, Consumer, BiConsumer { + + private final BaseTracer tracer; + private final Context context; + + public EndOnFirstNotificationConsumer(BaseTracer tracer, Context context) { + super(false); + this.tracer = tracer; + this.context = context; + } + + public void onCancelOrDispose() { + if (compareAndSet(false, true)) { + if (captureExperimentalSpanAttributes) { + Span.fromContext(context).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + tracer.end(context); + } + } + + @Override + public void run() { + accept(null); + } + + @Override + public void accept(Throwable exception) { + if (compareAndSet(false, true)) { + if (exception != null) { + tracer.endExceptionally(context, exception); + } else { + tracer.end(context); + } + } + } + + @Override + public void accept(T value, Throwable exception) { + accept(exception); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/RxJava3AsyncSpanEndStrategyBuilder.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/RxJava3AsyncSpanEndStrategyBuilder.java new file mode 100644 index 000000000..04ac6cf2a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/RxJava3AsyncSpanEndStrategyBuilder.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava3; + +public final class RxJava3AsyncSpanEndStrategyBuilder { + + private boolean captureExperimentalSpanAttributes; + + RxJava3AsyncSpanEndStrategyBuilder() {} + + public RxJava3AsyncSpanEndStrategyBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + public RxJava3AsyncSpanEndStrategy build() { + return new RxJava3AsyncSpanEndStrategy(captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingAssembly.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingAssembly.java new file mode 100644 index 000000000..08357bbcf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingAssembly.java @@ -0,0 +1,306 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.CompletableObserver; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.MaybeObserver; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import org.checkerframework.checker.lock.qual.GuardedBy; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.reactivestreams.Subscriber; + +/** + * RxJava3 library instrumentation. + * + *

In order to enable RxJava3 instrumentation one has to call the {@link + * TracingAssembly#enable()} method. + * + *

Instrumentation uses on*Assembly and on*Subscribe RxJavaPlugin hooks + * to wrap RxJava3 classes in their tracing equivalents. + * + *

Instrumentation can be disabled by calling the {@link TracingAssembly#disable()} method. + */ +public final class TracingAssembly { + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction + oldOnObservableSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction< + ? super Completable, ? super CompletableObserver, ? extends CompletableObserver> + oldOnCompletableSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction + oldOnSingleSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction + oldOnMaybeSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static BiFunction + oldOnFlowableSubscribe; + + @SuppressWarnings("rawtypes") + @GuardedBy("TracingAssembly.class") + @Nullable + private static Function + oldOnParallelAssembly; + + @GuardedBy("TracingAssembly.class") + private static boolean enabled; + + public static TracingAssembly create() { + return newBuilder().build(); + } + + public static TracingAssemblyBuilder newBuilder() { + return new TracingAssemblyBuilder(); + } + + private final boolean captureExperimentalSpanAttributes; + + TracingAssembly(boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + } + + public void enable() { + synchronized (TracingAssembly.class) { + if (enabled) { + return; + } + + enableObservable(); + + enableCompletable(); + + enableSingle(); + + enableMaybe(); + + enableFlowable(); + + enableParallel(); + + enableWithSpanStrategy(captureExperimentalSpanAttributes); + + enabled = true; + } + } + + public void disable() { + synchronized (TracingAssembly.class) { + if (!enabled) { + return; + } + + disableObservable(); + + disableCompletable(); + + disableSingle(); + + disableMaybe(); + + disableFlowable(); + + disableParallel(); + + disableWithSpanStrategy(); + + enabled = false; + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableParallel() { + oldOnParallelAssembly = RxJavaPlugins.getOnParallelAssembly(); + RxJavaPlugins.setOnParallelAssembly( + compose( + oldOnParallelAssembly, + parallelFlowable -> new TracingParallelFlowable(parallelFlowable, Context.current()))); + } + + private static void enableCompletable() { + oldOnCompletableSubscribe = RxJavaPlugins.getOnCompletableSubscribe(); + RxJavaPlugins.setOnCompletableSubscribe( + biCompose( + oldOnCompletableSubscribe, + (completable, observer) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + return new TracingCompletableObserver(observer, context); + } + })); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableFlowable() { + oldOnFlowableSubscribe = RxJavaPlugins.getOnFlowableSubscribe(); + RxJavaPlugins.setOnFlowableSubscribe( + biCompose( + oldOnFlowableSubscribe, + (flowable, subscriber) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + if (subscriber instanceof ConditionalSubscriber) { + return new TracingConditionalSubscriber<>( + (ConditionalSubscriber) subscriber, context); + } else { + return new TracingSubscriber<>(subscriber, context); + } + } + })); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableObservable() { + oldOnObservableSubscribe = RxJavaPlugins.getOnObservableSubscribe(); + RxJavaPlugins.setOnObservableSubscribe( + biCompose( + oldOnObservableSubscribe, + (observable, observer) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + return new TracingObserver(observer, context); + } + })); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableSingle() { + oldOnSingleSubscribe = RxJavaPlugins.getOnSingleSubscribe(); + RxJavaPlugins.setOnSingleSubscribe( + biCompose( + oldOnSingleSubscribe, + (single, singleObserver) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + return new TracingSingleObserver(singleObserver, context); + } + })); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void enableMaybe() { + oldOnMaybeSubscribe = RxJavaPlugins.getOnMaybeSubscribe(); + RxJavaPlugins.setOnMaybeSubscribe( + (BiFunction) + biCompose( + oldOnMaybeSubscribe, + (BiFunction) + (maybe, maybeObserver) -> { + Context context = Context.current(); + try (Scope ignored = context.makeCurrent()) { + return new TracingMaybeObserver(maybeObserver, context); + } + })); + } + + private static void enableWithSpanStrategy(boolean captureExperimentalSpanAttributes) { + AsyncSpanEndStrategies.getInstance() + .registerStrategy( + RxJava3AsyncSpanEndStrategy.newBuilder() + .setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes) + .build()); + } + + private static void disableParallel() { + RxJavaPlugins.setOnParallelAssembly(oldOnParallelAssembly); + oldOnParallelAssembly = null; + } + + private static void disableObservable() { + RxJavaPlugins.setOnObservableSubscribe(oldOnObservableSubscribe); + oldOnObservableSubscribe = null; + } + + private static void disableCompletable() { + RxJavaPlugins.setOnCompletableSubscribe(oldOnCompletableSubscribe); + oldOnCompletableSubscribe = null; + } + + private static void disableFlowable() { + RxJavaPlugins.setOnFlowableSubscribe(oldOnFlowableSubscribe); + oldOnFlowableSubscribe = null; + } + + private static void disableSingle() { + RxJavaPlugins.setOnSingleSubscribe(oldOnSingleSubscribe); + oldOnSingleSubscribe = null; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void disableMaybe() { + RxJavaPlugins.setOnMaybeSubscribe( + (BiFunction) oldOnMaybeSubscribe); + oldOnMaybeSubscribe = null; + } + + private static void disableWithSpanStrategy() { + AsyncSpanEndStrategies.getInstance().unregisterStrategy(RxJava3AsyncSpanEndStrategy.class); + } + + private static Function compose( + Function before, Function after) { + if (before == null) { + return after; + } + return (T v) -> after.apply(before.apply(v)); + } + + private static BiFunction biCompose( + BiFunction before, + BiFunction after) { + if (before == null) { + return after; + } + return (T v, U u) -> after.apply(v, before.apply(v, u)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingAssemblyBuilder.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingAssemblyBuilder.java new file mode 100644 index 000000000..c5321a7b5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingAssemblyBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava3; + +public final class TracingAssemblyBuilder { + private boolean captureExperimentalSpanAttributes; + + TracingAssemblyBuilder() {} + + public TracingAssemblyBuilder setCaptureExperimentalSpanAttributes( + boolean captureExperimentalSpanAttributes) { + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + return this; + } + + public TracingAssembly build() { + return new TracingAssembly(captureExperimentalSpanAttributes); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingCompletableObserver.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingCompletableObserver.java new file mode 100644 index 000000000..d310a8577 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingCompletableObserver.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.rxjava3.core.CompletableObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +class TracingCompletableObserver implements CompletableObserver, Disposable { + + private final CompletableObserver actual; + private final Context context; + private Disposable disposable; + + TracingCompletableObserver(CompletableObserver actual, Context context) { + this.actual = actual; + this.context = context; + } + + @Override + public void onSubscribe(Disposable d) { + if (!DisposableHelper.validate(disposable, d)) { + return; + } + disposable = d; + actual.onSubscribe(this); + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + actual.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + try (Scope ignored = context.makeCurrent()) { + actual.onError(e); + } + } + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingConditionalSubscriber.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingConditionalSubscriber.java new file mode 100644 index 000000000..844094140 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingConditionalSubscriber.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.rxjava3.internal.fuseable.ConditionalSubscriber; +import io.reactivex.rxjava3.internal.fuseable.QueueSubscription; +import io.reactivex.rxjava3.internal.subscribers.BasicFuseableConditionalSubscriber; + +class TracingConditionalSubscriber extends BasicFuseableConditionalSubscriber { + + private final Context context; + + TracingConditionalSubscriber(ConditionalSubscriber downstream, Context context) { + super(downstream); + this.context = context; + } + + @Override + public boolean tryOnNext(T t) { + try (Scope ignored = context.makeCurrent()) { + return downstream.tryOnNext(t); + } + } + + @Override + public void onNext(T t) { + try (Scope ignored = context.makeCurrent()) { + downstream.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + try (Scope ignored = context.makeCurrent()) { + downstream.onError(t); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + downstream.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + QueueSubscription qs = this.qs; + if (qs != null) { + int m = qs.requestFusion(mode); + sourceMode = m; + return m; + } + return NONE; + } + + @Override + public T poll() throws Throwable { + return qs.poll(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingMaybeObserver.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingMaybeObserver.java new file mode 100644 index 000000000..2e8e1f09c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingMaybeObserver.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.rxjava3.core.MaybeObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +class TracingMaybeObserver implements MaybeObserver, Disposable { + + private final MaybeObserver actual; + private final Context context; + private Disposable disposable; + + TracingMaybeObserver(MaybeObserver actual, Context context) { + this.actual = actual; + this.context = context; + } + + @Override + public void onSubscribe(Disposable d) { + if (!DisposableHelper.validate(disposable, d)) { + return; + } + disposable = d; + actual.onSubscribe(this); + } + + @Override + public void onSuccess(T t) { + try (Scope ignored = context.makeCurrent()) { + actual.onSuccess(t); + } + } + + @Override + public void onError(Throwable e) { + try (Scope ignored = context.makeCurrent()) { + actual.onError(e); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + actual.onComplete(); + } + } + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingObserver.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingObserver.java new file mode 100644 index 000000000..82c6226b3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingObserver.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.internal.fuseable.QueueDisposable; +import io.reactivex.rxjava3.internal.observers.BasicFuseableObserver; + +class TracingObserver extends BasicFuseableObserver { + + private final Context context; + + TracingObserver(Observer downstream, Context context) { + super(downstream); + this.context = context; + } + + @Override + public void onNext(T t) { + try (Scope ignored = context.makeCurrent()) { + downstream.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + try (Scope ignored = context.makeCurrent()) { + downstream.onError(t); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + downstream.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + QueueDisposable qd = this.qd; + if (qd != null) { + int m = qd.requestFusion(mode); + sourceMode = m; + return m; + } + return NONE; + } + + @Override + public T poll() throws Throwable { + return qd.poll(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingParallelFlowable.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingParallelFlowable.java new file mode 100644 index 000000000..bc6362b67 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingParallelFlowable.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.rxjava3.internal.fuseable.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import org.reactivestreams.Subscriber; + +class TracingParallelFlowable extends ParallelFlowable { + + private final ParallelFlowable source; + private final Context context; + + TracingParallelFlowable(ParallelFlowable source, Context context) { + this.source = source; + this.context = context; + } + + @SuppressWarnings("unchecked") + @Override + public void subscribe(Subscriber[] subscribers) { + if (!validate(subscribers)) { + return; + } + int n = subscribers.length; + Subscriber[] parents = new Subscriber[n]; + for (int i = 0; i < n; i++) { + Subscriber z = subscribers[i]; + if (z instanceof ConditionalSubscriber) { + parents[i] = + new TracingConditionalSubscriber<>((ConditionalSubscriber) z, context); + } else { + parents[i] = new TracingSubscriber<>(z, context); + } + } + try (Scope ignored = context.makeCurrent()) { + source.subscribe(parents); + } + } + + @Override + public int parallelism() { + return source.parallelism(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingSingleObserver.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingSingleObserver.java new file mode 100644 index 000000000..56ed482df --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingSingleObserver.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +class TracingSingleObserver implements SingleObserver, Disposable { + + private final SingleObserver actual; + private final Context context; + private Disposable disposable; + + TracingSingleObserver(SingleObserver actual, Context context) { + this.actual = actual; + this.context = context; + } + + @Override + public void onSubscribe(Disposable d) { + if (!DisposableHelper.validate(disposable, d)) { + return; + } + this.disposable = d; + actual.onSubscribe(this); + } + + @Override + public void onSuccess(T t) { + try (Scope ignored = context.makeCurrent()) { + actual.onSuccess(t); + } + } + + @Override + public void onError(Throwable throwable) { + try (Scope ignored = context.makeCurrent()) { + actual.onError(throwable); + } + } + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingSubscriber.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingSubscriber.java new file mode 100644 index 000000000..53b8589bf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/main/java/io/opentelemetry/instrumentation/rxjava3/TracingSubscriber.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.rxjava3.internal.fuseable.QueueSubscription; +import io.reactivex.rxjava3.internal.subscribers.BasicFuseableSubscriber; +import org.reactivestreams.Subscriber; + +class TracingSubscriber extends BasicFuseableSubscriber { + + private final Context context; + + TracingSubscriber(Subscriber downstream, Context context) { + super(downstream); + this.context = context; + } + + @Override + public void onNext(T t) { + try (Scope ignored = context.makeCurrent()) { + downstream.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + try (Scope ignored = context.makeCurrent()) { + downstream.onError(t); + } + } + + @Override + public void onComplete() { + try (Scope ignored = context.makeCurrent()) { + downstream.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + QueueSubscription qs = this.qs; + if (qs != null) { + int m = qs.requestFusion(mode); + sourceMode = m; + return m; + } + return NONE; + } + + @Override + public T poll() throws Throwable { + return qs.poll(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3AsyncSpanEndStrategyTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3AsyncSpanEndStrategyTest.groovy new file mode 100644 index 000000000..0e8956a3d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3AsyncSpanEndStrategyTest.groovy @@ -0,0 +1,1042 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.api.tracer.BaseTracer +import io.opentelemetry.instrumentation.rxjava3.RxJava3AsyncSpanEndStrategy +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.observers.TestObserver +import io.reactivex.rxjava3.parallel.ParallelFlowable +import io.reactivex.rxjava3.processors.ReplayProcessor +import io.reactivex.rxjava3.processors.UnicastProcessor +import io.reactivex.rxjava3.subjects.CompletableSubject +import io.reactivex.rxjava3.subjects.MaybeSubject +import io.reactivex.rxjava3.subjects.ReplaySubject +import io.reactivex.rxjava3.subjects.SingleSubject +import io.reactivex.rxjava3.subjects.UnicastSubject +import io.reactivex.rxjava3.subscribers.TestSubscriber +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import spock.lang.Specification + +class RxJava3AsyncSpanEndStrategyTest extends Specification { + BaseTracer tracer + + Context context + + Span span + + def underTest = RxJava3AsyncSpanEndStrategy.create() + + def underTestWithExperimentalAttributes = RxJava3AsyncSpanEndStrategy.newBuilder() + .setCaptureExperimentalSpanAttributes(true) + .build() + + void setup() { + tracer = Mock() + context = Mock() + span = Mock() + span.storeInContext(_) >> { callRealMethod() } + } + + static class CompletableTest extends RxJava3AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Completable) + } + + def "ends span on already completed"() { + given: + def observer = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, Completable.complete()) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, Completable.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = CompletableSubject.create() + def observer = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = CompletableSubject.create() + def observer = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = CompletableSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Completable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.dispose() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = CompletableSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Completable) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.dispose() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = CompletableSubject.create() + def observer1 = new TestObserver() + def observer2 = new TestObserver() + def observer3 = new TestObserver() + + when: + def result = (Completable) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer1.assertComplete() + observer2.assertComplete() + observer3.assertComplete() + } + } + + static class MaybeTest extends RxJava3AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Maybe) + } + + def "ends span on already completed"() { + given: + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, Maybe.just("Value")) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already empty"() { + given: + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, Maybe.empty()) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, Maybe.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = MaybeSubject.create() + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onSuccess("Value") + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when empty"() { + given: + def source = MaybeSubject.create() + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = MaybeSubject.create() + def observer = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = MaybeSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.dispose() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = MaybeSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Maybe) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.dispose() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = MaybeSubject.create() + def observer1 = new TestObserver() + def observer2 = new TestObserver() + def observer3 = new TestObserver() + + when: + def result = (Maybe) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onSuccess("Value") + + then: + 1 * tracer.end(context) + observer1.assertValue("Value") + observer1.assertComplete() + observer2.assertValue("Value") + observer2.assertComplete() + observer3.assertValue("Value") + observer3.assertComplete() + } + } + + static class SingleTest extends RxJava3AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Single) + } + + def "ends span on already completed"() { + given: + def observer = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, Single.just("Value")) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, Single.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = SingleSubject.create() + def observer = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onSuccess("Value") + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = SingleSubject.create() + def observer = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = SingleSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Single) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.dispose() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = SingleSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Single) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.dispose() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = SingleSubject.create() + def observer1 = new TestObserver() + def observer2 = new TestObserver() + def observer3 = new TestObserver() + + when: + def result = (Single) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onSuccess("Value") + + then: + 1 * tracer.end(context) + observer1.assertValue("Value") + observer1.assertComplete() + observer2.assertValue("Value") + observer2.assertComplete() + observer3.assertValue("Value") + observer3.assertComplete() + } + } + + static class ObservableTest extends RxJava3AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Observable) + } + + def "ends span on already completed"() { + given: + def observer = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, Observable.just("Value")) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, Observable.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = UnicastSubject.create() + def observer = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = UnicastSubject.create() + def observer = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = UnicastSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Observable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.dispose() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = UnicastSubject.create() + def observer = new TestObserver() + def context = span.storeInContext(Context.root()) + + when: + def result = (Observable) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.dispose() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = ReplaySubject.create() + def observer1 = new TestObserver() + def observer2 = new TestObserver() + def observer3 = new TestObserver() + + when: + def result = (Observable) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer1.assertComplete() + observer2.assertComplete() + observer3.assertComplete() + } + } + + static class FlowableTest extends RxJava3AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Flowable) + } + + def "ends span on already completed"() { + given: + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, Flowable.just("Value")) + result.subscribe(observer) + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, Flowable.error(exception)) + result.subscribe(observer) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when completed"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flowable) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + + def "ends span once for multiple subscribers"() { + given: + def source = ReplayProcessor.create() + def observer1 = new TestSubscriber() + def observer2 = new TestSubscriber() + def observer3 = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer1) + result.subscribe(observer2) + result.subscribe(observer3) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer1.assertComplete() + observer2.assertComplete() + observer3.assertComplete() + } + } + + static class ParallelFlowableTest extends RxJava3AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(ParallelFlowable) + } + + def "ends span on already completed"() { + given: + def observer = new TestSubscriber() + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, Flowable.just("Value").parallel()) + result.sequential().subscribe(observer) + + then: + observer.assertComplete() + 1 * tracer.end(context) + } + + def "ends span on already errored"() { + given: + def exception = new IllegalStateException() + def observer = new TestSubscriber() + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, Flowable.error(exception).parallel()) + result.sequential().subscribe(observer) + + then: + observer.assertError(exception) + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span when completed"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, source.parallel()) + result.sequential().subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + observer.assertComplete() + 1 * tracer.end(context) + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, source.parallel()) + result.sequential().subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + observer.assertError(exception) + 1 * tracer.endExceptionally(context, exception) + } + + def "ends span when cancelled"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (ParallelFlowable) underTest.end(tracer, context, source.parallel()) + result.sequential().subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = UnicastProcessor.create() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (ParallelFlowable) underTestWithExperimentalAttributes.end(tracer, context, source.parallel()) + result.sequential().subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + } + + static class PublisherTest extends RxJava3AsyncSpanEndStrategyTest { + def "is supported"() { + expect: + underTest.supports(Publisher) + } + + def "ends span when completed"() { + given: + def source = new CustomPublisher() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onComplete() + + then: + 1 * tracer.end(context) + observer.assertComplete() + } + + def "ends span when errored"() { + given: + def exception = new IllegalStateException() + def source = new CustomPublisher() + def observer = new TestSubscriber() + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + source.onError(exception) + + then: + 1 * tracer.endExceptionally(context, exception) + observer.assertError(exception) + } + + def "ends span when cancelled"() { + given: + def source = new CustomPublisher() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flowable) underTest.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 0 * span.setAttribute(_) + } + + def "ends span when cancelled and capturing experimental span attributes"() { + given: + def source = new CustomPublisher() + def observer = new TestSubscriber() + def context = span.storeInContext(Context.root()) + + when: + def result = (Flowable) underTestWithExperimentalAttributes.end(tracer, context, source) + result.subscribe(observer) + + then: + 0 * tracer._ + 0 * span._ + + when: + observer.cancel() + + then: + 1 * tracer.end(context) + 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) + } + } + + static class CustomPublisher implements Publisher, Subscription { + Subscriber subscriber + + @Override + void subscribe(Subscriber subscriber) { + this.subscriber = subscriber + subscriber.onSubscribe(this) + } + + def onComplete() { + this.subscriber.onComplete() + } + + def onError(Throwable exception) { + this.subscriber.onError(exception) + } + + @Override + void request(long l) { } + + @Override + void cancel() { } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3SubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3SubscriptionTest.groovy new file mode 100644 index 000000000..800f5adfe --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3SubscriptionTest.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava3.AbstractRxJava3SubscriptionTest +import io.opentelemetry.instrumentation.rxjava3.TracingAssembly +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import spock.lang.Shared + +class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest implements LibraryTestTrait { + @Shared + TracingAssembly tracingAssembly = TracingAssembly.create() + + def setupSpec() { + tracingAssembly.enable() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3Test.groovy new file mode 100644 index 000000000..24215f993 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3Test.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava3.AbstractRxJava3Test +import io.opentelemetry.instrumentation.rxjava3.TracingAssembly +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import spock.lang.Shared + +class RxJava3Test extends AbstractRxJava3Test implements LibraryTestTrait { + @Shared + TracingAssembly tracingAssembly = TracingAssembly.create() + + def setupSpec() { + tracingAssembly.enable() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/rxjava-3.0-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/rxjava-3.0-testing.gradle new file mode 100644 index 000000000..fcace56e2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/rxjava-3.0-testing.gradle @@ -0,0 +1,13 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "io.reactivex.rxjava3:rxjava:3.0.12" + + implementation "com.google.guava:guava" + + implementation "org.codehaus.groovy:groovy-all" + implementation "io.opentelemetry:opentelemetry-api" + implementation "org.spockframework:spock-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/AbstractRxJava3SubscriptionTest.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/AbstractRxJava3SubscriptionTest.groovy new file mode 100644 index 000000000..992a61318 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/AbstractRxJava3SubscriptionTest.groovy @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava3 + +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.functions.Consumer + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.instrumentation.test.InstrumentationSpecification + +import java.util.concurrent.CountDownLatch + +abstract class AbstractRxJava3SubscriptionTest extends InstrumentationSpecification { + + def "subscription test"() { + when: + CountDownLatch latch = new CountDownLatch(1) + runUnderTrace("parent") { + Single connection = Single.create { + it.onSuccess(new Connection()) + } + connection.subscribe(new Consumer() { + @Override + void accept(Connection t) { + t.query() + latch.countDown() + } + }) + } + latch.await() + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "Connection.query", span(0)) + } + } + } + + static class Connection { + static int query() { + def span = GlobalOpenTelemetry.getTracer("test").spanBuilder("Connection.query").startSpan() + span.end() + return new Random().nextInt() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/AbstractRxJava3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/AbstractRxJava3Test.groovy new file mode 100644 index 000000000..a7b10bd49 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/AbstractRxJava3Test.groovy @@ -0,0 +1,412 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava3 + +import io.opentelemetry.api.common.AttributeKey +import io.reactivex.rxjava3.core.BackpressureStrategy +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.internal.operators.flowable.FlowablePublish +import io.reactivex.rxjava3.internal.operators.observable.ObservablePublish +import io.reactivex.rxjava3.schedulers.Schedulers + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTraceWithoutExceptionCatch +import static java.util.concurrent.TimeUnit.MILLISECONDS + +import com.google.common.collect.Lists +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import spock.lang.Shared +import spock.lang.Unroll + +/** + *

Tests in this class may seem not exhaustive due to the fact that some classes are converted + * into others, ie. {@link Completable#toMaybe()}. Fortunately, RxJava3 uses helper classes like + * {@link io.reactivex.rxjava3.internal.operators.maybe.MaybeFromCompletable} and as a result we + * can test subscriptions and cancellations correctly. + */ +@Unroll +abstract class AbstractRxJava3Test extends InstrumentationSpecification { + + public static final String EXCEPTION_MESSAGE = "test exception" + + @Shared + def addOne = { i -> + addOneFunc(i) + } + + @Shared + def addTwo = { i -> + addTwoFunc(i) + } + + @Shared + def throwException = { + throw new IllegalStateException(EXCEPTION_MESSAGE) + } + + static addOneFunc(int i) { + runUnderTrace("addOne") { + return i + 1 + } + } + + static addTwoFunc(int i) { + runUnderTrace("addTwo") { + return i + 2 + } + } + + def "Publisher '#name' test"() { + when: + def result = assemblePublisherUnderTrace(publisherSupplier) + + then: + result == expected + and: + assertTraces(1) { + sortSpansByStartTime() + trace(0, workSpans + 1) { + + basicSpan(it, 0, "publisher-parent") + for (int i = 1; i < workSpans + 1; ++i) { + basicSpan(it, i, "addOne", span(0)) + } + } + } + + where: + name | expected | workSpans | publisherSupplier + "basic maybe" | 2 | 1 | { -> Maybe.just(1).map(addOne) } + "two operations maybe" | 4 | 2 | { -> Maybe.just(2).map(addOne).map(addOne) } + "delayed maybe" | 4 | 1 | { -> + Maybe.just(3).delay(100, MILLISECONDS).map(addOne) + } + "delayed twice maybe" | 6 | 2 | { -> + Maybe.just(4).delay(100, MILLISECONDS).map(addOne).delay(100, MILLISECONDS).map(addOne) + } + "basic flowable" | [6, 7] | 2 | { -> + Flowable.fromIterable([5, 6]).map(addOne) + } + "two operations flowable" | [8, 9] | 4 | { -> + Flowable.fromIterable([6, 7]).map(addOne).map(addOne) + } + "delayed flowable" | [8, 9] | 2 | { -> + Flowable.fromIterable([7, 8]).delay(100, MILLISECONDS).map(addOne) + } + "delayed twice flowable" | [10, 11] | 4 | { -> + Flowable.fromIterable([8, 9]).delay(100, MILLISECONDS).map(addOne).delay(100, MILLISECONDS).map(addOne) + } + "maybe from callable" | 12 | 2 | { -> + Maybe.fromCallable({ addOneFunc(10) }).map(addOne) + } + "basic single" | 1 | 1 | { -> Single.just(0).map(addOne) } + "basic observable" | [1] | 1 | { -> Observable.just(0).map(addOne) } + "connectable flowable" | [1] | 1 | { -> + FlowablePublish.just(0).delay(100, MILLISECONDS).map(addOne) + } + "connectable observable" | [1] | 1 | { -> + ObservablePublish.just(0).delay(100, MILLISECONDS).map(addOne) + } + } + + def "Publisher error '#name' test"() { + when: + assemblePublisherUnderTrace(publisherSupplier) + + then: + def thrownException = thrown RuntimeException + thrownException.message == EXCEPTION_MESSAGE + and: + assertTraces(1) { + sortSpansByStartTime() + trace(0, 1) { + // It's important that we don't attach errors at the Reactor level so that we don't + // impact the spans on reactor integrations such as netty and lettuce, as reactor is + // more of a context propagation mechanism than something we would be tracking for + // errors this is ok. + basicSpan(it, 0, "publisher-parent") + } + } + + where: + name | publisherSupplier + "maybe" | { -> Maybe.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "flowable" | { -> Flowable.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "single" | { -> Single.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "observable" | { -> Observable.error(new RuntimeException(EXCEPTION_MESSAGE)) } + "completable" | { -> Completable.error(new RuntimeException(EXCEPTION_MESSAGE)) } + } + + def "Publisher step '#name' test"() { + when: + assemblePublisherUnderTrace(publisherSupplier) + + then: + def exception = thrown RuntimeException + exception.message == EXCEPTION_MESSAGE + and: + assertTraces(1) { + sortSpansByStartTime() + trace(0, workSpans + 1) { + // It's important that we don't attach errors at the Reactor level so that we don't + // impact the spans on reactor integrations such as netty and lettuce, as reactor is + // more of a context propagation mechanism than something we would be tracking for + // errors this is ok. + basicSpan(it, 0, "publisher-parent") + + for (int i = 1; i < workSpans + 1; i++) { + basicSpan(it, i, "addOne", span(0)) + } + } + } + + where: + name | workSpans | publisherSupplier + "basic maybe failure" | 1 | { -> + Maybe.just(1).map(addOne).map({ throwException() }) + } + "basic flowable failure" | 1 | { -> + Flowable.fromIterable([5, 6]).map(addOne).map({ throwException() }) + } + } + + def "Publisher '#name' cancel"() { + when: + cancelUnderTrace(publisherSupplier) + + then: + assertTraces(1) { + trace(0, 1) { + basicSpan(it, 0, "publisher-parent") + } + } + + where: + name | publisherSupplier + "basic maybe" | { -> Maybe.just(1) } + "basic flowable" | { -> Flowable.fromIterable([5, 6]) } + "basic single" | { -> Single.just(1) } + "basic completable" | { -> Completable.fromCallable({ -> 1 }) } + "basic observable" | { -> Observable.just(1) } + } + + def "Publisher chain spans have the correct parent for '#name'"() { + when: + assemblePublisherUnderTrace(publisherSupplier) + + then: + assertTraces(1) { + trace(0, workSpans + 1) { + basicSpan(it, 0, "publisher-parent") + + for (int i = 1; i < workSpans + 1; i++) { + basicSpan(it, i, "addOne", span(0)) + } + } + } + + where: + name | workSpans | publisherSupplier + "basic maybe" | 3 | { -> + Maybe.just(1).map(addOne).map(addOne).concatWith(Maybe.just(1).map(addOne)) + } + "basic flowable" | 5 | { -> + Flowable.fromIterable([5, 6]).map(addOne).map(addOne).concatWith(Maybe.just(1).map(addOne).toFlowable()) + } + } + + def "Publisher chain spans have the correct parents from subscription time"() { + when: + def maybe = Maybe.just(42) + .map(addOne) + .map(addTwo) + + runUnderTrace("trace-parent") { + maybe.blockingGet() + } + + then: + assertTraces(1) { + trace(0, 3) { + sortSpansByStartTime() + basicSpan(it, 0, "trace-parent") + basicSpan(it, 1, "addOne", span(0)) + basicSpan(it, 2, "addTwo", span(0)) + } + } + } + + def "Publisher chain spans have the correct parents from subscription time '#name'"() { + when: + assemblePublisherUnderTrace { + // The "add one" operations in the publisher created here should be children of the publisher-parent + def publisher = publisherSupplier() + + runUnderTrace("intermediate") { + if (publisher instanceof Maybe) { + return ((Maybe) publisher).map(addTwo) + } else if (publisher instanceof Flowable) { + return ((Flowable) publisher).map(addTwo) + } else if (publisher instanceof Single) { + return ((Single) publisher).map(addTwo) + } else if (publisher instanceof Observable) { + return ((Observable) publisher).map(addTwo) + } else if (publisher instanceof Completable) { + return ((Completable) publisher).toMaybe().map(addTwo) + } + throw new IllegalStateException("Unknown publisher type") + } + } + + then: + assertTraces(1) { + trace(0, 2 + 2 * workItems) { + sortSpansByStartTime() + basicSpan(it, 0, "publisher-parent") + basicSpan(it, 1, "intermediate", span(0)) + + for (int i = 2; i < 2 + 2 * workItems; i = i + 2) { + basicSpan(it, i, "addOne", span(0)) + basicSpan(it, i + 1, "addTwo", span(0)) + } + } + } + + where: + name | workItems | publisherSupplier + "basic maybe" | 1 | { -> Maybe.just(1).map(addOne) } + "basic flowable" | 2 | { -> Flowable.fromIterable([1, 2]).map(addOne) } + "basic single" | 1 | { -> Single.just(1).map(addOne) } + "basic observable" | 1 | { -> Observable.just(1).map(addOne) } + } + + def "Flowables produce the right number of results '#scheduler'"() { + when: + List values = runUnderTrace("flowable root") { + Flowable.fromIterable([1, 2, 3, 4]) + .parallel() + .runOn(scheduler) + .flatMap({ num -> + Maybe.just(num).map(addOne).toFlowable() + }) + .sequential() + .toList() + .blockingGet() + } + + then: + values.size() == 4 + assertTraces(1) { + trace(0, 5) { + basicSpan(it, 0, "flowable root") + for (int i = 1; i < values.size() + 1; i++) { + basicSpan(it, i, "addOne", span(0)) + } + } + } + + where: + scheduler << [Schedulers.newThread(), Schedulers.computation(), Schedulers.single(), Schedulers.trampoline()] + } + + def "test many ongoing trace chains on '#scheduler'"() { + setup: + int iterations = 100 + Set remainingIterations = new HashSet<>((0L..(iterations - 1)).toList()) + + when: + RxJava3ConcurrencyTestHelper.launchAndWait(scheduler, iterations, 60000) + + then: + assertTraces(iterations) { + for (int i = 0; i < iterations; i++) { + trace(i, 3) { + long iteration = -1 + span(0) { + name("outer") + iteration = span.getAttributes().get(AttributeKey.longKey("iteration")).toLong() + assert remainingIterations.remove(iteration) + } + span(1) { + name("middle") + childOf(span(0)) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + span(2) { + name("inner") + childOf(span(1)) + assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration + } + } + } + } + + assert remainingIterations.isEmpty() + + where: + scheduler << [Schedulers.newThread(), Schedulers.computation(), Schedulers.single(), Schedulers.trampoline()] + } + + def cancelUnderTrace(def publisherSupplier) { + runUnderTraceWithoutExceptionCatch("publisher-parent") { + def publisher = publisherSupplier() + if (publisher instanceof Maybe) { + publisher = publisher.toFlowable() + } else if (publisher instanceof Single) { + publisher = publisher.toFlowable() + } else if (publisher instanceof Completable) { + publisher = publisher.toFlowable() + } else if (publisher instanceof Observable) { + publisher = publisher.toFlowable(BackpressureStrategy.LATEST) + } + + publisher.subscribe(new Subscriber() { + void onSubscribe(Subscription subscription) { + subscription.cancel() + } + + void onNext(Integer t) { + } + + void onError(Throwable error) { + } + + void onComplete() { + } + }) + } + } + + @SuppressWarnings("unchecked") + def assemblePublisherUnderTrace(def publisherSupplier) { + // The "add two" operations below should be children of this span + runUnderTraceWithoutExceptionCatch("publisher-parent") { + def publisher = publisherSupplier() + + // Read all data from publisher + if (publisher instanceof Maybe) { + return ((Maybe) publisher).blockingGet() + } else if (publisher instanceof Flowable) { + return Lists.newArrayList(((Flowable) publisher).blockingIterable()) + } else if (publisher instanceof Single) { + return ((Single) publisher).blockingGet() + } else if (publisher instanceof Observable) { + return Lists.newArrayList(((Observable) publisher).blockingIterable()) + } else if (publisher instanceof Completable) { + return ((Completable) publisher).toMaybe().blockingGet() + } + + throw new IllegalStateException("Unknown publisher: " + publisher) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/RxJava3ConcurrencyTestHelper.java b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/RxJava3ConcurrencyTestHelper.java new file mode 100644 index 000000000..bdcd4ef5d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/rxjava/rxjava-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava3/RxJava3ConcurrencyTestHelper.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava3; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.test.utils.TraceUtils; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Single; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * This test creates the specified number of traces with three spans: 1) Outer (root) span 2) Middle + * span, child of outer, created in success handler of the chain subscribed to in the context of the + * outer span (with some delay and map thrown in for good measure) 3) Inner span, child of middle, + * created in the success handler of a new chain started and subscribed to in the the middle span + * + *

The varying delays between the stages where each span is created should guarantee that + * scheduler threads handling various stages of the chain will have to alternate between contexts + * from different traces. + */ +public class RxJava3ConcurrencyTestHelper { + public static void launchAndWait(Scheduler scheduler, int iterations, long timeoutMillis) { + CountDownLatch latch = new CountDownLatch(iterations); + + for (int i = 0; i < iterations; i++) { + launchOuter(new Iteration(scheduler, latch, i)); + } + + try { + // Continue even on timeout so the test assertions can show what is missing + //noinspection ResultOfMethodCallIgnored + latch.await(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private static void launchOuter(Iteration iteration) { + TraceUtils.runUnderTrace( + "outer", + () -> { + Span.current().setAttribute("iteration", iteration.index); + + Single.fromCallable(() -> iteration) + .subscribeOn(iteration.scheduler) + .observeOn(iteration.scheduler) + // Use varying delay so that different stages of the chain would alternate. + .delay(iteration.index % 10, TimeUnit.MILLISECONDS, iteration.scheduler) + .map((it) -> it) + .delay(iteration.index % 10, TimeUnit.MILLISECONDS, iteration.scheduler) + .doOnSuccess(RxJava3ConcurrencyTestHelper::launchInner) + .subscribe(); + + return null; + }); + } + + private static void launchInner(Iteration iteration) { + TraceUtils.runUnderTrace( + "middle", + () -> { + Span.current().setAttribute("iteration", iteration.index); + + Single.fromCallable(() -> iteration) + .subscribeOn(iteration.scheduler) + .observeOn(iteration.scheduler) + .delay(iteration.index % 10, TimeUnit.MILLISECONDS, iteration.scheduler) + .doOnSuccess( + (it) -> { + TraceUtils.runUnderTrace( + "inner", + () -> { + Span.current().setAttribute("iteration", it.index); + return null; + }); + it.countDown.countDown(); + }) + .subscribe(); + + return null; + }); + } + + private static class Iteration { + public final Scheduler scheduler; + public final CountDownLatch countDown; + public final int index; + + private Iteration(Scheduler scheduler, CountDownLatch countDown, int index) { + this.scheduler = scheduler; + this.countDown = countDown; + this.index = index; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/scala-executors-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/scala-executors-javaagent.gradle new file mode 100644 index 000000000..e84711f5f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/scala-executors-javaagent.gradle @@ -0,0 +1,42 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: "otel.scala-conventions" +apply plugin: 'org.unbroken-dome.test-sets' + +muzzle { + pass { + group = 'org.scala-lang' + module = "scala-library" + versions = "[2.8.0,2.12.0)" + assertInverse = true + } +} + +testSets { + slickTest { + filter { + // this is needed because "test.dependsOn slickTest", and so without this, + // running a single test in the default test set will fail + setFailOnNoMatchingTests(false) + } + } +} + +compileSlickTestGroovy { + classpath += files(sourceSets.slickTest.scala.classesDirectory) +} + +dependencies { + library "org.scala-lang:scala-library:2.8.0" + + latestDepTestLibrary "org.scala-lang:scala-library:2.11.+" + + testInstrumentation project(':instrumentation:jdbc:javaagent') + + slickTestImplementation project(':testing-common') + slickTestImplementation "org.scala-lang:scala-library" + slickTestImplementation "com.typesafe.slick:slick_2.11:3.2.0" + slickTestImplementation "com.h2database:h2:1.4.197" +} + +// Run Slick library tests along with the rest of tests +test.dependsOn slickTest diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaConcurrentInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaConcurrentInstrumentationModule.java new file mode 100644 index 000000000..c1ff64fe4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaConcurrentInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.scalaexecutors; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ScalaConcurrentInstrumentationModule extends InstrumentationModule { + public ScalaConcurrentInstrumentationModule() { + super("scala-executors"); + } + + @Override + public List typeInstrumentations() { + return asList(new ScalaForkJoinPoolInstrumentation(), new ScalaForkJoinTaskInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinPoolInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinPoolInstrumentation.java new file mode 100644 index 000000000..25453817d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinPoolInstrumentation.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.scalaexecutors; + +import static net.bytebuddy.matcher.ElementMatchers.nameMatches; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import scala.concurrent.forkjoin.ForkJoinTask; + +public class ScalaForkJoinPoolInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + // This might need to be an extendsClass matcher... + return named("scala.concurrent.forkjoin.ForkJoinPool"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute") + .and(takesArgument(0, named(ScalaForkJoinTaskInstrumentation.TASK_CLASS_NAME))), + ScalaForkJoinPoolInstrumentation.class.getName() + "$SetScalaForkJoinStateAdvice"); + transformer.applyAdviceToMethod( + named("submit") + .and(takesArgument(0, named(ScalaForkJoinTaskInstrumentation.TASK_CLASS_NAME))), + ScalaForkJoinPoolInstrumentation.class.getName() + "$SetScalaForkJoinStateAdvice"); + transformer.applyAdviceToMethod( + nameMatches("invoke") + .and(takesArgument(0, named(ScalaForkJoinTaskInstrumentation.TASK_CLASS_NAME))), + ScalaForkJoinPoolInstrumentation.class.getName() + "$SetScalaForkJoinStateAdvice"); + } + + @SuppressWarnings("unused") + public static class SetScalaForkJoinStateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterJobSubmit( + @Advice.Argument(value = 0, readOnly = false) ForkJoinTask task) { + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(task)) { + ContextStore, State> contextStore = + InstrumentationContext.get(ForkJoinTask.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, task, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitJobSubmit( + @Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinTaskInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinTaskInstrumentation.java new file mode 100644 index 000000000..c3f5e0237 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinTaskInstrumentation.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.scalaexecutors; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.AdviceUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import java.util.concurrent.Callable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import scala.concurrent.forkjoin.ForkJoinPool; +import scala.concurrent.forkjoin.ForkJoinTask; + +/** + * Instrument {@link ForkJoinTask}. + * + *

Note: There are quite a few separate implementations of {@code ForkJoinTask}/{@code + * ForkJoinPool}: JVM, Akka, Scala, Netty to name a few. This class handles Scala version. + */ +public class ScalaForkJoinTaskInstrumentation implements TypeInstrumentation { + + static final String TASK_CLASS_NAME = "scala.concurrent.forkjoin.ForkJoinTask"; + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed(TASK_CLASS_NAME); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named(TASK_CLASS_NAME)); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("exec").and(takesArguments(0)).and(not(isAbstract())), + ScalaForkJoinTaskInstrumentation.class.getName() + "$ForkJoinTaskAdvice"); + } + + @SuppressWarnings("unused") + public static class ForkJoinTaskAdvice { + + /** + * When {@link ForkJoinTask} object is submitted to {@link ForkJoinPool} as {@link Runnable} or + * {@link Callable} it will not get wrapped, instead it will be casted to {@code ForkJoinTask} + * directly. This means state is still stored in {@code Runnable} or {@code Callable} and we + * need to use that state. + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter(@Advice.This ForkJoinTask thiz) { + ContextStore, State> contextStore = + InstrumentationContext.get(ForkJoinTask.class, State.class); + Scope scope = AdviceUtils.startTaskScope(contextStore, thiz); + if (thiz instanceof Runnable) { + ContextStore runnableContextStore = + InstrumentationContext.get(Runnable.class, State.class); + Scope newScope = AdviceUtils.startTaskScope(runnableContextStore, (Runnable) thiz); + if (null != newScope) { + if (null != scope) { + newScope.close(); + } else { + scope = newScope; + } + } + } + if (thiz instanceof Callable) { + ContextStore, State> callableContextStore = + InstrumentationContext.get(Callable.class, State.class); + Scope newScope = AdviceUtils.startTaskScope(callableContextStore, (Callable) thiz); + if (null != newScope) { + if (null != scope) { + newScope.close(); + } else { + scope = newScope; + } + } + } + return scope; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/slickTest/groovy/SlickTest.groovy b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/slickTest/groovy/SlickTest.groovy new file mode 100644 index 000000000..66759546d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/slickTest/groovy/SlickTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class SlickTest extends AgentInstrumentationSpecification { + + // Can't be @Shared, otherwise the work queue is initialized before the instrumentation is applied + def database = new SlickUtils() + + def "Basic statement generates spans"() { + setup: + def future = database.startQuery(SlickUtils.TestQuery()) + def result = database.getResults(future) + + expect: + result == SlickUtils.TestValue() + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "run query" + hasNoParent() + attributes { + } + } + span(1) { + name "SELECT ${SlickUtils.Db()}" + kind CLIENT + childOf span(0) + attributes { + "$SemanticAttributes.DB_SYSTEM.key" "h2" + "$SemanticAttributes.DB_NAME.key" SlickUtils.Db() + "$SemanticAttributes.DB_USER.key" SlickUtils.Username() + "$SemanticAttributes.DB_CONNECTION_STRING.key" "h2:mem:" + "$SemanticAttributes.DB_STATEMENT.key" "SELECT ?" + "$SemanticAttributes.DB_OPERATION.key" "SELECT" + } + } + } + } + } + + def "Concurrent requests do not throw exception"() { + setup: + def sleepFuture = database.startQuery(SlickUtils.SleepQuery()) + + def future = database.startQuery(SlickUtils.TestQuery()) + def result = database.getResults(future) + + database.getResults(sleepFuture) + + expect: + result == SlickUtils.TestValue() + + // Expect two traces because two queries have been run + assertTraces(2) { + trace(0, 2, { + span(0) {} + span(1) { kind CLIENT } + }) + trace(1, 2, { + span(0) {} + span(1) { kind CLIENT } + }) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/slickTest/scala/SlickUtils.scala b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/slickTest/scala/SlickUtils.scala new file mode 100644 index 000000000..d48212ec9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/slickTest/scala/SlickUtils.scala @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.javaagent.testing.common.Java8BytecodeBridge +import slick.jdbc.H2Profile.api._ + +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} + +class SlickUtils { + val tracer: Tracer = GlobalOpenTelemetry.getTracer("test") + + import SlickUtils._ + + val database = Database.forURL( + Url, + user = Username, + driver = "org.h2.Driver", + keepAliveConnection = true, + // Limit number of threads to hit Slick-specific case when we need to avoid re-wrapping + // wrapped runnables. + executor = AsyncExecutor("test", numThreads = 1, queueSize = 1000) + ) + Await.result( + database.run( + sqlu"""CREATE ALIAS IF NOT EXISTS SLEEP FOR "java.lang.Thread.sleep(long)"""" + ), + Duration.Inf + ) + + def startQuery(query: String): Future[Vector[Int]] = { + val span = tracer.spanBuilder("run query").startSpan() + val scope = Java8BytecodeBridge.currentContext().`with`(span).makeCurrent() + try { + return database.run(sql"#$query".as[Int]) + } finally { + span.end() + scope.close() + } + } + + def getResults(future: Future[Vector[Int]]): Int = { + Await.result(future, Duration.Inf).head + } +} + +object SlickUtils { + + val Driver = "h2" + val Db = "test" + val Username = "TESTUSER" + val Url = s"jdbc:${Driver}:mem:${Db}" + val TestValue = 3 + val TestQuery = "SELECT 3" + + val SleepQuery = "CALL SLEEP(3000)" + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/groovy/ScalaExecutorInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/groovy/ScalaExecutorInstrumentationTest.groovy new file mode 100644 index 000000000..1c8235ef4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/groovy/ScalaExecutorInstrumentationTest.groovy @@ -0,0 +1,143 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.lang.reflect.InvocationTargetException +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import scala.concurrent.forkjoin.ForkJoinPool +import scala.concurrent.forkjoin.ForkJoinTask +import spock.lang.Shared + +/** + * Test executor instrumentation for Scala specific classes. + * This is to large extent a copy of ExecutorInstrumentationTest. + */ +class ScalaExecutorInstrumentationTest extends AgentInstrumentationSpecification { + + @Shared + def executeRunnable = { e, c -> e.execute((Runnable) c) } + @Shared + def scalaExecuteForkJoinTask = { e, c -> e.execute((ForkJoinTask) c) } + @Shared + def submitRunnable = { e, c -> e.submit((Runnable) c) } + @Shared + def submitCallable = { e, c -> e.submit((Callable) c) } + @Shared + def scalaSubmitForkJoinTask = { e, c -> e.submit((ForkJoinTask) c) } + @Shared + def scalaInvokeForkJoinTask = { e, c -> e.invoke((ForkJoinTask) c) } + + def "#poolImpl '#name' propagates"() { + setup: + def pool = poolImpl + def m = method + + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + // this child will have a span + def child1 = new ScalaAsyncChild() + // this child won't + def child2 = new ScalaAsyncChild(false, false) + m(pool, child1) + m(pool, child2) + child1.waitForCompletion() + child2.waitForCompletion() + } + } + }.run() + + expect: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "asyncChild", span(0)) + } + } + + cleanup: + pool?.shutdown() + + // Unfortunately, there's no simple way to test the cross product of methods/pools. + where: + name | method | poolImpl + "execute Runnable" | executeRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "submit Runnable" | submitRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + "submit Callable" | submitCallable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue(1)) + + // ForkJoinPool has additional set of method overloads for ForkJoinTask to deal with + "execute Runnable" | executeRunnable | new ForkJoinPool() + "execute ForkJoinTask" | scalaExecuteForkJoinTask | new ForkJoinPool() + "submit Runnable" | submitRunnable | new ForkJoinPool() + "submit Callable" | submitCallable | new ForkJoinPool() + "submit ForkJoinTask" | scalaSubmitForkJoinTask | new ForkJoinPool() + "invoke ForkJoinTask" | scalaInvokeForkJoinTask | new ForkJoinPool() + } + + def "#poolImpl '#name' reports after canceled jobs"() { + setup: + ExecutorService pool = poolImpl + def m = method + List children = new ArrayList<>() + List jobFutures = new ArrayList<>() + + new Runnable() { + @Override + void run() { + runUnderTrace("parent") { + try { + for (int i = 0; i < 20; ++i) { + // Our current instrumentation instrumentation does not behave very well + // if we try to reuse Callable/Runnable. Namely we would be getting 'orphaned' + // child traces sometimes since state can contain only one parent span - and + // we do not really have a good way for attributing work to correct parent span + // if we reuse Callable/Runnable. + // Solution for now is to never reuse a Callable/Runnable. + ScalaAsyncChild child = new ScalaAsyncChild(false, true) + children.add(child) + try { + Future f = m(pool, child) + jobFutures.add(f) + } catch (InvocationTargetException e) { + throw e.getCause() + } + } + } catch (RejectedExecutionException e) { + } + + for (Future f : jobFutures) { + f.cancel(false) + } + for (ScalaAsyncChild child : children) { + child.unblock() + } + } + } + }.run() + + expect: + waitForTraces(1).size() == 1 + + // Wait for shutdown to make sure any remaining tasks finish and cleanup context since we don't + // wait on the tasks. + pool.shutdown() + pool.awaitTermination(10, TimeUnit.SECONDS) + + where: + name | method | poolImpl + "submit Runnable" | submitRunnable | new ForkJoinPool() + "submit Callable" | submitCallable | new ForkJoinPool() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/groovy/ScalaInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/groovy/ScalaInstrumentationTest.groovy new file mode 100644 index 000000000..8bedb482a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/groovy/ScalaInstrumentationTest.groovy @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +class ScalaInstrumentationTest extends AgentInstrumentationSpecification { + + def "scala futures and callbacks"() { + setup: + ScalaConcurrentTests scalaTest = new ScalaConcurrentTests() + + when: + scalaTest.traceWithFutureAndCallbacks() + + then: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "parent" + attributes { + } + } + span("goodFuture") { + childOf span(0) + attributes { + } + } + span("badFuture") { + childOf span(0) + attributes { + } + } + span("successCallback") { + childOf span(0) + attributes { + } + } + span("failureCallback") { + childOf span(0) + attributes { + } + } + } + } + } + + def "scala propagates across futures with no traces"() { + setup: + ScalaConcurrentTests scalaTest = new ScalaConcurrentTests() + + when: + scalaTest.tracedAcrossThreadsWithNoTrace() + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + attributes { + } + } + span("callback") { + childOf span(0) + attributes { + } + } + } + } + } + + def "scala either promise completion"() { + setup: + ScalaConcurrentTests scalaTest = new ScalaConcurrentTests() + + when: + scalaTest.traceWithPromises() + + then: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "parent" + attributes { + } + } + span("future1") { + childOf span(0) + attributes { + } + } + span("keptPromise") { + childOf span(0) + attributes { + } + } + span("keptPromise2") { + childOf span(0) + attributes { + } + } + span("brokenPromise") { + childOf span(0) + attributes { + } + } + } + } + } + + def "scala first completed future"() { + setup: + ScalaConcurrentTests scalaTest = new ScalaConcurrentTests() + + when: + scalaTest.tracedWithFutureFirstCompletions() + + then: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "parent" + attributes { + } + } + span("timeout1") { + childOf span(0) + attributes { + } + } + span("timeout2") { + childOf span(0) + attributes { + } + } + span("timeout3") { + childOf span(0) + attributes { + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/java/ScalaAsyncChild.java b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/java/ScalaAsyncChild.java new file mode 100644 index 000000000..26eeb390a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/java/ScalaAsyncChild.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import scala.concurrent.forkjoin.ForkJoinTask; + +public class ScalaAsyncChild extends ForkJoinTask implements Runnable, Callable { + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + private final AtomicBoolean blockThread; + private final boolean doTraceableWork; + private final CountDownLatch latch = new CountDownLatch(1); + + public ScalaAsyncChild() { + this(/* doTraceableWork= */ true, /* blockThread= */ false); + } + + public ScalaAsyncChild(boolean doTraceableWork, boolean blockThread) { + this.doTraceableWork = doTraceableWork; + this.blockThread = new AtomicBoolean(blockThread); + } + + @Override + public Object getRawResult() { + return null; + } + + @Override + protected void setRawResult(Object value) {} + + @Override + protected boolean exec() { + runImpl(); + return true; + } + + public void unblock() { + blockThread.set(false); + } + + @Override + public void run() { + runImpl(); + } + + @Override + public Object call() { + runImpl(); + return null; + } + + public void waitForCompletion() throws InterruptedException { + latch.await(); + } + + private void runImpl() { + while (blockThread.get()) { + // busy-wait to block thread + } + if (doTraceableWork) { + asyncChild(); + } + latch.countDown(); + } + + private static void asyncChild() { + tracer.spanBuilder("asyncChild").startSpan().end(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/scala/ScalaConcurrentTests.scala b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/scala/ScalaConcurrentTests.scala new file mode 100644 index 000000000..4da735ff0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/scala-executors/javaagent/src/test/scala/ScalaConcurrentTests.scala @@ -0,0 +1,180 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.javaagent.testing.common.Java8BytecodeBridge + +import java.util.concurrent.CountDownLatch +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future, Promise} + +class ScalaConcurrentTests { + val tracer: Tracer = GlobalOpenTelemetry.getTracer("test") + + /** @return Number of expected spans in the trace */ + def traceWithFutureAndCallbacks() { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = + Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent() + try { + val latch = new CountDownLatch(2) + val goodFuture: Future[Integer] = Future { + tracedChild("goodFuture") + 1 + } + goodFuture onSuccess { + case _ => { + tracedChild("successCallback") + latch.countDown() + } + } + val badFuture: Future[Integer] = Future { + tracedChild("badFuture") + throw new IllegalStateException("Uh-oh") + } + badFuture onFailure { + case t: Throwable => { + tracedChild("failureCallback") + latch.countDown() + } + } + + latch.await() + } finally { + parentSpan.end() + parentScope.close() + } + } + + def tracedAcrossThreadsWithNoTrace() { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = + Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent() + try { + val latch = new CountDownLatch(1) + val goodFuture: Future[Integer] = Future { + 1 + } + goodFuture onSuccess { + case _ => + Future { + 2 + } onSuccess { + case _ => { + tracedChild("callback") + latch.countDown() + } + } + } + + latch.await() + } finally { + parentSpan.end() + parentScope.close() + } + } + + /** @return Number of expected spans in the trace */ + def traceWithPromises() { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = + Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent() + try { + val keptPromise = Promise[Boolean]() + val brokenPromise = Promise[Boolean]() + val afterPromise = keptPromise.future + val afterPromise2 = keptPromise.future + + val failedAfterPromise = brokenPromise.future + + Future { + tracedChild("future1") + keptPromise success true + brokenPromise failure new IllegalStateException() + } + + val latch = new CountDownLatch(3) + afterPromise onSuccess { + case b => { + tracedChild("keptPromise") + latch.countDown() + } + } + afterPromise2 onSuccess { + case b => { + tracedChild("keptPromise2") + latch.countDown() + } + } + + failedAfterPromise onFailure { + case t => { + tracedChild("brokenPromise") + latch.countDown() + } + } + + latch.await() + } finally { + parentSpan.end() + parentScope.close() + } + } + + /** @return Number of expected spans in the trace */ + def tracedWithFutureFirstCompletions() { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = + Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent() + try { + val completedVal = Future.firstCompletedOf(List(Future { + tracedChild("timeout1") + false + }, Future { + tracedChild("timeout2") + false + }, Future { + tracedChild("timeout3") + true + })) + Await.result(completedVal, 30 seconds) + } finally { + parentSpan.end() + parentScope.close() + } + } + + /** @return Number of expected spans in the trace */ + def tracedTimeout(): Integer = { + val parentSpan = tracer.spanBuilder("parent").startSpan() + val parentScope = + Java8BytecodeBridge.currentContext().`with`(parentSpan).makeCurrent() + try { + val f: Future[String] = Future { + tracedChild("timeoutChild") + while (true) { + // never actually finish + } + "done" + } + + try { + Await.result(f, 1 milliseconds) + } catch { + case e: Exception => {} + } + return 2 + } finally { + parentSpan.end() + parentScope.close() + } + } + + def tracedChild(opName: String): Unit = { + tracer.spanBuilder(opName).startSpan().end() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/README.md b/opentelemetry-java-instrumentation/instrumentation/servlet/README.md new file mode 100644 index 000000000..8aade0b23 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/README.md @@ -0,0 +1,89 @@ +# Instrumentation for Java Servlets + +## A word about version + +We support Servlet API starting from version 2.2. +But various instrumentations apply to different versions of the API. + +They are divided into the following sub-modules: +- `servlet-common` contains shared code for both `javax.servlet` and `jakarta.servlet` packages + - `library` contains the abstract tracer applicable to all servlet versions given an + implementation of `ServletAccessor` to access request and response objects of the specific + version + - `javaagent` contains shared type instrumentations which can be used by version specific modules + by specifying the base package and advice class to use with them. Contains some helper classes + used by advices to reduce code duplication. It does not define any instrumentation modules and + is used only as a dependency for other `javaagent` modules. +- Version-specific modules where `library` contains the version-specific tracer and request/response + accessor, and `javaagent` contains the instrumentation modules and advices. + - `servlet-javax-common` contains instrumentations/abstract tracer common for Servlet API versions `[2.2, 5)` + - `servlet-2.2` contains instrumentations/tracer for Servlet API versions `[2.2, 3)` + - `servlet-3.0` contains instrumentations/tracer for Servlet API versions `[3.0, 5)` + - `servlet-5.0` contains instrumentations/tracer for Servlet API versions `[5,)` + +## Implementation details + +In order to fully understand how java servlet instrumentation work, +let us first take a look at the following stacktrace from Spring PetClinic application. +Unimportant frames are redacted, points of interests are highlighted and discussed below. + +

+at org.springframework.samples.petclinic.owner.OwnerController.initCreationForm(OwnerController.java:60)
+...
+at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
+at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
+at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
+at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
+at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
+at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
+at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
+at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
+...
+at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
+...
+at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
+at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
+at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
+at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
+at java.base/java.lang.Thread.run(Thread.java:834)
+
+ +Everything starts when HTTP request processing reaches the first class from Servlet specification. +In the example above this is the +`OncePerRequestFilter.doFilter(ServletRequest, ServletResponse, FilterChain)` method. +Let us call this first servlet specific method an "entry point". +This is the main target for `Servlet3Instrumentation` and `Servlet2Instrumentation`: + +`public void javax.servlet.Filter#doFilter(ServletRequest, ServletResponse, FilterChain)` + +`public void javax.servlet.http.HttpServlet#service(ServletRequest, ServletResponse)`. + +These instrumentations are located in separate submodules `servlet-3.0`, `servlet-2.2` and `servlet-5.0`, +because they and corresponding tests depend on different versions of the servlet specification. + +At last, request processing may reach the specific framework that your application uses. +In this case Spring MVC and `OwnerController.initCreationForm`. + +If all instrumentations are enabled, then a new span will be created for every highlighted frame. +All spans from Servlet API will have `kind=SERVER` and name based on corresponding class and method names, +such as `ApplicationFilterChain.doFilter` or `FrameworkServlet.doGet`. +Span created by Spring MVC instrumentation will have `kind=INTERNAL` and named `OwnerController.initCreationForm`. + +The state described above has one significant problem. +Observability backends usually aggregate traces based on their root spans. +This means that ALL traces from any application deployed to Servlet container will be grouped together. +Because their root spans will all have the same named based on common entry point. +In order to alleviate this problem, instrumentations for specific frameworks, such as Spring MVC here, +_update_ name of the span corresponding to the entry point. +Each framework instrumentation can decide what is the best span name based on framework implementation details. +Of course, still adhering to OpenTelemetry +[semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md). + +## Additional instrumentations +`RequestDispatcherInstrumentationModule` instruments `javax.servlet.RequestDispatcher.forward` and +`javax.servlet.RequestDispatcher.include` methods to create new `INTERNAL` spans around their +invocations. + +`HttpServletResponseInstrumentationModule` instruments `javax.servlet.http.HttpServletResponse.sendError` +and `javax.servlet.http.HttpServletResponse.sendRedirect` methods to create new `INTERNAL` spans +around their invocations. diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/servlet-2.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/servlet-2.2-javaagent.gradle new file mode 100644 index 000000000..74ea13a2c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/servlet-2.2-javaagent.gradle @@ -0,0 +1,33 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "javax.servlet" + module = "servlet-api" + versions = "[2.2, 3.0)" + assertInverse = true + } + + fail { + group = "javax.servlet" + module = 'javax.servlet-api' + versions = "[3.0,)" + } +} + +dependencies { + compileOnly "javax.servlet:servlet-api:2.2" + api(project(':instrumentation:servlet:servlet-2.2:library')) + implementation(project(':instrumentation:servlet:servlet-common:javaagent')) + + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + testImplementation(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + testLibrary "org.eclipse.jetty:jetty-server:7.0.0.v20091005" + testLibrary "org.eclipse.jetty:jetty-servlet:7.0.0.v20091005" + + latestDepTestLibrary "org.eclipse.jetty:jetty-server:7.+" + latestDepTestLibrary "org.eclipse.jetty:jetty-servlet:7.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/HttpServletResponseInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/HttpServletResponseInstrumentation.java new file mode 100644 index 000000000..e85ffc68d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/HttpServletResponseInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v2_2; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Class javax.servlet.http.HttpServletResponse got method getStatus only + * in Servlet specification version 3.0. This means that we cannot set {@link + * io.opentelemetry.semconv.trace.attributes.SemanticAttributes#HTTP_STATUS_CODE} attribute on the + * created span using just response object. + * + *

This instrumentation intercepts status setting methods from Servlet 2.0 specification and + * stores that status into context store. Then {@link Servlet2Advice#stopSpan(ServletRequest, + * ServletResponse, Throwable, Context, Scope)} can get it from context and set required span + * attribute. + */ +public class HttpServletResponseInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.servlet.http.HttpServletResponse"); + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(named("javax.servlet.http.HttpServletResponse")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf("sendError", "setStatus"), + HttpServletResponseInstrumentation.class.getName() + "$Servlet2ResponseStatusAdvice"); + transformer.applyAdviceToMethod( + named("sendRedirect"), + HttpServletResponseInstrumentation.class.getName() + "$Servlet2ResponseRedirectAdvice"); + } + + @SuppressWarnings("unused") + public static class Servlet2ResponseRedirectAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This HttpServletResponse response) { + InstrumentationContext.get(ServletResponse.class, Integer.class).put(response, 302); + } + } + + @SuppressWarnings("unused") + public static class Servlet2ResponseStatusAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This HttpServletResponse response, @Advice.Argument(0) Integer status) { + InstrumentationContext.get(ServletResponse.class, Integer.class).put(response, status); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2Advice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2Advice.java new file mode 100644 index 000000000..3a4eb39f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2Advice.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v2_2; + +import static io.opentelemetry.instrumentation.servlet.v2_2.Servlet2HttpServerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.servlet.v2_2.ResponseWithStatus; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; + +@SuppressWarnings("unused") +public class Servlet2Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) ServletRequest request, + @Advice.Argument(value = 1, typing = Assigner.Typing.DYNAMIC) ServletResponse response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + CallDepthThreadLocalMap.incrementCallDepth(AppServerBridge.getCallDepthKey()); + + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + return; + } + + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + + Context serverContext = tracer().getServerContext(httpServletRequest); + if (serverContext != null) { + Context updatedContext = tracer().updateContext(serverContext, httpServletRequest); + if (updatedContext != serverContext) { + // updateContext updated context, need to re-scope + scope = updatedContext.makeCurrent(); + } + return; + } + + context = tracer().startSpan(httpServletRequest); + scope = context.makeCurrent(); + // reset response status from previous request + // (some servlet containers reuse response objects to reduce memory allocations) + InstrumentationContext.get(ServletResponse.class, Integer.class).put(response, null); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(0) ServletRequest request, + @Advice.Argument(1) ServletResponse response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(AppServerBridge.getCallDepthKey()); + + if (scope != null) { + scope.close(); + } + + if (context == null && callDepth == 0) { + Context currentContext = Java8BytecodeBridge.currentContext(); + // Something else is managing the context, we're in the outermost level of Servlet + // instrumentation and we have an uncaught throwable. Let's add it to the current span. + if (throwable != null) { + tracer().addUnwrappedThrowable(currentContext, throwable); + } + tracer().setPrincipal(currentContext, (HttpServletRequest) request); + } + + if (scope == null || context == null) { + return; + } + + tracer().setPrincipal(context, (HttpServletRequest) request); + + int responseStatusCode = HttpServletResponse.SC_OK; + Integer responseStatus = + InstrumentationContext.get(ServletResponse.class, Integer.class).get(response); + if (responseStatus != null) { + responseStatusCode = responseStatus; + } + + ResponseWithStatus responseWithStatus = + new ResponseWithStatus((HttpServletResponse) response, responseStatusCode); + if (throwable == null) { + tracer().end(context, responseWithStatus); + } else { + tracer().endExceptionally(context, throwable, responseWithStatus); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2InstrumentationModule.java new file mode 100644 index 000000000..27fa3c3d7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2InstrumentationModule.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v2_2; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterInstrumentation; +import java.util.Arrays; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class Servlet2InstrumentationModule extends InstrumentationModule { + public Servlet2InstrumentationModule() { + super("servlet", "servlet-2.2"); + } + + // this is required to make sure servlet 2 instrumentation won't apply to servlet 3 + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return not(hasClassesNamed("javax.servlet.AsyncEvent", "javax.servlet.AsyncListener")); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new HttpServletResponseInstrumentation(), + new ServletAndFilterInstrumentation( + "javax.servlet", + Servlet2InstrumentationModule.class.getPackage().getName() + ".Servlet2Advice")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/JettyServlet2Test.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/JettyServlet2Test.groovy new file mode 100644 index 000000000..54eb145ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/JettyServlet2Test.groovy @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import javax.servlet.http.HttpServletRequest +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.handler.ErrorHandler +import org.eclipse.jetty.servlet.ServletContextHandler + +class JettyServlet2Test extends HttpServerTest implements AgentTestTrait { + + private static final CONTEXT = "ctx" + + @Override + Server startServer(int port) { + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + ServletContextHandler servletContext = new ServletContextHandler(null, "/$CONTEXT") + servletContext.errorHandler = new ErrorHandler() { + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { + Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception") + writer.write(th ? th.message : message) + } + } + + // FIXME: Add tests for security/authentication. +// ConstraintSecurityHandler security = setupAuthentication(jettyServer) +// servletContext.setSecurityHandler(security) + + servletContext.addServlet(TestServlet2.Sync, SUCCESS.path) + servletContext.addServlet(TestServlet2.Sync, QUERY_PARAM.path) + servletContext.addServlet(TestServlet2.Sync, REDIRECT.path) + servletContext.addServlet(TestServlet2.Sync, ERROR.path) + servletContext.addServlet(TestServlet2.Sync, EXCEPTION.path) + servletContext.addServlet(TestServlet2.Sync, AUTH_REQUIRED.path) + + jettyServer.setHandler(servletContext) + jettyServer.start() + + return jettyServer + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + URI buildAddress() { + return new URI("http://localhost:$port/$CONTEXT/") + } + + @Override + boolean testNotFound() { + false + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == ERROR + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name endpoint == REDIRECT ? "Response.sendRedirect" : "Response.sendError" + kind INTERNAL + childOf((SpanData) parent) + attributes { + } + } + } + + /** + * Setup simple authentication for tests + *

+ * requests to {@code /auth/*} need login 'user' and password 'password' + *

+ * For details @see http://www.eclipse.org/jetty/documentation/9.3.x/embedded-examples.html + * + * @param jettyServer server to attach login service + * @return SecurityHandler that can be assigned to servlet + */ +// private ConstraintSecurityHandler setupAuthentication(Server jettyServer) { +// ConstraintSecurityHandler security = new ConstraintSecurityHandler() +// +// Constraint constraint = new Constraint() +// constraint.setName("auth") +// constraint.setAuthenticate(true) +// constraint.setRoles("role") +// +// ConstraintMapping mapping = new ConstraintMapping() +// mapping.setPathSpec("/auth/*") +// mapping.setConstraint(constraint) +// +// security.setConstraintMappings(mapping) +// security.setAuthenticator(new BasicAuthenticator()) +// +// LoginService loginService = new HashLoginService("TestRealm", +// "src/test/resources/realm.properties") +// security.setLoginService(loginService) +// jettyServer.addBean(loginService) +// +// security +// } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/TestServlet2.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/TestServlet2.groovy new file mode 100644 index 000000000..20e2a9c8b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/TestServlet2.groovy @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import groovy.servlet.AbstractHttpServlet +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class TestServlet2 { + + static class Sync extends AbstractHttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + req.getRequestDispatcher() + HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath) + HttpServerTest.controller(endpoint) { + resp.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + break + case QUERY_PARAM: + resp.status = endpoint.status + resp.writer.print(req.queryString) + break + case REDIRECT: + resp.sendRedirect(endpoint.body) + break + case ERROR: + resp.sendError(endpoint.status, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/resources/realm.properties b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/resources/realm.properties new file mode 100644 index 000000000..cacb91707 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/javaagent/src/test/resources/realm.properties @@ -0,0 +1 @@ +user:password,role diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/servlet-2.2-library.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/servlet-2.2-library.gradle new file mode 100644 index 000000000..c583a523d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/servlet-2.2-library.gradle @@ -0,0 +1,9 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + implementation "org.slf4j:slf4j-api" + + api(project(':instrumentation:servlet:servlet-javax-common:library')) + + compileOnly "javax.servlet:servlet-api:2.2" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/ResponseWithStatus.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/ResponseWithStatus.java new file mode 100644 index 000000000..64bb260b6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/ResponseWithStatus.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v2_2; + +import javax.servlet.http.HttpServletResponse; + +public class ResponseWithStatus { + + private final HttpServletResponse response; + private final int status; + + public ResponseWithStatus(HttpServletResponse response, int status) { + this.response = response; + this.status = status; + } + + public HttpServletResponse getResponse() { + return response; + } + + public int getStatus() { + return status; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/Servlet2Accessor.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/Servlet2Accessor.java new file mode 100644 index 000000000..018a4933e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/Servlet2Accessor.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v2_2; + +import io.opentelemetry.instrumentation.servlet.ServletAsyncListener; +import io.opentelemetry.instrumentation.servlet.javax.JavaxServletAccessor; +import javax.servlet.http.HttpServletRequest; + +public class Servlet2Accessor extends JavaxServletAccessor { + public static final Servlet2Accessor INSTANCE = new Servlet2Accessor(); + + private Servlet2Accessor() {} + + @Override + public Integer getRequestRemotePort(HttpServletRequest httpServletRequest) { + return null; + } + + @Override + public void addRequestAsyncListener( + HttpServletRequest request, + ServletAsyncListener listener, + Object response) { + throw new UnsupportedOperationException(); + } + + @Override + public int getResponseStatus(ResponseWithStatus responseWithStatus) { + return responseWithStatus.getStatus(); + } + + @Override + public boolean isResponseCommitted(ResponseWithStatus responseWithStatus) { + return responseWithStatus.getResponse().isCommitted(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/Servlet2HttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/Servlet2HttpServerTracer.java new file mode 100644 index 000000000..d130d58c1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-2.2/library/src/main/java/io/opentelemetry/instrumentation/servlet/v2_2/Servlet2HttpServerTracer.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v2_2; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.SERVLET; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.servlet.javax.JavaxServletHttpServerTracer; +import javax.servlet.http.HttpServletRequest; + +public class Servlet2HttpServerTracer extends JavaxServletHttpServerTracer { + private static final Servlet2HttpServerTracer TRACER = new Servlet2HttpServerTracer(); + + public Servlet2HttpServerTracer() { + super(Servlet2Accessor.INSTANCE); + } + + @Override + protected String bussinessStatus(ResponseWithStatus responseWithStatus) { + return null; + } + + @Override + protected String bussinessMessage(ResponseWithStatus responseWithStatus) { + return null; + } + + public static Servlet2HttpServerTracer tracer() { + return TRACER; + } + + public Context startSpan(HttpServletRequest request) { + return startSpan(request, getSpanName(request), true); + } + + @Override + public Context updateContext(Context context, HttpServletRequest request) { + ServerSpanNaming.updateServerSpanName(context, SERVLET, () -> getSpanName(request)); + return super.updateContext(context, request); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.servlet-2.2"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/servlet-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/servlet-3.0-javaagent.gradle new file mode 100644 index 000000000..72a5ad138 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/servlet-3.0-javaagent.gradle @@ -0,0 +1,40 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "javax.servlet" + module = 'javax.servlet-api' + versions = "[3.0,)" + assertInverse = true + } + fail { + group = "javax.servlet" + module = 'servlet-api' + versions = "(,)" + } +} + +dependencies { + compileOnly "javax.servlet:javax.servlet-api:3.0.1" + api(project(':instrumentation:servlet:servlet-3.0:library')) + implementation(project(':instrumentation:servlet:servlet-common:javaagent')) + + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + + testImplementation(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + testLibrary "org.eclipse.jetty:jetty-server:8.0.0.v20110901" + testLibrary "org.eclipse.jetty:jetty-servlet:8.0.0.v20110901" + testLibrary "org.apache.tomcat.embed:tomcat-embed-core:8.0.41" + testLibrary "org.apache.tomcat.embed:tomcat-embed-jasper:8.0.41" + + // Jetty 10 seems to refuse to run on java8. + // TODO: we need to setup separate test for Jetty 10 when that is released. + latestDepTestLibrary "org.eclipse.jetty:jetty-server:9.+" + latestDepTestLibrary "org.eclipse.jetty:jetty-servlet:9.+" + + latestDepTestLibrary "org.apache.tomcat.embed:tomcat-embed-core:9.+" + latestDepTestLibrary "org.apache.tomcat.embed:tomcat-embed-jasper:9.+" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/AsyncDispatchAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/AsyncDispatchAdvice.java new file mode 100644 index 000000000..cb9eb49f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/AsyncDispatchAdvice.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.api.tracer.HttpServerTracer.CONTEXT_ATTRIBUTE; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.servlet.AsyncContext; +import javax.servlet.ServletRequest; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class AsyncDispatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static boolean enter( + @Advice.This AsyncContext context, @Advice.AllArguments Object[] args) { + int depth = CallDepthThreadLocalMap.incrementCallDepth(AsyncContext.class); + if (depth > 0) { + return false; + } + + ServletRequest request = context.getRequest(); + + Context currentContext = Java8BytecodeBridge.currentContext(); + Span currentSpan = Java8BytecodeBridge.spanFromContext(currentContext); + if (currentSpan.getSpanContext().isValid()) { + // this tells the dispatched servlet to use the current span as the parent for its work + // (if the currentSpan is not valid for some reason, the original servlet span should still + // be present in the same request attribute, and so that will be used) + // + // the original servlet span stored in the same request attribute does not need to be saved + // and restored on method exit, because dispatch() hands off control of the request + // processing, and nothing can be done with the request anymore after this + request.setAttribute(CONTEXT_ATTRIBUTE, currentContext); + } + + return true; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter boolean topLevel) { + if (topLevel) { + CallDepthThreadLocalMap.reset(AsyncContext.class); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Advice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Advice.java new file mode 100644 index 000000000..f277ce5cf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Advice.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.servlet.v3_0.Servlet3HttpServerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterAdviceHelper; +import javax.servlet.Filter; +import javax.servlet.Servlet; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; + +@SuppressWarnings("unused") +public class Servlet3Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This(typing = Assigner.Typing.DYNAMIC) Object servletOrFilter, + @Advice.Argument(value = 0, readOnly = false) ServletRequest request, + @Advice.Argument(value = 1, readOnly = false) ServletResponse response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + CallDepthThreadLocalMap.incrementCallDepth(AppServerBridge.getCallDepthKey()); + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + return; + } + + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + + boolean servlet = servletOrFilter instanceof Servlet; + MappingResolver mappingResolver; + if (servlet) { + mappingResolver = + InstrumentationContext.get(Servlet.class, MappingResolver.class) + .get((Servlet) servletOrFilter); + } else { + mappingResolver = + InstrumentationContext.get(Filter.class, MappingResolver.class) + .get((Filter) servletOrFilter); + } + + Context attachedContext = tracer().getServerContext(httpServletRequest); + if (attachedContext != null && tracer().needsRescoping(attachedContext)) { + attachedContext = + tracer().updateContext(attachedContext, httpServletRequest, mappingResolver, servlet); + scope = attachedContext.makeCurrent(); + // We are inside nested servlet/filter/app-server span, don't create new span + return; + } + + Context currentContext = Java8BytecodeBridge.currentContext(); + if (attachedContext != null || ServerSpan.fromContextOrNull(currentContext) != null) { + // Update context with info from current request to ensure that server span gets the best + // possible name. + // In case server span was created by app server instrumentations calling updateContext + // returns a new context that contains servlet context path that is used in other + // instrumentations for naming server span. + Context updatedContext = + tracer().updateContext(currentContext, httpServletRequest, mappingResolver, servlet); + if (currentContext != updatedContext) { + // updateContext updated context, need to re-scope + scope = updatedContext.makeCurrent(); + } + // We are inside nested servlet/filter/app-server span, don't create new span + return; + } + + context = tracer().startSpan(httpServletRequest, mappingResolver, servlet); + scope = context.makeCurrent(); + + tracer().setAsyncListenerResponse(httpServletRequest, (HttpServletResponse) response); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(0) ServletRequest request, + @Advice.Argument(1) ServletResponse response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + return; + } + + ServletAndFilterAdviceHelper.stopSpan( + tracer(), + (HttpServletRequest) request, + (HttpServletResponse) response, + throwable, + context, + scope); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3AsyncStartAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3AsyncStartAdvice.java new file mode 100644 index 000000000..a10fb1c10 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3AsyncStartAdvice.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.servlet.v3_0.Servlet3HttpServerTracer.tracer; + +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import javax.servlet.AsyncContext; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class Servlet3AsyncStartAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startAsyncEnter() { + // This allows to detect the outermost invocation of startAsync in method exit + CallDepthThreadLocalMap.incrementCallDepth(AsyncContext.class); + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void startAsyncExit(@Advice.This ServletRequest servletRequest) { + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(AsyncContext.class); + + if (callDepth != 0) { + // This is not the outermost invocation, ignore. + return; + } + + if (servletRequest instanceof HttpServletRequest) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + + if (!tracer().isAsyncListenerAttached(request)) { + tracer().attachAsyncListener(request); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterInitAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterInitAdvice.java new file mode 100644 index 000000000..e66f83a2f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterInitAdvice.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import javax.servlet.Filter; +import javax.servlet.FilterConfig; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class Servlet3FilterInitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void filterInit( + @Advice.This Filter filter, @Advice.Argument(0) FilterConfig filterConfig) { + if (filterConfig == null) { + return; + } + InstrumentationContext.get(Filter.class, MappingResolver.class) + .putIfAbsent(filter, new Servlet3FilterMappingResolverFactory(filterConfig)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterMappingResolverFactory.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterMappingResolverFactory.java new file mode 100644 index 000000000..6443f5bb5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3FilterMappingResolverFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.servlet.naming.ServletFilterMappingResolverFactory; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import java.util.Collection; +import javax.servlet.FilterConfig; +import javax.servlet.FilterRegistration; +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; + +public class Servlet3FilterMappingResolverFactory + extends ServletFilterMappingResolverFactory + implements ContextStore.Factory { + private final FilterConfig filterConfig; + + public Servlet3FilterMappingResolverFactory(FilterConfig filterConfig) { + this.filterConfig = filterConfig; + } + + @Override + protected FilterRegistration getFilterRegistration() { + String filterName = filterConfig.getFilterName(); + ServletContext servletContext = filterConfig.getServletContext(); + if (filterName == null || servletContext == null) { + return null; + } + return servletContext.getFilterRegistration(filterName); + } + + @Override + protected Collection getUrlPatternMappings(FilterRegistration filterRegistration) { + return filterRegistration.getUrlPatternMappings(); + } + + @Override + protected Collection getServletNameMappings(FilterRegistration filterRegistration) { + return filterRegistration.getServletNameMappings(); + } + + @Override + @SuppressWarnings("ReturnsNullCollection") + protected Collection getServletMappings(String servletName) { + ServletRegistration servletRegistration = + filterConfig.getServletContext().getServletRegistration(servletName); + if (servletRegistration == null) { + return null; + } + return servletRegistration.getMappings(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InitAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InitAdvice.java new file mode 100644 index 000000000..16cf005ad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InitAdvice.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class Servlet3InitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void servletInit( + @Advice.This Servlet servlet, @Advice.Argument(0) ServletConfig servletConfig) { + if (servletConfig == null) { + return; + } + InstrumentationContext.get(Servlet.class, MappingResolver.class) + .putIfAbsent(servlet, new Servlet3MappingResolverFactory(servletConfig)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InstrumentationModule.java new file mode 100644 index 000000000..46fbb2bd0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InstrumentationModule.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.async.AsyncContextInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.async.AsyncStartInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Servlet3InstrumentationModule extends InstrumentationModule { + private static final String BASE_PACKAGE = "javax.servlet"; + + public Servlet3InstrumentationModule() { + super("servlet", "servlet-3.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new AsyncContextInstrumentation(BASE_PACKAGE, adviceClassName(".AsyncDispatchAdvice")), + new ServletAndFilterInstrumentation( + BASE_PACKAGE, + adviceClassName(".Servlet3Advice"), + adviceClassName(".Servlet3InitAdvice"), + adviceClassName(".Servlet3FilterInitAdvice")), + new AsyncStartInstrumentation(BASE_PACKAGE, adviceClassName(".Servlet3AsyncStartAdvice"))); + } + + private static String adviceClassName(String suffix) { + return Servlet3InstrumentationModule.class.getPackage().getName() + suffix; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3MappingResolverFactory.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3MappingResolverFactory.java new file mode 100644 index 000000000..5c69a32de --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3MappingResolverFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.servlet.naming.ServletMappingResolverFactory; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import java.util.Collection; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; + +public class Servlet3MappingResolverFactory extends ServletMappingResolverFactory + implements ContextStore.Factory { + private final ServletConfig servletConfig; + + public Servlet3MappingResolverFactory(ServletConfig servletConfig) { + this.servletConfig = servletConfig; + } + + @Override + @SuppressWarnings("ReturnsNullCollection") + public Collection getMappings() { + String servletName = servletConfig.getServletName(); + ServletContext servletContext = servletConfig.getServletContext(); + if (servletName == null || servletContext == null) { + return null; + } + + ServletRegistration servletRegistration = servletContext.getServletRegistration(servletName); + if (servletRegistration == null) { + return null; + } + return servletRegistration.getMappings(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3MappingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3MappingTest.groovy new file mode 100644 index 000000000..a30a7dd8d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3MappingTest.groovy @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import javax.servlet.Servlet +import javax.servlet.ServletException +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import spock.lang.Unroll + +abstract class AbstractServlet3MappingTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + abstract void addServlet(CONTEXT context, String path, Class servlet) + + protected void setupServlets(CONTEXT context) { + addServlet(context, "/prefix/*", TestServlet) + addServlet(context, "*.suffix", TestServlet) + } + + static class TestServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.getWriter().write("Ok") + } + } + + @Unroll + def "test path #path"() { + setup: + AggregatedHttpResponse response = client.get(address.resolve(path).toString()).aggregate().join() + + expect: + response.status().code() == success ? 200 : 404 + + and: + def spanCount = success ? 1 : 2 + assertTraces(1) { + trace(0, spanCount) { + span(0) { + name getContextPath() + spanName + kind SpanKind.SERVER + if (!success) { + status ERROR + } + } + if (!success) { + span(1) { + } + } + } + } + + where: + path | spanName | success + 'prefix' | '/prefix/*' | true + 'prefix/' | '/prefix/*' | true + 'prefix/a' | '/prefix/*' | true + 'prefixa' | '/*' | false + 'a.suffix' | '/*.suffix' | true + '.suffix' | '/*.suffix' | true + 'suffix' | '/*' | false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3Test.groovy new file mode 100644 index 000000000..c1999042f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3Test.groovy @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest +import javax.servlet.Servlet + +abstract class AbstractServlet3Test extends HttpServerTest implements AgentTestTrait { + @Override + URI buildAddress() { + return new URI("http://localhost:$port$contextPath/") + } + + // FIXME: Add authentication tests back in... +// @Shared +// protected String user = "user" +// @Shared +// protected String pass = "password" + + abstract Class servlet() + + abstract void addServlet(CONTEXT context, String path, Class servlet) + + protected void setupServlets(CONTEXT context) { + def servlet = servlet() + + addServlet(context, SUCCESS.path, servlet) + addServlet(context, QUERY_PARAM.path, servlet) + addServlet(context, ERROR.path, servlet) + addServlet(context, EXCEPTION.path, servlet) + addServlet(context, REDIRECT.path, servlet) + addServlet(context, AUTH_REQUIRED.path, servlet) + addServlet(context, INDEXED_CHILD.path, servlet) + } + + protected ServerEndpoint lastRequest + + @Override + AggregatedHttpRequest request(ServerEndpoint uri, String method) { + lastRequest = uri + super.request(uri, method) + } + + boolean errorEndpointUsesSendError() { + true + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || (endpoint == ERROR && errorEndpointUsesSendError()) + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case REDIRECT: + redirectSpan(trace, index, parent) + break + case ERROR: + sendErrorSpan(trace, index, parent) + break + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServlet3MappingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServlet3MappingTest.groovy new file mode 100644 index 000000000..994621eb1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServlet3MappingTest.groovy @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.servlet.Servlet +import javax.servlet.ServletException +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.ServletContextHandler + +class JettyServlet3MappingTest extends AbstractServlet3MappingTest { + + @Override + Server startServer(int port) { + Server server = new Server(port) + ServletContextHandler handler = new ServletContextHandler(null, contextPath) + setupServlets(handler) + server.setHandler(handler) + server.start() + return server + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + protected void setupServlets(ServletContextHandler handler) { + super.setupServlets(handler) + + addServlet(handler, "/", DefaultServlet) + } + + @Override + void addServlet(ServletContextHandler handler, String path, Class servlet) { + handler.addServlet(servlet, path) + } + + static class DefaultServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.sendError(404) + } + } + + @Override + String getContextPath() { + "/jetty-context" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServlet3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServlet3Test.groovy new file mode 100644 index 000000000..9f8cefd36 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServlet3Test.groovy @@ -0,0 +1,286 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import javax.servlet.Servlet +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.handler.ErrorHandler +import org.eclipse.jetty.servlet.ServletContextHandler + +abstract class JettyServlet3Test extends AbstractServlet3Test { + + private static final boolean IS_BEFORE_94 = isBefore94() + + static isBefore94() { + def version = Server.getVersion().split("\\.") + def major = Integer.parseInt(version[0]) + def minor = Integer.parseInt(version[1]) + return major < 9 || (major == 9 && minor < 4) + } + + @Override + boolean testNotFound() { + false + } + + @Override + Class expectedExceptionClass() { + ServletException + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + return (IS_BEFORE_94 && endpoint == EXCEPTION) || super.hasResponseSpan(endpoint) + } + + @Override + void responseSpan(TraceAssert trace, int index, Object controllerSpan, Object handlerSpan, String method, ServerEndpoint endpoint) { + if (IS_BEFORE_94 && endpoint == EXCEPTION) { + sendErrorSpan(trace, index, handlerSpan) + } + super.responseSpan(trace, index, controllerSpan, handlerSpan, method, endpoint) + } + + @Override + Server startServer(int port) { + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + ServletContextHandler servletContext = new ServletContextHandler(null, contextPath) + servletContext.errorHandler = new ErrorHandler() { + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { + Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception") + writer.write(th ? th.message : message) + } + } +// setupAuthentication(jettyServer, servletContext) + setupServlets(servletContext) + jettyServer.setHandler(servletContext) + + jettyServer.start() + + return jettyServer + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/jetty-context" + } + + @Override + void addServlet(ServletContextHandler servletContext, String path, Class servlet) { + servletContext.addServlet(servlet, path) + } + + // FIXME: Add authentication tests back in... +// static setupAuthentication(Server jettyServer, ServletContextHandler servletContext) { +// ConstraintSecurityHandler authConfig = new ConstraintSecurityHandler() +// +// Constraint constraint = new Constraint() +// constraint.setName("auth") +// constraint.setAuthenticate(true) +// constraint.setRoles("role") +// +// ConstraintMapping mapping = new ConstraintMapping() +// mapping.setPathSpec("/auth/*") +// mapping.setConstraint(constraint) +// +// authConfig.setConstraintMappings(mapping) +// authConfig.setAuthenticator(new BasicAuthenticator()) +// +// LoginService loginService = new HashLoginService("TestRealm", +// "src/test/resources/realm.properties") +// authConfig.setLoginService(loginService) +// jettyServer.addBean(loginService) +// +// servletContext.setSecurityHandler(authConfig) +// } +} + +class JettyServlet3TestSync extends JettyServlet3Test { + + @Override + Class servlet() { + TestServlet3.Sync + } + + @Override + boolean testConcurrency() { + return true + } +} + +class JettyServlet3TestAsync extends JettyServlet3Test { + + @Override + Class servlet() { + TestServlet3.Async + } + + @Override + boolean errorEndpointUsesSendError() { + false + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } + + @Override + boolean testConcurrency() { + return true + } +} + +class JettyServlet3TestFakeAsync extends JettyServlet3Test { + + @Override + Class servlet() { + TestServlet3.FakeAsync + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } + + @Override + boolean testConcurrency() { + return true + } +} + +class JettyServlet3TestForward extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet3.Sync // dispatch to sync servlet + } + + @Override + protected void setupServlets(ServletContextHandler context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + QUERY_PARAM.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + REDIRECT.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + ERROR.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + EXCEPTION.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, RequestDispatcherServlet.Forward) + } +} + +class JettyServlet3TestInclude extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet3.Sync // dispatch to sync servlet + } + + @Override + boolean testRedirect() { + false + } + + @Override + boolean testError() { + false + } + + @Override + protected void setupServlets(ServletContextHandler context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + QUERY_PARAM.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + REDIRECT.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + ERROR.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + EXCEPTION.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, RequestDispatcherServlet.Include) + } +} + + +class JettyServlet3TestDispatchImmediate extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet3.Sync + } + + @Override + protected void setupServlets(ServletContextHandler context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + QUERY_PARAM.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + ERROR.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + REDIRECT.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive) + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } +} + +class JettyServlet3TestDispatchAsync extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet3.Async + } + + @Override + protected void setupServlets(ServletContextHandler context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + QUERY_PARAM.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + ERROR.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + REDIRECT.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive) + } + + @Override + boolean errorEndpointUsesSendError() { + false + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } +} + +abstract class JettyDispatchTest extends JettyServlet3Test { + @Override + URI buildAddress() { + return new URI("http://localhost:$port$contextPath/dispatch/") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy new file mode 100644 index 000000000..2d6779dac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import javax.servlet.Servlet +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.handler.ErrorHandler +import org.eclipse.jetty.servlet.ServletHandler + +class JettyServletHandlerTest extends AbstractServlet3Test { + + private static final boolean IS_BEFORE_94 = isBefore94() + + static isBefore94() { + def version = Server.getVersion().split("\\.") + def major = Integer.parseInt(version[0]) + def minor = Integer.parseInt(version[1]) + return major < 9 || (major == 9 && minor < 4) + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + return (IS_BEFORE_94 && endpoint == EXCEPTION) || super.hasResponseSpan(endpoint) + } + + @Override + void responseSpan(TraceAssert trace, int index, Object controllerSpan, Object handlerSpan, String method, ServerEndpoint endpoint) { + if (IS_BEFORE_94 && endpoint == EXCEPTION) { + sendErrorSpan(trace, index, handlerSpan) + } + super.responseSpan(trace, index, controllerSpan, handlerSpan, method, endpoint) + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + "HTTP GET" + } + + @Override + Server startServer(int port) { + Server server = new Server(port) + ServletHandler handler = new ServletHandler() + server.setHandler(handler) + setupServlets(handler) + server.addBean(new ErrorHandler() { + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { + Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception") + writer.write(th ? th.message : message) + } + }) + server.start() + return server + } + + @Override + void addServlet(ServletHandler servletHandler, String path, Class servlet) { + servletHandler.addServletWithMapping(servlet, path) + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + "" + } + + @Override + Class servlet() { + TestServlet3.Sync + } + + @Override + boolean testNotFound() { + false + } + + @Override + Class expectedExceptionClass() { + ServletException + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TestServlet3.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TestServlet3.groovy new file mode 100644 index 000000000..90b144062 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TestServlet3.groovy @@ -0,0 +1,191 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import groovy.servlet.AbstractHttpServlet +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import java.util.concurrent.Phaser +import javax.servlet.RequestDispatcher +import javax.servlet.ServletException +import javax.servlet.annotation.WebServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class TestServlet3 { + + @WebServlet + static class Sync extends AbstractHttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + String servletPath = req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH) + if (servletPath == null) { + servletPath = req.servletPath + } + HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(servletPath) + HttpServerTest.controller(endpoint) { + resp.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + break + case INDEXED_CHILD: + resp.status = endpoint.status + endpoint.collectSpanAttributes { req.getParameter(it) } + break + case QUERY_PARAM: + resp.status = endpoint.status + resp.writer.print(req.queryString) + break + case REDIRECT: + resp.sendRedirect(endpoint.body) + break + case ERROR: + resp.sendError(endpoint.status, endpoint.body) + break + case EXCEPTION: + throw new ServletException(endpoint.body) + } + } + } + } + + @WebServlet(asyncSupported = true) + static class Async extends AbstractHttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath) + def phaser = new Phaser(2) + def context = req.startAsync() + context.start { + try { + phaser.arriveAndAwaitAdvance() + HttpServerTest.controller(endpoint) { + resp.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + context.complete() + break + case INDEXED_CHILD: + endpoint.collectSpanAttributes { req.getParameter(it) } + resp.status = endpoint.status + context.complete() + break + case QUERY_PARAM: + resp.status = endpoint.status + resp.writer.print(req.queryString) + context.complete() + break + case REDIRECT: + resp.sendRedirect(endpoint.body) + context.complete() + break + case ERROR: + resp.status = endpoint.status + resp.writer.print(endpoint.body) +// resp.sendError(endpoint.status, endpoint.body) + context.complete() + break + case EXCEPTION: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + context.complete() + throw new Exception(endpoint.body) + } + } + } finally { + phaser.arriveAndDeregister() + } + } + phaser.arriveAndAwaitAdvance() + phaser.arriveAndAwaitAdvance() + } + } + + @WebServlet(asyncSupported = true) + static class FakeAsync extends AbstractHttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + def context = req.startAsync() + try { + HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath) + HttpServerTest.controller(endpoint) { + resp.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + break + case INDEXED_CHILD: + endpoint.collectSpanAttributes { req.getParameter(it) } + resp.status = endpoint.status + break + case QUERY_PARAM: + resp.status = endpoint.status + resp.writer.print(req.queryString) + break + case REDIRECT: + resp.sendRedirect(endpoint.body) + break + case ERROR: + resp.sendError(endpoint.status, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + } + } + } finally { + context.complete() + } + } + } + + @WebServlet(asyncSupported = true) + static class DispatchImmediate extends AbstractHttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + def target = req.servletPath.replace("/dispatch", "") + req.startAsync().dispatch(target) + } + } + + @WebServlet(asyncSupported = true) + static class DispatchAsync extends AbstractHttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + def target = req.servletPath.replace("/dispatch", "") + def context = req.startAsync() + context.start { + context.dispatch(target) + } + } + } + + // TODO: Add tests for this! + @WebServlet(asyncSupported = true) + static class DispatchRecursive extends AbstractHttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + if (req.servletPath.equals("/recursive")) { + resp.writer.print("Hello Recursive") + return + } + def depth = Integer.parseInt(req.getParameter("depth")) + if (depth > 0) { + req.startAsync().dispatch("/dispatch/recursive?depth=" + (depth - 1)) + } else { + req.startAsync().dispatch("/recursive") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3FilterMappingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3FilterMappingTest.groovy new file mode 100644 index 000000000..dbbdb6715 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3FilterMappingTest.groovy @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import javax.servlet.Filter +import javax.servlet.FilterChain +import javax.servlet.FilterConfig +import javax.servlet.ServletException +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import org.apache.catalina.Context +import org.apache.catalina.startup.Tomcat +import org.apache.tomcat.util.descriptor.web.FilterDef +import org.apache.tomcat.util.descriptor.web.FilterMap + +abstract class TomcatServlet3FilterMappingTest extends TomcatServlet3MappingTest { + + void addFilter(Context servletContext, String path, Class filter) { + String name = UUID.randomUUID() + FilterDef filterDef = new FilterDef() + filterDef.setFilter(filter.newInstance()) + filterDef.setFilterName(name) + servletContext.addFilterDef(filterDef) + FilterMap filterMap = new FilterMap() + filterMap.setFilterName(name) + filterMap.addURLPattern(path) + servletContext.addFilterMap(filterMap) + } + + void addFilterWithServletName(Context servletContext, String servletName, Class filter) { + String name = UUID.randomUUID() + FilterDef filterDef = new FilterDef() + filterDef.setFilter(filter.newInstance()) + filterDef.setFilterName(name) + servletContext.addFilterDef(filterDef) + FilterMap filterMap = new FilterMap() + filterMap.setFilterName(name) + filterMap.addServletName(servletName) + servletContext.addFilterMap(filterMap) + } + + static class TestFilter implements Filter { + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + if (servletRequest.getAttribute("firstFilterCalled") != null) { + servletRequest.setAttribute("testFilterCalled", Boolean.TRUE) + filterChain.doFilter(servletRequest, servletResponse) + } else { + throw new IllegalStateException("First filter should have been called.") + } + } + + @Override + void destroy() { + } + } + + static class FirstFilter implements Filter { + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + servletRequest.setAttribute("firstFilterCalled", Boolean.TRUE) + filterChain.doFilter(servletRequest, servletResponse) + } + + @Override + void destroy() { + } + } + + static class LastFilter implements Filter { + + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + if (servletRequest.getAttribute("testFilterCalled") != null) { + HttpServletResponse response = (HttpServletResponse) servletResponse + response.getWriter().write("Ok") + response.setStatus(HttpServletResponse.SC_OK) + } else { + filterChain.doFilter(servletRequest, servletResponse) + } + } + + @Override + void destroy() { + } + } + + static class DefaultServlet extends HttpServlet { + protected void service(HttpServletRequest req, HttpServletResponse resp) { + throw new IllegalStateException("Servlet should not have been called, filter should have handled the request.") + } + } +} + +class TomcatServlet3FilterUrlPatternMappingTest extends TomcatServlet3FilterMappingTest { + @Override + protected void setupServlets(Context context) { + addFilter(context, "/*", FirstFilter) + addFilter(context, "/prefix/*", TestFilter) + addFilter(context, "*.suffix", TestFilter) + addFilter(context, "/*", LastFilter) + } +} + +class TomcatServlet3FilterServletNameMappingTest extends TomcatServlet3FilterMappingTest { + @Override + protected void setupServlets(Context context) { + Tomcat.addServlet(context, "prefix-servlet", new DefaultServlet()) + context.addServletMappingDecoded("/prefix/*", "prefix-servlet") + Tomcat.addServlet(context, "suffix-servlet", new DefaultServlet()) + context.addServletMappingDecoded("*.suffix", "suffix-servlet") + + addFilter(context, "/*", FirstFilter) + addFilterWithServletName(context, "prefix-servlet", TestFilter) + addFilterWithServletName(context, "suffix-servlet", TestFilter) + addFilterWithServletName(context, "prefix-servlet", LastFilter) + addFilterWithServletName(context, "suffix-servlet", LastFilter) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3MappingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3MappingTest.groovy new file mode 100644 index 000000000..4e05b2f77 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3MappingTest.groovy @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.nio.file.Files +import javax.servlet.Servlet +import org.apache.catalina.Context +import org.apache.catalina.startup.Tomcat +import org.apache.tomcat.JarScanFilter +import org.apache.tomcat.JarScanType + +class TomcatServlet3MappingTest extends AbstractServlet3MappingTest { + + @Override + Tomcat startServer(int port) { + def tomcatServer = new Tomcat() + + def baseDir = Files.createTempDirectory("tomcat").toFile() + baseDir.deleteOnExit() + tomcatServer.setBaseDir(baseDir.getAbsolutePath()) + + tomcatServer.setPort(port) + tomcatServer.getConnector().enableLookups = true // get localhost instead of 127.0.0.1 + + File applicationDir = new File(baseDir, "/webapps/ROOT") + if (!applicationDir.exists()) { + applicationDir.mkdirs() + applicationDir.deleteOnExit() + } + Context servletContext = tomcatServer.addWebapp(contextPath, applicationDir.getAbsolutePath()) + // Speed up startup by disabling jar scanning: + servletContext.getJarScanner().setJarScanFilter(new JarScanFilter() { + @Override + boolean check(JarScanType jarScanType, String jarName) { + return false + } + }) + + setupServlets(servletContext) + + tomcatServer.start() + + return tomcatServer + } + + @Override + void stopServer(Tomcat server) { + server.stop() + server.destroy() + } + + @Override + void addServlet(Context servletContext, String path, Class servlet) { + String name = UUID.randomUUID() + Tomcat.addServlet(servletContext, name, servlet.newInstance()) + servletContext.addServletMappingDecoded(path, name) + } + + @Override + String getContextPath() { + return "/tomcat-context" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3Test.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3Test.groovy new file mode 100644 index 000000000..255e8cf47 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3Test.groovy @@ -0,0 +1,458 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static org.junit.Assume.assumeTrue + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import java.nio.file.Files +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import javax.servlet.Servlet +import javax.servlet.ServletException +import org.apache.catalina.AccessLog +import org.apache.catalina.Context +import org.apache.catalina.connector.Request +import org.apache.catalina.connector.Response +import org.apache.catalina.core.StandardHost +import org.apache.catalina.startup.Tomcat +import org.apache.catalina.valves.ErrorReportValve +import org.apache.catalina.valves.ValveBase +import org.apache.tomcat.JarScanFilter +import org.apache.tomcat.JarScanType +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class TomcatServlet3Test extends AbstractServlet3Test { + + @Override + Class expectedExceptionClass() { + ServletException + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == NOT_FOUND || super.hasResponseSpan(endpoint) + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case NOT_FOUND: + sendErrorSpan(trace, index, parent) + break + } + super.responseSpan(trace, index, parent, method, endpoint) + } + + @Shared + def accessLogValue = new TestAccessLogValve() + + @Override + Tomcat startServer(int port) { + def tomcatServer = new Tomcat() + + def baseDir = Files.createTempDirectory("tomcat").toFile() + baseDir.deleteOnExit() + tomcatServer.setBaseDir(baseDir.getAbsolutePath()) + + tomcatServer.setPort(port) + tomcatServer.getConnector().enableLookups = true // get localhost instead of 127.0.0.1 + + File applicationDir = new File(baseDir, "/webapps/ROOT") + if (!applicationDir.exists()) { + applicationDir.mkdirs() + applicationDir.deleteOnExit() + } + Context servletContext = tomcatServer.addWebapp(contextPath, applicationDir.getAbsolutePath()) + // Speed up startup by disabling jar scanning: + servletContext.getJarScanner().setJarScanFilter(new JarScanFilter() { + @Override + boolean check(JarScanType jarScanType, String jarName) { + return false + } + }) + +// setupAuthentication(tomcatServer, servletContext) + setupServlets(servletContext) + + (tomcatServer.host as StandardHost).errorReportValveClass = ErrorHandlerValve.name + (tomcatServer.host as StandardHost).getPipeline().addValve(accessLogValue) + + tomcatServer.start() + + return tomcatServer + } + + def setup() { + accessLogValue.loggedIds.clear() + } + + @Override + void stopServer(Tomcat server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/tomcat-context" + } + + @Override + void addServlet(Context servletContext, String path, Class servlet) { + String name = UUID.randomUUID() + Tomcat.addServlet(servletContext, name, servlet.newInstance()) + servletContext.addServletMappingDecoded(path, name) + } + + def "access log has ids for #count requests"() { + given: + def request = request(SUCCESS, method) + + when: + List responses = (1..count).collect { + return client.execute(request).aggregate().join() + } + + then: + responses.each { response -> + assert response.status().code() == SUCCESS.status + assert response.contentUtf8() == SUCCESS.body + } + + and: + assertTraces(count) { + accessLogValue.waitForLoggedIds(count) + assert accessLogValue.loggedIds.size() == count + def loggedTraces = accessLogValue.loggedIds*.first + def loggedSpans = accessLogValue.loggedIds*.second + + (0..count - 1).each { + trace(it, 2) { + serverSpan(it, 0, null, null, "GET", SUCCESS.body.length()) + controllerSpan(it, 1, span(0)) + } + + assert loggedTraces.contains(traces[it][0].traceId) + assert loggedSpans.contains(traces[it][0].spanId) + } + } + + where: + method = "GET" + count << [1, 4] // make multiple requests. + } + + def "access log has ids for error request"() { + setup: + assumeTrue(testError()) + def request = request(ERROR, method) + def response = client.execute(request).aggregate().join() + + expect: + response.status().code() == ERROR.status + response.contentUtf8() == ERROR.body + + and: + def spanCount = 2 + if (errorEndpointUsesSendError()) { + spanCount++ + } + assertTraces(1) { + trace(0, spanCount) { + serverSpan(it, 0, null, null, method, response.content().length(), ERROR) + def spanIndex = 1 + controllerSpan(it, spanIndex, span(spanIndex - 1)) + spanIndex++ + if (errorEndpointUsesSendError()) { + sendErrorSpan(it, spanIndex, span(spanIndex - 1)) + spanIndex++ + } + } + + accessLogValue.waitForLoggedIds(1) + def (String traceId, String spanId) = accessLogValue.loggedIds[0] + assert traces[0][0].traceId == traceId + assert traces[0][0].spanId == spanId + } + + where: + method = "GET" + } + + // FIXME: Add authentication tests back in... +// private setupAuthentication(Tomcat server, Context servletContext) { +// // Login Config +// LoginConfig authConfig = new LoginConfig() +// authConfig.setAuthMethod("BASIC") +// +// // adding constraint with role "test" +// SecurityConstraint constraint = new SecurityConstraint() +// constraint.addAuthRole("role") +// +// // add constraint to a collection with pattern /second +// SecurityCollection collection = new SecurityCollection() +// collection.addPattern("/auth/*") +// constraint.addCollection(collection) +// +// servletContext.setLoginConfig(authConfig) +// // does the context need a auth role too? +// servletContext.addSecurityRole("role") +// servletContext.addConstraint(constraint) +// +// // add tomcat users to realm +// MemoryRealm realm = new MemoryRealm() { +// protected void startInternal() { +// credentialHandler = new MessageDigestCredentialHandler() +// setState(LifecycleState.STARTING) +// } +// } +// realm.addUser(user, pass, "role") +// server.getEngine().setRealm(realm) +// +// servletContext.setLoginConfig(authConfig) +// } +} + +class ErrorHandlerValve extends ErrorReportValve { + @Override + protected void report(Request request, Response response, Throwable t) { + if (response.getStatus() < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) { + return + } + try { + response.writer.print(t ? t.cause.message : response.message) + } catch (IOException e) { + e.printStackTrace() + } + } +} + +class TestAccessLogValve extends ValveBase implements AccessLog { + final List> loggedIds = [] + + TestAccessLogValve() { + super(true) + } + + void log(Request request, Response response, long time) { + synchronized (loggedIds) { + loggedIds.add(new Tuple2(request.getAttribute("trace_id"), + request.getAttribute("span_id"))) + loggedIds.notifyAll() + } + } + + void waitForLoggedIds(int expected) { + def timeout = TimeUnit.SECONDS.toMillis(20) + def startTime = System.currentTimeMillis() + def endTime = startTime + timeout + def toWait = timeout + synchronized (loggedIds) { + while (loggedIds.size() < expected && toWait > 0) { + loggedIds.wait(toWait) + toWait = endTime - System.currentTimeMillis() + } + if (toWait <= 0) { + throw new TimeoutException("Timeout waiting for " + expected + " access log ids, got " + loggedIds.size()) + } + } + } + + @Override + void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + } + + @Override + boolean getRequestAttributesEnabled() { + return false + } + + @Override + void invoke(Request request, Response response) throws IOException, ServletException { + getNext().invoke(request, response) + } +} + +class TomcatServlet3TestSync extends TomcatServlet3Test { + + @Override + Class servlet() { + TestServlet3.Sync + } +} + +class TomcatServlet3TestAsync extends TomcatServlet3Test { + + @Override + Class servlet() { + TestServlet3.Async + } + + @Override + boolean errorEndpointUsesSendError() { + false + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } + + @Override + boolean testConcurrency() { + return true + } +} + +class TomcatServlet3TestFakeAsync extends TomcatServlet3Test { + + @Override + Class servlet() { + TestServlet3.FakeAsync + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } + + @Override + boolean testConcurrency() { + return true + } +} + +class TomcatServlet3TestForward extends TomcatDispatchTest { + @Override + Class servlet() { + TestServlet3.Sync // dispatch to sync servlet + } + + @Override + boolean testNotFound() { + false + } + + @Override + protected void setupServlets(Context context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + QUERY_PARAM.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + REDIRECT.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + ERROR.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + EXCEPTION.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, RequestDispatcherServlet.Forward) + } +} + +class TomcatServlet3TestInclude extends TomcatDispatchTest { + @Override + Class servlet() { + TestServlet3.Sync // dispatch to sync servlet + } + + @Override + boolean testNotFound() { + false + } + + @Override + boolean testRedirect() { + false + } + + @Override + boolean testError() { + false + } + + @Override + protected void setupServlets(Context context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + QUERY_PARAM.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + REDIRECT.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + ERROR.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + EXCEPTION.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, RequestDispatcherServlet.Include) + } +} + +class TomcatServlet3TestDispatchImmediate extends TomcatDispatchTest { + @Override + Class servlet() { + TestServlet3.Sync + } + + @Override + boolean testNotFound() { + false + } + + @Override + protected void setupServlets(Context context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + QUERY_PARAM.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + ERROR.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + REDIRECT.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet3.DispatchImmediate) + addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive) + } +} + +class TomcatServlet3TestDispatchAsync extends TomcatDispatchTest { + @Override + Class servlet() { + TestServlet3.Async + } + + @Override + protected void setupServlets(Context context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + QUERY_PARAM.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + ERROR.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + REDIRECT.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet3.DispatchAsync) + addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive) + } + + @Override + boolean errorEndpointUsesSendError() { + false + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } +} + +abstract class TomcatDispatchTest extends TomcatServlet3Test { + @Override + URI buildAddress() { + return new URI("http://localhost:$port$contextPath/dispatch/") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/java/RequestDispatcherServlet.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/java/RequestDispatcherServlet.java new file mode 100644 index 000000000..a0914660d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/java/RequestDispatcherServlet.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.IOException; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class RequestDispatcherServlet { + /* There's something about the getRequestDispatcher call that breaks horribly when these classes + * are written in groovy. + */ + + @WebServlet(asyncSupported = true) + public static class Forward extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String target = req.getServletPath().replace("/dispatch", ""); + ServletContext context = getServletContext(); + RequestDispatcher dispatcher = context.getRequestDispatcher(target); + dispatcher.forward(req, resp); + } + } + + @WebServlet(asyncSupported = true) + public static class Include extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String target = req.getServletPath().replace("/dispatch", ""); + ServletContext context = getServletContext(); + RequestDispatcher dispatcher = context.getRequestDispatcher(target); + dispatcher.include(req, resp); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/resources/realm.properties b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/resources/realm.properties new file mode 100644 index 000000000..cacb91707 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/javaagent/src/test/resources/realm.properties @@ -0,0 +1 @@ +user:password,role diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/servlet-3.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/servlet-3.0-library.gradle new file mode 100644 index 000000000..bbffa5fad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/servlet-3.0-library.gradle @@ -0,0 +1,7 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + api(project(':instrumentation:servlet:servlet-javax-common:library')) + + compileOnly "javax.servlet:javax.servlet-api:3.0.1" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3Accessor.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3Accessor.java new file mode 100644 index 000000000..7abb9ca31 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3Accessor.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import io.opentelemetry.instrumentation.servlet.ServletAsyncListener; +import io.opentelemetry.instrumentation.servlet.javax.JavaxServletAccessor; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class Servlet3Accessor extends JavaxServletAccessor { + public static final Servlet3Accessor INSTANCE = new Servlet3Accessor(); + + private Servlet3Accessor() {} + + @Override + public Integer getRequestRemotePort(HttpServletRequest request) { + return request.getRemotePort(); + } + + @Override + public void addRequestAsyncListener( + HttpServletRequest request, + ServletAsyncListener listener, + Object response) { + if (response instanceof HttpServletResponse) { + request + .getAsyncContext() + .addListener(new Listener(listener), request, (HttpServletResponse) response); + } else { + request.getAsyncContext().addListener(new Listener(listener)); + } + } + + @Override + public int getResponseStatus(HttpServletResponse response) { + return response.getStatus(); + } + + @Override + public boolean isResponseCommitted(HttpServletResponse response) { + return response.isCommitted(); + } + + private static class Listener implements AsyncListener { + private final ServletAsyncListener listener; + + private Listener(ServletAsyncListener listener) { + this.listener = listener; + } + + @Override + public void onComplete(AsyncEvent event) { + listener.onComplete((HttpServletResponse) event.getSuppliedResponse()); + } + + @Override + public void onTimeout(AsyncEvent event) { + listener.onTimeout(event.getAsyncContext().getTimeout()); + } + + @Override + public void onError(AsyncEvent event) { + listener.onError(event.getThrowable(), (HttpServletResponse) event.getSuppliedResponse()); + } + + @Override + public void onStartAsync(AsyncEvent event) { + event.getAsyncContext().addListener(this); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3HttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3HttpServerTracer.java new file mode 100644 index 000000000..548cd3329 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-3.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/v3_0/Servlet3HttpServerTracer.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.v3_0; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.FILTER; +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.SERVLET; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.servlet.javax.JavaxServletHttpServerTracer; +import io.opentelemetry.instrumentation.servlet.naming.ServletSpanNameProvider; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class Servlet3HttpServerTracer extends JavaxServletHttpServerTracer { + private static final Servlet3HttpServerTracer TRACER = new Servlet3HttpServerTracer(); + private static final ServletSpanNameProvider SPAN_NAME_PROVIDER = + new ServletSpanNameProvider<>(Servlet3Accessor.INSTANCE); + + protected Servlet3HttpServerTracer() { + super(Servlet3Accessor.INSTANCE); + } + + @Override + protected String bussinessStatus(HttpServletResponse response) { + return null; + } + + @Override + protected String bussinessMessage(HttpServletResponse response) { + return null; + } + + public static Servlet3HttpServerTracer tracer() { + return TRACER; + } + + public Context startSpan( + HttpServletRequest request, MappingResolver mappingResolver, boolean servlet) { + return startSpan(request, SPAN_NAME_PROVIDER.getSpanName(mappingResolver, request), servlet); + } + + public Context updateContext( + Context context, + HttpServletRequest request, + MappingResolver mappingResolver, + boolean servlet) { + ServerSpanNaming.updateServerSpanName( + context, + servlet ? SERVLET : FILTER, + () -> SPAN_NAME_PROVIDER.getSpanNameOrNull(mappingResolver, request)); + return updateContext(context, request); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.servlet-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/servlet-5.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/servlet-5.0-javaagent.gradle new file mode 100644 index 000000000..366f61171 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/servlet-5.0-javaagent.gradle @@ -0,0 +1,21 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "jakarta.servlet" + module = 'jakarta.servlet-api' + versions = "[5.0.0,)" + assertInverse = true + } +} + +dependencies { + api(project(':instrumentation:servlet:servlet-5.0:library')) + implementation(project(':instrumentation:servlet:servlet-common:javaagent')) + compileOnly "jakarta.servlet:jakarta.servlet-api:5.0.0" + + testLibrary "org.eclipse.jetty:jetty-server:11.0.0" + testLibrary "org.eclipse.jetty:jetty-servlet:11.0.0" + testLibrary "org.apache.tomcat.embed:tomcat-embed-core:10.0.0" + testLibrary "org.apache.tomcat.embed:tomcat-embed-jasper:10.0.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/JakartaServletInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/JakartaServletInstrumentationModule.java new file mode 100644 index 000000000..68630df17 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/JakartaServletInstrumentationModule.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.async.AsyncContextInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.async.AsyncStartInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.response.HttpServletResponseInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterInstrumentation; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JakartaServletInstrumentationModule extends InstrumentationModule { + private static final String BASE_PACKAGE = "jakarta.servlet"; + + public JakartaServletInstrumentationModule() { + super("servlet", "servlet-5.0"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new AsyncContextInstrumentation( + BASE_PACKAGE, adviceClassName(".async.AsyncDispatchAdvice")), + new AsyncStartInstrumentation(BASE_PACKAGE, adviceClassName(".async.AsyncStartAdvice")), + new ServletAndFilterInstrumentation( + BASE_PACKAGE, + adviceClassName(".service.JakartaServletServiceAdvice"), + adviceClassName(".service.JakartaServletInitAdvice"), + adviceClassName(".service.JakartaServletFilterInitAdvice")), + new HttpServletResponseInstrumentation( + BASE_PACKAGE, adviceClassName(".response.ResponseSendAdvice"))); + } + + private static String adviceClassName(String suffix) { + return JakartaServletInstrumentationModule.class.getPackage().getName() + suffix; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/async/AsyncDispatchAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/async/AsyncDispatchAdvice.java new file mode 100644 index 000000000..1d9b2eb70 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/async/AsyncDispatchAdvice.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.async; + +import static io.opentelemetry.instrumentation.api.tracer.HttpServerTracer.CONTEXT_ATTRIBUTE; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletRequest; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class AsyncDispatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static boolean enter( + @Advice.This AsyncContext context, @Advice.AllArguments Object[] args) { + int depth = CallDepthThreadLocalMap.incrementCallDepth(AsyncContext.class); + if (depth > 0) { + return false; + } + + ServletRequest request = context.getRequest(); + + Context currentContext = Java8BytecodeBridge.currentContext(); + Span currentSpan = Java8BytecodeBridge.spanFromContext(currentContext); + if (currentSpan.getSpanContext().isValid()) { + // this tells the dispatched servlet to use the current span as the parent for its work + // (if the currentSpan is not valid for some reason, the original servlet span should still + // be present in the same request attribute, and so that will be used) + // + // the original servlet span stored in the same request attribute does not need to be saved + // and restored on method exit, because dispatch() hands off control of the request + // processing, and nothing can be done with the request anymore after this + request.setAttribute(CONTEXT_ATTRIBUTE, currentContext); + } + + return true; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter boolean topLevel) { + if (topLevel) { + CallDepthThreadLocalMap.reset(AsyncContext.class); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/async/AsyncStartAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/async/AsyncStartAdvice.java new file mode 100644 index 000000000..6ca9d3cc3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/async/AsyncStartAdvice.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.async; + +import static io.opentelemetry.instrumentation.servlet.jakarta.v5_0.JakartaServletHttpServerTracer.tracer; + +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.http.HttpServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; + +@SuppressWarnings("unused") +public class AsyncStartAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startAsyncEnter() { + // This allows to detect the outermost invocation of startAsync in method exit + CallDepthThreadLocalMap.incrementCallDepth(AsyncContext.class); + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void startAsyncExit( + @Advice.This(typing = Assigner.Typing.DYNAMIC) HttpServletRequest request) { + + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(AsyncContext.class); + + if (callDepth != 0) { + // This is not the outermost invocation, ignore. + return; + } + + if (request != null) { + if (!tracer().isAsyncListenerAttached(request)) { + tracer().attachAsyncListener(request); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseSendAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseSendAdvice.java new file mode 100644 index 000000000..099eec3c7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseSendAdvice.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.response; + +import static io.opentelemetry.javaagent.instrumentation.servlet.v5_0.response.ResponseTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.CallDepth; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.servlet.common.response.HttpServletResponseAdviceHelper; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class ResponseSendAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void start( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepthThreadLocalMap.getCallDepth(HttpServletResponse.class); + // Don't want to generate a new top-level span + if (callDepth.getAndIncrement() == 0 + && Java8BytecodeBridge.currentSpan().getSpanContext().isValid()) { + context = tracer().startSpan(method); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + HttpServletResponseAdviceHelper.stopSpan(tracer(), throwable, context, scope, callDepth); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseTracer.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseTracer.java new file mode 100644 index 000000000..cfd40e4a1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseTracer.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.response; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import java.lang.reflect.Method; + +public class ResponseTracer extends BaseTracer { + private static final ResponseTracer TRACER = new ResponseTracer(); + + public static ResponseTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.servlet-5.0"; + } + + public Context startSpan(Method method) { + return startSpan(SpanNames.fromMethod(method)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletFilterInitAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletFilterInitAdvice.java new file mode 100644 index 000000000..92ab8ca50 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletFilterInitAdvice.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterConfig; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class JakartaServletFilterInitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void filterInit( + @Advice.This Filter filter, @Advice.Argument(0) FilterConfig filterConfig) { + if (filterConfig == null) { + return; + } + InstrumentationContext.get(Filter.class, MappingResolver.class) + .putIfAbsent(filter, new JakartaServletFilterMappingResolverFactory(filterConfig)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletFilterMappingResolverFactory.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletFilterMappingResolverFactory.java new file mode 100644 index 000000000..f9d8b777f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletFilterMappingResolverFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.servlet.naming.ServletFilterMappingResolverFactory; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; +import java.util.Collection; + +public class JakartaServletFilterMappingResolverFactory + extends ServletFilterMappingResolverFactory + implements ContextStore.Factory { + private final FilterConfig filterConfig; + + public JakartaServletFilterMappingResolverFactory(FilterConfig filterConfig) { + this.filterConfig = filterConfig; + } + + @Override + protected FilterRegistration getFilterRegistration() { + String filterName = filterConfig.getFilterName(); + ServletContext servletContext = filterConfig.getServletContext(); + if (filterName == null || servletContext == null) { + return null; + } + return servletContext.getFilterRegistration(filterName); + } + + @Override + protected Collection getUrlPatternMappings(FilterRegistration filterRegistration) { + return filterRegistration.getUrlPatternMappings(); + } + + @Override + protected Collection getServletNameMappings(FilterRegistration filterRegistration) { + return filterRegistration.getServletNameMappings(); + } + + @Override + @SuppressWarnings("ReturnsNullCollection") + protected Collection getServletMappings(String servletName) { + ServletRegistration servletRegistration = + filterConfig.getServletContext().getServletRegistration(servletName); + if (servletRegistration == null) { + return null; + } + return servletRegistration.getMappings(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletInitAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletInitAdvice.java new file mode 100644 index 000000000..35ca8f1a4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletInitAdvice.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class JakartaServletInitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void servletInit( + @Advice.This Servlet servlet, @Advice.Argument(0) ServletConfig servletConfig) { + if (servletConfig == null) { + return; + } + InstrumentationContext.get(Servlet.class, MappingResolver.class) + .putIfAbsent(servlet, new JakartaServletMappingResolverFactory(servletConfig)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletMappingResolverFactory.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletMappingResolverFactory.java new file mode 100644 index 000000000..3b1a922ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletMappingResolverFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.servlet.naming.ServletMappingResolverFactory; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; +import java.util.Collection; + +public class JakartaServletMappingResolverFactory extends ServletMappingResolverFactory + implements ContextStore.Factory { + private final ServletConfig servletConfig; + + public JakartaServletMappingResolverFactory(ServletConfig servletConfig) { + this.servletConfig = servletConfig; + } + + @Override + @SuppressWarnings("ReturnsNullCollection") + public Collection getMappings() { + String servletName = servletConfig.getServletName(); + ServletContext servletContext = servletConfig.getServletContext(); + if (servletName == null || servletContext == null) { + return null; + } + + ServletRegistration servletRegistration = servletContext.getServletRegistration(servletName); + if (servletRegistration == null) { + return null; + } + return servletRegistration.getMappings(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletServiceAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletServiceAdvice.java new file mode 100644 index 000000000..2579612c0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/service/JakartaServletServiceAdvice.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service; + +import static io.opentelemetry.instrumentation.servlet.jakarta.v5_0.JakartaServletHttpServerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterAdviceHelper; +import jakarta.servlet.Filter; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; + +@SuppressWarnings("unused") +public class JakartaServletServiceAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This(typing = Assigner.Typing.DYNAMIC) Object servletOrFilter, + @Advice.Argument(value = 0, readOnly = false) ServletRequest request, + @Advice.Argument(value = 1, readOnly = false) ServletResponse response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + CallDepthThreadLocalMap.incrementCallDepth(AppServerBridge.getCallDepthKey()); + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + return; + } + + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + + boolean servlet = servletOrFilter instanceof Servlet; + MappingResolver mappingResolver; + if (servlet) { + mappingResolver = + InstrumentationContext.get(Servlet.class, MappingResolver.class) + .get((Servlet) servletOrFilter); + } else { + mappingResolver = + InstrumentationContext.get(Filter.class, MappingResolver.class) + .get((Filter) servletOrFilter); + } + + Context attachedContext = tracer().getServerContext(httpServletRequest); + if (attachedContext != null && tracer().needsRescoping(attachedContext)) { + attachedContext = + tracer().updateContext(attachedContext, httpServletRequest, mappingResolver, servlet); + scope = attachedContext.makeCurrent(); + // We are inside nested servlet/filter/app-server span, don't create new span + return; + } + + Context currentContext = Java8BytecodeBridge.currentContext(); + if (attachedContext != null || ServerSpan.fromContextOrNull(currentContext) != null) { + // Update context with info from current request to ensure that server span gets the best + // possible name. + // In case server span was created by app server instrumentations calling updateContext + // returns a new context that contains servlet context path that is used in other + // instrumentations for naming server span. + Context updatedContext = + tracer().updateContext(currentContext, httpServletRequest, mappingResolver, servlet); + if (currentContext != updatedContext) { + // updateContext updated context, need to re-scope + scope = updatedContext.makeCurrent(); + } + // We are inside nested servlet/filter/app-server span, don't create new span + return; + } + + context = tracer().startSpan(httpServletRequest, mappingResolver, servlet); + scope = context.makeCurrent(); + + tracer().setAsyncListenerResponse(httpServletRequest, (HttpServletResponse) response); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(0) ServletRequest request, + @Advice.Argument(1) ServletResponse response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + return; + } + + ServletAndFilterAdviceHelper.stopSpan( + tracer(), + (HttpServletRequest) request, + (HttpServletResponse) response, + throwable, + context, + scope); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5MappingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5MappingTest.groovy new file mode 100644 index 000000000..df4317512 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5MappingTest.groovy @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import jakarta.servlet.Servlet +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import spock.lang.Unroll + +abstract class AbstractServlet5MappingTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + abstract void addServlet(CONTEXT context, String path, Class servlet) + + protected void setupServlets(CONTEXT context) { + addServlet(context, "/prefix/*", TestServlet) + addServlet(context, "*.suffix", TestServlet) + } + + static class TestServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.getWriter().write("Ok") + } + } + + @Unroll + def "test path #path"() { + setup: + AggregatedHttpResponse response = client.get(address.resolve(path).toString()).aggregate().join() + + expect: + response.status().code() == (success ? 200 : 404) + + and: + def spanCount = success ? 1 : 2 + assertTraces(1) { + trace(0, spanCount) { + span(0) { + name getContextPath() + spanName + kind SpanKind.SERVER + if (!success) { + status ERROR + } + } + if (!success) { + span(1) { + } + } + } + } + + where: + path | spanName | success + 'prefix' | '/prefix/*' | true + 'prefix/' | '/prefix/*' | true + 'prefix/a' | '/prefix/*' | true + 'prefixa' | '/*' | false + 'a.suffix' | '/*.suffix' | true + '.suffix' | '/*.suffix' | true + 'suffix' | '/*' | false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5Test.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5Test.groovy new file mode 100644 index 000000000..ef46f4c5d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5Test.groovy @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest +import jakarta.servlet.Servlet + +abstract class AbstractServlet5Test extends HttpServerTest implements AgentTestTrait { + @Override + URI buildAddress() { + return new URI("http://localhost:$port$contextPath/") + } + + // FIXME: Add authentication tests back in... +// @Shared +// protected String user = "user" +// @Shared +// protected String pass = "password" + + abstract Class servlet() + + abstract void addServlet(CONTEXT context, String path, Class servlet) + + protected void setupServlets(CONTEXT context) { + def servlet = servlet() + + addServlet(context, SUCCESS.path, servlet) + addServlet(context, QUERY_PARAM.path, servlet) + addServlet(context, ERROR.path, servlet) + addServlet(context, EXCEPTION.path, servlet) + addServlet(context, REDIRECT.path, servlet) + addServlet(context, AUTH_REQUIRED.path, servlet) + addServlet(context, INDEXED_CHILD.path, servlet) + } + + protected ServerEndpoint lastRequest + + @Override + AggregatedHttpRequest request(ServerEndpoint uri, String method) { + lastRequest = uri + super.request(uri, method) + } + + boolean errorEndpointUsesSendError() { + true + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || (endpoint == ERROR && errorEndpointUsesSendError()) + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case REDIRECT: + redirectSpan(trace, index, parent) + break + case ERROR: + sendErrorSpan(trace, index, parent) + break + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5MappingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5MappingTest.groovy new file mode 100644 index 000000000..bac23eae6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5MappingTest.groovy @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.servlet.Servlet +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.ServletContextHandler +import spock.lang.IgnoreIf + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServlet5MappingTest extends AbstractServlet5MappingTest { + + @Override + Object startServer(int port) { + Server server = new Server(port) + ServletContextHandler handler = new ServletContextHandler(null, contextPath) + setupServlets(handler) + server.setHandler(handler) + server.start() + return server + } + + @Override + void stopServer(Object serverObject) { + Server server = (Server) serverObject + server.stop() + server.destroy() + } + + @Override + protected void setupServlets(Object handlerObject) { + ServletContextHandler handler = (ServletContextHandler) handlerObject + super.setupServlets(handler) + + addServlet(handler, "/", DefaultServlet) + } + + @Override + void addServlet(Object handlerObject, String path, Class servlet) { + ServletContextHandler handler = (ServletContextHandler) handlerObject + handler.addServlet(servlet, path) + } + + static class DefaultServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.sendError(404) + } + } + + @Override + String getContextPath() { + "/jetty-context" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5Test.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5Test.groovy new file mode 100644 index 000000000..b3118dd09 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5Test.groovy @@ -0,0 +1,273 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import jakarta.servlet.Servlet +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServletRequest +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.handler.ErrorHandler +import org.eclipse.jetty.servlet.ServletContextHandler +import spock.lang.IgnoreIf + +abstract class JettyServlet5Test extends AbstractServlet5Test { + + @Override + boolean testNotFound() { + false + } + + @Override + Class expectedExceptionClass() { + ServletException + } + + @Override + Object startServer(int port) { + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + ServletContextHandler servletContext = new ServletContextHandler(null, contextPath) + servletContext.errorHandler = new ErrorHandler() { + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { + Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception") + writer.write(th ? th.message : message) + } + } +// setupAuthentication(jettyServer, servletContext) + setupServlets(servletContext) + jettyServer.setHandler(servletContext) + + jettyServer.start() + + return jettyServer + } + + @Override + void stopServer(Object serverObject) { + Server server = (Server) serverObject + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/jetty-context" + } + + @Override + void addServlet(Object handlerObject, String path, Class servlet) { + ServletContextHandler handler = (ServletContextHandler) handlerObject + handler.addServlet(servlet, path) + } + + // FIXME: Add authentication tests back in... +// static setupAuthentication(Server jettyServer, ServletContextHandler servletContext) { +// ConstraintSecurityHandler authConfig = new ConstraintSecurityHandler() +// +// Constraint constraint = new Constraint() +// constraint.setName("auth") +// constraint.setAuthenticate(true) +// constraint.setRoles("role") +// +// ConstraintMapping mapping = new ConstraintMapping() +// mapping.setPathSpec("/auth/*") +// mapping.setConstraint(constraint) +// +// authConfig.setConstraintMappings(mapping) +// authConfig.setAuthenticator(new BasicAuthenticator()) +// +// LoginService loginService = new HashLoginService("TestRealm", +// "src/test/resources/realm.properties") +// authConfig.setLoginService(loginService) +// jettyServer.addBean(loginService) +// +// servletContext.setSecurityHandler(authConfig) +// } +} + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServlet5TestSync extends JettyServlet5Test { + + @Override + Class servlet() { + TestServlet5.Sync + } + + @Override + boolean testConcurrency() { + return true + } +} + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServlet5TestAsync extends JettyServlet5Test { + + @Override + Class servlet() { + TestServlet5.Async + } + + @Override + boolean errorEndpointUsesSendError() { + false + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } + + @Override + boolean testConcurrency() { + return true + } +} + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServlet5TestFakeAsync extends JettyServlet5Test { + + @Override + Class servlet() { + TestServlet5.FakeAsync + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } + + @Override + boolean testConcurrency() { + return true + } +} + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServlet5TestForward extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet5.Sync // dispatch to sync servlet + } + + @Override + protected void setupServlets(Object context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + QUERY_PARAM.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + REDIRECT.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + ERROR.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + EXCEPTION.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, RequestDispatcherServlet.Forward) + } +} + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServlet5TestInclude extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet5.Sync // dispatch to sync servlet + } + + @Override + boolean testRedirect() { + false + } + + @Override + boolean testError() { + false + } + + @Override + protected void setupServlets(Object context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + QUERY_PARAM.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + REDIRECT.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + ERROR.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + EXCEPTION.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, RequestDispatcherServlet.Include) + } +} + + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServlet5TestDispatchImmediate extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet5.Sync + } + + @Override + protected void setupServlets(Object context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + QUERY_PARAM.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + ERROR.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + REDIRECT.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch/recursive", TestServlet5.DispatchRecursive) + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } +} + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServlet5TestDispatchAsync extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet5.Async + } + + @Override + protected void setupServlets(Object context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + QUERY_PARAM.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + ERROR.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + REDIRECT.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch/recursive", TestServlet5.DispatchRecursive) + } + + @Override + boolean errorEndpointUsesSendError() { + false + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } +} + +abstract class JettyDispatchTest extends JettyServlet5Test { + @Override + URI buildAddress() { + return new URI("http://localhost:$port$contextPath/dispatch/") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy new file mode 100644 index 000000000..edf552816 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.servlet.Servlet +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServletRequest +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.handler.ErrorHandler +import org.eclipse.jetty.servlet.ServletHandler +import spock.lang.IgnoreIf + +@IgnoreIf({ !jvm.java11Compatible }) +class JettyServletHandlerTest extends AbstractServlet5Test { + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + "HTTP GET" + } + + @Override + Object startServer(int port) { + Server server = new Server(port) + ServletHandler handler = new ServletHandler() + server.setHandler(handler) + setupServlets(handler) + server.addBean(new ErrorHandler() { + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { + Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception") + writer.write(th ? th.message : message) + } + }) + server.start() + return server + } + + @Override + void addServlet(Object handlerObject, String path, Class servlet) { + ServletHandler servletHandler = (ServletHandler) handlerObject + servletHandler.addServletWithMapping(servlet, path) + } + + @Override + void stopServer(Object serverObject) { + Server server = (Server) serverObject + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + "" + } + + @Override + Class servlet() { + TestServlet5.Sync + } + + @Override + boolean testNotFound() { + false + } + + @Override + Class expectedExceptionClass() { + ServletException + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TestServlet5.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TestServlet5.groovy new file mode 100644 index 000000000..478664767 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TestServlet5.groovy @@ -0,0 +1,191 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import jakarta.servlet.RequestDispatcher +import jakarta.servlet.ServletException +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import java.util.concurrent.Phaser + +class TestServlet5 { + + @WebServlet + static class Sync extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + String servletPath = req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH) + if (servletPath == null) { + servletPath = req.servletPath + } + HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(servletPath) + HttpServerTest.controller(endpoint) { + resp.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + break + case INDEXED_CHILD: + resp.status = endpoint.status + endpoint.collectSpanAttributes { req.getParameter(it) } + break + case QUERY_PARAM: + resp.status = endpoint.status + resp.writer.print(req.queryString) + break + case REDIRECT: + resp.sendRedirect(endpoint.body) + break + case ERROR: + resp.sendError(endpoint.status, endpoint.body) + break + case EXCEPTION: + throw new ServletException(endpoint.body) + } + } + } + } + + @WebServlet(asyncSupported = true) + static class Async extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath) + def phaser = new Phaser(2) + def context = req.startAsync() + context.start { + try { + phaser.arriveAndAwaitAdvance() + HttpServerTest.controller(endpoint) { + resp.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + context.complete() + break + case INDEXED_CHILD: + endpoint.collectSpanAttributes { req.getParameter(it) } + resp.status = endpoint.status + context.complete() + break + case QUERY_PARAM: + resp.status = endpoint.status + resp.writer.print(req.queryString) + context.complete() + break + case REDIRECT: + resp.sendRedirect(endpoint.body) + context.complete() + break + case ERROR: + resp.status = endpoint.status + resp.writer.print(endpoint.body) +// resp.sendError(endpoint.status, endpoint.body) + context.complete() + break + case EXCEPTION: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + context.complete() + throw new Exception(endpoint.body) + } + } + } finally { + phaser.arriveAndDeregister() + } + } + phaser.arriveAndAwaitAdvance() + phaser.arriveAndAwaitAdvance() + } + } + + @WebServlet(asyncSupported = true) + static class FakeAsync extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + def context = req.startAsync() + try { + HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath) + HttpServerTest.controller(endpoint) { + resp.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + break + case INDEXED_CHILD: + endpoint.collectSpanAttributes { req.getParameter(it) } + resp.status = endpoint.status + break + case QUERY_PARAM: + resp.status = endpoint.status + resp.writer.print(req.queryString) + break + case REDIRECT: + resp.sendRedirect(endpoint.body) + break + case ERROR: + resp.sendError(endpoint.status, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + } + } + } finally { + context.complete() + } + } + } + + @WebServlet(asyncSupported = true) + static class DispatchImmediate extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + def target = req.servletPath.replace("/dispatch", "") + req.startAsync().dispatch(target) + } + } + + @WebServlet(asyncSupported = true) + static class DispatchAsync extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + def target = req.servletPath.replace("/dispatch", "") + def context = req.startAsync() + context.start { + context.dispatch(target) + } + } + } + + // TODO: Add tests for this! + @WebServlet(asyncSupported = true) + static class DispatchRecursive extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + if (req.servletPath == "/recursive") { + resp.writer.print("Hello Recursive") + return + } + def depth = Integer.parseInt(req.getParameter("depth")) + if (depth > 0) { + req.startAsync().dispatch("/dispatch/recursive?depth=" + (depth - 1)) + } else { + req.startAsync().dispatch("/recursive") + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5FilterMappingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5FilterMappingTest.groovy new file mode 100644 index 000000000..71278c52f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5FilterMappingTest.groovy @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.FilterConfig +import jakarta.servlet.ServletException +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.apache.catalina.Context +import org.apache.catalina.startup.Tomcat +import org.apache.tomcat.util.descriptor.web.FilterDef +import org.apache.tomcat.util.descriptor.web.FilterMap + +abstract class TomcatServlet5FilterMappingTest extends TomcatServlet5MappingTest { + + void addFilter(Context servletContext, String path, Class filter) { + String name = UUID.randomUUID() + FilterDef filterDef = new FilterDef() + filterDef.setFilter(filter.newInstance()) + filterDef.setFilterName(name) + servletContext.addFilterDef(filterDef) + FilterMap filterMap = new FilterMap() + filterMap.setFilterName(name) + filterMap.addURLPattern(path) + servletContext.addFilterMap(filterMap) + } + + void addFilterWithServletName(Context servletContext, String servletName, Class filter) { + String name = UUID.randomUUID() + FilterDef filterDef = new FilterDef() + filterDef.setFilter(filter.newInstance()) + filterDef.setFilterName(name) + servletContext.addFilterDef(filterDef) + FilterMap filterMap = new FilterMap() + filterMap.setFilterName(name) + filterMap.addServletName(servletName) + servletContext.addFilterMap(filterMap) + } + + static class TestFilter implements Filter { + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + if (servletRequest.getAttribute("firstFilterCalled") != null) { + servletRequest.setAttribute("testFilterCalled", Boolean.TRUE) + filterChain.doFilter(servletRequest, servletResponse) + } else { + throw new IllegalStateException("First filter should have been called.") + } + } + + @Override + void destroy() { + } + } + + static class FirstFilter implements Filter { + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + servletRequest.setAttribute("firstFilterCalled", Boolean.TRUE) + filterChain.doFilter(servletRequest, servletResponse) + } + + @Override + void destroy() { + } + } + + static class LastFilter implements Filter { + + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + if (servletRequest.getAttribute("testFilterCalled") != null) { + HttpServletResponse response = (HttpServletResponse) servletResponse + response.getWriter().write("Ok") + response.setStatus(HttpServletResponse.SC_OK) + } else { + filterChain.doFilter(servletRequest, servletResponse) + } + } + + @Override + void destroy() { + } + } + + static class DefaultServlet extends HttpServlet { + protected void service(HttpServletRequest req, HttpServletResponse resp) { + throw new IllegalStateException("Servlet should not have been called, filter should have handled the request.") + } + } +} + +class TomcatServlet5FilterUrlPatternMappingTest extends TomcatServlet5FilterMappingTest { + @Override + protected void setupServlets(Context context) { + addFilter(context, "/*", FirstFilter) + addFilter(context, "/prefix/*", TestFilter) + addFilter(context, "*.suffix", TestFilter) + addFilter(context, "/*", LastFilter) + } +} + +class TomcatServlet5FilterServletNameMappingTest extends TomcatServlet5FilterMappingTest { + @Override + protected void setupServlets(Context context) { + Tomcat.addServlet(context, "prefix-servlet", DefaultServlet.newInstance()) + context.addServletMappingDecoded("/prefix/*", "prefix-servlet") + Tomcat.addServlet(context, "suffix-servlet", DefaultServlet.newInstance()) + context.addServletMappingDecoded("*.suffix", "suffix-servlet") + + addFilter(context, "/*", FirstFilter) + addFilterWithServletName(context, "prefix-servlet", TestFilter) + addFilterWithServletName(context, "suffix-servlet", TestFilter) + addFilterWithServletName(context, "prefix-servlet", LastFilter) + addFilterWithServletName(context, "suffix-servlet", LastFilter) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5MappingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5MappingTest.groovy new file mode 100644 index 000000000..f328855eb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5MappingTest.groovy @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.servlet.Servlet +import java.nio.file.Files +import org.apache.catalina.Context +import org.apache.catalina.startup.Tomcat +import org.apache.tomcat.JarScanFilter +import org.apache.tomcat.JarScanType + +class TomcatServlet5MappingTest extends AbstractServlet5MappingTest { + + @Override + Tomcat startServer(int port) { + def tomcatServer = new Tomcat() + + def baseDir = Files.createTempDirectory("tomcat").toFile() + baseDir.deleteOnExit() + tomcatServer.setBaseDir(baseDir.getAbsolutePath()) + + tomcatServer.setPort(port) + tomcatServer.getConnector().enableLookups = true // get localhost instead of 127.0.0.1 + + File applicationDir = new File(baseDir, "/webapps/ROOT") + if (!applicationDir.exists()) { + applicationDir.mkdirs() + applicationDir.deleteOnExit() + } + Context servletContext = tomcatServer.addWebapp(contextPath, applicationDir.getAbsolutePath()) + // Speed up startup by disabling jar scanning: + servletContext.getJarScanner().setJarScanFilter(new JarScanFilter() { + @Override + boolean check(JarScanType jarScanType, String jarName) { + return false + } + }) + + setupServlets(servletContext) + + tomcatServer.start() + + return tomcatServer + } + + @Override + void stopServer(Tomcat server) { + server.stop() + server.destroy() + } + + @Override + void addServlet(Context servletContext, String path, Class servlet) { + String name = UUID.randomUUID() + Tomcat.addServlet(servletContext, name, servlet.newInstance()) + servletContext.addServletMappingDecoded(path, name) + } + + @Override + String getContextPath() { + return "/tomcat-context" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5Test.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5Test.groovy new file mode 100644 index 000000000..a268d073f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5Test.groovy @@ -0,0 +1,458 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static org.junit.Assume.assumeTrue + +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import jakarta.servlet.Servlet +import jakarta.servlet.ServletException +import java.nio.file.Files +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import org.apache.catalina.AccessLog +import org.apache.catalina.Context +import org.apache.catalina.connector.Request +import org.apache.catalina.connector.Response +import org.apache.catalina.core.StandardHost +import org.apache.catalina.startup.Tomcat +import org.apache.catalina.valves.ErrorReportValve +import org.apache.catalina.valves.ValveBase +import org.apache.tomcat.JarScanFilter +import org.apache.tomcat.JarScanType +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class TomcatServlet5Test extends AbstractServlet5Test { + + @Override + Class expectedExceptionClass() { + ServletException + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == NOT_FOUND || super.hasResponseSpan(endpoint) + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case NOT_FOUND: + sendErrorSpan(trace, index, parent) + break + } + super.responseSpan(trace, index, parent, method, endpoint) + } + + @Shared + def accessLogValue = new TestAccessLogValve() + + @Override + Tomcat startServer(int port) { + def tomcatServer = new Tomcat() + + def baseDir = Files.createTempDirectory("tomcat").toFile() + baseDir.deleteOnExit() + tomcatServer.setBaseDir(baseDir.getAbsolutePath()) + + tomcatServer.setPort(port) + tomcatServer.getConnector().enableLookups = true // get localhost instead of 127.0.0.1 + + File applicationDir = new File(baseDir, "/webapps/ROOT") + if (!applicationDir.exists()) { + applicationDir.mkdirs() + applicationDir.deleteOnExit() + } + Context servletContext = tomcatServer.addWebapp(contextPath, applicationDir.getAbsolutePath()) + // Speed up startup by disabling jar scanning: + servletContext.getJarScanner().setJarScanFilter(new JarScanFilter() { + @Override + boolean check(JarScanType jarScanType, String jarName) { + return false + } + }) + +// setupAuthentication(tomcatServer, servletContext) + setupServlets(servletContext) + + (tomcatServer.host as StandardHost).errorReportValveClass = ErrorHandlerValve.name + (tomcatServer.host as StandardHost).getPipeline().addValve(accessLogValue) + + tomcatServer.start() + + return tomcatServer + } + + def setup() { + accessLogValue.loggedIds.clear() + } + + @Override + void stopServer(Tomcat server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/tomcat-context" + } + + @Override + void addServlet(Context servletContext, String path, Class servlet) { + String name = UUID.randomUUID() + Tomcat.addServlet(servletContext, name, servlet.newInstance()) + servletContext.addServletMappingDecoded(path, name) + } + + def "access log has ids for #count requests"() { + given: + def request = request(SUCCESS, method) + + when: + List responses = (1..count).collect { + return client.execute(request).aggregate().join() + } + + then: + responses.each { response -> + assert response.status().code() == SUCCESS.status + assert response.contentUtf8() == SUCCESS.body + } + + and: + assertTraces(count) { + accessLogValue.waitForLoggedIds(count) + assert accessLogValue.loggedIds.size() == count + def loggedTraces = accessLogValue.loggedIds*.first + def loggedSpans = accessLogValue.loggedIds*.second + + (0..count - 1).each { + trace(it, 2) { + serverSpan(it, 0, null, null, "GET", SUCCESS.body.length()) + controllerSpan(it, 1, span(0)) + } + + assert loggedTraces.contains(traces[it][0].traceId) + assert loggedSpans.contains(traces[it][0].spanId) + } + } + + where: + method = "GET" + count << [1, 4] // make multiple requests. + } + + def "access log has ids for error request"() { + setup: + assumeTrue(testError()) + def request = request(ERROR, method) + def response = client.execute(request).aggregate().join() + + expect: + response.status().code() == ERROR.status + response.contentUtf8() == ERROR.body + + and: + def spanCount = 2 + if (errorEndpointUsesSendError()) { + spanCount++ + } + assertTraces(1) { + trace(0, spanCount) { + serverSpan(it, 0, null, null, method, response.content().length(), ERROR) + def spanIndex = 1 + controllerSpan(it, spanIndex, span(spanIndex - 1)) + spanIndex++ + if (errorEndpointUsesSendError()) { + sendErrorSpan(it, spanIndex, span(spanIndex - 1)) + spanIndex++ + } + } + + accessLogValue.waitForLoggedIds(1) + def (String traceId, String spanId) = accessLogValue.loggedIds[0] + assert traces[0][0].traceId == traceId + assert traces[0][0].spanId == spanId + } + + where: + method = "GET" + } + + // FIXME: Add authentication tests back in... +// private setupAuthentication(Tomcat server, Context servletContext) { +// // Login Config +// LoginConfig authConfig = new LoginConfig() +// authConfig.setAuthMethod("BASIC") +// +// // adding constraint with role "test" +// SecurityConstraint constraint = new SecurityConstraint() +// constraint.addAuthRole("role") +// +// // add constraint to a collection with pattern /second +// SecurityCollection collection = new SecurityCollection() +// collection.addPattern("/auth/*") +// constraint.addCollection(collection) +// +// servletContext.setLoginConfig(authConfig) +// // does the context need a auth role too? +// servletContext.addSecurityRole("role") +// servletContext.addConstraint(constraint) +// +// // add tomcat users to realm +// MemoryRealm realm = new MemoryRealm() { +// protected void startInternal() { +// credentialHandler = new MessageDigestCredentialHandler() +// setState(LifecycleState.STARTING) +// } +// } +// realm.addUser(user, pass, "role") +// server.getEngine().setRealm(realm) +// +// servletContext.setLoginConfig(authConfig) +// } +} + +class ErrorHandlerValve extends ErrorReportValve { + @Override + protected void report(Request request, Response response, Throwable t) { + if (response.getStatus() < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) { + return + } + try { + response.writer.print(t ? t.cause.message : response.message) + } catch (IOException e) { + e.printStackTrace() + } + } +} + +class TestAccessLogValve extends ValveBase implements AccessLog { + final List> loggedIds = [] + + TestAccessLogValve() { + super(true) + } + + void log(Request request, Response response, long time) { + synchronized (loggedIds) { + loggedIds.add(new Tuple2(request.getAttribute("trace_id"), + request.getAttribute("span_id"))) + loggedIds.notifyAll() + } + } + + void waitForLoggedIds(int expected) { + def timeout = TimeUnit.SECONDS.toMillis(20) + def startTime = System.currentTimeMillis() + def endTime = startTime + timeout + def toWait = timeout + synchronized (loggedIds) { + while (loggedIds.size() < expected && toWait > 0) { + loggedIds.wait(toWait) + toWait = endTime - System.currentTimeMillis() + } + if (toWait <= 0) { + throw new TimeoutException("Timeout waiting for " + expected + " access log ids, got " + loggedIds.size()) + } + } + } + + @Override + void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + } + + @Override + boolean getRequestAttributesEnabled() { + return false + } + + @Override + void invoke(Request request, Response response) throws IOException, ServletException { + getNext().invoke(request, response) + } +} + +class TomcatServlet5TestSync extends TomcatServlet5Test { + + @Override + Class servlet() { + TestServlet5.Sync + } +} + +class TomcatServlet5TestAsync extends TomcatServlet5Test { + + @Override + Class servlet() { + TestServlet5.Async + } + + @Override + boolean errorEndpointUsesSendError() { + false + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } + + @Override + boolean testConcurrency() { + return true + } +} + +class TomcatServlet5TestFakeAsync extends TomcatServlet5Test { + + @Override + Class servlet() { + TestServlet5.FakeAsync + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } + + @Override + boolean testConcurrency() { + return true + } +} + +class TomcatServlet5TestForward extends TomcatDispatchTest { + @Override + Class servlet() { + TestServlet5.Sync // dispatch to sync servlet + } + + @Override + boolean testNotFound() { + false + } + + @Override + protected void setupServlets(Context context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + QUERY_PARAM.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + REDIRECT.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + ERROR.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + EXCEPTION.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, RequestDispatcherServlet.Forward) + } +} + +class TomcatServlet5TestInclude extends TomcatDispatchTest { + @Override + Class servlet() { + TestServlet5.Sync // dispatch to sync servlet + } + + @Override + boolean testNotFound() { + false + } + + @Override + boolean testRedirect() { + false + } + + @Override + boolean testError() { + false + } + + @Override + protected void setupServlets(Context context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + QUERY_PARAM.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + REDIRECT.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + ERROR.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + EXCEPTION.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, RequestDispatcherServlet.Include) + } +} + +class TomcatServlet5TestDispatchImmediate extends TomcatDispatchTest { + @Override + Class servlet() { + TestServlet5.Sync + } + + @Override + boolean testNotFound() { + false + } + + @Override + protected void setupServlets(Context context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + QUERY_PARAM.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + ERROR.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + REDIRECT.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch/recursive", TestServlet5.DispatchRecursive) + } +} + +class TomcatServlet5TestDispatchAsync extends TomcatDispatchTest { + @Override + Class servlet() { + TestServlet5.Async + } + + @Override + protected void setupServlets(Context context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + QUERY_PARAM.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + ERROR.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + REDIRECT.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch/recursive", TestServlet5.DispatchRecursive) + } + + @Override + boolean errorEndpointUsesSendError() { + false + } + + @Override + boolean testException() { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/807 + return false + } +} + +abstract class TomcatDispatchTest extends TomcatServlet5Test { + @Override + URI buildAddress() { + return new URI("http://localhost:$port$contextPath/dispatch/") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/java/RequestDispatcherServlet.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/java/RequestDispatcherServlet.java new file mode 100644 index 000000000..e84e56b76 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/java/RequestDispatcherServlet.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class RequestDispatcherServlet { + /* There's something about the getRequestDispatcher call that breaks horribly when these classes + * are written in groovy. + */ + + @WebServlet(asyncSupported = true) + public static class Forward extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String target = req.getServletPath().replace("/dispatch", ""); + ServletContext context = getServletContext(); + RequestDispatcher dispatcher = context.getRequestDispatcher(target); + dispatcher.forward(req, resp); + } + } + + @WebServlet(asyncSupported = true) + public static class Include extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String target = req.getServletPath().replace("/dispatch", ""); + ServletContext context = getServletContext(); + RequestDispatcher dispatcher = context.getRequestDispatcher(target); + dispatcher.include(req, resp); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/resources/realm.properties b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/resources/realm.properties new file mode 100644 index 000000000..cacb91707 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/javaagent/src/test/resources/realm.properties @@ -0,0 +1 @@ +user:password,role diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/servlet-5.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/servlet-5.0-library.gradle new file mode 100644 index 000000000..2ae155aca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/servlet-5.0-library.gradle @@ -0,0 +1,11 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + api(project(':instrumentation:servlet:servlet-common:library')) + + compileOnly "jakarta.servlet:jakarta.servlet-api:5.0.0" + + testImplementation "jakarta.servlet:jakarta.servlet-api:5.0.0" + testImplementation "org.mockito:mockito-core:3.6.0" + testImplementation "org.assertj:assertj-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaHttpServletRequestGetter.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaHttpServletRequestGetter.java new file mode 100644 index 000000000..24ab41aab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaHttpServletRequestGetter.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.jakarta.v5_0; + +import io.opentelemetry.context.propagation.TextMapGetter; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Collections; + +public class JakartaHttpServletRequestGetter implements TextMapGetter { + + public static final JakartaHttpServletRequestGetter GETTER = + new JakartaHttpServletRequestGetter(); + + @Override + public Iterable keys(HttpServletRequest carrier) { + return Collections.list(carrier.getHeaderNames()); + } + + @Override + public String get(HttpServletRequest carrier, String key) { + return carrier.getHeader(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaServletAccessor.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaServletAccessor.java new file mode 100644 index 000000000..3f67d54e1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaServletAccessor.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.jakarta.v5_0; + +import io.opentelemetry.instrumentation.servlet.ServletAccessor; +import io.opentelemetry.instrumentation.servlet.ServletAsyncListener; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.security.Principal; + +public class JakartaServletAccessor + implements ServletAccessor { + public static final JakartaServletAccessor INSTANCE = new JakartaServletAccessor(); + + private JakartaServletAccessor() {} + + @Override + public String getRequestContextPath(HttpServletRequest request) { + return request.getContextPath(); + } + + @Override + public String getRequestScheme(HttpServletRequest request) { + return request.getScheme(); + } + + @Override + public String getRequestServerName(HttpServletRequest request) { + return request.getServerName(); + } + + @Override + public int getRequestServerPort(HttpServletRequest request) { + return request.getServerPort(); + } + + @Override + public String getRequestUri(HttpServletRequest request) { + return request.getRequestURI(); + } + + @Override + public String getRequestQueryString(HttpServletRequest request) { + return request.getQueryString(); + } + + @Override + public Object getRequestAttribute(HttpServletRequest request, String name) { + return request.getAttribute(name); + } + + @Override + public void setRequestAttribute(HttpServletRequest request, String name, Object value) { + request.setAttribute(name, value); + } + + @Override + public String getRequestProtocol(HttpServletRequest request) { + return request.getProtocol(); + } + + @Override + public String getRequestMethod(HttpServletRequest request) { + return request.getMethod(); + } + + @Override + public String getRequestRemoteAddr(HttpServletRequest request) { + return request.getRemoteAddr(); + } + + @Override + public String getRequestHeader(HttpServletRequest request, String name) { + return request.getHeader(name); + } + + @Override + public String getRequestServletPath(HttpServletRequest request) { + return request.getServletPath(); + } + + @Override + public String getRequestPathInfo(HttpServletRequest request) { + return request.getPathInfo(); + } + + @Override + public Principal getRequestUserPrincipal(HttpServletRequest request) { + return request.getUserPrincipal(); + } + + @Override + public Integer getRequestRemotePort(HttpServletRequest request) { + return request.getRemotePort(); + } + + @Override + public void addRequestAsyncListener( + HttpServletRequest request, + ServletAsyncListener listener, + Object response) { + if (response instanceof HttpServletResponse) { + request + .getAsyncContext() + .addListener(new Listener(listener), request, (HttpServletResponse) response); + } else { + request.getAsyncContext().addListener(new Listener(listener)); + } + } + + @Override + public int getResponseStatus(HttpServletResponse response) { + return response.getStatus(); + } + + @Override + public boolean isResponseCommitted(HttpServletResponse response) { + return response.isCommitted(); + } + + @Override + public boolean isServletException(Throwable throwable) { + return throwable instanceof ServletException; + } + + private static class Listener implements AsyncListener { + private final ServletAsyncListener listener; + + private Listener(ServletAsyncListener listener) { + this.listener = listener; + } + + @Override + public void onComplete(AsyncEvent event) { + listener.onComplete((HttpServletResponse) event.getSuppliedResponse()); + } + + @Override + public void onTimeout(AsyncEvent event) { + listener.onTimeout(event.getAsyncContext().getTimeout()); + } + + @Override + public void onError(AsyncEvent event) { + listener.onError(event.getThrowable(), (HttpServletResponse) event.getSuppliedResponse()); + } + + @Override + public void onStartAsync(AsyncEvent event) { + event.getAsyncContext().addListener(this); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaServletHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaServletHttpServerTracer.java new file mode 100644 index 000000000..4e20cdff8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/main/java/io/opentelemetry/instrumentation/servlet/jakarta/v5_0/JakartaServletHttpServerTracer.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.jakarta.v5_0; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.FILTER; +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.SERVLET; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.servlet.ServletHttpServerTracer; +import io.opentelemetry.instrumentation.servlet.naming.ServletSpanNameProvider; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class JakartaServletHttpServerTracer + extends ServletHttpServerTracer { + private static final JakartaServletHttpServerTracer TRACER = new JakartaServletHttpServerTracer(); + private static final ServletSpanNameProvider SPAN_NAME_PROVIDER = + new ServletSpanNameProvider<>(JakartaServletAccessor.INSTANCE); + + public JakartaServletHttpServerTracer() { + super(JakartaServletAccessor.INSTANCE); + } + + public static JakartaServletHttpServerTracer tracer() { + return TRACER; + } + + public Context startSpan( + HttpServletRequest request, MappingResolver mappingResolver, boolean servlet) { + return startSpan(request, SPAN_NAME_PROVIDER.getSpanName(mappingResolver, request), servlet); + } + + public Context updateContext( + Context context, + HttpServletRequest request, + MappingResolver mappingResolver, + boolean servlet) { + ServerSpanNaming.updateServerSpanName( + context, + servlet ? SERVLET : FILTER, + () -> SPAN_NAME_PROVIDER.getSpanNameOrNull(mappingResolver, request)); + return updateContext(context, request); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.servlet-5.0"; + } + + @Override + protected TextMapGetter getGetter() { + return JakartaHttpServletRequestGetter.GETTER; + } + + @Override + protected String bussinessStatus(HttpServletResponse httpServletResponse) { + return null; + } + + @Override + protected String bussinessMessage(HttpServletResponse httpServletResponse) { + return null; + } + + @Override + protected String errorExceptionAttributeName() { + return RequestDispatcher.ERROR_EXCEPTION; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/test/java/JakartaServletHttpServerTracerTest.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/test/java/JakartaServletHttpServerTracerTest.java new file mode 100644 index 000000000..11b64d9b9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-5.0/library/src/test/java/JakartaServletHttpServerTracerTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.servlet.jakarta.v5_0.JakartaServletHttpServerTracer; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; + +public class JakartaServletHttpServerTracerTest { + private static final JakartaServletHttpServerTracer tracer = + JakartaServletHttpServerTracer.tracer(); + + @Test + void testGetSpanName_emptySpanName() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn(""); + when(request.getMethod()).thenReturn("PUT"); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("HTTP PUT"); + } + + @Test + void testGetSpanName_nullSpanName() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn(null); + assertThatThrownBy(() -> tracer.getSpanName(request)).isInstanceOf(NullPointerException.class); + } + + @Test + void testGetSpanName_nullContextPath() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/swizzler"); + when(request.getContextPath()).thenReturn(null); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("/swizzler"); + } + + @Test + void testGetSpanName_emptyContextPath() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/swizzler"); + when(request.getContextPath()).thenReturn(""); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("/swizzler"); + } + + @Test + void testGetSpanName_slashContextPath() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/swizzler"); + when(request.getContextPath()).thenReturn("/"); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("/swizzler"); + } + + @Test + void testGetSpanName_appendsSpanNameToContext() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/swizzler"); + when(request.getContextPath()).thenReturn("/path/to"); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("/path/to/swizzler"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/servlet-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/servlet-common-javaagent.gradle new file mode 100644 index 000000000..8889b2b38 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/servlet-common-javaagent.gradle @@ -0,0 +1,8 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +// This module is only used as a dependency for other javaagent modules and does not contain any +// non-abstract implementations of InstrumentationModule + +dependencies { + api(project(':instrumentation:servlet:servlet-common:library')) +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/async/AsyncContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/async/AsyncContextInstrumentation.java new file mode 100644 index 000000000..6acb528a5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/async/AsyncContextInstrumentation.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.common.async; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AsyncContextInstrumentation implements TypeInstrumentation { + private final String basePackageName; + private final String adviceClassName; + + public AsyncContextInstrumentation(String basePackageName, String adviceClassName) { + this.basePackageName = basePackageName; + this.adviceClassName = adviceClassName; + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed(basePackageName + ".AsyncContext"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named(basePackageName + ".AsyncContext")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("dispatch")), adviceClassName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/async/AsyncStartInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/async/AsyncStartInstrumentation.java new file mode 100644 index 000000000..6b247a3de --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/async/AsyncStartInstrumentation.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.common.async; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AsyncStartInstrumentation implements TypeInstrumentation { + private final String basePackageName; + private final String adviceClassName; + + public AsyncStartInstrumentation(String basePackageName, String adviceClassName) { + this.basePackageName = basePackageName; + this.adviceClassName = adviceClassName; + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed(basePackageName + ".Servlet"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named(basePackageName + ".ServletRequest")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("startAsync").and(returns(named(basePackageName + ".AsyncContext"))).and(isPublic()), + adviceClassName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseAdviceHelper.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseAdviceHelper.java new file mode 100644 index 000000000..20eef8935 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseAdviceHelper.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.common.response; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepth; + +public class HttpServletResponseAdviceHelper { + public static void stopSpan( + BaseTracer tracer, Throwable throwable, Context context, Scope scope, CallDepth callDepth) { + if (callDepth.decrementAndGet() == 0 && context != null) { + callDepth.reset(); + + scope.close(); + + if (throwable != null) { + tracer.endExceptionally(context, throwable); + } else { + tracer.end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseInstrumentation.java new file mode 100644 index 000000000..f88719909 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseInstrumentation.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.common.response; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HttpServletResponseInstrumentation implements TypeInstrumentation { + private final String basePackageName; + private final String adviceClassName; + + public HttpServletResponseInstrumentation(String basePackageName, String adviceClassName) { + this.basePackageName = basePackageName; + this.adviceClassName = adviceClassName; + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed(basePackageName + ".http.HttpServletResponse"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named(basePackageName + ".http.HttpServletResponse")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(namedOneOf("sendError", "sendRedirect"), adviceClassName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/service/ServletAndFilterAdviceHelper.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/service/ServletAndFilterAdviceHelper.java new file mode 100644 index 000000000..a5c2645db --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/service/ServletAndFilterAdviceHelper.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.common.service; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.servlet.ServletHttpServerTracer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; + +public class ServletAndFilterAdviceHelper { + public static void stopSpan( + ServletHttpServerTracer tracer, + REQUEST request, + RESPONSE response, + Throwable throwable, + Context context, + Scope scope) { + int callDepth = CallDepthThreadLocalMap.decrementCallDepth(AppServerBridge.getCallDepthKey()); + + if (scope != null) { + scope.close(); + } + + if (context == null && callDepth == 0) { + Context currentContext = Java8BytecodeBridge.currentContext(); + // Something else is managing the context, we're in the outermost level of Servlet + // instrumentation and we have an uncaught throwable. Let's add it to the current span. + if (throwable != null) { + tracer.addUnwrappedThrowable(currentContext, throwable); + } + tracer.setPrincipal(currentContext, request); + } + + if (scope == null || context == null) { + return; + } + + tracer.setPrincipal(context, request); + if (throwable != null) { + tracer.endExceptionally(context, throwable, response); + return; + } + + if (mustEndOnHandlerMethodExit(tracer, request)) { + tracer.end(context, response); + } + } + + /** + * Helper method to determine whether the appserver handler/servlet service/servlet filter method + * that started a span must also end it, even if no error was detected. Extracted as a separate + * method to avoid duplicating the comments on the logic behind this choice. + */ + public static boolean mustEndOnHandlerMethodExit( + ServletHttpServerTracer tracer, REQUEST request) { + + if (tracer.isAsyncListenerAttached(request)) { + // This request is handled asynchronously and startAsync instrumentation has already attached + // the listener. + return false; + } + + // This means that startAsync was not called (assuming startAsync instrumentation works + // correctly on this servlet engine), therefore the request was handled synchronously, and + // handler method end must also end the span. + return true; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/service/ServletAndFilterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/service/ServletAndFilterInstrumentation.java new file mode 100644 index 000000000..9cf0866b5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/service/ServletAndFilterInstrumentation.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.common.service; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ServletAndFilterInstrumentation implements TypeInstrumentation { + private final String basePackageName; + private final String adviceClassName; + private final String servletInitAdviceClassName; + private final String filterInitAdviceClassName; + + public ServletAndFilterInstrumentation( + String basePackageName, + String adviceClassName, + String servletInitAdviceClassName, + String filterInitAdviceClassName) { + this.basePackageName = basePackageName; + this.adviceClassName = adviceClassName; + this.servletInitAdviceClassName = servletInitAdviceClassName; + this.filterInitAdviceClassName = filterInitAdviceClassName; + } + + public ServletAndFilterInstrumentation(String basePackageName, String adviceClassName) { + this(basePackageName, adviceClassName, null, null); + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed(basePackageName + ".Servlet"); + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(namedOneOf(basePackageName + ".Filter", basePackageName + ".Servlet")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf("doFilter", "service") + .and(takesArgument(0, named(basePackageName + ".ServletRequest"))) + .and(takesArgument(1, named(basePackageName + ".ServletResponse"))) + .and(isPublic()), + adviceClassName); + if (servletInitAdviceClassName != null) { + transformer.applyAdviceToMethod( + named("init").and(takesArgument(0, named(basePackageName + ".ServletConfig"))), + servletInitAdviceClassName); + } + if (filterInitAdviceClassName != null) { + transformer.applyAdviceToMethod( + named("init").and(takesArgument(0, named(basePackageName + ".FilterConfig"))), + filterInitAdviceClassName); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/servlet-common-library.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/servlet-common-library.gradle new file mode 100644 index 000000000..77e3d73e9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/servlet-common-library.gradle @@ -0,0 +1,5 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + implementation "org.slf4j:slf4j-api" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletAccessor.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletAccessor.java new file mode 100644 index 000000000..ff10d6940 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletAccessor.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet; + +import java.security.Principal; + +/** + * This interface is used to access methods of ServletContext, HttpServletRequest and + * HttpServletResponse classes in shared code that is used for both jakarta.servlet and + * javax.servlet versions of those classes. A wrapper class with extra information attached may be + * used as well in cases where the class itself does not provide some field (such as response status + * for Servlet API 2.2). + * + * @param HttpServletRequest class (or a wrapper) + * @param HttpServletResponse class (or a wrapper) + */ +public interface ServletAccessor { + String getRequestContextPath(REQUEST request); + + String getRequestScheme(REQUEST request); + + String getRequestServerName(REQUEST request); + + int getRequestServerPort(REQUEST request); + + String getRequestUri(REQUEST request); + + String getRequestQueryString(REQUEST request); + + Object getRequestAttribute(REQUEST request, String name); + + void setRequestAttribute(REQUEST request, String name, Object value); + + String getRequestProtocol(REQUEST request); + + String getRequestMethod(REQUEST request); + + String getRequestRemoteAddr(REQUEST request); + + String getRequestHeader(REQUEST request, String name); + + String getRequestServletPath(REQUEST request); + + String getRequestPathInfo(REQUEST request); + + Principal getRequestUserPrincipal(REQUEST request); + + Integer getRequestRemotePort(REQUEST request); + + void addRequestAsyncListener( + REQUEST request, ServletAsyncListener listener, Object response); + + int getResponseStatus(RESPONSE response); + + boolean isResponseCommitted(RESPONSE response); + + boolean isServletException(Throwable throwable); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletAsyncListener.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletAsyncListener.java new file mode 100644 index 000000000..53dd94d87 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletAsyncListener.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet; + +public interface ServletAsyncListener { + void onComplete(RESPONSE response); + + void onTimeout(long timeout); + + void onError(Throwable throwable, RESPONSE response); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletHttpServerTracer.java new file mode 100644 index 000000000..a2290fc85 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/ServletHttpServerTracer.java @@ -0,0 +1,277 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTAINER; +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.FILTER; +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.SERVLET; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class ServletHttpServerTracer + extends HttpServerTracer { + + private static final Logger log = LoggerFactory.getLogger(ServletHttpServerTracer.class); + + public static final String ASYNC_LISTENER_ATTRIBUTE = + ServletHttpServerTracer.class.getName() + ".AsyncListener"; + public static final String ASYNC_LISTENER_RESPONSE_ATTRIBUTE = + ServletHttpServerTracer.class.getName() + ".AsyncListenerResponse"; + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBoolean("otel.instrumentation.servlet.experimental-span-attributes", false); + + private final ServletAccessor accessor; + + protected ServletHttpServerTracer(ServletAccessor accessor) { + this.accessor = accessor; + } + + public Context startSpan(REQUEST request, String spanName, boolean servlet) { + Context context = startSpan(request, request, request, spanName); + + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + // we do this e.g. so that servlet containers can use these values in their access logs + accessor.setRequestAttribute(request, "trace_id", spanContext.getTraceId()); + accessor.setRequestAttribute(request, "span_id", spanContext.getSpanId()); + + // server span name shouldn't be updated when server span was created from a call to Servlet + // (if created from a call to Filter then name may be updated from updateContext) + ServerSpanNaming.updateSource(context, servlet ? SERVLET : FILTER); + + return addServletContextPath(context, request); + } + + @Override + protected Context customizeContext(Context context, REQUEST request) { + // add context for tracking whether servlet instrumentation has updated the server span name + context = ServerSpanNaming.init(context, CONTAINER); + // add context for current request's context path + return addServletContextPath(context, request); + } + + private Context addServletContextPath(Context context, REQUEST request) { + String contextPath = accessor.getRequestContextPath(request); + if (contextPath != null && !contextPath.isEmpty() && !contextPath.equals("/")) { + return context.with(ServletContextPath.CONTEXT_KEY, contextPath); + } + return context; + } + + @Override + public void endExceptionally( + Context context, Throwable throwable, RESPONSE response, long timestamp) { + if (accessor.isResponseCommitted(response)) { + super.endExceptionally(context, throwable, response, timestamp); + } else { + // passing null response to super, in order to capture as 500 / INTERNAL, due to servlet spec + // https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf: + // "If a servlet generates an error that is not handled by the error page mechanism as + // described above, the container must ensure to send a response with status 500." + super.endExceptionally(context, throwable, null, timestamp); + } + } + + @Override + protected String url(REQUEST httpServletRequest) { + try { + return new URI( + accessor.getRequestScheme(httpServletRequest), + null, + accessor.getRequestServerName(httpServletRequest), + accessor.getRequestServerPort(httpServletRequest), + accessor.getRequestUri(httpServletRequest), + accessor.getRequestQueryString(httpServletRequest), + null) + .toString(); + } catch (URISyntaxException e) { + log.debug("Failed to construct request URI", e); + return null; + } + } + + @Override + public Context getServerContext(REQUEST request) { + Object context = accessor.getRequestAttribute(request, CONTEXT_ATTRIBUTE); + return context instanceof Context ? (Context) context : null; + } + + @Override + protected void attachServerContext(Context context, REQUEST request) { + accessor.setRequestAttribute(request, CONTEXT_ATTRIBUTE, context); + } + + @Override + protected Integer peerPort(REQUEST connection) { + return accessor.getRequestRemotePort(connection); + } + + @Override + protected String peerHostIP(REQUEST connection) { + return accessor.getRequestRemoteAddr(connection); + } + + @Override + protected String method(REQUEST request) { + return accessor.getRequestMethod(request); + } + + @Override + protected int responseStatus(RESPONSE response) { + return accessor.getResponseStatus(response); + } + + @Override + protected abstract TextMapGetter getGetter(); + + /** + * Response object must be attached to a request prior to {@link #attachAsyncListener(Object)} + * being called, as otherwise in some environments it is not possible to access response from + * async event in listeners. + */ + public void setAsyncListenerResponse(REQUEST request, RESPONSE response) { + accessor.setRequestAttribute(request, ASYNC_LISTENER_RESPONSE_ATTRIBUTE, response); + } + + public void attachAsyncListener(REQUEST request) { + Context context = getServerContext(request); + + if (context != null) { + Object response = accessor.getRequestAttribute(request, ASYNC_LISTENER_RESPONSE_ATTRIBUTE); + + accessor.addRequestAsyncListener( + request, new TagSettingAsyncListener<>(this, new AtomicBoolean(), context), response); + accessor.setRequestAttribute(request, ASYNC_LISTENER_ATTRIBUTE, true); + } + } + + public boolean isAsyncListenerAttached(REQUEST request) { + return accessor.getRequestAttribute(request, ASYNC_LISTENER_ATTRIBUTE) != null; + } + + public void addUnwrappedThrowable(Context context, Throwable throwable) { + if (AppServerBridge.shouldRecordException(context)) { + onException(context, throwable); + } + } + + @Override + protected Throwable unwrapThrowable(Throwable throwable) { + if (accessor.isServletException(throwable) && throwable.getCause() != null) { + throwable = throwable.getCause(); + } + return super.unwrapThrowable(throwable); + } + + public void setPrincipal(Context context, REQUEST request) { + Principal principal = accessor.getRequestUserPrincipal(request); + if (principal != null) { + Span.fromContext(context).setAttribute(SemanticAttributes.ENDUSER_ID, principal.getName()); + } + } + + @Override + protected String flavor(REQUEST connection, REQUEST request) { + return accessor.getRequestProtocol(connection); + } + + @Override + protected String requestHeader(REQUEST httpServletRequest, String name) { + return accessor.getRequestHeader(httpServletRequest, name); + } + + public Throwable errorException(REQUEST request) { + Object value = accessor.getRequestAttribute(request, errorExceptionAttributeName()); + + if (value instanceof Throwable) { + return (Throwable) value; + } else { + return null; + } + } + + protected abstract String errorExceptionAttributeName(); + + public String getSpanName(REQUEST request) { + String servletPath = accessor.getRequestServletPath(request); + if (servletPath.isEmpty()) { + return "HTTP " + accessor.getRequestMethod(request); + } + String contextPath = accessor.getRequestContextPath(request); + if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) { + return servletPath; + } + return contextPath + servletPath; + } + + /** + * When server spans are managed by app server instrumentation we need to add context path of + * current request to context if it isn't already added. Servlet instrumentation adds it when it + * starts server span. + */ + public Context updateContext(Context context, REQUEST request) { + String contextPath = context.get(ServletContextPath.CONTEXT_KEY); + if (contextPath == null) { + context = addServletContextPath(context, request); + } + + return context; + } + + public void updateSpanName(REQUEST request) { + updateSpanName(getServerSpan(request), request); + } + + private void updateSpanName(Span span, REQUEST request) { + span.updateName(getSpanName(request)); + } + + public void onTimeout(Context context, long timeout) { + Span span = Span.fromContext(context); + span.setStatus(StatusCode.ERROR); + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute("servlet.timeout", timeout); + } + span.end(); + } + + /* + Given request already has a context associated with it. + As there should not be nested spans of kind SERVER, we should NOT create a new span here. + + But it may happen that there is no span in current Context or it is from a different trace. + E.g. in case of async servlet request processing we create span for incoming request in one thread, + but actual request continues processing happens in another thread. + Depending on servlet container implementation, this processing may again arrive into this method. + E.g. Jetty handles async requests in a way that calls HttpServlet.service method twice. + + In this case we have to put the span from the request into current context before continuing. + */ + public boolean needsRescoping(Context attachedContext) { + return !sameTrace(Span.fromContext(Context.current()), Span.fromContext(attachedContext)); + } + + private static boolean sameTrace(Span oneSpan, Span otherSpan) { + return oneSpan.getSpanContext().getTraceId().equals(otherSpan.getSpanContext().getTraceId()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/TagSettingAsyncListener.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/TagSettingAsyncListener.java new file mode 100644 index 000000000..4597609e4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/TagSettingAsyncListener.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet; + +import io.opentelemetry.context.Context; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TagSettingAsyncListener implements ServletAsyncListener { + private final ServletHttpServerTracer tracer; + private final AtomicBoolean responseHandled; + private final Context context; + + public TagSettingAsyncListener( + ServletHttpServerTracer tracer, + AtomicBoolean responseHandled, + Context context) { + this.tracer = tracer; + this.responseHandled = responseHandled; + this.context = context; + } + + @Override + public void onComplete(RESPONSE response) { + if (responseHandled.compareAndSet(false, true)) { + tracer.end(context, response); + } + } + + @Override + public void onTimeout(long timeout) { + if (responseHandled.compareAndSet(false, true)) { + tracer.onTimeout(context, timeout); + } + } + + @Override + public void onError(Throwable throwable, RESPONSE response) { + if (responseHandled.compareAndSet(false, true)) { + tracer.endExceptionally(context, throwable, response); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletFilterMappingResolverFactory.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletFilterMappingResolverFactory.java new file mode 100644 index 000000000..018439d42 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletFilterMappingResolverFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.naming; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public abstract class ServletFilterMappingResolverFactory { + + protected abstract FILTERREGISTRATION getFilterRegistration(); + + protected abstract Collection getUrlPatternMappings( + FILTERREGISTRATION filterRegistration); + + protected abstract Collection getServletNameMappings( + FILTERREGISTRATION filterRegistration); + + protected abstract Collection getServletMappings(String servletName); + + // TODO(anuraaga): We currently treat null as no mappings, and empty as having a default mapping. + // Error prone is correctly flagging this behavior as error prone. + @SuppressWarnings("ReturnsNullCollection") + private Collection getMappings() { + FILTERREGISTRATION filterRegistration = getFilterRegistration(); + if (filterRegistration == null) { + return null; + } + Set mappings = new HashSet<>(); + Collection urlPatternMappings = getUrlPatternMappings(filterRegistration); + if (urlPatternMappings != null) { + mappings.addAll(urlPatternMappings); + } + Collection servletNameMappings = getServletNameMappings(filterRegistration); + if (servletNameMappings != null) { + for (String servletName : servletNameMappings) { + Collection servletMappings = getServletMappings(servletName); + if (servletMappings != null) { + mappings.addAll(servletMappings); + } + } + } + + if (mappings.isEmpty()) { + return null; + } + + List mappingsList = new ArrayList<>(mappings); + // sort longest mapping first + mappingsList.sort((s1, s2) -> s2.length() - s1.length()); + + return mappingsList; + } + + public final MappingResolver create() { + Collection mappings = getMappings(); + if (mappings == null) { + return null; + } + + return MappingResolver.build(mappings); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletMappingResolverFactory.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletMappingResolverFactory.java new file mode 100644 index 000000000..ea051b876 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletMappingResolverFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.naming; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import java.util.Collection; + +public abstract class ServletMappingResolverFactory { + + protected abstract Collection getMappings(); + + public final MappingResolver create() { + Collection mappings = getMappings(); + if (mappings == null) { + return null; + } + + return MappingResolver.build(mappings); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletSpanNameProvider.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletSpanNameProvider.java new file mode 100644 index 000000000..32ba2e17c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/naming/ServletSpanNameProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.naming; + +import io.opentelemetry.instrumentation.api.servlet.MappingResolver; +import io.opentelemetry.instrumentation.servlet.ServletAccessor; + +/** Helper class for constructing span name for given servlet/filter mapping and request. */ +public class ServletSpanNameProvider { + private final ServletAccessor servletAccessor; + + public ServletSpanNameProvider(ServletAccessor servletAccessor) { + this.servletAccessor = servletAccessor; + } + + public String getSpanName(MappingResolver mappingResolver, REQUEST request) { + String spanName = getSpanNameOrNull(mappingResolver, request); + if (spanName == null) { + String contextPath = servletAccessor.getRequestContextPath(request); + if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) { + return "HTTP " + servletAccessor.getRequestMethod(request); + } + return contextPath + "/*"; + } + return spanName; + } + + public String getSpanNameOrNull(MappingResolver mappingResolver, REQUEST request) { + if (mappingResolver == null) { + return null; + } + + String servletPath = servletAccessor.getRequestServletPath(request); + String pathInfo = servletAccessor.getRequestPathInfo(request); + String mapping = mappingResolver.resolve(servletPath, pathInfo); + // mapping was not found + if (mapping == null) { + return null; + } + + // prepend context path + String contextPath = servletAccessor.getRequestContextPath(request); + if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) { + return mapping; + } + return contextPath + mapping; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/servlet-javax-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/servlet-javax-common-javaagent.gradle new file mode 100644 index 000000000..a5d453a09 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/servlet-javax-common-javaagent.gradle @@ -0,0 +1,30 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "javax.servlet" + module = "servlet-api" + versions = "(0,)" + } + pass { + group = "javax.servlet" + module = 'javax.servlet-api' + versions = "[3.0,)" + assertInverse = true + } +} + +dependencies { + api(project(':instrumentation:servlet:servlet-javax-common:library')) + implementation(project(':instrumentation:servlet:servlet-common:javaagent')) + + compileOnly "javax.servlet:servlet-api:2.3" + + testImplementation(project(':testing-common')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } + + // We don't check testLatestDeps for this module since we have coverage in others like servlet-3.0 + testImplementation "org.eclipse.jetty:jetty-server:7.0.0.v20091005" + testImplementation "org.eclipse.jetty:jetty-servlet:7.0.0.v20091005" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/JavaxServletInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/JavaxServletInstrumentationModule.java new file mode 100644 index 000000000..5509ecd21 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/JavaxServletInstrumentationModule.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.javax; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.servlet.common.response.HttpServletResponseInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JavaxServletInstrumentationModule extends InstrumentationModule { + private static final String BASE_PACKAGE = "javax.servlet"; + + public JavaxServletInstrumentationModule() { + super("servlet", "servlet-javax-common"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList( + new HttpServletResponseInstrumentation( + BASE_PACKAGE, adviceClassName(".response.ResponseSendAdvice"))); + } + + private static String adviceClassName(String suffix) { + return JavaxServletInstrumentationModule.class.getPackage().getName() + suffix; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/response/ResponseSendAdvice.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/response/ResponseSendAdvice.java new file mode 100644 index 000000000..19abd9af9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/response/ResponseSendAdvice.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.javax.response; + +import static io.opentelemetry.javaagent.instrumentation.servlet.javax.response.ResponseTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.CallDepth; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.servlet.common.response.HttpServletResponseAdviceHelper; +import java.lang.reflect.Method; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class ResponseSendAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void start( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepthThreadLocalMap.getCallDepth(HttpServletResponse.class); + // Don't want to generate a new top-level span + if (callDepth.getAndIncrement() == 0 + && Java8BytecodeBridge.currentSpan().getSpanContext().isValid()) { + context = tracer().startSpan(method); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + HttpServletResponseAdviceHelper.stopSpan(tracer(), throwable, context, scope, callDepth); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/response/ResponseTracer.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/response/ResponseTracer.java new file mode 100644 index 000000000..0e2318403 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/response/ResponseTracer.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.servlet.javax.response; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import java.lang.reflect.Method; + +public class ResponseTracer extends BaseTracer { + private static final ResponseTracer TRACER = new ResponseTracer(); + + public static ResponseTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.servlet-javax-common"; + } + + public Context startSpan(Method method) { + return startSpan(SpanNames.fromMethod(method)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/test/groovy/HttpServletResponseTest.groovy b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/test/groovy/HttpServletResponseTest.groovy new file mode 100644 index 000000000..8a9a52a4e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/javaagent/src/test/groovy/HttpServletResponseTest.groovy @@ -0,0 +1,281 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static java.util.Collections.emptyEnumeration + +import groovy.servlet.AbstractHttpServlet +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import javax.servlet.ServletOutputStream +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse +import javax.servlet.http.Cookie +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import spock.lang.Subject + +class HttpServletResponseTest extends AgentInstrumentationSpecification { + + @Subject + def response = new TestResponse() + def request = Mock(HttpServletRequest) { + getMethod() >> "GET" + getProtocol() >> "TEST" + getHeaderNames() >> emptyEnumeration() + getAttributeNames() >> emptyEnumeration() + } + + def setup() { + def servlet = new AbstractHttpServlet() {} + // We need to call service so HttpServletAdvice can link the request to the response. + servlet.service((ServletRequest) request, (ServletResponse) response) + clearExportedData() + } + + def "test send no-parent"() { + when: + response.sendError(0) + response.sendError(0, "") + response.sendRedirect("") + + then: + assertTraces(0) {} + } + + def "test send with parent"() { + when: + runUnderTrace("parent") { + response.sendError(0) + response.sendError(0, "") + response.sendRedirect("") + } + + then: + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "parent") + span(1) { + name "TestResponse.sendError" + childOf span(0) + attributes { + } + } + span(2) { + name "TestResponse.sendError" + childOf span(0) + attributes { + } + } + span(3) { + name "TestResponse.sendRedirect" + childOf span(0) + attributes { + } + } + } + } + } + + def "test send with exception"() { + setup: + def ex = new Exception("some error") + def response = new TestResponse() { + @Override + void sendRedirect(String s) { + throw ex + } + } + def servlet = new AbstractHttpServlet() {} + // We need to call service so HttpServletAdvice can link the request to the response. + servlet.service((ServletRequest) request, (ServletResponse) response) + clearExportedData() + + when: + runUnderTrace("parent") { + response.sendRedirect("") + } + + then: + def th = thrown(Exception) + th == ex + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, ex) + span(1) { + name 'HttpServletResponseTest$2.sendRedirect' + childOf span(0) + status ERROR + errorEvent(ex.class, ex.message) + } + } + } + } + + static class TestResponse implements HttpServletResponse { + + @Override + void addCookie(Cookie cookie) { + + } + + @Override + boolean containsHeader(String s) { + return false + } + + @Override + String encodeURL(String s) { + return null + } + + @Override + String encodeRedirectURL(String s) { + return null + } + + @Override + String encodeUrl(String s) { + return null + } + + @Override + String encodeRedirectUrl(String s) { + return null + } + + @Override + void sendError(int i, String s) throws IOException { + + } + + @Override + void sendError(int i) throws IOException { + + } + + @Override + void sendRedirect(String s) throws IOException { + + } + + @Override + void setDateHeader(String s, long l) { + + } + + @Override + void addDateHeader(String s, long l) { + + } + + @Override + void setHeader(String s, String s1) { + + } + + @Override + void addHeader(String s, String s1) { + + } + + @Override + void setIntHeader(String s, int i) { + + } + + @Override + void addIntHeader(String s, int i) { + + } + + @Override + void setStatus(int i) { + + } + + @Override + void setStatus(int i, String s) { + + } + + @Override + String getCharacterEncoding() { + return null + } + + @Override + String getContentType() { + return null + } + + @Override + ServletOutputStream getOutputStream() throws IOException { + return null + } + + @Override + PrintWriter getWriter() throws IOException { + return null + } + + @Override + void setCharacterEncoding(String charset) { + + } + + @Override + void setContentLength(int i) { + + } + + @Override + void setContentType(String s) { + + } + + @Override + void setBufferSize(int i) { + + } + + @Override + int getBufferSize() { + return 0 + } + + @Override + void flushBuffer() throws IOException { + + } + + @Override + void resetBuffer() { + + } + + @Override + boolean isCommitted() { + return false + } + + @Override + void reset() { + + } + + @Override + void setLocale(Locale locale) { + + } + + @Override + Locale getLocale() { + return null + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/servlet-javax-common-library.gradle b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/servlet-javax-common-library.gradle new file mode 100644 index 000000000..137a1089c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/servlet-javax-common-library.gradle @@ -0,0 +1,13 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + implementation "org.slf4j:slf4j-api" + + api(project(':instrumentation:servlet:servlet-common:library')) + + compileOnly "javax.servlet:servlet-api:2.2" + + testImplementation "javax.servlet:servlet-api:2.2" + testImplementation "org.mockito:mockito-core" + testImplementation "org.assertj:assertj-core" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxHttpServletRequestGetter.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxHttpServletRequestGetter.java new file mode 100644 index 000000000..3618bc62f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxHttpServletRequestGetter.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.javax; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Collections; +import javax.servlet.http.HttpServletRequest; + +public class JavaxHttpServletRequestGetter implements TextMapGetter { + + public static final JavaxHttpServletRequestGetter GETTER = new JavaxHttpServletRequestGetter(); + + @Override + public Iterable keys(HttpServletRequest carrier) { + return Collections.list(carrier.getHeaderNames()); + } + + @Override + public String get(HttpServletRequest carrier, String key) { + return carrier.getHeader(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxServletAccessor.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxServletAccessor.java new file mode 100644 index 000000000..f1310f9a3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxServletAccessor.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.javax; + +import io.opentelemetry.instrumentation.servlet.ServletAccessor; +import java.security.Principal; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +public abstract class JavaxServletAccessor implements ServletAccessor { + @Override + public String getRequestContextPath(HttpServletRequest request) { + return request.getContextPath(); + } + + @Override + public String getRequestScheme(HttpServletRequest request) { + return request.getScheme(); + } + + @Override + public String getRequestServerName(HttpServletRequest request) { + return request.getServerName(); + } + + @Override + public int getRequestServerPort(HttpServletRequest request) { + return request.getServerPort(); + } + + @Override + public String getRequestUri(HttpServletRequest request) { + return request.getRequestURI(); + } + + @Override + public String getRequestQueryString(HttpServletRequest request) { + return request.getQueryString(); + } + + @Override + public Object getRequestAttribute(HttpServletRequest request, String name) { + return request.getAttribute(name); + } + + @Override + public void setRequestAttribute(HttpServletRequest request, String name, Object value) { + request.setAttribute(name, value); + } + + @Override + public String getRequestProtocol(HttpServletRequest request) { + return request.getProtocol(); + } + + @Override + public String getRequestMethod(HttpServletRequest request) { + return request.getMethod(); + } + + @Override + public String getRequestRemoteAddr(HttpServletRequest request) { + return request.getRemoteAddr(); + } + + @Override + public String getRequestHeader(HttpServletRequest request, String name) { + return request.getHeader(name); + } + + @Override + public String getRequestServletPath(HttpServletRequest request) { + return request.getServletPath(); + } + + @Override + public String getRequestPathInfo(HttpServletRequest request) { + return request.getPathInfo(); + } + + @Override + public Principal getRequestUserPrincipal(HttpServletRequest request) { + return request.getUserPrincipal(); + } + + @Override + public boolean isServletException(Throwable throwable) { + return throwable instanceof ServletException; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxServletHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxServletHttpServerTracer.java new file mode 100644 index 000000000..573b47f52 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/main/java/io/opentelemetry/instrumentation/servlet/javax/JavaxServletHttpServerTracer.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.servlet.javax; + +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.servlet.ServletHttpServerTracer; +import javax.servlet.http.HttpServletRequest; + +public abstract class JavaxServletHttpServerTracer + extends ServletHttpServerTracer { + protected JavaxServletHttpServerTracer(JavaxServletAccessor accessor) { + super(accessor); + } + + @Override + protected TextMapGetter getGetter() { + return JavaxHttpServletRequestGetter.GETTER; + } + + @Override + protected String errorExceptionAttributeName() { + return "javax.servlet.error.exception"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/test/java/RequestOnlyTracer.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/test/java/RequestOnlyTracer.java new file mode 100644 index 000000000..cd3a47e91 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/test/java/RequestOnlyTracer.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.servlet.ServletAsyncListener; +import io.opentelemetry.instrumentation.servlet.javax.JavaxServletAccessor; +import io.opentelemetry.instrumentation.servlet.javax.JavaxServletHttpServerTracer; +import javax.servlet.http.HttpServletRequest; + +public class RequestOnlyTracer extends JavaxServletHttpServerTracer { + public RequestOnlyTracer() { + super( + new JavaxServletAccessor() { + @Override + public Integer getRequestRemotePort(HttpServletRequest httpServletRequest) { + throw new UnsupportedOperationException(); + } + + @Override + public void addRequestAsyncListener( + HttpServletRequest request, ServletAsyncListener listener, Object response) { + throw new UnsupportedOperationException(); + } + + @Override + public int getResponseStatus(Void unused) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isResponseCommitted(Void unused) { + throw new UnsupportedOperationException(); + } + }); + } + + @Override + protected String getInstrumentationName() { + return null; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/test/java/ServletHttpServerTracerTest.java b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/test/java/ServletHttpServerTracerTest.java new file mode 100644 index 000000000..2dd8c5b99 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/servlet/servlet-javax-common/library/src/test/java/ServletHttpServerTracerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import javax.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; + +public class ServletHttpServerTracerTest { + private static final RequestOnlyTracer tracer = new RequestOnlyTracer(); + + @Test + void testGetSpanName_emptySpanName() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn(""); + when(request.getMethod()).thenReturn("PUT"); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("HTTP PUT"); + } + + @Test + void testGetSpanName_nullSpanName() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn(null); + assertThatThrownBy(() -> tracer.getSpanName(request)).isInstanceOf(NullPointerException.class); + } + + @Test + void testGetSpanName_nullContextPath() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/swizzler"); + when(request.getContextPath()).thenReturn(null); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("/swizzler"); + } + + @Test + void testGetSpanName_emptyContextPath() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/swizzler"); + when(request.getContextPath()).thenReturn(""); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("/swizzler"); + } + + @Test + void testGetSpanName_slashContextPath() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/swizzler"); + when(request.getContextPath()).thenReturn("/"); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("/swizzler"); + } + + @Test + void testGetSpanName_appendsSpanNameToContext() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/swizzler"); + when(request.getContextPath()).thenReturn("/path/to"); + String spanName = tracer.getSpanName(request); + assertThat(spanName).isEqualTo("/path/to/swizzler"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/spark-2.3-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/spark-2.3-javaagent.gradle new file mode 100644 index 000000000..54d10dca1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/spark-2.3-javaagent.gradle @@ -0,0 +1,19 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +// building against 2.3 and testing against 2.4 because JettyHandler is available since 2.4 only +muzzle { + pass { + group = "com.sparkjava" + module = 'spark-core' + versions = "[2.3,)" + assertInverse = true + } +} + +dependencies { + library "com.sparkjava:spark-core:2.3" + + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') + + testLibrary "com.sparkjava:spark-core:2.4" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/RoutesInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/RoutesInstrumentation.java new file mode 100644 index 000000000..0cc0cd0b8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/RoutesInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.sparkjava; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import spark.routematch.RouteMatch; + +public class RoutesInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("spark.route.Routes"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("find") + .and(takesArgument(0, named("spark.route.HttpMethod"))) + .and(returns(named("spark.routematch.RouteMatch"))) + .and(isPublic()), + this.getClass().getName() + "$FindAdvice"); + } + + @SuppressWarnings("unused") + public static class FindAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void routeMatchEnricher(@Advice.Return RouteMatch routeMatch) { + + Span span = Java8BytecodeBridge.currentSpan(); + if (span != null && routeMatch != null) { + span.updateName(routeMatch.getMatchUri()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkInstrumentationModule.java new file mode 100644 index 000000000..6e78dbc62 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.sparkjava; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class SparkInstrumentationModule extends InstrumentationModule { + + public SparkInstrumentationModule() { + super("spark", "spark-2.3"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RoutesInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/test/groovy/SparkJavaBasedTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/test/groovy/SparkJavaBasedTest.groovy new file mode 100644 index 000000000..9880cd23d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/test/groovy/SparkJavaBasedTest.groovy @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.SERVER + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.client.WebClient +import spark.Spark +import spock.lang.Shared + +class SparkJavaBasedTest extends AgentInstrumentationSpecification { + + @Shared + int port + + @Shared + WebClient client + + def setupSpec() { + port = PortUtils.findOpenPort() + TestSparkJavaApplication.initSpark(port) + client = WebClient.of("http://localhost:${port}") + } + + def cleanupSpec() { + Spark.stop() + } + + def "generates spans"() { + when: + def response = client.get("/param/asdf1234").aggregate().join() + + then: + port != 0 + def content = response.contentUtf8() + content == "Hello asdf1234" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "/param/:param" + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/param/asdf1234" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + } + } + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/test/java/TestSparkJavaApplication.java b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/test/java/TestSparkJavaApplication.java new file mode 100644 index 000000000..da146bde5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spark-2.3/javaagent/src/test/java/TestSparkJavaApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import spark.Spark; + +public class TestSparkJavaApplication { + + public static void initSpark(int port) { + Spark.port(port); + Spark.get("/", (req, res) -> "Hello World"); + + Spark.get("/param/:param", (req, res) -> "Hello " + req.params("param")); + + Spark.get( + "/exception/:param", + (req, res) -> { + throw new IllegalStateException(req.params("param")); + }); + + Spark.awaitInitialization(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/README.md new file mode 100644 index 000000000..8d81abfc6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/README.md @@ -0,0 +1,975 @@ +# OpenTelemetry Instrumentation: Spring and Spring Boot + + + +This package streamlines the manual instrumentation process of OpenTelemetry for [Spring](https://spring.io/projects/spring-framework) and [Spring Boot](https://spring.io/projects/spring-boot) applications. It will enable you to add traces to requests and database calls with minimal changes to application code. This package will not fully automate your OpenTelemetry instrumentation, instead, it will provide you with better tools to instrument your own code. + +The [first section](#manual-instrumentation-with-java-sdk) will walk you through span creation and propagation using the OpenTelemetry Java API and [Spring's RestTemplate Http Web Client](https://spring.io/guides/gs/consuming-rest/). This approach will use the "vanilla" OpenTelemetry API to make explicit tracing calls within an application's controller. + +The [second section](#manual-instrumentation-using-handlers-and-filters) will build on the first. It will walk you through implementing spring-web handler and filter interfaces to create traces with minimal changes to existing application code. Using the OpenTelemetry API, this approach involves copy and pasting files and a significant amount of manual configurations. + +The [third section](#auto-instrumentation-using-spring-starters) with build on the first two sections. We will use spring auto-configurations and instrumentation tools packaged in OpenTelemetry [Spring Starters](starters) to streamline the set up of OpenTelemetry using Spring. With these tools you will be able to setup distributed tracing with little to no changes to existing configurations and easily customize traces with minor additions to application code. + +In this guide we will be using a running example. In section one and two, we will create two spring web services using Spring Boot. We will then trace requests between these services using two different approaches. Finally, in section three we will explore tools documented in [opentelemetry-spring-boot-autoconfigure](/spring-boot-autoconfigure/README.md#features) which can improve this process. + +# Manual Instrumentation Guide + +## Create two Spring Projects + +Using the [spring project initializer](https://start.spring.io/), we will create two spring projects. Name one project `MainService` and the other `TimeService`. In this example `MainService` will be a client of `TimeService` and they will be dealing with time. Make sure to select maven, Spring Boot 2.3, Java, and add the spring-web dependency. After downloading the two projects include the OpenTelemetry dependencies and configuration listed below. + +## Setup for Manual Instrumentation + +Add the dependencies below to enable OpenTelemetry in `MainService` and `TimeService`. The Jaeger and LoggingExporter packages are recommended for exporting traces but are not required. As of May 2020, Jaeger, Zipkin, OTLP, and Logging exporters are supported by opentelemetry-java. Feel free to use whatever exporter you are most comfortable with. + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://search.maven.org/search?q=g:io.opentelemetry). + - Minimum version: `1.1.0` + - Note: You may need to include our bintray maven repository to your build file: `https://dl.bintray.com/open-telemetry/maven/`. As of August 2020 the latest opentelemetry-java-instrumentation artifacts are not published to maven-central. Please check the [releasing](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/master/RELEASING.md) doc for updates to this process. + +### Maven + +#### OpenTelemetry +```xml + + io.opentelemetry + opentelemetry-api + OPENTELEMETRY_VERSION + + + io.opentelemetry + opentelemetry-sdk + OPENTELEMETRY_VERSION + +``` + +#### LoggingSpanExporter +```xml + + io.opentelemetry + opentelemetry-exporters-logging + OPENTELEMETRY_VERSION + +``` + +#### Jaeger Exporter +```xml + + io.opentelemetry + opentelemetry-exporters-jaeger + OPENTELEMETRY_VERSION + + + io.grpc + grpc-netty + 1.30.2 + +``` + +### Gradle + +#### OpenTelemetry +```gradle +implementation "io.opentelemetry:opentelemetry-api:OPENTELEMETRY_VERSION" +implementation "io.opentelemetry:opentelemetry-sdk:OPENTELEMETRY_VERSION" +``` + +#### LoggingExporter +```gradle +implementation "io.opentelemetry:opentelemetry-exporters-logging:OPENTELEMETRY_VERSION" +``` + +#### Jaeger Exporter +```gradle +implementation "io.opentelemetry:opentelemetry-exporters-jaeger:OPENTELEMETRY_VERSION" +compile "io.grpc:grpc-netty:1.30.2" +``` + +### Tracer Configuration + +To enable tracing in your OpenTelemetry project configure a Tracer Bean. This bean will be auto wired to controllers to create and propagate spans. This can be seen in the `Tracer otelTracer()` method below. If you plan to use a trace exporter remember to also include it in this configuration class. In section 3 we will use an annotation to set up this configuration. + +A sample OpenTelemetry configuration using LoggingExporter is shown below: + +```java +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.ManagedChannelBuilder; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.exporters.jaeger.JaegerGrpcSpanExporter; +import io.opentelemetry.exporters.logging.*; + +@Configuration +public class OtelConfig { + private static final String tracerName = "fooTracer"; + @Bean + public Tracer otelTracer() { + Tracer tracer = OpenTelemetry.getGlobalTracer(tracerName); + + SpanProcessor logProcessor = SimpleSpanProcessor.newBuilder(new LoggingSpanExporter()).build(); + OpenTelemetrySdk.getTracerManagement().addSpanProcessor(logProcessor); + + return tracer; + } +} +``` + + +The file above configures an OpenTelemetry tracer and a span processor. The span processor builds a log exporter which will output spans to the console. Similarly, one could add another exporter, such as the `JaegerExporter`, to visualize traces on a different back-end. Similar to how the `LoggingExporter` is configured, a Jaeger configuration can be added to the `OtelConfig` class above. + +Sample configuration for a Jaeger Exporter: + +```java + +SpanProcessor jaegerProcessor = SimpleSpanProcessor + .newBuilder(JaegerGrpcSpanExporter.newBuilder().setServiceName(tracerName) + .setChannel(ManagedChannelBuilder.forAddress("localhost", 14250).usePlaintext().build()) + .build()) + .build(); +OpenTelemetrySdk.getTracerManagement().addSpanProcessor(jaegerProcessor); +``` + +### Project Background + +Here we will create REST controllers for `MainService` and `TimeService`. +`MainService` will send a GET request to `TimeService` to retrieve the current time. After this request is resolved, `MainService` then will append a message to time and return a string to the client. + +## Manual Instrumentation with Java SDK + +### Add OpenTelemetry to MainService and TimeService + +Required dependencies and configurations for MainService and TimeService projects can be found [here](#setup-for-manual-instrumentation). + +### Instrumentation of MainService + +1. Ensure OpenTelemetry dependencies are included +2. Ensure an OpenTelemetry Tracer is configured + +3. Ensure a Spring Boot main class was created by the Spring initializer + +```java +@SpringBootApplication +public class MainServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(MainServiceApplication.class, args); + } +} +``` + +4. Create a REST controller for MainService +5. Create a span to wrap MainServiceController.message() + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; + +import HttpUtils; + +@RestController +@RequestMapping(value = "/message") +public class MainServiceController { + private static int requestCount = 1; + private static final String TIME_SERVICE_URL = "http://localhost:8081/time"; + + @Autowired + private Tracer tracer; + + @Autowired + private HttpUtils httpUtils; + + @GetMapping + public String message() { + Span span = tracer.spanBuilder("message").startSpan(); + + try (Scope scope = tracer.withSpan(span)) { + span.addEvent("Controller Entered"); + span.setAttribute("timeservicecontroller.request.count", requestCount++); + return "Time Service says: " + httpUtils.callEndpoint(TIME_SERVICE_URL); + } catch (Exception e) { + span.setAttribute("error", true); + return "ERROR: I can't tell the time"; + } finally { + span.addEvent("Exit Controller"); + span.end(); + } + } +} +``` + +6. Configure `HttpUtils.callEndpoint` to inject span context into request. This is key to propagate the trace to the TimeService + +HttpUtils is a helper class that injects the current span context into outgoing requests. This involves adding the tracer id and the trace-state to a request header. For this example, we used `RestTemplate` to send requests from `MainService` to `TimeService`. A similar approach can be used with popular Java Web Clients such as [okhttp](https://square.github.io/okhttp/) and [apache http client](https://www.tutorialspoint.com/apache_httpclient/apache_httpclient_quick_guide.htm). The key to this implementation is to override the put method in `TextMapPropagator.Setter` to handle your request format. `TextMapPropagator.inject` will use this setter to set `traceparent` and `tracestate` headers in your requests. These values will be used to propagate your span context to external services. + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import io.opentelemetry.context.Context; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.Tracer; + +@Component +public class HttpUtils { + + private static final TextMapPropagator.Setter setter = new TextMapPropagator.Setter() { + @Override + public void set(HttpHeaders headers, String key, String value) { + headers.set(key, value); + } + }; + + @Autowired + private Tracer tracer; + + private final TextMapPropagator textFormat; + + public HttpUtils(Tracer tracer) { + textFormat = tracer.getTextMapPropagator(); + } + + public String callEndpoint(String url) { + HttpHeaders headers = new HttpHeaders(); + + textFormat.inject(Context.current(), headers, setter); + + HttpEntity entity = new HttpEntity(headers); + RestTemplate restTemplate = new RestTemplate(); + + ResponseEntity response = + restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + return response.getBody(); + } +} +``` +### Instrumentation of TimeService + +1. Ensure OpenTelemetry dependencies are included +2. Ensure an OpenTelemetry Tracer is configured +3. Ensure a Spring Boot main class was created by the Spring initializer + +```java +import java.io.IOException; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TimeServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(TimeServiceApplication.class, args); + } +} +``` + +4. Create a REST controller for TimeService +5. Start a span to wrap TimeServiceController.time() + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; + +@RestController +@RequestMapping(value = "/time") +public class TimeServiceController { + @Autowired + private Tracer tracer; + + @GetMapping + public String time() { + Span span = tracer.spanBuilder("time").startSpan(); + + try (Scope scope = tracer.withSpan(span)) { + span.addEvent("TimeServiceController Entered"); + span.setAttribute("what.am.i", "Tu es une legume"); + return "It's time to get a watch"; + } finally { + span.end(); + } + } +} +``` + +### Run MainService and TimeService + +***To view your distributed traces ensure either LogExporter or Jaeger is configured in the OtelConfig.java file*** + +To view traces on the Jaeger UI, deploy a Jaeger Exporter on localhost by running the command in terminal: + +`docker run --rm -it --network=host jaegertracing/all-in-one` + +After running Jaeger locally, navigate to the url below. Make sure to refresh the UI to view the exported traces from the two web services: + +`http://localhost:16686` + +Run MainService and TimeService from command line or using an IDE. The end point of interest for MainService is `http://localhost:8080/message` and `http://localhost:8081/time` for TimeService. Entering `localhost:8080/message` in a browser should call MainService and then TimeService, creating a trace. + +***Note: The default port for the Apache Tomcat is 8080. On localhost both MainService and TimeService services will attempt to run on this port raising an error. To avoid this add `server.port=8081` to the resources/application.properties file. Ensure the port specified corresponds to port referenced by MainServiceController.TIME_SERVICE_URL. *** + +Congrats, we just created a distributed service with OpenTelemetry! + +## Manual Instrumentation using Handlers and Filters + +In this section, we will implement the javax Servlet Filter interface to wrap all requests to MainService and TimeService controllers in a span. + +We will also use the RestTemplate HTTP client to send requests from MainService to TimeService. To propagate the trace in this request we will also implement the ClientHttpRequestInterceptor interface. This implementation is only required for projects that send outbound requests. In this example it is only required for MainService. + +### Set up MainService and TimeService + +Using the earlier instructions [create two spring projects](#create-two-spring-projects) and add the required [dependencies and configurations](#setup-for-manual-instrumentation). + +### Instrumentation of TimeService + +Ensure the main method in TimeServiceApplication is defined. This will be the entry point to the TimeService project. This file should be created by the Spring Boot project initializer. + +```java +import java.io.IOException; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TimeServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(TimeServiceApplication.class, args); + } +} +``` + +Add the REST controller below to your TimeService project. This controller will return a string when TimeServiceController.time is called: + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "/time") +public class TimeServiceController { + @Autowired + private Tracer tracer; + + @GetMapping + public String time() { + return "It's time to get a watch"; + } +} +``` + +#### Create Controller Filter + +Add the class below to wrap all requests to the TimeServiceController in a span. This class will call the preHandle method before the REST controller is entered and the postHandle method after a response is created. + +The preHandle method starts a span for each request. This implementation is shown below: + +```java + +@Component +public class ControllerFilter implements Filter { + private static final Logger LOG = Logger.getLogger(ControllerFilter.class.getName()); + + @Autowired + Tracer tracer; + + private final TextMapPropagator.Getter GETTER = + new TextMapPropagator.Getter() { + public String get(HttpServletRequest req, String key) { + return req.getHeader(key); + } + }; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { + LOG.info("start doFilter"); + + HttpServletRequest req = (HttpServletRequest) request; + Span currentSpan; + try (Scope scope = tracer.withSpan(currentSpan)) { + Context context = OpenTelemetry.getPropagators().getTextMapPropagator() + .extract(Context.current(), req, GETTER); + currentSpan = createSpanWithParent(req, context); + currentSpan.addEvent("dofilter"); + chain.doFilter(req, response); + } finally { + currentSpan.end(); + } + + LOG.info("end doFilter"); + } + + private Span createSpanWithParent(HttpServletRequest request, Context context) { + return tracer.spanBuilder(request.getRequestURI()).setSpanKind(SpanKind.SERVER).startSpan(); + } +} + +``` + +Now your TimeService application is complete. Create the MainService application using the instructions below and then run your distributed service! + +### Instrumentation of MainService + +Ensure the main method in MainServiceApplication is defined. This will be the entry point to the MainService project. This file should be created by the Spring Boot project initializer. + +```java +@SpringBootApplication +public class MainServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(MainServiceApplication.class, args); + } +} +``` + +Create a REST controller for MainService. This controller will send a request to TimeService and then return the response to the client: + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping(value = "/message") +public class MainServiceController { + private static final String TIME_SERVICE_URL = "http://localhost:8081/time"; + + @Autowired + private Tracer tracer; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private HttpUtils httpUtils; + + @GetMapping + public String message() { + + ResponseEntity response = + restTemplate.exchange(TIME_SERVICE_URL, HttpMethod.GET, null, String.class); + String currentTime = response.getBody(); + + return "Time Service: " + currentTime; + + } +} +``` + +As seen in the setup of TimeService, implement the javax servlet filter interface to wrap requests to the TimeServiceController in a span. In effect, we will be taking a copy of the [ControllerFilter.java](#create-controller-filter) file defined in TimeService and adding it to MainService. + +#### Create Client Http Request Interceptor + +Next, we will configure the ClientHttpRequestInterceptor to intercept all client HTTP requests made using RestTemplate. + +To propagate the span context from MainService to TimeService we must inject the trace parent and trace state into the outgoing request header. In section 1 this was done using the helper class HttpUtils. In this section, we will implement the ClientHttpRequestInterceptor interface and register this interceptor in our application. + +Include the two classes below to your MainService project to add this functionality: + +```java + +import java.io.IOException; + +import io.opentelemetry.context.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpRequest; + +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; + +@Component +public class RestTemplateInterceptor implements ClientHttpRequestInterceptor { + + @Autowired + private Tracer tracer; + + private static final TextMapPropagator.Setter setter = + new TextMapPropagator.Setter() { + @Override + public void set(HttpRequest carrier, String key, String value) { + carrier.getHeaders().set(key, value); + } + }; + + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) { + + String spanName = request.getMethodValue() + " " + request.getURI().toString(); + Span currentSpan = tracer.spanBuilder(spanName).setSpanKind(SpanKind.CLIENT).startSpan(); + + try (Scope scope = tracer.withSpan(currentSpan)) { + OpenTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), request, setter); + ClientHttpResponse response = execution.execute(request, body); + LOG.info("Request sent from RestTemplateInterceptor"); + + return response; + } finally { + currentSpan.end(); + } + } +} + +``` + +```java +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestClientConfig { + + @Autowired + RestTemplateHeaderModifierInterceptor restTemplateHeaderModifierInterceptor; + + @Bean + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); + + restTemplate.getInterceptors().add(restTemplateHeaderModifierInterceptor); + + return restTemplate; + } +} +``` + +### Create a distributed trace + +By default Spring Boot runs a Tomcat server on port 8080. This tutorial assumes MainService runs on the default port (8080) and TimeService runs on port 8081. This is because we hard coded the TimeService end point in MainServiceController.TIME_SERVICE_URL. To run TimeServiceApplication on port 8081 include `server.port=8081` in the resources/application.properties file. + +Run both the MainService and TimeService projects in terminal or using an IDE (ex. Eclipse). The end point for MainService should be `http://localhost:8080/message` and `http://localhost:8081/time` for TimeService. Type both urls in a browser and ensure you receive a 200 response. + +To visualize this trace add a trace exporter to one or both of your applications. Instructions on how to setup LogExporter and Jaeger can be seen [above](#tracer-configuration). + +To create a sample trace enter `localhost:8080/message` in a browser. This trace should include a span for MainService and a span for TimeService. + + + +## Auto Instrumentation using Spring Starters + +In this tutorial we will create two SpringBoot applications (MainService and TimeService). We will use [opentelemetry-spring-starter](starters/spring-starter) to enable distributed tracing using OpenTelemetry and export spans using the default LoggingSpanExporter. We will also use the [opentelemetry-zipkin-exporter-starter](starters/zipkin-exporter-starter) to export traces to Zipkin. + +### OpenTelemetry Spring Starter Dependencies + +Add the following dependencies to your build file. + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://search.maven.org/search?q=g:io.opentelemetry). + - Minimum version: `1.1.0` + - Note: You may need to include our bintray maven repository to your build file: `https://dl.bintray.com/open-telemetry/maven/`. As of August 2020 the latest opentelemetry-java-instrumentation artifacts are not published to maven-central. Please check the [releasing](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/master/RELEASING.md) doc for updates to this process. + +#### Maven +```xml + + io.opentelemetry.instrumentation + opentelemetry-spring-starter + OPENTELEMETRY_VERSION + +``` + +#### Gradle +```gradle +implementation "io.opentelemetry.instrumentation:opentelemetry-spring-starter:OPENTELEMETRY_VERSION" +``` + +### Create two Spring Projects + +Using the [spring project initializer](https://start.spring.io/), we will create two spring projects. Name one project `MainService` and the other `TimeService`. Make sure to select maven, Spring Boot 2.3, Java, and add the spring-web dependency. After downloading the two projects include the OpenTelemetry dependencies listed above. + +### Main Service Application + +Configure the main class in your `MainService` project to match the file below. In this example `MainService` will be a client of `TimeService`. The RestController and RestTemplate Bean initialized in the file below will be auto-instrumented by the opentelemetry spring starter. + +```java +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +@SpringBootApplication +public class MainServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(MainServiceApplication.class, args); + } + + @RestController + @RequestMapping(value = "/message") + public static class MainServiceController { + private static final String TIME_SERVICE_URL = "http://localhost:8080/time"; + + @Autowired + private RestTemplate restTemplate; + + @GetMapping + public String message() { + return restTemplate.exchange(TIME_SERVICE_URL, HttpMethod.GET, null, String.class).getBody(); + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + } +} +``` + +#### Application Configurations + +The following tracer configurations can be used to customize your instrumentation. Add the following values to your project's resource/application.properties file: + +```properties + +## TimeService will run on port 8080 +## Setting the server port of MainService to 8081 will prevent conflicts +server.port=8081 + +## Default configurations +#otel.traces.sampler.probability=1 +#otel.springboot.web.enabled=true +#otel.springboot.httpclients.enabled=true +#otel.springboot.aspects.enabled=true + +``` + +Check out [OpenTelemetry Spring Boot AutoConfigure](spring-boot-autoconfigure/README.md) to learn more. + + + +### TimeService + +Configure the main class in your `Time Service` project to match the file below. Here we use the Tracer bean provided by the OpenTelemetry starter to create an internal span and set some additional events and attributes. + +```java + +import java.io.IOException; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; + +@SpringBootApplication +public class TimeServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(TimeServiceApplication.class, args); + } + + @RestController + @RequestMapping(value = "/time") + public class TimeServiceController { + @Autowired + private Tracer tracer; + + @GetMapping + public String time() { + withSpanMethod(); + + Span span = tracer.spanBuilder("time").startSpan(); + try (Scope scope = tracer.withSpan(span)) { + span.addEvent("TimeServiceController Entered"); + span.setAttribute("what.am.i", "Tu es une legume"); + return "It's time to get a watch"; + } finally { + span.end(); + } + } + + @WithSpan(kind=SpanKind.SERVER) + public void withSpanMethod() {} + } +} +``` + + +### Generating Trace - LoggingSpanExporter + +To generate a trace, run MainServiceApplication and TimeServiceApplication, and then send a request to `localhost:8080/message`. Shown below is the output of the default span exporter - (LoggingSpanExporter)[https://github.com/open-telemetry/opentelemetry-java/tree/master/exporters/logging]. + +#### MainService + +```java +SpanWrapper{ +delegate=RecordEventsReadableSpan{traceId=TraceId{traceId=52d6edec17bbf842cf5032ebce2043f8}, spanId=SpanId{spanId=15b72a8e85c842c5}, +parentSpanId=SpanId{spanId=57f0106dd1121b54}, name=HTTP GET, kind=CLIENT, attributes={net.peer.name=AttributeValueString{stringValue=localhost}, +http.status_code=AttributeValueLong{longValue=200}, net.peer.port=AttributeValueLong{longValue=8080}, +http.url=AttributeValueString{stringValue=http://localhost:8080/time}, http.method=AttributeValueString{stringValue=GET}}, +status=Status{canonicalCode=OK, description=null}, totalRecordedEvents=0, totalRecordedLinks=0, startEpochNanos=1598409410457933181, +endEpochNanos=1598409410925420912}, resolvedLinks=[], resolvedEvents=[], attributes={net.peer.name=AttributeValueString{stringValue=localhost}, +http.status_code=AttributeValueLong{longValue=200}, net.peer.port=AttributeValueLong{longValue=8080}, +http.url=AttributeValueString{stringValue=http://localhost:8080/time}, http.method=AttributeValueString{stringValue=GET}}, totalAttributeCount=5, +totalRecordedEvents=0, status=Status{canonicalCode=OK, description=null}, name=HTTP GET, endEpochNanos=1598409410925420912, hasEnded=true +} + +SpanWrapper{ +delegate=RecordEventsReadableSpan{traceId=TraceId{traceId=52d6edec17bbf842cf5032ebce2043f8}, spanId=SpanId{spanId=57f0106dd1121b54}, +parentSpanId=SpanId{spanId=0000000000000000}, name=WebMVCTracingFilter.doFilterInteral, kind=SERVER, attributes={http.status_code=AttributeValueLong{longValue=200}, +sampling.probability=AttributeValueDouble{doubleValue=1.0}, net.peer.port=AttributeValueLong{longValue=57578}, +http.user_agent=AttributeValueString{stringValue=PostmanRuntime/7.26.2}, http.flavor=AttributeValueString{stringValue=1.1}, +http.url=AttributeValueString{stringValue=/message}, net.peer.ip=AttributeValueString{stringValue=0:0:0:0:0:0:0:1}, +http.method=AttributeValueString{stringValue=GET}, http.client_ip=AttributeValueString{stringValue=0:0:0:0:0:0:0:1}}, +status=Status{canonicalCode=OK, description=null}, totalRecordedEvents=0, totalRecordedLinks=0, startEpochNanos=1598409410399317331, endEpochNanos=1598409411045782693}, +resolvedLinks=[], resolvedEvents=[], attributes={http.status_code=AttributeValueLong{longValue=200}, sampling.probability=AttributeValueDouble{doubleValue=1.0}, +net.peer.port=AttributeValueLong{longValue=57578}, http.user_agent=AttributeValueString{stringValue=PostmanRuntime/7.26.2}, +http.flavor=AttributeValueString{stringValue=1.1}, http.url=AttributeValueString{stringValue=/message}, +net.peer.ip=AttributeValueString{stringValue=0:0:0:0:0:0:0:1}, http.method=AttributeValueString{stringValue=GET}, +http.client_ip=AttributeValueString{stringValue=0:0:0:0:0:0:0:1}}, totalAttributeCount=9, totalRecordedEvents=0, +status=Status{canonicalCode=OK, description=null}, name=WebMVCTracingFilter.doFilterInteral, endEpochNanos=1598409411045782693, hasEnded=true +} +``` + +#### TimeService + +```java +SpanWrapper{ +delegate=RecordEventsReadableSpan{traceId=TraceId{traceId=52d6edec17bbf842cf5032ebce2043f8}, +spanId=SpanId{spanId=f2d824704be8ab10}, parentSpanId=SpanId{spanId=b4ae77c523215f9d}, +name=time, kind=INTERNAL, attributes={what.am.i=AttributeValueString{stringValue=Tu es une legume}}, status=null, +totalRecordedEvents=1,totalRecordedLinks=0, startEpochNanos=1598409410738665807, endEpochNanos=1598409410740607921}, resolvedLinks=[], +resolvedEvents=[RawTimedEvent{name=TimeServiceController Entered, attributes={}, epochNanos=1598409410738760924, totalAttributeCount=0}], +attributes={what.am.i=AttributeValueString{stringValue=Tu es une legume}}, totalAttributeCount=1, totalRecordedEvents=1, +status=Status{canonicalCode=OK, description=null}, name=time, endEpochNanos=1598409410740607921, hasEnded=true +} + +SpanWrapper{ +delegate=RecordEventsReadableSpan{traceId=TraceId{traceId=52d6edec17bbf842cf5032ebce2043f8}, spanId=SpanId{spanId=b4ae77c523215f9d}, +parentSpanId=SpanId{spanId=15b72a8e85c842c5}, name=WebMVCTracingFilter.doFilterInteral, kind=SERVER, +attributes={http.status_code=AttributeValueLong{longValue=200}, net.peer.port=AttributeValueLong{longValue=40174}, +http.user_agent=AttributeValueString{stringValue=Java/11.0.8}, http.flavor=AttributeValueString{stringValue=1.1}, +http.url=AttributeValueString{stringValue=/time}, net.peer.ip=AttributeValueString{stringValue=127.0.0.1}, +http.method=AttributeValueString{stringValue=GET}, http.client_ip=AttributeValueString{stringValue=127.0.0.1}}, +status=Status{canonicalCode=OK, description=null}, totalRecordedEvents=0, totalRecordedLinks=0, startEpochNanos=1598409410680549805, +endEpochNanos=1598409410921631068}, resolvedLinks=[], resolvedEvents=[], attributes={http.status_code=AttributeValueLong{longValue=200}, +net.peer.port=AttributeValueLong{longValue=40174}, http.user_agent=AttributeValueString{stringValue=Java/11.0.8}, +http.flavor=AttributeValueString{stringValue=1.1}, http.url=AttributeValueString{stringValue=/time}, +net.peer.ip=AttributeValueString{stringValue=127.0.0.1}, http.method=AttributeValueString{stringValue=GET}, +http.client_ip=AttributeValueString{stringValue=127.0.0.1}}, totalAttributeCount=8, totalRecordedEvents=0, +status=Status{canonicalCode=OK, description=null}, name=WebMVCTracingFilter.doFilterInteral, endEpochNanos=1598409410921631068, hasEnded=true +} + +``` + + +### Exporter Starters + +To configure OpenTelemetry tracing with the OTLP, Zipkin, or Jaeger span exporters replace the OpenTelemetry Spring Starter dependency with one of the artifacts listed below: + +#### Maven +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-zipkin-exporter-starter + OPENTELEMETRY_VERSION + + + + + io.opentelemetry.instrumentation + opentelemetry-jaeger-exporter-starter + OPENTELEMETRY_VERSION + + + + + io.opentelemetry.instrumentation + opentelemetry-otlp-exporter-starter + OPENTELEMETRY_VERSION + +``` + +#### Gradle +```gradle + +//opentelemetry starter with zipkin configurations +implementation "io.opentelemetry.instrumentation:opentelemetry-zipkin-exporter-starter:OPENTELEMETRY_VERSION" + +//opentelemetry starter with jaeger configurations +implementation "io.opentelemetry.instrumentation:opentelemetry-jaeger-exporter-starter:OPENTELEMETRY_VERSION" + +//opentelemetry starter with otlp configurations +implementation "io.opentelemetry.instrumentation:opentelemetry-otlp-exporter-starter:OPENTELEMETRY_VERSION" +``` + +#### Exporter Configuration Properties + +Add the following configurations to overwrite the default exporter values listed below. + +``` +## Default tracer configurations +#otel.traces.sampler.probability=1 + +## Default exporter configurations +#otel.exporter.otlp.endpoint=localhost:55680 +#otel.exporter.otlp.timeout=10s +#otel.exporter.jaeger.endpoint=localhost:14250 +#otel.exporter.jaeger.timeout=10s +#otel.exporter.zipkin.endpoint=http://localhost:9411/api/v2/spans +``` + +### Sample Trace Zipkin + +To generate a trace using the zipkin exporter follow the steps below: + 1. Replace `opentelemetry-spring-starter` with `opentelemetry-zipkin-starter` in your pom or gradle build file + 2. Use the Zipkin [quick starter](https://zipkin.io/pages/quickstart) to download and run the zipkin executable jar + - Ensure the zipkin endpoint matches the default value listed in your application properties + 3. Run `MainServiceApplication.java` and `TimeServiceApplication.java` + 4. Use your favorite browser to send a request to `http://localhost:8080/message` + 5. Navigate to `http://localhost:9411` to see your trace + + +Shown below is the sample trace generated by `MainService` and `TimeService` using the opentelemetry-zipkin-exporter-starter. + +```json +[ + { + "traceId":"52d6edec17bbf842cf5032ebce2043f8", + "parentId":"b4ae77c523215f9d", + "id":"f2d824704be8ab10", + "name":"time", + "timestamp":1598409410738665, + "duration":1942, + "localEndpoint":{ + "serviceName":"time_service_zipkin_trace", + "ipv4":"192.XXX.X.XXX" + }, + "annotations":[ + { + "timestamp":1598409410738760, + "value":"TimeServiceController Entered" + } + ], + "tags":{ + "what.am.i":"Tu es une legume" + } + }, + { + "traceId":"52d6edec17bbf842cf5032ebce2043f8", + "parentId":"15b72a8e85c842c5", + "id":"b4ae77c523215f9d", + "kind":"SERVER", + "name":"webmvctracingfilter.dofilterinteral", + "timestamp":1598409410680549, + "duration":241082, + "localEndpoint":{ + "serviceName":"time_service_zipkin_trace", + "ipv4":"192.XXX.X.XXX" + }, + "tags":{ + "http.client_ip":"127.0.0.1", + "http.flavor":"1.1", + "http.method":"GET", + "http.status_code":"200", + "http.url":"/time", + "http.user_agent":"Java/11.0.8", + "net.peer.ip":"127.0.0.1", + "net.peer.port":"40174" + } + }, + { + "traceId":"52d6edec17bbf842cf5032ebce2043f8", + "parentId":"57f0106dd1121b54", + "id":"15b72a8e85c842c5", + "kind":"CLIENT", + "name":"http get", + "timestamp":1598409410457933, + "duration":467487, + "localEndpoint":{ + "serviceName":"main_service_zipkin_trace", + "ipv4":"192.XXX.X.XXX" + }, + "tags":{ + "http.method":"GET", + "http.status_code":"200", + "http.url":"http://localhost:8080/time", + "net.peer.name":"localhost", + "net.peer.port":"8080" + } + }, + { + "traceId":"52d6edec17bbf842cf5032ebce2043f8", + "id":"57f0106dd1121b54", + "kind":"SERVER", + "name":"webmvctracingfilter.dofilterinteral", + "timestamp":1598409410399317, + "duration":646465, + "localEndpoint":{ + "serviceName":"main_service_zipkin_trace", + "ipv4":"192.XXX.X.XXX" + }, + "tags":{ + "http.client_ip":"0:0:0:0:0:0:0:1", + "http.flavor":"1.1", + "http.method":"GET", + "http.status_code":"200", + "http.url":"/message", + "http.user_agent":"PostmanRuntime/7.26.2", + "net.peer.ip":"0:0:0:0:0:0:0:1", + "net.peer.port":"57578", + "sampling.probability":"1.0" + } + } +] + +``` diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/spring-batch-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/spring-batch-3.0-javaagent.gradle new file mode 100644 index 000000000..e16c415fb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/spring-batch-3.0-javaagent.gradle @@ -0,0 +1,39 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.springframework.batch" + module = "spring-batch-core" + versions = "[3.0.0.RELEASE,)" + assertInverse = true + } +} + +dependencies { + library "org.springframework.batch:spring-batch-core:3.0.0.RELEASE" + + testImplementation "javax.inject:javax.inject:1" + // SimpleAsyncTaskExecutor context propagation + testInstrumentation project(':instrumentation:spring:spring-core-2.0:javaagent') +} + +tasks.withType(Test).configureEach { + jvmArgs '-Dotel.instrumentation.spring-batch.enabled=true' +} +test { + filter { + excludeTestsMatching '*ChunkRootSpanTest' + excludeTestsMatching '*ItemLevelSpanTest' + } +} +test.finalizedBy(tasks.register("testChunkRootSpan", Test) { + filter { + includeTestsMatching '*ChunkRootSpanTest' + } + jvmArgs '-Dotel.instrumentation.spring-batch.experimental.chunk.new-trace=true' +}).finalizedBy(tasks.register("testItemLevelSpan", Test) { + filter { + includeTestsMatching '*ItemLevelSpanTest' + } + jvmArgs '-Dotel.instrumentation.spring-batch.item.enabled=true' +}) diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/ContextAndScope.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/ContextAndScope.java new file mode 100644 index 000000000..e0834bcd2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/ContextAndScope.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public final class ContextAndScope { + private final Context context; + private final Scope scope; + + public ContextAndScope(Context context, Scope scope) { + this.context = context; + this.scope = scope; + } + + public Context getContext() { + return context; + } + + public void closeScope() { + scope.close(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationConfig.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationConfig.java new file mode 100644 index 000000000..fe6ff8a5d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import io.opentelemetry.instrumentation.api.config.Config; +import java.util.List; + +public final class SpringBatchInstrumentationConfig { + private static final List INSTRUMENTATION_NAMES = + unmodifiableList(asList("spring-batch", "spring-batch-3.0")); + + // the item level instrumentation is very chatty so it's disabled by default + private static final boolean ITEM_TRACING_ENABLED = + Config.get() + .isInstrumentationPropertyEnabled( + instrumentationNames(), "item.enabled", /* defaultEnabled= */ false); + private static final boolean CREATE_ROOT_SPAN_FOR_CHUNK = + Config.get() + .isInstrumentationPropertyEnabled( + instrumentationNames(), "experimental.chunk.new-trace", /* defaultEnabled= */ false); + + public static List instrumentationNames() { + return INSTRUMENTATION_NAMES; + } + + public static boolean shouldTraceItems() { + return ITEM_TRACING_ENABLED; + } + + public static boolean shouldCreateRootSpanForChunk() { + return CREATE_ROOT_SPAN_FOR_CHUNK; + } + + private SpringBatchInstrumentationConfig() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationModule.java new file mode 100644 index 000000000..bd9c1ef53 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/SpringBatchInstrumentationModule.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.instrumentationNames; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.chunk.StepBuilderInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.item.ChunkOrientedTaskletInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.item.JsrChunkProcessorInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.item.SimpleChunkProcessorInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.item.SimpleChunkProviderInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.job.JobBuilderHelperInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.job.JobFactoryBeanInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.job.JobParserJobFactoryBeanInstrumentation; +import io.opentelemetry.javaagent.instrumentation.spring.batch.step.StepBuilderHelperInstrumentation; +import java.util.Arrays; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class SpringBatchInstrumentationModule extends InstrumentationModule { + public SpringBatchInstrumentationModule() { + super(instrumentationNames()); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // JSR-352 Batch API + return hasClassesNamed("org.springframework.batch.core.jsr.launch.JsrJobOperator"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + // job instrumentations + new JobBuilderHelperInstrumentation(), + new JobFactoryBeanInstrumentation(), + new JobParserJobFactoryBeanInstrumentation(), + // step instrumentation + new StepBuilderHelperInstrumentation(), + // chunk instrumentation + new StepBuilderInstrumentation(), + // item instrumentations + new ChunkOrientedTaskletInstrumentation(), + new SimpleChunkProviderInstrumentation(), + new SimpleChunkProcessorInstrumentation(), + new JsrChunkProcessorInstrumentation()); + } + + @Override + protected boolean defaultEnabled() { + // TODO: replace this with an experimental flag + return false; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/ChunkExecutionTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/ChunkExecutionTracer.java new file mode 100644 index 000000000..6993e2e26 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/ChunkExecutionTracer.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.chunk; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldCreateRootSpanForChunk; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import org.springframework.batch.core.scope.context.ChunkContext; + +public class ChunkExecutionTracer extends BaseTracer { + private static final ChunkExecutionTracer TRACER = new ChunkExecutionTracer(); + + public static ChunkExecutionTracer tracer() { + return TRACER; + } + + public Context startSpan(ChunkContext chunkContext) { + Context parentContext = Context.current(); + SpanBuilder spanBuilder = spanBuilder(parentContext, spanName(chunkContext), INTERNAL); + if (shouldCreateRootSpanForChunk()) { + linkParentSpan(spanBuilder, parentContext); + } + return parentContext.with(spanBuilder.startSpan()); + } + + private static String spanName(ChunkContext chunkContext) { + String jobName = chunkContext.getStepContext().getJobName(); + String stepName = chunkContext.getStepContext().getStepName(); + return "BatchJob " + jobName + "." + stepName + ".Chunk"; + } + + private static void linkParentSpan(SpanBuilder spanBuilder, Context parentContext) { + spanBuilder.setNoParent(); + + SpanContext parentSpanContext = Span.fromContext(parentContext).getSpanContext(); + if (parentSpanContext.isValid()) { + spanBuilder.addLink(parentSpanContext); + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-batch-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/StepBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/StepBuilderInstrumentation.java new file mode 100644 index 000000000..4dd0368ce --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/StepBuilderInstrumentation.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.chunk; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.spring.batch.ContextAndScope; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.builder.AbstractTaskletStepBuilder; + +public class StepBuilderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // Spring Batch Java DSL and XML config + return namedOneOf( + "org.springframework.batch.core.step.builder.AbstractTaskletStepBuilder", + // JSR-352 XML config + "org.springframework.batch.core.jsr.step.builder.JsrSimpleStepBuilder", + "org.springframework.batch.core.jsr.step.builder.JsrBatchletStepBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("build").and(isPublic()).and(takesArguments(0)), + this.getClass().getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This AbstractTaskletStepBuilder stepBuilder) { + ContextStore chunkExecutionContextStore = + InstrumentationContext.get(ChunkContext.class, ContextAndScope.class); + stepBuilder.listener(new TracingChunkExecutionListener(chunkExecutionContextStore)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/TracingChunkExecutionListener.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/TracingChunkExecutionListener.java new file mode 100644 index 000000000..542a8f41e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/chunk/TracingChunkExecutionListener.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.chunk; + +import static io.opentelemetry.javaagent.instrumentation.spring.batch.chunk.ChunkExecutionTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.spring.batch.ContextAndScope; +import org.springframework.batch.core.ChunkListener; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.core.Ordered; + +public final class TracingChunkExecutionListener implements ChunkListener, Ordered { + private final ContextStore executionContextStore; + + public TracingChunkExecutionListener( + ContextStore executionContextStore) { + this.executionContextStore = executionContextStore; + } + + @Override + public void beforeChunk(ChunkContext chunkContext) { + Context context = tracer().startSpan(chunkContext); + // beforeJob & afterJob always execute on the same thread + Scope scope = context.makeCurrent(); + executionContextStore.put(chunkContext, new ContextAndScope(context, scope)); + } + + @Override + public void afterChunk(ChunkContext chunkContext) { + ContextAndScope contextAndScope = executionContextStore.get(chunkContext); + if (contextAndScope != null) { + executionContextStore.put(chunkContext, null); + contextAndScope.closeScope(); + tracer().end(contextAndScope.getContext()); + } + } + + @Override + public void afterChunkError(ChunkContext chunkContext) { + ContextAndScope contextAndScope = executionContextStore.get(chunkContext); + if (contextAndScope != null) { + executionContextStore.put(chunkContext, null); + contextAndScope.closeScope(); + Throwable throwable = + (Throwable) chunkContext.getAttribute(ChunkListener.ROLLBACK_EXCEPTION_KEY); + tracer().endExceptionally(contextAndScope.getContext(), throwable); + } + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } + + // equals() and hashCode() methods guarantee that only one instance of + // TracingJobExecutionListener will be present in an ordered set of listeners + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return o instanceof TracingChunkExecutionListener; + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/ChunkOrientedTaskletInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/ChunkOrientedTaskletInstrumentation.java new file mode 100644 index 000000000..995986fef --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/ChunkOrientedTaskletInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.item; + +import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldTraceItems; +import static io.opentelemetry.javaagent.instrumentation.spring.batch.item.ItemTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.batch.core.scope.context.ChunkContext; + +public class ChunkOrientedTaskletInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.batch.core.step.item.ChunkOrientedTasklet"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isPublic() + .and(named("execute")) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.springframework.batch.core.StepContribution"))) + .and( + takesArgument( + 1, named("org.springframework.batch.core.scope.context.ChunkContext"))), + this.getClass().getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(1) ChunkContext chunkContext, @Advice.Local("otelScope") Scope scope) { + if (shouldTraceItems()) { + Context context = tracer().startChunk(chunkContext); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/ItemTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/ItemTracer.java new file mode 100644 index 000000000..5d8ed409d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/ItemTracer.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.item; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.batch.core.scope.context.ChunkContext; + +public class ItemTracer extends BaseTracer { + private static final ContextKey CHUNK_CONTEXT_KEY = + ContextKey.named("opentelemetry-spring-batch-chunk-context-context-key"); + + private static final ItemTracer TRACER = new ItemTracer(); + + public static ItemTracer tracer() { + return TRACER; + } + + /** + * Item-level listeners do not receive chunk/step context as parameters. Fortunately the whole + * chunk always executes on one thread - in Spring Batch chunk is almost synonymous with a DB + * transaction; this makes {@link ChunkContext} a good candidate to be stored in {@link Context}. + */ + public Context startChunk(ChunkContext chunkContext) { + return Context.current().with(CHUNK_CONTEXT_KEY, chunkContext); + } + + @Nullable + public Context startReadSpan() { + return startItemSpan("ItemRead"); + } + + @Nullable + public Context startProcessSpan() { + return startItemSpan("ItemProcess"); + } + + @Nullable + public Context startWriteSpan() { + return startItemSpan("ItemWrite"); + } + + @Nullable + private Context startItemSpan(String itemOperationName) { + Context currentContext = Context.current(); + + ChunkContext chunkContext = currentContext.get(CHUNK_CONTEXT_KEY); + if (chunkContext == null) { + return null; + } + + String jobName = chunkContext.getStepContext().getJobName(); + String stepName = chunkContext.getStepContext().getStepName(); + + return startSpan( + currentContext, "BatchJob " + jobName + "." + stepName + "." + itemOperationName, INTERNAL); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-batch-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/JsrChunkProcessorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/JsrChunkProcessorInstrumentation.java new file mode 100644 index 000000000..730dfbc0c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/JsrChunkProcessorInstrumentation.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.item; + +import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldTraceItems; +import static io.opentelemetry.javaagent.instrumentation.spring.batch.item.ItemTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JsrChunkProcessorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.batch.core.jsr.step.item.JsrChunkProcessor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isProtected().and(named("doProvide")).and(takesArguments(2)), + this.getClass().getName() + "$ProvideAdvice"); + transformer.applyAdviceToMethod( + isProtected().and(named("doTransform")).and(takesArguments(1)), + this.getClass().getName() + "$TransformAdvice"); + transformer.applyAdviceToMethod( + isProtected().and(named("doPersist")).and(takesArguments(2)), + this.getClass().getName() + "$PersistAdvice"); + } + + @SuppressWarnings("unused") + public static class ProvideAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { + if (!shouldTraceItems()) { + return; + } + context = tracer().startReadSpan(); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable thrown, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + if (thrown == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, thrown); + } + } + } + } + + @SuppressWarnings("unused") + public static class TransformAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { + if (!shouldTraceItems()) { + return; + } + context = tracer().startProcessSpan(); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable thrown, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + if (thrown == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, thrown); + } + } + } + } + + @SuppressWarnings("unused") + public static class PersistAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { + if (!shouldTraceItems()) { + return; + } + context = tracer().startWriteSpan(); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable thrown, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + if (thrown == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, thrown); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/SimpleChunkProcessorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/SimpleChunkProcessorInstrumentation.java new file mode 100644 index 000000000..a2d9af2fd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/SimpleChunkProcessorInstrumentation.java @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.item; + +import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldTraceItems; +import static io.opentelemetry.javaagent.instrumentation.spring.batch.item.ItemTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemWriter; + +public class SimpleChunkProcessorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.batch.core.step.item.SimpleChunkProcessor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isProtected().and(named("doProcess")).and(takesArguments(1)), + this.getClass().getName() + "$ProcessAdvice"); + transformer.applyAdviceToMethod( + isProtected().and(named("doWrite")).and(takesArguments(1)), + this.getClass().getName() + "$WriteAdvice"); + } + + @SuppressWarnings("unused") + public static class ProcessAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue("itemProcessor") ItemProcessor itemProcessor, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (!shouldTraceItems()) { + return; + } + if (itemProcessor == null) { + return; + } + context = tracer().startProcessSpan(); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable thrown, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + if (thrown == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, thrown); + } + } + } + } + + @SuppressWarnings("unused") + public static class WriteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue("itemWriter") ItemWriter itemWriter, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (!shouldTraceItems()) { + return; + } + if (itemWriter == null) { + return; + } + context = tracer().startWriteSpan(); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable thrown, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + if (thrown == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, thrown); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/SimpleChunkProviderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/SimpleChunkProviderInstrumentation.java new file mode 100644 index 000000000..e5437c371 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/item/SimpleChunkProviderInstrumentation.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.item; + +import static io.opentelemetry.javaagent.instrumentation.spring.batch.SpringBatchInstrumentationConfig.shouldTraceItems; +import static io.opentelemetry.javaagent.instrumentation.spring.batch.item.ItemTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// item read instrumentation *cannot* use ItemReadListener: sometimes afterRead() is not called +// after beforeRead(), using listener here would cause unfinished spans/scopes +public class SimpleChunkProviderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.batch.core.step.item.SimpleChunkProvider"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isProtected().and(named("doRead")).and(takesArguments(0)), + this.getClass().getName() + "$ReadAdvice"); + } + + @SuppressWarnings("unused") + public static class ReadAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { + if (!shouldTraceItems()) { + return; + } + context = tracer().startReadSpan(); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable thrown, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + if (thrown == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, thrown); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobBuilderHelperInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobBuilderHelperInstrumentation.java new file mode 100644 index 000000000..cfaf23e11 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobBuilderHelperInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.job; + +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.spring.batch.ContextAndScope; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.job.builder.JobBuilderHelper; + +public class JobBuilderHelperInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // Java DSL Job config + return named("org.springframework.batch.core.job.builder.JobBuilderHelper"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("enhance") + .and(isProtected()) + .and(takesArguments(1)) + .and(takesArgument(0, named("org.springframework.batch.core.Job"))), + this.getClass().getName() + "$EnhanceAdvice"); + } + + @SuppressWarnings("unused") + public static class EnhanceAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This JobBuilderHelper jobBuilder) { + ContextStore executionContextStore = + InstrumentationContext.get(JobExecution.class, ContextAndScope.class); + jobBuilder.listener(new TracingJobExecutionListener(executionContextStore)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobExecutionTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobExecutionTracer.java new file mode 100644 index 000000000..18d829716 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobExecutionTracer.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.job; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import org.springframework.batch.core.JobExecution; + +public class JobExecutionTracer extends BaseTracer { + private static final JobExecutionTracer TRACER = new JobExecutionTracer(); + + public static JobExecutionTracer tracer() { + return TRACER; + } + + public Context startSpan(JobExecution jobExecution) { + String jobName = jobExecution.getJobInstance().getJobName(); + return startSpan("BatchJob " + jobName); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-batch-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobFactoryBeanInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobFactoryBeanInstrumentation.java new file mode 100644 index 000000000..555303813 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobFactoryBeanInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.job; + +import static net.bytebuddy.matcher.ElementMatchers.isArray; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.spring.batch.ContextAndScope; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.jsr.configuration.xml.JobFactoryBean; + +public class JobFactoryBeanInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // JSR-352 XML config + return named("org.springframework.batch.core.jsr.configuration.xml.JobFactoryBean"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(isConstructor(), this.getClass().getName() + "$InitAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("setJobExecutionListeners")) + .and(takesArguments(1)) + .and(takesArgument(0, isArray())), + this.getClass().getName() + "$SetListenersAdvice"); + } + + @SuppressWarnings("unused") + public static class InitAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This JobFactoryBean jobFactory) { + // this will trigger the advice below, which will make sure that the tracing listener is + // registered even if the application never calls setJobExecutionListeners() directly + jobFactory.setJobExecutionListeners(new Object[] {}); + } + } + + @SuppressWarnings("unused") + public static class SetListenersAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(value = 0, readOnly = false) Object[] listeners) { + ContextStore executionContextStore = + InstrumentationContext.get(JobExecution.class, ContextAndScope.class); + JobExecutionListener tracingListener = new TracingJobExecutionListener(executionContextStore); + + if (listeners == null) { + listeners = new Object[] {tracingListener}; + } else { + Object[] newListeners = new Object[listeners.length + 1]; + newListeners[0] = tracingListener; + System.arraycopy(listeners, 0, newListeners, 1, listeners.length); + listeners = newListeners; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobParserJobFactoryBeanInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobParserJobFactoryBeanInstrumentation.java new file mode 100644 index 000000000..e432b4df1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/JobParserJobFactoryBeanInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.job; + +import static net.bytebuddy.matcher.ElementMatchers.isArray; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.spring.batch.ContextAndScope; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.configuration.xml.JobParserJobFactoryBean; + +public class JobParserJobFactoryBeanInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // Spring Batch XML config + return named("org.springframework.batch.core.configuration.xml.JobParserJobFactoryBean"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(isConstructor(), this.getClass().getName() + "$InitAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("setJobExecutionListeners")) + .and(takesArguments(1)) + .and(takesArgument(0, isArray())), + this.getClass().getName() + "$SetListenersAdvice"); + } + + @SuppressWarnings("unused") + public static class InitAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This JobParserJobFactoryBean jobFactory) { + // this will trigger the advice below, which will make sure that the tracing listener is + // registered even if the application never calls setJobExecutionListeners() directly + jobFactory.setJobExecutionListeners(new JobExecutionListener[] {}); + } + } + + @SuppressWarnings("unused") + public static class SetListenersAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) JobExecutionListener[] listeners) { + ContextStore executionContextStore = + InstrumentationContext.get(JobExecution.class, ContextAndScope.class); + JobExecutionListener tracingListener = new TracingJobExecutionListener(executionContextStore); + + if (listeners == null) { + listeners = new JobExecutionListener[] {tracingListener}; + } else { + JobExecutionListener[] newListeners = new JobExecutionListener[listeners.length + 1]; + newListeners[0] = tracingListener; + System.arraycopy(listeners, 0, newListeners, 1, listeners.length); + listeners = newListeners; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/TracingJobExecutionListener.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/TracingJobExecutionListener.java new file mode 100644 index 000000000..13ff4e29e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/job/TracingJobExecutionListener.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.job; + +import static io.opentelemetry.javaagent.instrumentation.spring.batch.job.JobExecutionTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.spring.batch.ContextAndScope; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.core.Ordered; + +public final class TracingJobExecutionListener implements JobExecutionListener, Ordered { + private final ContextStore executionContextStore; + + public TracingJobExecutionListener( + ContextStore executionContextStore) { + this.executionContextStore = executionContextStore; + } + + @Override + public void beforeJob(JobExecution jobExecution) { + Context context = tracer().startSpan(jobExecution); + // beforeJob & afterJob always execute on the same thread + Scope scope = context.makeCurrent(); + executionContextStore.put(jobExecution, new ContextAndScope(context, scope)); + } + + @Override + public void afterJob(JobExecution jobExecution) { + ContextAndScope contextAndScope = executionContextStore.get(jobExecution); + if (contextAndScope != null) { + executionContextStore.put(jobExecution, null); + contextAndScope.closeScope(); + tracer().end(contextAndScope.getContext()); + } + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } + + // equals() and hashCode() methods guarantee that only one instance of + // TracingJobExecutionListener will be present in an ordered set of listeners + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return o instanceof TracingJobExecutionListener; + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/StepBuilderHelperInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/StepBuilderHelperInstrumentation.java new file mode 100644 index 000000000..3f28d261a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/StepBuilderHelperInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.step; + +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.spring.batch.ContextAndScope; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.step.builder.StepBuilderHelper; + +public class StepBuilderHelperInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.batch.core.step.builder.StepBuilderHelper"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("enhance") + .and(isProtected()) + .and(takesArguments(1)) + .and(takesArgument(0, named("org.springframework.batch.core.Step"))), + this.getClass().getName() + "$EnhanceAdvice"); + } + + @SuppressWarnings("unused") + public static class EnhanceAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This StepBuilderHelper stepBuilder) { + ContextStore executionContextStore = + InstrumentationContext.get(StepExecution.class, ContextAndScope.class); + stepBuilder.listener(new TracingStepExecutionListener(executionContextStore)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/StepExecutionTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/StepExecutionTracer.java new file mode 100644 index 000000000..88f3fc34f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/StepExecutionTracer.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.step; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import org.springframework.batch.core.StepExecution; + +public class StepExecutionTracer extends BaseTracer { + private static final StepExecutionTracer TRACER = new StepExecutionTracer(); + + public static StepExecutionTracer tracer() { + return TRACER; + } + + public Context startSpan(StepExecution stepExecution) { + String jobName = stepExecution.getJobExecution().getJobInstance().getJobName(); + String stepName = stepExecution.getStepName(); + return startSpan("BatchJob " + jobName + "." + stepName); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-batch-3.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/TracingStepExecutionListener.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/TracingStepExecutionListener.java new file mode 100644 index 000000000..98887e365 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/step/TracingStepExecutionListener.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.step; + +import static io.opentelemetry.javaagent.instrumentation.spring.batch.step.StepExecutionTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.spring.batch.ContextAndScope; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.core.Ordered; + +public final class TracingStepExecutionListener implements StepExecutionListener, Ordered { + private final ContextStore executionContextStore; + + public TracingStepExecutionListener( + ContextStore executionContextStore) { + this.executionContextStore = executionContextStore; + } + + @Override + public void beforeStep(StepExecution stepExecution) { + Context context = tracer().startSpan(stepExecution); + // beforeStep & afterStep always execute on the same thread + Scope scope = context.makeCurrent(); + executionContextStore.put(stepExecution, new ContextAndScope(context, scope)); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + ContextAndScope contextAndScope = executionContextStore.get(stepExecution); + if (contextAndScope != null) { + executionContextStore.put(stepExecution, null); + contextAndScope.closeScope(); + tracer().end(contextAndScope.getContext()); + } + return null; + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } + + // equals() and hashCode() methods guarantee that only one instance of + // TracingStepExecutionListener will be present in an ordered set of listeners + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return o instanceof TracingStepExecutionListener; + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ApplicationConfigTrait.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ApplicationConfigTrait.groovy new file mode 100644 index 000000000..82ea1fdbc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ApplicationConfigTrait.groovy @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.springframework.batch.core.Job +import org.springframework.batch.core.JobParameter +import org.springframework.batch.core.JobParameters +import org.springframework.batch.core.launch.JobLauncher +import org.springframework.context.ConfigurableApplicationContext + +trait ApplicationConfigTrait { + static ConfigurableApplicationContext applicationContext + static JobLauncher jobLauncher + + abstract ConfigurableApplicationContext createApplicationContext() + + def setupSpec() { + applicationContext = createApplicationContext() + applicationContext.start() + + jobLauncher = applicationContext.getBean(JobLauncher) + } + + def cleanupSpec() { + applicationContext.stop() + applicationContext.close() + + additionalCleanup() + } + + def additionalCleanup() {} + + def runJob(String jobName, Map params) { + def job = applicationContext.getBean(jobName, Job) + jobLauncher.run(job, new JobParameters(params)) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ChunkRootSpanTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ChunkRootSpanTest.groovy new file mode 100644 index 000000000..fc79b15b3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ChunkRootSpanTest.groovy @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static java.util.Collections.emptyMap + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.springframework.batch.core.JobParameter +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.support.ClassPathXmlApplicationContext + +abstract class ChunkRootSpanTest extends AgentInstrumentationSpecification { + + abstract runJob(String jobName, Map params = emptyMap()) + + def "should create separate traces for each chunk"() { + when: + runJob("itemsAndTaskletJob") + + then: + assertTraces(5) { + def itemStepSpan = null + def taskletStepSpan = null + + trace(0, 3) { + itemStepSpan = span(1) + taskletStepSpan = span(2) + + span(0) { + name "BatchJob itemsAndTaskletJob" + kind INTERNAL + } + span(1) { + name "BatchJob itemsAndTaskletJob.itemStep" + kind INTERNAL + childOf span(0) + } + span(2) { + name "BatchJob itemsAndTaskletJob.taskletStep" + kind INTERNAL + childOf span(0) + } + } + trace(1, 1) { + span(0) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + hasLink itemStepSpan + } + } + trace(2, 1) { + span(0) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + hasLink itemStepSpan + } + } + trace(3, 1) { + span(0) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + hasLink itemStepSpan + } + } + trace(4, 1) { + span(0) { + name "BatchJob itemsAndTaskletJob.taskletStep.Chunk" + kind INTERNAL + hasLink taskletStepSpan + } + } + } + } +} + +class JavaConfigChunkRootSpanTest extends ChunkRootSpanTest implements ApplicationConfigTrait { + @Override + ConfigurableApplicationContext createApplicationContext() { + new AnnotationConfigApplicationContext(SpringBatchApplication) + } +} + +class XmlConfigChunkRootSpanTest extends ChunkRootSpanTest implements ApplicationConfigTrait { + @Override + ConfigurableApplicationContext createApplicationContext() { + new ClassPathXmlApplicationContext("spring-batch.xml") + } +} + +class JsrConfigChunkRootSpanTest extends ChunkRootSpanTest implements JavaxBatchConfigTrait { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ItemLevelSpanTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ItemLevelSpanTest.groovy new file mode 100644 index 000000000..051308543 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/ItemLevelSpanTest.groovy @@ -0,0 +1,421 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static java.util.Collections.emptyMap + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.sdk.trace.data.SpanData +import org.springframework.batch.core.JobParameter +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.support.ClassPathXmlApplicationContext + +abstract class ItemLevelSpanTest extends AgentInstrumentationSpecification { + abstract runJob(String jobName, Map params = emptyMap()) + + def "should trace item read, process and write calls"() { + when: + runJob("itemsAndTaskletJob") + + then: + assertTraces(1) { + trace(0, 37) { + span(0) { + name "BatchJob itemsAndTaskletJob" + kind INTERNAL + } + + // item step + span(1) { + name "BatchJob itemsAndTaskletJob.itemStep" + kind INTERNAL + childOf span(0) + } + + // chunk 1, items 0-5 + span(2) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + (3..7).forEach { + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemRead" + kind INTERNAL + childOf span(2) + } + } + (8..12).forEach { + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess" + kind INTERNAL + childOf span(2) + } + } + span(13) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite" + kind INTERNAL + childOf span(2) + } + + // chunk 2, items 5-10 + span(14) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + (15..19).forEach { + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemRead" + kind INTERNAL + childOf span(14) + } + } + (20..24).forEach { + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess" + kind INTERNAL + childOf span(14) + } + } + span(25) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite" + kind INTERNAL + childOf span(14) + } + + // chunk 3, items 10-13 + span(26) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + // +1 for last read returning end of stream marker + (27..30).forEach { + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemRead" + kind INTERNAL + childOf span(26) + } + } + (31..33).forEach { + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess" + kind INTERNAL + childOf span(26) + } + } + span(34) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite" + kind INTERNAL + childOf span(26) + } + + // tasklet step + span(35) { + name "BatchJob itemsAndTaskletJob.taskletStep" + kind INTERNAL + childOf span(0) + } + span(36) { + name "BatchJob itemsAndTaskletJob.taskletStep.Chunk" + kind INTERNAL + childOf span(35) + } + } + } + } + + def "should trace all item operations on a parallel items job"() { + when: + runJob("parallelItemsJob") + + then: + assertTraces(1) { + trace(0, 23) { + // as chunks are processed in parallel we need to sort them to guarantee that they are + // in the expected order + // firstly compute child span count for each chunk, we'll sort chunks from larger to smaller + // based on child count + def childCount = new HashMap() + spans.forEach { span -> + if (span.name == "BatchJob parallelItemsJob.parallelItemsStep.Chunk") { + childCount.put(span, spans.count {it.parentSpanId == span.spanId }) + } + } + // sort spans with a ranking function + spans.sort({ + // job span is first + if (it.name == "BatchJob parallelItemsJob") { + return 0 + } + // step span is second + if (it.name == "BatchJob parallelItemsJob.parallelItemsStep") { + return 1 + } + + // find the chunk this span belongs to + def chunkSpan = it + while (chunkSpan != null && chunkSpan.name != "BatchJob parallelItemsJob.parallelItemsStep.Chunk") { + chunkSpan = spans.find {it.spanId == chunkSpan.parentSpanId } + } + if (chunkSpan != null) { + // sort larger chunks first + return 100 - childCount.get(chunkSpan) + } + throw new IllegalStateException("item spans should have a parent chunk span") + }) + + span(0) { + name "BatchJob parallelItemsJob" + kind INTERNAL + } + span(1) { + name "BatchJob parallelItemsJob.parallelItemsStep" + kind INTERNAL + childOf span(0) + } + + // chunk 1, first two items; thread 1 + span(2) { + name "BatchJob parallelItemsJob.parallelItemsStep.Chunk" + kind INTERNAL + childOf span(1) + } + [3, 4].forEach { + span(it) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemRead" + kind INTERNAL + childOf span(2) + } + } + [5, 6].forEach { + span(it) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemProcess" + kind INTERNAL + childOf span(2) + } + } + span(7) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemWrite" + kind INTERNAL + childOf span(2) + } + + // chunk 2, items 3 & 4; thread 2 + span(8) { + name "BatchJob parallelItemsJob.parallelItemsStep.Chunk" + kind INTERNAL + childOf span(1) + } + [9, 10].forEach { + span(it) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemRead" + kind INTERNAL + childOf span(8) + } + } + [11, 12].forEach { + span(it) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemProcess" + kind INTERNAL + childOf span(8) + } + } + span(13) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemWrite" + kind INTERNAL + childOf span(8) + } + + // chunk 3, 5th item; thread 1 + span(14) { + name "BatchJob parallelItemsJob.parallelItemsStep.Chunk" + kind INTERNAL + childOf span(1) + } + // +1 for last read returning end of stream marker + [15, 16].forEach { + span(it) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemRead" + kind INTERNAL + childOf span(14) + } + } + span(17) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemProcess" + kind INTERNAL + childOf span(14) + } + span(18) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemWrite" + kind INTERNAL + childOf span(14) + } + + // empty chunk on thread 2, end processing + span(19) { + name "BatchJob parallelItemsJob.parallelItemsStep.Chunk" + kind INTERNAL + childOf span(1) + } + // end of stream marker + span(20) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemRead" + kind INTERNAL + childOf span(19) + } + + // empty chunk on thread 1, end processing + span(21) { + name "BatchJob parallelItemsJob.parallelItemsStep.Chunk" + kind INTERNAL + childOf span(1) + } + // end of stream marker + span(22) { + name "BatchJob parallelItemsJob.parallelItemsStep.ItemRead" + kind INTERNAL + childOf span(21) + } + } + } + } +} + +class JavaConfigItemLevelSpanTest extends ItemLevelSpanTest implements ApplicationConfigTrait { + @Override + ConfigurableApplicationContext createApplicationContext() { + new AnnotationConfigApplicationContext(SpringBatchApplication) + } +} + +class XmlConfigItemLevelSpanTest extends ItemLevelSpanTest implements ApplicationConfigTrait { + @Override + ConfigurableApplicationContext createApplicationContext() { + new ClassPathXmlApplicationContext("spring-batch.xml") + } +} + +// JsrChunkProcessor works a bit differently than the "standard" one and does not read the whole +// chunk at once, it reads every item separately; it results in a different span ordering, that's +// why it has a completely separate test class +class JsrConfigItemLevelSpanTest extends AgentInstrumentationSpecification implements JavaxBatchConfigTrait { + def "should trace item read, process and write calls"() { + when: + runJob("itemsAndTaskletJob", [:]) + + then: + assertTraces(1) { + trace(0, 37) { + span(0) { + name "BatchJob itemsAndTaskletJob" + kind INTERNAL + } + + // item step + span(1) { + name "BatchJob itemsAndTaskletJob.itemStep" + kind INTERNAL + childOf span(0) + } + + // chunk 1, items 0-5 + span(2) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + (3..11).step(2) { + println it + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemRead" + kind INTERNAL + childOf span(2) + } + span(it + 1) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess" + kind INTERNAL + childOf span(2) + } + } + span(13) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite" + kind INTERNAL + childOf span(2) + } + + // chunk 2, items 5-10 + span(14) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + (15..23).step(2) { + println it + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemRead" + kind INTERNAL + childOf span(14) + } + span(it + 1) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess" + kind INTERNAL + childOf span(14) + } + } + span(25) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite" + kind INTERNAL + childOf span(14) + } + + // chunk 3, items 10-13 + span(26) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + (27..32).step(2) { + println it + span(it) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemRead" + kind INTERNAL + childOf span(26) + } + span(it + 1) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemProcess" + kind INTERNAL + childOf span(26) + } + } + // last read returning end of stream marker + span(33) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemRead" + kind INTERNAL + childOf span(26) + } + span(34) { + name "BatchJob itemsAndTaskletJob.itemStep.ItemWrite" + kind INTERNAL + childOf span(26) + } + + // tasklet step + span(35) { + name "BatchJob itemsAndTaskletJob.taskletStep" + kind INTERNAL + childOf span(0) + } + span(36) { + name "BatchJob itemsAndTaskletJob.taskletStep.Chunk" + kind INTERNAL + childOf span(35) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/JavaxBatchConfigTrait.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/JavaxBatchConfigTrait.groovy new file mode 100644 index 000000000..fc59dc0ef --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/JavaxBatchConfigTrait.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.concurrent.atomic.AtomicInteger +import javax.batch.operations.JobOperator +import javax.batch.runtime.BatchRuntime +import org.springframework.batch.core.JobParameter + +trait JavaxBatchConfigTrait { + static JobOperator jobOperator + static AtomicInteger counter = new AtomicInteger() + + def setupSpec() { + jobOperator = BatchRuntime.jobOperator + } + + // just for consistency with ApplicationConfigTrait + def cleanupSpec() { + additionalCleanup() + } + + def additionalCleanup() {} + + def runJob(String jobName, Map params) { + def jobParams = new Properties() + params.forEach({k, v -> + jobParams.setProperty(k, v.toString()) + }) + // each job instance with the same name needs to be unique + jobParams.setProperty("uniqueJobIdCounter", counter.getAndIncrement().toString()) + jobOperator.start(jobName, jobParams) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchApplication.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchApplication.groovy new file mode 100644 index 000000000..8f89dbb6d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchApplication.groovy @@ -0,0 +1,273 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.springframework.batch.core.Job +import org.springframework.batch.core.Step +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory +import org.springframework.batch.core.job.builder.FlowBuilder +import org.springframework.batch.core.job.flow.Flow +import org.springframework.batch.core.job.flow.support.SimpleFlow +import org.springframework.batch.core.launch.JobLauncher +import org.springframework.batch.core.launch.support.SimpleJobLauncher +import org.springframework.batch.core.partition.support.Partitioner +import org.springframework.batch.core.repository.JobRepository +import org.springframework.batch.item.ItemProcessor +import org.springframework.batch.item.ItemReader +import org.springframework.batch.item.ItemWriter +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.task.AsyncTaskExecutor +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor +import springbatch.TestDecider +import springbatch.TestItemProcessor +import springbatch.TestItemReader +import springbatch.TestItemWriter +import springbatch.TestPartitionedItemReader +import springbatch.TestPartitioner +import springbatch.TestSyncItemReader +import springbatch.TestTasklet + +@Configuration +@EnableBatchProcessing +class SpringBatchApplication { + + @Autowired + JobBuilderFactory jobs + @Autowired + StepBuilderFactory steps + @Autowired + JobRepository jobRepository + + @Bean + AsyncTaskExecutor asyncTaskExecutor() { + def executor = new ThreadPoolTaskExecutor() + executor.corePoolSize = 10 + executor.maxPoolSize = 10 + executor + } + + @Bean + JobLauncher jobLauncher() { + def launcher = new SimpleJobLauncher() + launcher.jobRepository = jobRepository + launcher.taskExecutor = asyncTaskExecutor() + launcher + } + + // common + @Bean + ItemReader itemReader() { + new TestItemReader() + } + + @Bean + ItemProcessor itemProcessor() { + new TestItemProcessor() + } + + @Bean + ItemWriter itemWriter() { + new TestItemWriter() + } + + // simple tasklet job + @Bean + Job taskletJob() { + jobs.get("taskletJob") + .start(step()) + .build() + } + + @Bean + Step step() { + steps.get("step") + .tasklet(new TestTasklet()) + .build() + } + + // 2-step tasklet + chunked items job + @Bean + Job itemsAndTaskletJob() { + jobs.get("itemsAndTaskletJob") + .start(itemStep()) + .next(taskletStep()) + .build() + } + + @Bean + Step taskletStep() { + steps.get("taskletStep") + .tasklet(new TestTasklet()) + .build() + } + + @Bean + Step itemStep() { + steps.get("itemStep") + .chunk(5) + .reader(itemReader()) + .processor(itemProcessor()) + .writer(itemWriter()) + .build() + } + + // parallel items job + @Bean + Job parallelItemsJob() { + jobs.get("parallelItemsJob") + .start(parallelItemsStep()) + .build() + } + + @Bean + Step parallelItemsStep() { + steps.get("parallelItemsStep") + .chunk(2) + .reader(new TestSyncItemReader(5)) + .processor(itemProcessor()) + .writer(itemWriter()) + .taskExecutor(asyncTaskExecutor()) + .throttleLimit(2) + .build() + } + + // job using a flow + @Bean + Job flowJob() { + jobs.get("flowJob") + .start(flow()) + .build() + .build() + } + + @Bean + Flow flow() { + new FlowBuilder("flow") + .start(flowStep1()) + .on("*") + .to(flowStep2()) + .build() + } + + @Bean + Step flowStep1() { + steps.get("flowStep1") + .tasklet(new TestTasklet()) + .build() + } + + @Bean + Step flowStep2() { + steps.get("flowStep2") + .tasklet(new TestTasklet()) + .build() + } + + // split job + @Bean + Job splitJob() { + jobs.get("splitJob") + .start(splitFlowStep1()) + .split(asyncTaskExecutor()) + .add(splitFlow2()) + .build() + .build() + } + + @Bean + Step splitFlowStep1() { + steps.get("splitFlowStep1") + .tasklet(new TestTasklet()) + .build() + } + + @Bean + Flow splitFlow2() { + new FlowBuilder("splitFlow2") + .start(splitFlowStep2()) + .build() + } + + @Bean + Step splitFlowStep2() { + steps.get("splitFlowStep2") + .tasklet(new TestTasklet()) + .build() + } + + // job with decisions + @Bean + Job decisionJob() { + jobs.get("decisionJob") + .start(decisionStepStart()) + .next(new TestDecider()) + .on("LEFT").to(decisionStepLeft()) + .on("RIGHT").to(decisionStepRight()) + .end() + .build() + } + + @Bean + Step decisionStepStart() { + steps.get("decisionStepStart") + .tasklet(new TestTasklet()) + .build() + } + + @Bean + Step decisionStepLeft() { + steps.get("decisionStepLeft") + .tasklet(new TestTasklet()) + .build() + } + + @Bean + Step decisionStepRight() { + steps.get("decisionStepRight") + .tasklet(new TestTasklet()) + .build() + } + + // partitioned job + @Bean + Job partitionedJob() { + jobs.get("partitionedJob") + .start(partitionManagerStep()) + .build() + } + + @Bean + Step partitionManagerStep() { + steps.get("partitionManagerStep") + .partitioner("partitionWorkerStep", partitioner()) + .step(partitionWorkerStep()) + .gridSize(2) + .taskExecutor(asyncTaskExecutor()) + .build() + } + + @Bean + Partitioner partitioner() { + new TestPartitioner() + } + + @Bean + Step partitionWorkerStep() { + steps.get("partitionWorkerStep") + .chunk(5) + .reader(partitionedItemReader()) + .processor(itemProcessor()) + .writer(itemWriter()) + .build() + } + + @Bean + ItemReader partitionedItemReader() { + new TestPartitionedItemReader() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchTest.groovy new file mode 100644 index 000000000..af9f6faeb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchTest.groovy @@ -0,0 +1,296 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static java.util.Collections.emptyMap + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import org.springframework.batch.core.JobParameter +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.support.ClassPathXmlApplicationContext + +abstract class SpringBatchTest extends AgentInstrumentationSpecification { + + abstract runJob(String jobName, Map params = emptyMap()) + + def "should trace tasklet job+step"() { + when: + runJob("taskletJob") + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "BatchJob taskletJob" + kind INTERNAL + } + span(1) { + name "BatchJob taskletJob.step" + kind INTERNAL + childOf span(0) + } + span(2) { + name "BatchJob taskletJob.step.Chunk" + kind INTERNAL + childOf span(1) + } + } + } + } + + def "should handle exception in tasklet job+step"() { + when: + runJob("taskletJob", ["fail": new JobParameter(1)]) + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "BatchJob taskletJob" + kind INTERNAL + } + span(1) { + name "BatchJob taskletJob.step" + kind INTERNAL + childOf span(0) + } + span(2) { + name "BatchJob taskletJob.step.Chunk" + kind INTERNAL + childOf span(1) + status ERROR + errorEvent IllegalStateException, "fail" + } + } + } + } + + def "should trace chunked items job"() { + when: + runJob("itemsAndTaskletJob") + + then: + assertTraces(1) { + trace(0, 7) { + span(0) { + name "BatchJob itemsAndTaskletJob" + kind INTERNAL + } + span(1) { + name "BatchJob itemsAndTaskletJob.itemStep" + kind INTERNAL + childOf span(0) + } + span(2) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + span(3) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + span(4) { + name "BatchJob itemsAndTaskletJob.itemStep.Chunk" + kind INTERNAL + childOf span(1) + } + span(5) { + name "BatchJob itemsAndTaskletJob.taskletStep" + kind INTERNAL + childOf span(0) + } + span(6) { + name "BatchJob itemsAndTaskletJob.taskletStep.Chunk" + kind INTERNAL + childOf span(5) + } + } + } + } + + def "should trace flow job"() { + when: + runJob("flowJob") + + then: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "BatchJob flowJob" + kind INTERNAL + } + span(1) { + name "BatchJob flowJob.flowStep1" + kind INTERNAL + childOf span(0) + } + span(2) { + name "BatchJob flowJob.flowStep1.Chunk" + kind INTERNAL + childOf span(1) + } + span(3) { + name "BatchJob flowJob.flowStep2" + kind INTERNAL + childOf span(0) + } + span(4) { + name "BatchJob flowJob.flowStep2.Chunk" + kind INTERNAL + childOf span(3) + } + } + } + } + + def "should trace split flow job"() { + when: + runJob("splitJob") + + then: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "BatchJob splitJob" + kind INTERNAL + } + span(1) { + name ~/BatchJob splitJob\.splitFlowStep[12]/ + kind INTERNAL + childOf span(0) + } + span(2) { + name ~/BatchJob splitJob\.splitFlowStep[12]\.Chunk/ + kind INTERNAL + childOf span(1) + } + span(3) { + name ~/BatchJob splitJob\.splitFlowStep[12]/ + kind INTERNAL + childOf span(0) + } + span(4) { + name ~/BatchJob splitJob\.splitFlowStep[12]\.Chunk/ + kind INTERNAL + childOf span(3) + } + } + } + } + + def "should trace job with decision"() { + when: + runJob("decisionJob") + + then: + assertTraces(1) { + trace(0, 5) { + span(0) { + name "BatchJob decisionJob" + kind INTERNAL + } + span(1) { + name "BatchJob decisionJob.decisionStepStart" + kind INTERNAL + childOf span(0) + } + span(2) { + name "BatchJob decisionJob.decisionStepStart.Chunk" + kind INTERNAL + childOf span(1) + } + span(3) { + name "BatchJob decisionJob.decisionStepLeft" + kind INTERNAL + childOf span(0) + } + span(4) { + name "BatchJob decisionJob.decisionStepLeft.Chunk" + kind INTERNAL + childOf span(3) + } + } + } + } + + def "should trace partitioned job"() { + when: + runJob("partitionedJob") + + then: + assertTraces(1) { + trace(0, 8) { + span(0) { + name "BatchJob partitionedJob" + kind INTERNAL + } + span(1) { + def stepName = hasPartitionManagerStep() ? "partitionManagerStep" : "partitionWorkerStep" + name "BatchJob partitionedJob.$stepName" + kind INTERNAL + childOf span(0) + } + span(2) { + name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01]/ + kind INTERNAL + childOf span(1) + } + span(3) { + name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk/ + kind INTERNAL + childOf span(2) + } + span(4) { + name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk/ + kind INTERNAL + childOf span(2) + } + span(5) { + name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01]/ + kind INTERNAL + childOf span(1) + } + span(6) { + name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk/ + kind INTERNAL + childOf span(5) + } + span(7) { + name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk/ + kind INTERNAL + childOf span(5) + } + } + } + } + + protected boolean hasPartitionManagerStep() { + true + } +} + +class JavaConfigBatchJobTest extends SpringBatchTest implements ApplicationConfigTrait { + @Override + ConfigurableApplicationContext createApplicationContext() { + new AnnotationConfigApplicationContext(SpringBatchApplication) + } +} + +class XmlConfigBatchJobTest extends SpringBatchTest implements ApplicationConfigTrait { + @Override + ConfigurableApplicationContext createApplicationContext() { + new ClassPathXmlApplicationContext("spring-batch.xml") + } +} + +class JsrConfigBatchJobTest extends SpringBatchTest implements JavaxBatchConfigTrait { + protected boolean hasPartitionManagerStep() { + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestBatchlet.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestBatchlet.groovy new file mode 100644 index 000000000..d8d8b9b68 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestBatchlet.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jsr + +import javax.batch.api.BatchProperty +import javax.batch.api.Batchlet +import javax.inject.Inject + +class TestBatchlet implements Batchlet { + @Inject + @BatchProperty(name = "fail") + String fail + + @Override + String process() throws Exception { + if (fail != null && Integer.valueOf(fail) == 1) { + throw new IllegalStateException("fail") + } + return "FINISHED" + } + + @Override + void stop() throws Exception { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestDecider.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestDecider.groovy new file mode 100644 index 000000000..4401f1cef --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestDecider.groovy @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jsr + +import javax.batch.api.Decider +import javax.batch.runtime.StepExecution + +class TestDecider implements Decider { + @Override + String decide(StepExecution[] stepExecutions) throws Exception { + "LEFT" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemProcessor.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemProcessor.groovy new file mode 100644 index 000000000..15eaf9832 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemProcessor.groovy @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jsr + +import javax.batch.api.chunk.ItemProcessor + +class TestItemProcessor implements ItemProcessor { + @Override + Object processItem(Object item) throws Exception { + Integer.parseInt(item as String) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemReader.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemReader.groovy new file mode 100644 index 000000000..e6611ec17 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemReader.groovy @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jsr + +import java.util.stream.Collectors +import java.util.stream.IntStream +import javax.batch.api.chunk.ItemReader + +class TestItemReader implements ItemReader { + private final List items = IntStream.range(0, 13).mapToObj(String.&valueOf).collect(Collectors.toList()) + private Iterator itemsIt + + @Override + void open(Serializable serializable) throws Exception { + itemsIt = items.iterator() + } + + @Override + void close() throws Exception { + itemsIt = null + } + + @Override + Object readItem() throws Exception { + if (itemsIt == null) { + return null + } + return itemsIt.hasNext() ? itemsIt.next() : null + } + + @Override + Serializable checkpointInfo() throws Exception { + return null + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemWriter.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemWriter.groovy new file mode 100644 index 000000000..1ef066e72 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestItemWriter.groovy @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jsr + +import javax.batch.api.chunk.ItemWriter + +class TestItemWriter implements ItemWriter { + final List items = new ArrayList() + + @Override + void open(Serializable checkpoint) throws Exception { + } + + @Override + void close() throws Exception { + } + + @Override + void writeItems(List items) throws Exception { + for (item in items) { + this.items.add(item as Integer) + } + } + + @Override + Serializable checkpointInfo() throws Exception { + return null + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestPartitionedItemReader.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestPartitionedItemReader.groovy new file mode 100644 index 000000000..722c1646b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/jsr/TestPartitionedItemReader.groovy @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jsr + +import javax.batch.api.BatchProperty +import javax.batch.api.chunk.ItemReader +import javax.inject.Inject + +class TestPartitionedItemReader implements ItemReader { + @Inject + @BatchProperty(name = "start") + String startStr + @Inject + @BatchProperty(name = "end") + String endStr + + int start + int end + + @Override + void open(Serializable checkpoint) throws Exception { + start = Integer.parseInt(startStr) + end = Integer.parseInt(endStr) + } + + @Override + void close() throws Exception { + } + + @Override + Object readItem() throws Exception { + if (start >= end) { + return null + } + return String.valueOf(start++) + } + + @Override + Serializable checkpointInfo() throws Exception { + return null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestDecider.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestDecider.groovy new file mode 100644 index 000000000..b7ab85150 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestDecider.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springbatch + +import org.springframework.batch.core.JobExecution +import org.springframework.batch.core.StepExecution +import org.springframework.batch.core.job.flow.FlowExecutionStatus +import org.springframework.batch.core.job.flow.JobExecutionDecider + +class TestDecider implements JobExecutionDecider { + @Override + FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) { + new FlowExecutionStatus("LEFT") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemProcessor.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemProcessor.groovy new file mode 100644 index 000000000..c8fecc20e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemProcessor.groovy @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springbatch + +import org.springframework.batch.item.ItemProcessor + +class TestItemProcessor implements ItemProcessor { + @Override + Integer process(String item) throws Exception { + Integer.parseInt(item) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemReader.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemReader.groovy new file mode 100644 index 000000000..d8cf41a9f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemReader.groovy @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springbatch + +import java.util.stream.Collectors +import java.util.stream.IntStream +import org.springframework.batch.item.support.ListItemReader + +class TestItemReader extends ListItemReader { + TestItemReader() { + super(IntStream.range(0, 13).mapToObj(String.&valueOf).collect(Collectors.toList()) as List) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemWriter.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemWriter.groovy new file mode 100644 index 000000000..eb4fcf334 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestItemWriter.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springbatch + +import org.springframework.batch.item.ItemWriter + +class TestItemWriter implements ItemWriter { + final List items = new ArrayList() + + @Override + void write(List items) throws Exception { + this.items.addAll(items) + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestPartitionedItemReader.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestPartitionedItemReader.groovy new file mode 100644 index 000000000..bce1797cb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestPartitionedItemReader.groovy @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springbatch + +import org.springframework.batch.item.ExecutionContext +import org.springframework.batch.item.ItemReader +import org.springframework.batch.item.ItemStream +import org.springframework.batch.item.ItemStreamException +import org.springframework.batch.item.NonTransientResourceException +import org.springframework.batch.item.ParseException +import org.springframework.batch.item.UnexpectedInputException + +class TestPartitionedItemReader implements ItemReader, ItemStream { + ThreadLocal start = new ThreadLocal<>() + ThreadLocal end = new ThreadLocal<>() + + @Override + String read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException { + if (start.get() >= end.get()) { + return null + } + def value = start.get() + start.set(value + 1) + return String.valueOf(value) + } + + @Override + void open(ExecutionContext executionContext) throws ItemStreamException { + start.set(executionContext.getInt("start")) + end.set(executionContext.getInt("end")) + } + + @Override + void update(ExecutionContext executionContext) throws ItemStreamException { + } + + @Override + void close() throws ItemStreamException { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestPartitioner.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestPartitioner.groovy new file mode 100644 index 000000000..aaea9a104 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestPartitioner.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springbatch + +import org.springframework.batch.core.partition.support.Partitioner +import org.springframework.batch.item.ExecutionContext + +class TestPartitioner implements Partitioner { + @Override + Map partition(int gridSize) { + return [ + "partition0": new ExecutionContext([ + "start": 0, "end": 8 + ]), + "partition1": new ExecutionContext([ + "start": 8, "end": 13 + ]) + ] + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestSyncItemReader.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestSyncItemReader.groovy new file mode 100644 index 000000000..66e997d1b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestSyncItemReader.groovy @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springbatch + +import java.util.stream.Collectors +import java.util.stream.IntStream +import org.springframework.batch.item.ItemReader + +class TestSyncItemReader implements ItemReader { + private final Iterator items + + TestSyncItemReader(int max) { + items = IntStream.range(0, max).mapToObj(String.&valueOf).collect(Collectors.toList()).iterator() + } + + synchronized String read() { + if (items.hasNext()) { + return items.next() + } + return null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestTasklet.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestTasklet.groovy new file mode 100644 index 000000000..7394dc068 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/springbatch/TestTasklet.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springbatch + +import org.springframework.batch.core.StepContribution +import org.springframework.batch.core.scope.context.ChunkContext +import org.springframework.batch.core.step.tasklet.Tasklet +import org.springframework.batch.repeat.RepeatStatus + +class TestTasklet implements Tasklet { + @Override + RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + if (chunkContext.stepContext.stepExecution.jobParameters.getLong("fail") == 1) { + throw new IllegalStateException("fail") + } + RepeatStatus.FINISHED + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/decisionJob.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/decisionJob.xml new file mode 100644 index 000000000..3f2a98def --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/decisionJob.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/flowJob.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/flowJob.xml new file mode 100644 index 000000000..a43b024d9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/flowJob.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/itemsAndTaskletJob.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/itemsAndTaskletJob.xml new file mode 100644 index 000000000..7d2a890e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/itemsAndTaskletJob.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/partitionedJob.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/partitionedJob.xml new file mode 100644 index 000000000..330f99c2c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/partitionedJob.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/splitJob.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/splitJob.xml new file mode 100644 index 000000000..a9f8356f4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/splitJob.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/taskletJob.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/taskletJob.xml new file mode 100644 index 000000000..fe2f62430 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/taskletJob.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/baseContext.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/baseContext.xml new file mode 100644 index 000000000..c6396ad05 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/baseContext.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/jsrBaseContext.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/jsrBaseContext.xml new file mode 100644 index 000000000..cd48ce0aa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/jsrBaseContext.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/spring-batch.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/spring-batch.xml new file mode 100644 index 000000000..e8f24b0d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/spring-batch.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/README.md new file mode 100644 index 000000000..dbe44ae1b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/README.md @@ -0,0 +1,406 @@ +# OpenTelemetry Spring Auto-Configuration + +Auto-configures OpenTelemetry instrumentation for [spring-web](../spring-web-3.1), [spring-webmvc](../spring-webmvc-3.1), and [spring-webflux](../spring-webflux-5.0). Leverages Spring Aspect Oriented Programming, dependency injection, and bean post-processing to trace spring applications. To include all features listed below use the [opentelemetry-spring-starter](../starters/spring-starter/README.md). + +## Quickstart + +### Add these dependencies to your project. + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://search.maven.org/search?q=g:io.opentelemetry). + - Minimum version: `0.17.0` + - Note: You may need to include our bintray maven repository to your build file: `https://dl.bintray.com/open-telemetry/maven/`. As of August 2020 the latest opentelemetry-java-instrumentation artifacts are not published to maven-central. Please check the [releasing](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/master/RELEASING.md) doc for updates to this process. + + +For Maven add to your `pom.xml`: + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-spring-boot-autoconfigure + OPENTELEMETRY_VERSION + + + + io.opentelemetry + opentelemetry-api + OPENTELEMETRY_VERSION + + + + + + + io.opentelemetry + opentelemetry-exporters-logging + OPENTELEMETRY_VERSION + + + +``` + +For Gradle add to your dependencies: + +```groovy +//opentelemetry spring auto-configuration +implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-autoconfigure:OPENTELEMETRY_VERSION' +//opentelemetry +implementation 'io.opentelemetry:opentelemetry-api:OPENTELEMETRY_VERSION' +//opentelemetry exporter +implementation 'io.opentelemetry:opentelemetry-exporters-otlp:OPENTELEMETRY_VERSION' +``` + +### Features + +#### Dependencies + +The following dependencies are optional but are required to use the corresponding features. + +Replace `SPRING_VERSION` with the version of spring you're using. + - Minimum version: `3.1` + +Replace `SPRING_WEBFLUX_VERSION` with the version of spring-webflux you're using. + - Minimum version: `5.0` + +Replace `SLF4J_VERSION` with the version of slf4j you're using. + +For Maven add to your `pom.xml`: + +```xml + + + + io.opentelemetry + opentelemetry-exporter-jaeger + OPENTELEMETRY_VERSION + + + io.opentelemetry + opentelemetry-exporter-zipkin + OPENTELEMETRY_VERSION + + + io.opentelemetry + opentelemetry-exporter-otlp + OPENTELEMETRY_VERSION + + + + + org.springframework + spring-web + SPRING_VERSION + + + + + org.springframework + spring-webmvc + SPRING_VERSION + + + + + org.springframework + spring-webflux + SPRING_WEBFLUX_VERSION + + + + + org.springframework + spring-aop + SPRING_VERSION + + + io.opentelemetry + opentelemetry-extension-annotations + OPENTELEMETRY_VERSION + + +``` + +For Gradle add to your dependencies: + +```groovy +//opentelemetry exporter +implementation 'io.opentelemetry:opentelemetry-exporters-jaeger:OPENTELEMETRY_VERSION' +implementation 'io.opentelemetry:opentelemetry-exporters-zipkin:OPENTELEMETRY_VERSION' +implementation 'io.opentelemetry:opentelemetry-exporters-otlp:OPENTELEMETRY_VERSION' + +//Used to autoconfigure spring-web +implementation "org.springframework:spring-web:SPRING_VERSION" + +//Used to autoconfigure spring-webmvc +implementation "org.springframework:spring-webmvc:SPRING_VERSION" + +//Used to autoconfigure spring-webflux +implementation "org.springframework:spring-webflux:SPRING_WEBFLUX_VERSION" + +//Enables instrumentation using @WithSpan +implementation "org.springframework:spring-aop:SPRING_VERSION" +implementation "io.opentelemetry:opentelemetry-extension-annotations:OPENTELEMETRY_VERSION" +``` + +#### OpenTelemetry Auto Configuration + + +#### OpenTelemetry Tracer Auto Configuration + +Provides a OpenTelemetry tracer bean (`io.opentelemetry.api.trace.Tracer`) if one does not exist in the application context of the spring project. This tracer bean will be used in all configurations listed below. Feel free to declare your own Opentelemetry tracer bean to overwrite this configuration. + +#### Spring Web Auto Configuration + +Provides auto-configuration for the OpenTelemetry RestTemplate trace interceptor defined in [opentelemetry-spring-web-3.1](../spring-web-3.1). This auto-configuration instruments all requests sent using Spring RestTemplate beans by applying a RestTemplate bean post processor. This feature is supported for spring web versions 3.1+ and can be disabled by adding `opentelemetry.trace.httpclients.enabled=false` to your `resources/applications.properties` file. [Spring Web - RestTemplate Client Span](#spring-web---resttemplate-client-span) show cases a sample client span generated by this auto-configuration. Check out [opentelemetry-spring-web-3.1](../spring-web-3.1) to learn more about the OpenTelemetry RestTemplateInterceptor. + +#### Spring Web MVC Auto Configuration + +This feature auto-configures instrumentation for spring-webmvc controllers by adding a [WebMvcTracingFilter](../spring-webmvc-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/WebMvcTracingFilter.java) bean to the application context. This request filter implements the [OncePerRequestFilter](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html) interface to capture OpenTelemetry server spans and propagate distribute tracing context if provided in the request. [Spring Web MVC - Server Span](#spring-web-mvc---server-span) show cases a sample span generated by the WebMvcTracingFilter. Check out [opentelemetry-spring-webmvc-3.1](../spring-webmvc-3.1/) to learn more about the OpenTelemetry WebMvcTracingFilter. + +#### Spring WebFlux Auto Configuration + +Provides auto-configurations for the OpenTelemetry WebClient ExchangeFilter defined in [opentelemetry-spring-webflux-5.0](../spring-webflux-5.0). This auto-configuration instruments all outgoing http requests sent using Spring's WebClient and WebClient Builder beans by applying a bean post processor. This feature is supported for spring webflux versions 5.0+ and can be disabled by adding `opentelemetry.trace.httpclients.enabled=false` to your `resources/applications.properties` file. [Spring Web-Flux - WebClient Span](#spring-web-flux---webclient-span) showcases a sample span generated by the WebClientFilter. Check out [opentelemetry-spring-webflux-5.0](../spring-webflux-5.0) to learn more about the OpenTelemetry WebClientFilter. + +#### Manual Instrumentation Support - @WithSpan + +This feature uses spring-aop to wrap methods annotated with `@WithSpan` in a span. + +Note - This annotation can only be applied to bean methods managed by the spring application context. Check out [spring-aop](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop) to learn more about aspect weaving in spring. + +##### Usage + +```java +import org.springframework.stereotype.Component; + +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; + +/** + * Test WithSpan + */ +@Component +public class TracedClass { + + @WithSpan + public void tracedMethod() { + } + + @WithSpan(value="span name") + public void tracedMethodWithName() { + Span currentSpan = Span.current(); + currentSpan.addEvent("ADD EVENT TO tracedMethodWithName SPAN"); + currentSpan.setAttribute("isTestAttribute", true); + } + + @WithSpan(kind = SpanKind.CLIENT) + public void tracedClientSpan() { + } +} + +``` + +#### Sample Traces + +The traces below were exported using Zipkin. + +##### Spring Web MVC - Server Span +```json + { + "traceId":"0371febbbfa76b2e285a08b53a055d17", + "id":"9b782243ad7df179", + "kind":"SERVER", + "name":"webmvctracingfilter.dofilterinteral", + "timestamp":1596841405866633, + "duration":355648, + "localEndpoint":{ + "serviceName":"sample_trace", + "ipv4":"XXX.XXX.X.XXX" + }, + "tags":{ + "http.client_ip":"0:0:0:0:0:0:0:1", + "http.flavor":"1.1", + "http.method":"GET", + "http.status_code":"200", + "http.url":"/spring-webmvc/sample", + "http.user_agent":"PostmanRuntime/7.26.2", + "net.peer.ip":"0:0:0:0:0:0:0:1", + "net.peer.port":"33916", + "sampling.probability":"1.0" + } + } +``` + +##### Spring Web - RestTemplate Client Span + +```json + { + "traceId":"0371febbbfa76b2e285a08b53a055d17", + "parentId":"9b782243ad7df179", + "id":"43990118a8bdbdf5", + "kind":"CLIENT", + "name":"http get", + "timestamp":1596841405949825, + "duration":21288, + "localEndpoint":{ + "serviceName":"sample_trace", + "ipv4":"XXX.XXX.X.XXX" + }, + "tags":{ + "http.method":"GET", + "http.status_code":"200", + "http.url":"/spring-web/sample/rest-template", + "net.peer.name":"localhost", + "net.peer.port":"8081" + } + } +``` + +##### Spring Web-Flux - WebClient Span + +```json + { + "traceId":"0371febbbfa76b2e285a08b53a055d17", + "parentId":"9b782243ad7df179", + "id":"1b14a2fc89d7a762", + "kind":"CLIENT", + "name":"http post", + "timestamp":1596841406109125, + "duration":25137, + "localEndpoint":{ + "serviceName":"sample_trace", + "ipv4":"XXX.XXX.X.XXX" + }, + "tags":{ + "http.method":"POST", + "http.status_code":"200", + "http.url":"/spring-webflux/sample/web-client", + "net.peer.name":"localhost", + "net.peer.port":"8082" + } + } +``` + +##### @WithSpan Instrumentation + +``` +[ + { + "traceId":"0371febbbfa76b2e285a08b53a055d17", + "parentId":"9b782243ad7df179", + "id":"c3ef24b9bff5901c", + "name":"tracedclass.withspanmethod", + "timestamp":1596841406165439, + "duration":6912, + "localEndpoint":{ + "serviceName":"sample_trace", + "ipv4":"XXX.XXX.X.XXX" + }, + "tags":{ + "test.type":"@WithSpan annotation", + "test.case":'@WithSpan', + "test.hasEvent":'true', + } + }, + { + "traceId":"0371febbbfa76b2e285a08b53a055d17", + "parentId":"9b782243ad7df179", + "id":"1a6cb395a8a33cc0", + "name":"@withspan set span name", + "timestamp":1596841406182759, + "duration":2187, + "localEndpoint":{ + "serviceName":"sample_trace", + "ipv4":"XXX.XXX.X.XXX" + }, + "annotations":[ + { + "timestamp":1596841406182920, + "value":"ADD EVENT TO tracedMethodWithName SPAN" + } + ], + "tags":{ + "test.type":"@WithSpan annotation", + "test.case":'@WithSpan(value="@withspan set span name")', + "test.hasEvent":'true', + } + }, + { + "traceId":"0371febbbfa76b2e285a08b53a055d17", + "parentId":"9b782243ad7df179", + "id":"74dd19a8a9883f80", + "kind":"CLIENT", + "name":"tracedClientSpan", + "timestamp":1596841406194210, + "duration":130, + "localEndpoint":{ + "serviceName":"sample_trace", + "ipv4":"XXX.XXX.X.XXX" + } + "tags":{ + "test.type":"@WithSpan annotation", + "test.case":"@WithSpan(kind=SpanKind.Client)", + } + }, +] +``` + +#### Spring Support + +Auto-configuration is natively supported by Springboot applications. To enable these features in "vanilla" use `@EnableOpenTelemetryTracing` to complete a component scan of this package. + +##### Usage + +```java +import io.opentelemetry.instrumentation.spring.autoconfigure.EnableOpenTelemetry; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableOpenTelemetry +public class OpenTelemetryConfig {} +``` + +#### Exporter Configurations + +This package provides auto configurations for [OTLP](https://github.com/open-telemetry/opentelemetry-java/tree/master/exporters/otlp), [Jaeger](https://github.com/open-telemetry/opentelemetry-java/tree/master/exporters/jaeger), [Zipkin](https://github.com/open-telemetry/opentelemetry-java/tree/master/exporters/zipkin), and [Logging](https://github.com/open-telemetry/opentelemetry-java/tree/master/exporters/logging) Span Exporters. + +If an exporter is present in the classpath during runtime and a spring bean of the exporter is missing from the spring application context. An exporter bean is initialized and added to a simple span processor in the active tracer provider. Check out the implementation [here](/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java). + + +#### Configuration Properties + +##### Enabling/Disabling Features + +| Feature | Property | Default Value | ConditionalOnClass | +|------------------|------------------------------------------|---------------|------------------------| +| spring-web | otel.springboot.httpclients.enabled | true | RestTemplate | +| spring-webmvc | otel.springboot.httpclients.enabled | true | OncePerRequestFilter | +| spring-webflux | otel.springboot.httpclients.enabled | true | WebClient | +| @WithSpan | otel.springboot.aspects.enabled | true | WithSpan, Aspect | +| Otlp Exporter | otel.exporter.otlp.enabled | true | OtlpGrpcSpanExporter | +| Jaeger Exporter | otel.exporter.jaeger.enabled | true | JaegerGrpcSpanExporter | +| Zipkin Exporter | otel.exporter.zipkin.enabled | true | ZipkinSpanExporter | +| Logging Exporter | otel.exporter.logging.enabled | true | LoggingSpanExporter | + + + +##### Exporter Properties + +| Feature | Property | Default Value | +|-----------------|-------------------------------|------------------------------------| +| Otlp Exporter | otel.exporter.otlp.endpoint | localhost:55680 | +| | otel.exporter.otlp.timeout | 1s | +| Jaeger Exporter | otel.exporter.jaeger.endpoint | localhost:14250 | +| | otel.exporter.jaeger.timeout | 1s | +| Zipkin Exporter | otel.exporter.jaeger.endpoint | http://localhost:9411/api/v2/spans | + +##### Tracer Properties + +| Feature | Property | Default Value | +|---------|--------------------------------|---------------| +| Tracer | otel.traces.sampler.probability | 1.0 | + +### Starter Guide + +Check out the opentelemetry [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation. diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle new file mode 100644 index 000000000..30328d77a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle @@ -0,0 +1,46 @@ +group = 'io.opentelemetry.instrumentation' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +sourceCompatibility = '8' + +dependencies { + implementation "org.springframework.boot:spring-boot-autoconfigure:${versions["org.springframework.boot"]}" + annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor:${versions["org.springframework.boot"]}" + implementation "javax.validation:validation-api:2.0.1.Final" + + implementation project(':instrumentation:spring:spring-web-3.1:library') + implementation project(':instrumentation:spring:spring-webmvc-3.1:library') + implementation project(':instrumentation:spring:spring-webflux-5.0:library') + + compileOnly "org.springframework.boot:spring-boot-starter-aop:${versions["org.springframework.boot"]}" + compileOnly "org.springframework.boot:spring-boot-starter-web:${versions["org.springframework.boot"]}" + compileOnly "org.springframework.boot:spring-boot-starter-webflux:${versions["org.springframework.boot"]}" + + compileOnly "run.mone:opentelemetry-extension-annotations" + compileOnly "run.mone:opentelemetry-exporter-logging" + compileOnly "run.mone:opentelemetry-exporter-jaeger" + compileOnly "run.mone:opentelemetry-exporter-otlp" + compileOnly "run.mone:opentelemetry-exporter-zipkin" + compileOnly "io.grpc:grpc-api:1.30.2" + + testImplementation "org.springframework.boot:spring-boot-starter-aop:${versions["org.springframework.boot"]}" + testImplementation "org.springframework.boot:spring-boot-starter-webflux:${versions["org.springframework.boot"]}" + testImplementation "org.springframework.boot:spring-boot-starter-web:${versions["org.springframework.boot"]}" + testImplementation("org.springframework.boot:spring-boot-starter-test:${versions["org.springframework.boot"]}") { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + + testImplementation "org.assertj:assertj-core" + testImplementation project(':testing-common') + testImplementation "run.mone:opentelemetry-sdk" + testImplementation "run.mone:opentelemetry-sdk-testing" + testImplementation "run.mone:opentelemetry-extension-annotations" + testImplementation "run.mone:opentelemetry-exporter-logging" + testImplementation "run.mone:opentelemetry-exporter-jaeger" + testImplementation "run.mone:opentelemetry-exporter-otlp" + testImplementation "run.mone:opentelemetry-exporter-zipkin" + testImplementation "io.grpc:grpc-api:1.30.2" + testImplementation "io.grpc:grpc-netty-shaded:1.30.2" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EnableOpenTelemetry.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EnableOpenTelemetry.java new file mode 100644 index 000000000..8a5b2b95c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EnableOpenTelemetry.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** Auto-configures OpenTelemetry. Enables OpenTelemetry in Spring applications */ +@Configuration +@ComponentScan(basePackages = "io.opentelemetry.instrumentation.spring.autoconfigure") +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnableOpenTelemetry {} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java new file mode 100644 index 000000000..71db85336 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.Collections; +import java.util.List; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Create {@link io.opentelemetry.api.trace.Tracer} bean if bean is missing. + * + *

Adds span exporter beans to the active tracer provider. + * + *

Updates the sampler probability for the configured {@link TracerProvider}. + */ +@Configuration +@EnableConfigurationProperties(SamplerProperties.class) +public class OpenTelemetryAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OpenTelemetry openTelemetry( + SamplerProperties samplerProperties, + ObjectProvider> spanExportersProvider) { + SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder(); + + spanExportersProvider.getIfAvailable(Collections::emptyList).stream() + // todo SimpleSpanProcessor...is that really what we want here? + .map(SimpleSpanProcessor::create) + .forEach(tracerProviderBuilder::addSpanProcessor); + + SdkTracerProvider tracerProvider = + tracerProviderBuilder + .setSampler(Sampler.traceIdRatioBased(samplerProperties.getProbability())) + .build(); + return OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/SamplerProperties.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/SamplerProperties.java new file mode 100644 index 000000000..7d8de1918 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/SamplerProperties.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure; + +import javax.validation.constraints.DecimalMax; +import javax.validation.constraints.DecimalMin; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration for OpenTelemetry Sampler. + * + *

Get Sampling Probability + */ +@ConfigurationProperties(prefix = "otel.traces.sampler") +public final class SamplerProperties { + + // if Sample probability == 1: always sample + // if Sample probability == 0: never sample + @DecimalMin("0.0") + @DecimalMax("1.0") + private double probability = 1.0; + + public double getProbability() { + return probability; + } + + public void setProbability(double probability) { + this.probability = probability; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java new file mode 100644 index 000000000..44908c6fb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.extension.annotations.WithSpan; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** Configures {@link WithSpanAspect} to trace bean methods annotated with {@link WithSpan}. */ +@Configuration +@EnableConfigurationProperties(TraceAspectProperties.class) +@ConditionalOnProperty(prefix = "otel.springboot.aspects", name = "enabled", matchIfMissing = true) +@ConditionalOnClass({Aspect.class, WithSpan.class}) +public class TraceAspectAutoConfiguration { + + @Bean + public WithSpanAspect withSpanAspect(OpenTelemetry openTelemetry) { + return new WithSpanAspect(openTelemetry); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java new file mode 100644 index 000000000..3bd0ffb36 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** Configuration for enabling tracing aspects. */ +@ConfigurationProperties(prefix = "otel.springboot.aspects") +public final class TraceAspectProperties { + private boolean enabled; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java new file mode 100644 index 000000000..a6715966a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.extension.annotations.WithSpan; +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; + +/** + * Uses Spring-AOP to wrap methods marked by {@link WithSpan} in a {@link + * io.opentelemetry.api.trace.Span}. + * + *

Ensure methods annotated with {@link WithSpan} are implemented on beans managed by the Spring + * container. + * + *

Note: This Aspect uses spring-aop to proxy beans. Therefore the {@link WithSpan} annotation + * can not be applied to constructors. + */ +@Aspect +public class WithSpanAspect { + private final WithSpanAspectTracer tracer; + + public WithSpanAspect(OpenTelemetry openTelemetry) { + tracer = new WithSpanAspectTracer(openTelemetry); + } + + @Around("@annotation(io.opentelemetry.extension.annotations.WithSpan)") + public Object traceMethod(ProceedingJoinPoint pjp) throws Throwable { + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + WithSpan withSpan = method.getAnnotation(WithSpan.class); + + Context parentContext = Context.current(); + if (!tracer.shouldStartSpan(parentContext, withSpan.kind())) { + return pjp.proceed(); + } + + Context context = tracer.startSpan(parentContext, withSpan, method); + try (Scope ignored = context.makeCurrent()) { + Object result = pjp.proceed(); + return tracer.end(context, method.getReturnType(), result); + } catch (Throwable t) { + tracer.endExceptionally(context, t); + throw t; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTracer.java new file mode 100644 index 000000000..96e74fe63 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTracer.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies; +import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy; +import java.lang.reflect.Method; + +class WithSpanAspectTracer extends BaseTracer { + + private final AsyncSpanEndStrategies asyncSpanEndStrategies = + AsyncSpanEndStrategies.getInstance(); + + WithSpanAspectTracer(OpenTelemetry openTelemetry) { + super(openTelemetry); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.spring-boot-autoconfigure-aspect"; + } + + Context startSpan(Context parentContext, WithSpan annotation, Method method) { + Span span = + spanBuilder(parentContext, spanName(annotation, method), annotation.kind()).startSpan(); + switch (annotation.kind()) { + case SERVER: + return withServerSpan(parentContext, span); + case CLIENT: + return withClientSpan(parentContext, span); + default: + return parentContext.with(span); + } + } + + private static String spanName(WithSpan annotation, Method method) { + String spanName = annotation.value(); + if (spanName.isEmpty()) { + return SpanNames.fromMethod(method); + } + return spanName; + } + + /** + * Denotes the end of the invocation of the traced method with a successful result which will end + * the span stored in the passed {@code context}. If the method returned a value representing an + * asynchronous operation then the span will not be finished until the asynchronous operation has + * completed. + * + * @param returnType Return type of the traced method. + * @param returnValue Return value from the traced method. + * @return Either {@code returnValue} or a value composing over {@code returnValue} for + * notification of completion. + */ + public Object end(Context context, Class returnType, Object returnValue) { + if (returnType.isInstance(returnValue)) { + AsyncSpanEndStrategy asyncSpanEndStrategy = + asyncSpanEndStrategies.resolveStrategy(returnType); + if (asyncSpanEndStrategy != null) { + return asyncSpanEndStrategy.end(this, context, returnValue); + } + } + end(context); + return returnValue; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfiguration.java new file mode 100644 index 000000000..d88c88b91 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger; + +import io.grpc.ManagedChannel; +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter; +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporterBuilder; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configures {@link JaegerGrpcSpanExporter} for tracing. + * + *

Initializes {@link JaegerGrpcSpanExporter} bean if bean is missing. + */ +@Configuration +@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) +@EnableConfigurationProperties(JaegerSpanExporterProperties.class) +@ConditionalOnProperty(prefix = "otel.exporter.jaeger", name = "enabled", matchIfMissing = true) +@ConditionalOnClass({JaegerGrpcSpanExporter.class, ManagedChannel.class}) +public class JaegerSpanExporterAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public JaegerGrpcSpanExporter otelJaegerSpanExporter(JaegerSpanExporterProperties properties) { + + JaegerGrpcSpanExporterBuilder builder = JaegerGrpcSpanExporter.builder(); + if (properties.getEndpoint() != null) { + builder.setEndpoint(properties.getEndpoint()); + } + if (properties.getTimeout() != null) { + builder.setTimeout(properties.getTimeout()); + } + return builder.build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterProperties.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterProperties.java new file mode 100644 index 000000000..d717bc841 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterProperties.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger; + +import java.time.Duration; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration for {@link io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter}. + * + *

Get Exporter Service Name + * + *

Get Exporter Endpoint + * + *

Get max wait time for Collector to process Span Batches + */ +@ConfigurationProperties(prefix = "otel.exporter.jaeger") +public final class JaegerSpanExporterProperties { + + private boolean enabled = true; + @Nullable private String endpoint; + @Nullable private Duration timeout; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Nullable + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + @Nullable + public Duration getTimeout() { + return timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfiguration.java new file mode 100644 index 000000000..5141863ad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging; + +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** Configures {@link LoggingSpanExporter} bean for tracing. */ +@Configuration +@EnableConfigurationProperties(LoggingSpanExporterProperties.class) +@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) +@ConditionalOnProperty(prefix = "otel.exporter.logging", name = "enabled", matchIfMissing = true) +@ConditionalOnClass(LoggingSpanExporter.class) +public class LoggingSpanExporterAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public LoggingSpanExporter otelLoggingSpanExporter() { + return new LoggingSpanExporter(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterProperties.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterProperties.java new file mode 100644 index 000000000..7915822fb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterProperties.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** Configuration for {@link io.opentelemetry.exporter.logging.LoggingSpanExporter}. */ +@ConfigurationProperties(prefix = "otel.exporter.logging") +public final class LoggingSpanExporterProperties { + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpGrpcSpanExporterAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpGrpcSpanExporterAutoConfiguration.java new file mode 100644 index 000000000..2a2e1b294 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpGrpcSpanExporterAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; + +import io.grpc.ManagedChannel; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configures {@link OtlpGrpcSpanExporter} for tracing. + * + *

Initializes {@link OtlpGrpcSpanExporter} bean if bean is missing. + */ +@Configuration +@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) +@EnableConfigurationProperties(OtlpGrpcSpanExporterProperties.class) +@ConditionalOnProperty(prefix = "otel.exporter.otlp", name = "enabled", matchIfMissing = true) +@ConditionalOnClass({OtlpGrpcSpanExporter.class, ManagedChannel.class}) +public class OtlpGrpcSpanExporterAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OtlpGrpcSpanExporter otelOtlpGrpcSpanExporter(OtlpGrpcSpanExporterProperties properties) { + + OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder(); + if (properties.getEndpoint() != null) { + builder.setEndpoint(properties.getEndpoint()); + } + if (properties.getTimeout() != null) { + builder.setTimeout(properties.getTimeout()); + } + return builder.build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpGrpcSpanExporterProperties.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpGrpcSpanExporterProperties.java new file mode 100644 index 000000000..a301dcfc9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpGrpcSpanExporterProperties.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; + +import java.time.Duration; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration for {@link io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter}. + * + *

Get Exporter Service Name + * + *

Get Exporter Endpoint + * + *

Get max wait time for Collector to process Span Batches + */ +@ConfigurationProperties(prefix = "otel.exporter.otlp") +public final class OtlpGrpcSpanExporterProperties { + + private boolean enabled = true; + @Nullable private String endpoint; + @Nullable private Duration timeout; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Nullable + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + @Nullable + public Duration getTimeout() { + return timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfiguration.java new file mode 100644 index 000000000..82ca5298a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin; + +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configures {@link ZipkinSpanExporter} for tracing. + * + *

Initializes {@link ZipkinSpanExporter} bean if bean is missing. + */ +@Configuration +@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) +@EnableConfigurationProperties(ZipkinSpanExporterProperties.class) +@ConditionalOnProperty(prefix = "otel.exporter.zipkin", name = "enabled", matchIfMissing = true) +@ConditionalOnClass(ZipkinSpanExporter.class) +public class ZipkinSpanExporterAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ZipkinSpanExporter otelZipkinSpanExporter(ZipkinSpanExporterProperties properties) { + + ZipkinSpanExporterBuilder builder = ZipkinSpanExporter.builder(); + if (properties.getEndpoint() != null) { + builder.setEndpoint(properties.getEndpoint()); + } + return builder.build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterProperties.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterProperties.java new file mode 100644 index 000000000..a28d23720 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterProperties.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration for {@link io.opentelemetry.exporter.zipkin.ZipkinSpanExporter}. + * + *

Get Exporter Endpoint + */ +@ConfigurationProperties(prefix = "otel.exporter.zipkin") +public class ZipkinSpanExporterProperties { + + private boolean enabled = true; + @Nullable private String endpoint; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Nullable + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/HttpClientsProperties.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/HttpClientsProperties.java new file mode 100644 index 000000000..05d02f054 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/HttpClientsProperties.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration for the tracing instrumentation of HTTP clients. + * + *

Sets default value of otel.springboot.httpclients.enabled to true if the configuration does + * not exist in application context. + */ +@ConfigurationProperties(prefix = "otel.springboot.httpclients") +public final class HttpClientsProperties { + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfiguration.java new file mode 100644 index 000000000..d6d1e79a1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.HttpClientsProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * Configures {@link RestTemplate} for tracing. + * + *

Adds Open Telemetry instrumentation to RestTemplate beans after initialization + */ +@Configuration +@ConditionalOnClass(RestTemplate.class) +@EnableConfigurationProperties(HttpClientsProperties.class) +@ConditionalOnProperty( + prefix = "otel.springboot.httpclients", + name = "enabled", + matchIfMissing = true) +public class RestTemplateAutoConfiguration { + + @Bean + public RestTemplateBeanPostProcessor otelRestTemplateBeanPostProcessor( + ObjectProvider openTelemetryProvider) { + return new RestTemplateBeanPostProcessor(openTelemetryProvider); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessor.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessor.java new file mode 100644 index 000000000..f3796e2f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessor.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.httpclients.RestTemplateInterceptor; +import java.util.List; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +final class RestTemplateBeanPostProcessor implements BeanPostProcessor { + private final ObjectProvider openTelemetryProvider; + + RestTemplateBeanPostProcessor(ObjectProvider openTelemetryProvider) { + this.openTelemetryProvider = openTelemetryProvider; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (!(bean instanceof RestTemplate)) { + return bean; + } + + RestTemplate restTemplate = (RestTemplate) bean; + OpenTelemetry openTelemetry = openTelemetryProvider.getIfUnique(); + if (openTelemetry != null) { + addRestTemplateInterceptorIfNotPresent(restTemplate, openTelemetry); + } + return restTemplate; + } + + private static void addRestTemplateInterceptorIfNotPresent( + RestTemplate restTemplate, OpenTelemetry openTelemetry) { + List restTemplateInterceptors = restTemplate.getInterceptors(); + if (restTemplateInterceptors.stream() + .noneMatch(interceptor -> interceptor instanceof RestTemplateInterceptor)) { + restTemplateInterceptors.add(0, new RestTemplateInterceptor(openTelemetry)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfiguration.java new file mode 100644 index 000000000..3497aae7a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.HttpClientsProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Configures {@link WebClient} for tracing. + * + *

Adds Open Telemetry instrumentation to WebClient beans after initialization + */ +@Configuration +@ConditionalOnClass(WebClient.class) +@EnableConfigurationProperties(HttpClientsProperties.class) +@ConditionalOnProperty( + prefix = "otel.springboot.httpclients", + name = "enabled", + matchIfMissing = true) +public class WebClientAutoConfiguration { + + @Bean + public WebClientBeanPostProcessor otelWebClientBeanPostProcessor( + ObjectProvider openTelemetryProvider) { + return new WebClientBeanPostProcessor(openTelemetryProvider); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessor.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessor.java new file mode 100644 index 000000000..b87cd52a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessor.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.webflux.client.WebClientTracingFilter; +import java.util.List; +import java.util.function.Consumer; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Inspired by Spring + * Cloud Sleuth. + */ +final class WebClientBeanPostProcessor implements BeanPostProcessor { + + private final ObjectProvider openTelemetryProvider; + + WebClientBeanPostProcessor(ObjectProvider openTelemetryProvider) { + this.openTelemetryProvider = openTelemetryProvider; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof WebClient) { + WebClient webClient = (WebClient) bean; + return wrapBuilder(openTelemetryProvider, webClient.mutate()).build(); + } else if (bean instanceof WebClient.Builder) { + WebClient.Builder webClientBuilder = (WebClient.Builder) bean; + return wrapBuilder(openTelemetryProvider, webClientBuilder); + } + return bean; + } + + private static WebClient.Builder wrapBuilder( + ObjectProvider openTelemetryProvider, WebClient.Builder webClientBuilder) { + + OpenTelemetry openTelemetry = openTelemetryProvider.getIfUnique(); + if (openTelemetry != null) { + return webClientBuilder.filters(webClientFilterFunctionConsumer(openTelemetry)); + } else { + return webClientBuilder; + } + } + + private static Consumer> webClientFilterFunctionConsumer( + OpenTelemetry openTelemetry) { + return functions -> { + if (functions.stream().noneMatch(filter -> filter instanceof WebClientTracingFilter)) { + WebClientTracingFilter.addFilter(openTelemetry, functions); + } + }; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfiguration.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfiguration.java new file mode 100644 index 000000000..eaacb95c2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.webmvc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.webmvc.WebMvcTracingFilter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.OncePerRequestFilter; + +/** Configures {@link WebMvcTracingFilter} for tracing. */ +@Configuration +@EnableConfigurationProperties(WebMvcProperties.class) +@ConditionalOnProperty(prefix = "otel.springboot.web", name = "enabled", matchIfMissing = true) +@ConditionalOnClass(OncePerRequestFilter.class) +public class WebMvcFilterAutoConfiguration { + + @Bean + public WebMvcTracingFilter otelWebMvcTracingFilter(OpenTelemetry openTelemetry) { + return new WebMvcTracingFilter(openTelemetry); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcProperties.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcProperties.java new file mode 100644 index 000000000..8b7df171c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcProperties.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.webmvc; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration for the tracing instrumentation of Spring WebMVC + * + *

Sets default value of otel.springboot.web.enabled to true if the configuration does not exist + * in application context + */ +@ConfigurationProperties(prefix = "otel.springboot.web") +public final class WebMvcProperties { + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..fbd317ade --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -0,0 +1,10 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger.JaegerSpanExporterAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpGrpcSpanExporterAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin.ZipkinSpanExporterAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging.LoggingSpanExporterAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate.RestTemplateAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient.WebClientAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.webmvc.WebMvcFilterAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.aspects.TraceAspectAutoConfiguration diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfigurationTest.java new file mode 100644 index 000000000..2dd8fccfc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfigurationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; + +/** Spring Boot auto configuration test for {@link OpenTelemetryAutoConfiguration}. */ +class OpenTelemetryAutoConfigurationTest { + @TestConfiguration + static class CustomTracerConfiguration { + @Bean + public OpenTelemetry customOpenTelemetry() { + return OpenTelemetry.noop(); + } + } + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName( + "when Application Context contains OpenTelemetry bean should NOT initialize openTelemetry") + void customTracer() { + this.contextRunner + .withUserConfiguration(CustomTracerConfiguration.class) + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + .run( + context -> { + assertThat(context.containsBean("customOpenTelemetry")).isTrue(); + assertThat(context.containsBean("openTelemetry")).isFalse(); + }); + } + + @Test + @DisplayName( + "when Application Context DOES NOT contain OpenTelemetry bean should initialize openTelemetry") + void initializeTracer() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + .run(context -> assertThat(context.containsBean("openTelemetry")).isTrue()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java new file mode 100644 index 000000000..1ecbf4456 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** Spring Boot auto configuration test for {@link TraceAspectAutoConfiguration}. */ +public class TraceAspectAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, TraceAspectAutoConfiguration.class)); + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName("when aspects are ENABLED should initialize WithSpanAspect bean") + void aspectsEnabled() { + this.contextRunner + .withPropertyValues("otel.springboot.aspects.enabled=true") + .run( + context -> + assertThat(context.getBean("withSpanAspect", WithSpanAspect.class)).isNotNull()); + } + + @Test + @DisplayName("when aspects are DISABLED should NOT initialize WithSpanAspect bean") + void disabledProperty() { + this.contextRunner + .withPropertyValues("otel.springboot.aspects.enabled=false") + .run(context -> assertThat(context.containsBean("withSpanAspect")).isFalse()); + } + + @Test + @DisplayName("when aspects enabled property is MISSING should initialize WithSpanAspect bean") + void noProperty() { + this.contextRunner.run( + context -> assertThat(context.getBean("withSpanAspect", WithSpanAspect.class)).isNotNull()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java new file mode 100644 index 000000000..54f780e16 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java @@ -0,0 +1,425 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.api.trace.SpanKind.SERVER; +import static io.opentelemetry.instrumentation.testing.util.TraceUtils.withClientSpan; +import static io.opentelemetry.instrumentation.testing.util.TraceUtils.withServerSpan; +import static io.opentelemetry.instrumentation.testing.util.TraceUtils.withSpan; +import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; + +/** Spring AOP Test for {@link WithSpanAspect}. */ +public class WithSpanAspectTest { + @RegisterExtension + static final LibraryInstrumentationExtension instrumentation = + LibraryInstrumentationExtension.create(); + + static class WithSpanTester { + @WithSpan + public String testWithSpan() { + return "Span with name testWithSpan was created"; + } + + @WithSpan("greatestSpanEver") + public String testWithSpanWithValue() { + return "Span with name greatestSpanEver was created"; + } + + @WithSpan + public String testWithSpanWithException() throws Exception { + throw new Exception("Test @WithSpan With Exception"); + } + + @WithSpan(kind = CLIENT) + public String testWithClientSpan() { + return "Span with name testWithClientSpan and SpanKind.CLIENT was created"; + } + + @WithSpan(kind = SpanKind.SERVER) + public String testWithServerSpan() { + return "Span with name testWithServerSpan and SpanKind.SERVER was created"; + } + + @WithSpan + public CompletionStage testAsyncCompletionStage(CompletionStage stage) { + return stage; + } + + @WithSpan + public CompletableFuture testAsyncCompletableFuture(CompletableFuture stage) { + return stage; + } + } + + private WithSpanTester withSpanTester; + + @BeforeEach + void setup() { + AspectJProxyFactory factory = new AspectJProxyFactory(new WithSpanTester()); + WithSpanAspect aspect = new WithSpanAspect(instrumentation.getOpenTelemetry()); + factory.addAspect(aspect); + + withSpanTester = factory.getProxy(); + } + + @AfterAll + static void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName("when method is annotated with @WithSpan should wrap method execution in a Span") + void withSpanWithDefaults() throws Throwable { + // when + withSpan("parent", () -> withSpanTester.testWithSpan()); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testWithSpan") + .hasKind(INTERNAL) + // otel SDK assertions need some work before we can comfortably use + // them in this project... + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName( + "when @WithSpan value is set should wrap method execution in a Span with custom name") + void withSpanName() throws Throwable { + // when + withSpan("parent", () -> withSpanTester.testWithSpanWithValue()); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("greatestSpanEver") + .hasKind(INTERNAL) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName( + "when method is annotated with @WithSpan AND an exception is thrown span should record the exception") + void withSpanError() throws Throwable { + assertThatThrownBy(() -> withSpanTester.testWithSpanWithException()) + .isInstanceOf(Exception.class); + + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("WithSpanTester.testWithSpanWithException") + .hasKind(INTERNAL) + .hasStatus(StatusData.error()))); + } + + @Test + @DisplayName( + "when method is annotated with @WithSpan(kind=CLIENT) should build span with the declared SpanKind") + void withSpanKind() throws Throwable { + // when + withSpan("parent", () -> withSpanTester.testWithClientSpan()); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testWithClientSpan") + .hasKind(CLIENT) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName( + "when method is annotated with @WithSpan(kind=CLIENT) and context already contains a CLIENT span should suppress span") + void suppressClientSpan() throws Throwable { + // when + withClientSpan("parent", () -> withSpanTester.testWithClientSpan()); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(CLIENT))); + } + + @Test + @DisplayName( + "when method is annotated with @WithSpan(kind=SERVER) and context already contains a SERVER span should suppress span") + void suppressServerSpan() throws Throwable { + // when + withServerSpan("parent", () -> withSpanTester.testWithServerSpan()); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(SERVER))); + } + + @Nested + @DisplayName("with a method annotated with @WithSpan returns CompletionStage") + class WithCompletionStage { + + @Test + @DisplayName("should end Span on complete") + void onComplete() throws Throwable { + CompletableFuture future = new CompletableFuture<>(); + + // when + withSpan("parent", () -> withSpanTester.testAsyncCompletionStage(future)); + + // then + assertThat(instrumentation.waitForTraces(1)) + .hasTracesSatisfyingExactly( + trace -> + trace + .hasSize(1) + .hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL))); + + // when + future.complete("DONE"); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testAsyncCompletionStage") + .hasKind(INTERNAL) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName("should end Span on completeException AND should record the exception") + void onCompleteExceptionally() throws Throwable { + CompletableFuture future = new CompletableFuture<>(); + + // when + withSpan("parent", () -> withSpanTester.testAsyncCompletionStage(future)); + + // then + assertThat(instrumentation.waitForTraces(1)) + .hasTracesSatisfyingExactly( + trace -> + trace + .hasSize(1) + .hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL))); + + // when + future.completeExceptionally(new Exception("Test @WithSpan With completeExceptionally")); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testAsyncCompletionStage") + .hasKind(INTERNAL) + .hasStatus(StatusData.error()) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName("should end Span on incompatible return value") + void onIncompatibleReturnValue() throws Throwable { + // when + withSpan("parent", () -> withSpanTester.testAsyncCompletionStage(null)); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testAsyncCompletionStage") + .hasKind(INTERNAL) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + } + + @Nested + @DisplayName("with a method annotated with @WithSpan returns CompletableFuture") + class WithCompletableFuture { + + @Test + @DisplayName("should end Span on complete") + void onComplete() throws Throwable { + CompletableFuture future = new CompletableFuture<>(); + + // when + withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(future)); + + // then + assertThat(instrumentation.waitForTraces(1)) + .hasTracesSatisfyingExactly( + trace -> + trace + .hasSize(1) + .hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL))); + + // when + future.complete("DONE"); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testAsyncCompletableFuture") + .hasKind(INTERNAL) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName("should end Span on completeException AND should record the exception") + void onCompleteExceptionally() throws Throwable { + CompletableFuture future = new CompletableFuture<>(); + + // when + withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(future)); + + // then + assertThat(instrumentation.waitForTraces(1)) + .hasTracesSatisfyingExactly( + trace -> + trace + .hasSize(1) + .hasSpansSatisfyingExactly(span -> span.hasName("parent").hasKind(INTERNAL))); + + // when + future.completeExceptionally(new Exception("Test @WithSpan With completeExceptionally")); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testAsyncCompletableFuture") + .hasKind(INTERNAL) + .hasStatus(StatusData.error()) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName("should end the Span when already complete") + void onCompletedFuture() throws Throwable { + CompletableFuture future = CompletableFuture.completedFuture("Done"); + + // when + withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(future)); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testAsyncCompletableFuture") + .hasKind(INTERNAL) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName("should end the Span when already failed") + void onFailedFuture() throws Throwable { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new Exception("Test @WithSpan With completeExceptionally")); + + // when + withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(future)); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testAsyncCompletableFuture") + .hasKind(INTERNAL) + .hasStatus(StatusData.error()) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + + @Test + @DisplayName("should end Span on incompatible return value") + void onIncompatibleReturnValue() throws Throwable { + // when + withSpan("parent", () -> withSpanTester.testAsyncCompletableFuture(null)); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent").hasKind(INTERNAL), + span -> + span.hasName("WithSpanTester.testAsyncCompletableFuture") + .hasKind(INTERNAL) + .hasParentSpanId(traces.get(0).get(0).getSpanId()))); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/JaegerSpanExporterAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/JaegerSpanExporterAutoConfigurationTest.java new file mode 100644 index 000000000..817ac71e6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/JaegerSpanExporterAutoConfigurationTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger.JaegerSpanExporterAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger.JaegerSpanExporterProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** Spring Boot auto configuration test for {@link JaegerGrpcSpanExporter}. */ +class JaegerSpanExporterAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, JaegerSpanExporterAutoConfiguration.class)); + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName("when exporters are ENABLED should initialize JaegerGrpcSpanExporter bean") + void exportersEnabled() { + this.contextRunner + .withPropertyValues("otel.exporter.jaeger.enabled=true") + .run( + context -> + assertThat(context.getBean("otelJaegerSpanExporter", JaegerGrpcSpanExporter.class)) + .isNotNull()); + } + + @Test + @DisplayName( + "when otel.exporter.jaeger properties are set should initialize JaegerSpanExporterProperties") + void handlesProperties() { + this.contextRunner + .withPropertyValues( + "otel.exporter.jaeger.enabled=true", + "otel.exporter.jaeger.endpoint=http://localhost:8080/test", + "otel.exporter.jaeger.timeout=420ms") + .run( + context -> { + JaegerSpanExporterProperties jaegerSpanExporterProperties = + context.getBean(JaegerSpanExporterProperties.class); + assertThat(jaegerSpanExporterProperties.getEndpoint()) + .isEqualTo("http://localhost:8080/test"); + assertThat(jaegerSpanExporterProperties.getTimeout()).hasMillis(420); + }); + } + + @Test + @DisplayName("when exporters are DISABLED should NOT initialize JaegerGrpcSpanExporter bean") + void disabledProperty() { + this.contextRunner + .withPropertyValues("otel.exporter.jaeger.enabled=false") + .run(context -> assertThat(context.containsBean("otelJaegerSpanExporter")).isFalse()); + } + + @Test + @DisplayName( + "when jaeger enabled property is MISSING should initialize JaegerGrpcSpanExporter bean") + void noProperty() { + this.contextRunner.run( + context -> + assertThat(context.getBean("otelJaegerSpanExporter", JaegerGrpcSpanExporter.class)) + .isNotNull()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/LoggingSpanExporterAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/LoggingSpanExporterAutoConfigurationTest.java new file mode 100644 index 000000000..776ec0437 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/LoggingSpanExporterAutoConfigurationTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging.LoggingSpanExporterAutoConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** Spring Boot auto configuration test for {@link LoggingSpanExporter}. */ +class LoggingSpanExporterAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + LoggingSpanExporterAutoConfiguration.class)); + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName("when exporters are ENABLED should initialize LoggingSpanExporter bean") + void exportersEnabled() { + this.contextRunner + .withPropertyValues("otel.exporter.logging.enabled=true") + .run( + context -> + assertThat(context.getBean("otelLoggingSpanExporter", LoggingSpanExporter.class)) + .isNotNull()); + } + + @Test + @DisplayName("when exporters are DISABLED should NOT initialize LoggingSpanExporter bean") + void disabledProperty() { + this.contextRunner + .withPropertyValues("otel.exporter.logging.enabled=false") + .run(context -> assertThat(context.containsBean("otelLoggingSpanExporter")).isFalse()); + } + + @Test + @DisplayName( + "when exporter enabled property is MISSING should initialize LoggingSpanExporter bean") + void noProperty() { + this.contextRunner.run( + context -> + assertThat(context.getBean("otelLoggingSpanExporter", LoggingSpanExporter.class)) + .isNotNull()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/OtlpGrpcSpanExporterAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/OtlpGrpcSpanExporterAutoConfigurationTest.java new file mode 100644 index 000000000..d5dbcc64f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/OtlpGrpcSpanExporterAutoConfigurationTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpGrpcSpanExporterAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpGrpcSpanExporterProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** Spring Boot auto configuration test for {@link OtlpGrpcSpanExporterAutoConfiguration}. */ +class OtlpGrpcSpanExporterAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + OtlpGrpcSpanExporterAutoConfiguration.class)); + + @Test + @DisplayName("when exporters are ENABLED should initialize OtlpGrpcSpanExporter bean") + void exportersEnabled() { + this.contextRunner + .withPropertyValues("otel.exporter.otlp.enabled=true") + .run( + context -> + assertThat(context.getBean("otelOtlpGrpcSpanExporter", OtlpGrpcSpanExporter.class)) + .isNotNull()); + } + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName( + "when otel.exporter.otlp properties are set should initialize OtlpGrpcSpanExporterProperties") + void handlesProperties() { + this.contextRunner + .withPropertyValues( + "otel.exporter.otlp.enabled=true", + "otel.exporter.otlp.endpoint=http://localhost:8080/test", + "otel.exporter.otlp.timeout=69ms") + .run( + context -> { + OtlpGrpcSpanExporterProperties otlpSpanExporterProperties = + context.getBean(OtlpGrpcSpanExporterProperties.class); + assertThat(otlpSpanExporterProperties.getEndpoint()) + .isEqualTo("http://localhost:8080/test"); + assertThat(otlpSpanExporterProperties.getTimeout()).hasMillis(69); + }); + } + + @Test + @DisplayName("when exporters are DISABLED should NOT initialize OtlpGrpcSpanExporter bean") + void disabledProperty() { + this.contextRunner + .withPropertyValues("otel.exporter.otlp.enabled=false") + .run(context -> assertThat(context.containsBean("otelOtlpGrpcSpanExporter")).isFalse()); + } + + @Test + @DisplayName("when otlp enabled property is MISSING should initialize OtlpGrpcSpanExporter bean") + void noProperty() { + this.contextRunner.run( + context -> + assertThat(context.getBean("otelOtlpGrpcSpanExporter", OtlpGrpcSpanExporter.class)) + .isNotNull()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/ZipkinSpanExporterAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/ZipkinSpanExporterAutoConfigurationTest.java new file mode 100644 index 000000000..23eb98eb8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/ZipkinSpanExporterAutoConfigurationTest.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.exporters; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin.ZipkinSpanExporterAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin.ZipkinSpanExporterProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** Spring Boot auto configuration test for {@link ZipkinSpanExporter}. */ +class ZipkinSpanExporterAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, ZipkinSpanExporterAutoConfiguration.class)); + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName("when exporters are ENABLED should initialize ZipkinSpanExporter bean") + void exportersEnabled() { + this.contextRunner + .withPropertyValues("otel.exporter.zipkin.enabled=true") + .run( + context -> + assertThat(context.getBean("otelZipkinSpanExporter", ZipkinSpanExporter.class)) + .isNotNull()); + } + + @Test + @DisplayName( + "when otel.exporter.zipkin properties are set should initialize ZipkinSpanExporterProperties with property values") + void handlesProperties() { + this.contextRunner + .withPropertyValues( + "otel.exporter.zipkin.enabled=true", + "otel.exporter.zipkin.endpoint=http://localhost:8080/test") + .run( + context -> { + ZipkinSpanExporterProperties zipkinSpanExporterProperties = + context.getBean(ZipkinSpanExporterProperties.class); + assertThat(zipkinSpanExporterProperties.getEndpoint()) + .isEqualTo("http://localhost:8080/test"); + }); + } + + @Test + @DisplayName("when exporters are DISABLED should NOT initialize ZipkinSpanExporter bean") + void disabledProperty() { + this.contextRunner + .withPropertyValues("otel.exporter.zipkin.enabled=false") + .run(context -> assertThat(context.containsBean("otelZipkinSpanExporter")).isFalse()); + } + + @Test + @DisplayName("when zipkin enabled property is MISSING should initialize ZipkinSpanExporter bean") + void noProperty() { + this.contextRunner.run( + context -> + assertThat(context.getBean("otelZipkinSpanExporter", ZipkinSpanExporter.class)) + .isNotNull()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfigurationTest.java new file mode 100644 index 000000000..4f2160029 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfigurationTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** Spring Boot auto configuration test for {@link RestTemplateAutoConfiguration}. */ +class RestTemplateAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, RestTemplateAutoConfiguration.class)); + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName("when httpclients are ENABLED should initialize RestTemplateInterceptor bean") + void httpClientsEnabled() { + this.contextRunner + .withPropertyValues("otel.springboot.httpclients.enabled=true") + .run( + context -> + assertThat( + context.getBean( + "otelRestTemplateBeanPostProcessor", + RestTemplateBeanPostProcessor.class)) + .isNotNull()); + } + + @Test + @DisplayName("when httpclients are DISABLED should NOT initialize RestTemplateInterceptor bean") + void disabledProperty() { + this.contextRunner + .withPropertyValues("otel.springboot.httpclients.enabled=false") + .run( + context -> + assertThat(context.containsBean("otelRestTemplateBeanPostProcessor")).isFalse()); + } + + @Test + @DisplayName( + "when httpclients enabled property is MISSING should initialize RestTemplateInterceptor bean") + void noProperty() { + this.contextRunner.run( + context -> + assertThat( + context.getBean( + "otelRestTemplateBeanPostProcessor", RestTemplateBeanPostProcessor.class)) + .isNotNull()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessorTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessorTest.java new file mode 100644 index 000000000..fb66546de --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessorTest.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.httpclients.RestTemplateInterceptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.web.client.RestTemplate; + +@ExtendWith(MockitoExtension.class) +class RestTemplateBeanPostProcessorTest { + + @Mock ObjectProvider openTelemetryProvider; + + RestTemplateBeanPostProcessor restTemplateBeanPostProcessor; + + @BeforeEach + void setUp() { + restTemplateBeanPostProcessor = new RestTemplateBeanPostProcessor(openTelemetryProvider); + } + + @Test + @DisplayName("when processed bean is not of type RestTemplate should return object") + void returnsObject() { + assertThat( + restTemplateBeanPostProcessor.postProcessAfterInitialization( + new Object(), "testObject")) + .isExactlyInstanceOf(Object.class); + + verifyNoInteractions(openTelemetryProvider); + } + + @Test + @DisplayName("when processed bean is of type RestTemplate should return RestTemplate") + void returnsRestTemplate() { + when(openTelemetryProvider.getIfUnique()).thenReturn(OpenTelemetry.noop()); + + assertThat( + restTemplateBeanPostProcessor.postProcessAfterInitialization( + new RestTemplate(), "testRestTemplate")) + .isInstanceOf(RestTemplate.class); + + verify(openTelemetryProvider).getIfUnique(); + } + + @Test + @DisplayName("when processed bean is of type RestTemplate should add ONE RestTemplateInterceptor") + void addsRestTemplateInterceptor() { + when(openTelemetryProvider.getIfUnique()).thenReturn(OpenTelemetry.noop()); + + RestTemplate restTemplate = new RestTemplate(); + + restTemplateBeanPostProcessor.postProcessAfterInitialization(restTemplate, "testRestTemplate"); + restTemplateBeanPostProcessor.postProcessAfterInitialization(restTemplate, "testRestTemplate"); + restTemplateBeanPostProcessor.postProcessAfterInitialization(restTemplate, "testRestTemplate"); + + assertThat( + restTemplate.getInterceptors().stream() + .filter(rti -> rti instanceof RestTemplateInterceptor) + .count()) + .isEqualTo(1); + + verify(openTelemetryProvider, times(3)).getIfUnique(); + } + + @Test + @DisplayName("when OpenTelemetry is not available should NOT add RestTemplateInterceptor") + void doesNotAddRestTemplateInterceptorIfOpenTelemetryUnavailable() { + when(openTelemetryProvider.getIfUnique()).thenReturn(null); + RestTemplate restTemplate = new RestTemplate(); + + restTemplateBeanPostProcessor.postProcessAfterInitialization(restTemplate, "testRestTemplate"); + restTemplateBeanPostProcessor.postProcessAfterInitialization(restTemplate, "testRestTemplate"); + restTemplateBeanPostProcessor.postProcessAfterInitialization(restTemplate, "testRestTemplate"); + + assertThat( + restTemplate.getInterceptors().stream() + .filter(rti -> rti instanceof RestTemplateInterceptor) + .count()) + .isEqualTo(0); + + verify(openTelemetryProvider, times(3)).getIfUnique(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfigurationTest.java new file mode 100644 index 000000000..e8f9295fe --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfigurationTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** Spring Boot auto configuration test for {@link WebClientAutoConfiguration}. */ +class WebClientAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, WebClientAutoConfiguration.class)); + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName("when httpclients are ENABLED should initialize WebClientBeanPostProcessor bean") + void httpClientsEnabled() { + this.contextRunner + .withPropertyValues("otel.springboot.httpclients.enabled=true") + .run( + context -> + assertThat( + context.getBean( + "otelWebClientBeanPostProcessor", WebClientBeanPostProcessor.class)) + .isNotNull()); + } + + @Test + @DisplayName( + "when httpclients are DISABLED should NOT initialize WebClientBeanPostProcessor bean") + void disabledProperty() { + this.contextRunner + .withPropertyValues("otel.springboot.httpclients.enabled=false") + .run( + context -> + assertThat(context.containsBean("otelWebClientBeanPostProcessor")).isFalse()); + } + + @Test + @DisplayName( + "when httpclients enabled property is MISSING should initialize WebClientBeanPostProcessor bean") + void noProperty() { + this.contextRunner.run( + context -> + assertThat( + context.getBean( + "otelWebClientBeanPostProcessor", WebClientBeanPostProcessor.class)) + .isNotNull()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessorTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessorTest.java new file mode 100644 index 000000000..5955bbadf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessorTest.java @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.webflux.client.WebClientTracingFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.web.reactive.function.client.WebClient; + +@ExtendWith(MockitoExtension.class) +class WebClientBeanPostProcessorTest { + + @Mock ObjectProvider openTelemetryProvider; + + WebClientBeanPostProcessor webClientBeanPostProcessor; + + @BeforeEach + void setUp() { + webClientBeanPostProcessor = new WebClientBeanPostProcessor(openTelemetryProvider); + } + + @Test + @DisplayName( + "when processed bean is NOT of type WebClient or WebClientBuilder should return Object") + void returnsObject() { + + assertThat( + webClientBeanPostProcessor.postProcessAfterInitialization(new Object(), "testObject")) + .isExactlyInstanceOf(Object.class); + + verifyNoInteractions(openTelemetryProvider); + } + + @Test + @DisplayName("when processed bean is of type WebClient should return WebClient") + void returnsWebClient() { + when(openTelemetryProvider.getIfUnique()).thenReturn(OpenTelemetry.noop()); + + assertThat( + webClientBeanPostProcessor.postProcessAfterInitialization( + WebClient.create(), "testWebClient")) + .isInstanceOf(WebClient.class); + + verify(openTelemetryProvider).getIfUnique(); + } + + @Test + @DisplayName("when processed bean is of type WebClientBuilder should return WebClientBuilder") + void returnsWebClientBuilder() { + when(openTelemetryProvider.getIfUnique()).thenReturn(OpenTelemetry.noop()); + + assertThat( + webClientBeanPostProcessor.postProcessAfterInitialization( + WebClient.builder(), "testWebClientBuilder")) + .isInstanceOf(WebClient.Builder.class); + + verify(openTelemetryProvider).getIfUnique(); + } + + @Test + @DisplayName("when processed bean is of type WebClient should add exchange filter to WebClient") + void addsExchangeFilterWebClient() { + when(openTelemetryProvider.getIfUnique()).thenReturn(OpenTelemetry.noop()); + + WebClient webClient = WebClient.create(); + Object processedWebClient = + webClientBeanPostProcessor.postProcessAfterInitialization(webClient, "testWebClient"); + + assertThat(processedWebClient).isInstanceOf(WebClient.class); + ((WebClient) processedWebClient) + .mutate() + .filters( + functions -> + assertThat( + functions.stream() + .filter(wctf -> wctf instanceof WebClientTracingFilter) + .count()) + .isEqualTo(1)); + + verify(openTelemetryProvider).getIfUnique(); + } + + @Test + @DisplayName( + "when processed bean is of type WebClient and OpenTelemetry is not available should NOT add exchange filter to WebClient") + void doesNotAddExchangeFilterWebClientIfOpenTelemetryUnavailable() { + when(openTelemetryProvider.getIfUnique()).thenReturn(null); + + WebClient webClient = WebClient.create(); + Object processedWebClient = + webClientBeanPostProcessor.postProcessAfterInitialization(webClient, "testWebClient"); + + assertThat(processedWebClient).isInstanceOf(WebClient.class); + ((WebClient) processedWebClient) + .mutate() + .filters( + functions -> + assertThat( + functions.stream() + .filter(wctf -> wctf instanceof WebClientTracingFilter) + .count()) + .isEqualTo(0)); + + verify(openTelemetryProvider).getIfUnique(); + } + + @Test + @DisplayName( + "when processed bean is of type WebClientBuilder should add ONE exchange filter to WebClientBuilder") + void addsExchangeFilterWebClientBuilder() { + when(openTelemetryProvider.getIfUnique()).thenReturn(OpenTelemetry.noop()); + + WebClient.Builder webClientBuilder = WebClient.builder(); + webClientBeanPostProcessor.postProcessAfterInitialization( + webClientBuilder, "testWebClientBuilder"); + webClientBeanPostProcessor.postProcessAfterInitialization( + webClientBuilder, "testWebClientBuilder"); + webClientBeanPostProcessor.postProcessAfterInitialization( + webClientBuilder, "testWebClientBuilder"); + + webClientBuilder.filters( + functions -> + assertThat( + functions.stream() + .filter(wctf -> wctf instanceof WebClientTracingFilter) + .count()) + .isEqualTo(1)); + + verify(openTelemetryProvider, times(3)).getIfUnique(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationTest.java new file mode 100644 index 000000000..910f90c29 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.webmvc; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.webmvc.WebMvcTracingFilter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** Spring Boot auto configuration test for {@link WebMvcFilterAutoConfiguration}. */ +class WebMvcFilterAutoConfigurationTest { + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, WebMvcFilterAutoConfiguration.class)); + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + @DisplayName("when web is ENABLED should initialize WebMvcTracingFilter bean") + void webEnabled() { + this.contextRunner + .withPropertyValues("otel.springboot.web.enabled=true") + .run( + context -> + assertThat(context.getBean("otelWebMvcTracingFilter", WebMvcTracingFilter.class)) + .isNotNull()); + } + + @Test + @DisplayName("when web is DISABLED should NOT initialize WebMvcTracingFilter bean") + void disabledProperty() { + this.contextRunner + .withPropertyValues("otel.springboot.web.enabled=false") + .run(context -> assertThat(context.containsBean("otelWebMvcTracingFilter")).isFalse()); + } + + @Test + @DisplayName("when web property is MISSING should initialize WebMvcTracingFilter bean") + void noProperty() { + this.contextRunner.run( + context -> + assertThat(context.getBean("otelWebMvcTracingFilter", WebMvcTracingFilter.class)) + .isNotNull()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/spring-core-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/spring-core-2.0-javaagent.gradle new file mode 100644 index 000000000..46c4d1185 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/spring-core-2.0-javaagent.gradle @@ -0,0 +1,17 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'org.springframework' + module = 'spring-core' + versions = "[2.0,]" + } +} + +dependencies { + library "org.springframework:spring-core:2.0" + + // 3.0 introduces submit() methods + // 4.0 introduces submitListenable() methods + testLibrary "org.springframework:spring-core:4.0.0.RELEASE" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/SimpleAsyncTaskExecutorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/SimpleAsyncTaskExecutorInstrumentation.java new file mode 100644 index 000000000..7ffc84c5d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/SimpleAsyncTaskExecutorInstrumentation.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.core; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.ExecutorInstrumentationUtils; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.RunnableWrapper; +import io.opentelemetry.javaagent.instrumentation.api.concurrent.State; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class SimpleAsyncTaskExecutorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.core.task.SimpleAsyncTaskExecutor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isProtected()) + .and(named("doExecute")) + .and(takesArguments(1)) + .and(takesArgument(0, Runnable.class)), + getClass().getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static State enterJobSubmit( + @Advice.Argument(value = 0, readOnly = false) Runnable task) { + Runnable newTask = RunnableWrapper.wrapIfNeeded(task); + if (ExecutorInstrumentationUtils.shouldAttachStateToTask(newTask)) { + task = newTask; + ContextStore contextStore = + InstrumentationContext.get(Runnable.class, State.class); + return ExecutorInstrumentationUtils.setupState( + contextStore, newTask, Java8BytecodeBridge.currentContext()); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitJobSubmit( + @Advice.Enter State state, @Advice.Thrown Throwable throwable) { + ExecutorInstrumentationUtils.cleanUpOnMethodExit(state, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/SpringCoreInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/SpringCoreInstrumentationModule.java new file mode 100644 index 000000000..22765f163 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/SpringCoreInstrumentationModule.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.core; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class SpringCoreInstrumentationModule extends InstrumentationModule { + public SpringCoreInstrumentationModule() { + super("spring-core", "spring-core-2.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("org.springframework.core.task.SimpleAsyncTaskExecutor"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new SimpleAsyncTaskExecutorInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/test/groovy/SimpleAsyncTaskExecutorInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/test/groovy/SimpleAsyncTaskExecutorInstrumentationTest.groovy new file mode 100644 index 000000000..762646de4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-core-2.0/javaagent/src/test/groovy/SimpleAsyncTaskExecutorInstrumentationTest.groovy @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.Callable +import java.util.concurrent.CountDownLatch +import org.springframework.core.task.SimpleAsyncTaskExecutor +import spock.lang.Shared +import spock.lang.Unroll + +class SimpleAsyncTaskExecutorInstrumentationTest extends AgentInstrumentationSpecification { + + @Shared + def executeRunnable = { e, c -> e.execute((Runnable) c) } + @Shared + def submitRunnable = { e, c -> e.submit((Runnable) c) } + @Shared + def submitCallable = { e, c -> e.submit((Callable) c) } + @Shared + def submitListenableRunnable = { e, c -> e.submitListenable((Runnable) c) } + @Shared + def submitListenableCallable = { e, c -> e.submitListenable((Callable) c) } + + @Unroll + def "should propagate context on #desc"() { + given: + def executor = new SimpleAsyncTaskExecutor() + + when: + runUnderTrace("parent") { + def child1 = new AsyncTask(startSpan: true) + def child2 = new AsyncTask(startSpan: false) + method(executor, child1) + method(executor, child2) + child1.waitForCompletion() + child2.waitForCompletion() + } + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + kind INTERNAL + } + span(1) { + name "asyncChild" + kind INTERNAL + childOf(span(0)) + } + } + } + + where: + desc | method + "execute Runnable" | executeRunnable + "submit Runnable" | submitRunnable + "submit Callable" | submitCallable + "submitListenable Runnable" | submitListenableRunnable + "submitListenable Callable" | submitListenableCallable + } +} + +class AsyncTask implements Runnable, Callable { + private static final TRACER = GlobalOpenTelemetry.getTracer("test") + + final latch = new CountDownLatch(1) + boolean startSpan + + @Override + void run() { + if (startSpan) { + TRACER.spanBuilder("asyncChild").startSpan().end() + } + latch.countDown() + } + + @Override + Object call() throws Exception { + run() + return null + } + + void waitForCompletion() throws InterruptedException { + latch.await() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/spring-data-1.8-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/spring-data-1.8-javaagent.gradle new file mode 100644 index 000000000..a0a83b0ea --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/spring-data-1.8-javaagent.gradle @@ -0,0 +1,40 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + // We have two independent covariants, so we have to test them independently. + pass { + group = 'org.springframework.data' + module = 'spring-data-commons' + versions = "[1.8.0.RELEASE,]" + extraDependency "org.springframework:spring-aop:1.2" + assertInverse = true + } + pass { + group = 'org.springframework' + module = 'spring-aop' + versions = "[1.2,]" + extraDependency "org.springframework.data:spring-data-commons:1.8.0.RELEASE" + assertInverse = true + } +} + +// DQH - API changes that impact instrumentation occurred in spring-data-commons in March 2014. +// For now, that limits support to spring-data-commons 1.9.0 (maybe 1.8.0). +// For testing, chose a couple spring-data modules that are old enough to work with 1.9.0. +dependencies { + library "org.springframework.data:spring-data-commons:1.8.0.RELEASE" + compileOnly("org.springframework:spring-aop:1.2") + + testImplementation "org.spockframework:spock-spring:${versions["org.spockframework"]}" + testLibrary "org.springframework:spring-test:3.0.0.RELEASE" + testLibrary "org.springframework.data:spring-data-jpa:1.8.0.RELEASE" + + // JPA dependencies + testInstrumentation project(':instrumentation:jdbc:javaagent') + testImplementation "com.mysema.querydsl:querydsl-jpa:3.7.4" + testImplementation "org.hsqldb:hsqldb:2.0.0" + testLibrary "org.hibernate:hibernate-entitymanager:4.3.0.Final" + + latestDepTestLibrary "org.hibernate:hibernate-entitymanager:5.+" +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/SpringDataInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/SpringDataInstrumentationModule.java new file mode 100644 index 000000000..b81994cf3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/SpringDataInstrumentationModule.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.data; + +import static io.opentelemetry.javaagent.instrumentation.spring.data.SpringDataTracer.tracer; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; + +@AutoService(InstrumentationModule.class) +public class SpringDataInstrumentationModule extends InstrumentationModule { + + public SpringDataInstrumentationModule() { + super("spring-data", "spring-data-1.8"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RepositoryFactorySupportInstrumentation()); + } + + private static final class RepositoryFactorySupportInstrumentation + implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.data.repository.core.support.RepositoryFactorySupport"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), + SpringDataInstrumentationModule.class.getName() + "$RepositoryFactorySupportAdvice"); + } + } + + @SuppressWarnings("unused") + public static class RepositoryFactorySupportAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onConstruction( + @Advice.This RepositoryFactorySupport repositoryFactorySupport) { + repositoryFactorySupport.addRepositoryProxyPostProcessor( + InterceptingRepositoryProxyPostProcessor.INSTANCE); + } + } + + public static final class InterceptingRepositoryProxyPostProcessor + implements RepositoryProxyPostProcessor { + public static final RepositoryProxyPostProcessor INSTANCE = + new InterceptingRepositoryProxyPostProcessor(); + + // DQH - TODO: Support older versions? + // The signature of postProcess changed to add RepositoryInformation in + // spring-data-commons 1.9.0 + // public void postProcess(final ProxyFactory factory) { + // factory.addAdvice(0, RepositoryInterceptor.INSTANCE); + // } + + @Override + public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { + factory.addAdvice(0, RepositoryInterceptor.INSTANCE); + } + } + + static final class RepositoryInterceptor implements MethodInterceptor { + public static final MethodInterceptor INSTANCE = new RepositoryInterceptor(); + + private RepositoryInterceptor() {} + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + Method invokedMethod = methodInvocation.getMethod(); + Class clazz = invokedMethod.getDeclaringClass(); + + boolean isRepositoryOp = Repository.class.isAssignableFrom(clazz); + // Since this interceptor is the outer most interceptor, non-Repository methods + // including Object methods will also flow through here. Don't create spans for those. + if (!isRepositoryOp) { + return methodInvocation.proceed(); + } + + Context context = tracer().startSpan(invokedMethod); + try (Scope ignored = context.makeCurrent()) { + Object result = methodInvocation.proceed(); + tracer().end(context); + return result; + } catch (Throwable t) { + tracer().endExceptionally(context, t); + throw t; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/SpringDataTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/SpringDataTracer.java new file mode 100644 index 000000000..8c2a73d4c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/SpringDataTracer.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.data; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import java.lang.reflect.Method; + +public final class SpringDataTracer extends BaseTracer { + private static final SpringDataTracer TRACER = new SpringDataTracer(); + + public static SpringDataTracer tracer() { + return TRACER; + } + + private SpringDataTracer() {} + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-data-1.8"; + } + + public Context startSpan(Method method) { + return startSpan(SpanNames.fromMethod(method)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/groovy/SpringJpaTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/groovy/SpringJpaTest.groovy new file mode 100644 index 000000000..2990ddd5f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/groovy/SpringJpaTest.groovy @@ -0,0 +1,255 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.hibernate.Version +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spring.jpa.JpaCustomer +import spring.jpa.JpaCustomerRepository +import spring.jpa.JpaPersistenceConfig + +class SpringJpaTest extends AgentInstrumentationSpecification { + def "test object method"() { + setup: + def context = new AnnotationConfigApplicationContext(JpaPersistenceConfig) + def repo = context.getBean(JpaCustomerRepository) + + // when Spring JPA sets up, it issues metadata queries -- clear those traces + clearExportedData() + + when: + runUnderTrace("toString test") { + repo.toString() + } + + then: + // Asserting that a span is NOT created for toString + assertTraces(1) { + trace(0, 1) { + span(0) { + name "toString test" + attributes { + } + } + } + } + } + + def "test CRUD"() { + def isHibernate4 = Version.getVersionString().startsWith("4.") + // moved inside test -- otherwise, miss the opportunity to instrument + def context = new AnnotationConfigApplicationContext(JpaPersistenceConfig) + def repo = context.getBean(JpaCustomerRepository) + + // when Spring JPA sets up, it issues metadata queries -- clear those traces + clearExportedData() + + setup: + def customer = new JpaCustomer("Bob", "Anonymous") + + expect: + customer.id == null + !repo.findAll().iterator().hasNext() // select + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "JpaRepository.findAll" + kind INTERNAL + attributes { + } + } + span(1) { // select + name "SELECT test.JpaCustomer" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "JpaCustomer" + } + } + } + } + clearExportedData() + + when: + repo.save(customer) // insert + def savedId = customer.id + + then: + customer.id != null + assertTraces(1) { + trace(0, 2 + (isHibernate4 ? 0 : 1)) { + span(0) { + name "CrudRepository.save" + kind INTERNAL + attributes { + } + } + def offset = 0 + // hibernate5 has extra span + if (!isHibernate4) { + offset = 1 + span(1) { + name "test" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" "call next value for hibernate_sequence" + } + } + } + span(1 + offset) { // insert + name "INSERT test.JpaCustomer" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^insert / + "${SemanticAttributes.DB_OPERATION.key}" "INSERT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "JpaCustomer" + } + } + } + } + clearExportedData() + + when: + customer.firstName = "Bill" + repo.save(customer) + + then: + customer.id == savedId + assertTraces(1) { + trace(0, 3) { + span(0) { + name "CrudRepository.save" + kind INTERNAL + attributes { + } + } + span(1) { // select + name "SELECT test.JpaCustomer" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "JpaCustomer" + } + } + span(2) { // update + name "UPDATE test.JpaCustomer" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^update / + "${SemanticAttributes.DB_OPERATION.key}" "UPDATE" + "${SemanticAttributes.DB_SQL_TABLE.key}" "JpaCustomer" + } + } + } + } + clearExportedData() + + when: + customer = repo.findByLastName("Anonymous")[0] // select + + then: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "JpaCustomerRepository.findByLastName" + kind INTERNAL + attributes { + } + } + span(1) { // select + name "SELECT test.JpaCustomer" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "JpaCustomer" + } + } + } + } + clearExportedData() + + when: + repo.delete(customer) // delete + + then: + assertTraces(1) { + trace(0, 3) { + span(0) { + name "CrudRepository.delete" + kind INTERNAL + attributes { + } + } + span(1) { // select + name "SELECT test.JpaCustomer" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^select / + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "JpaCustomer" + } + } + span(2) { // delete + name "DELETE test.JpaCustomer" + kind CLIENT + childOf span(0) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "sa" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" ~/^delete / + "${SemanticAttributes.DB_OPERATION.key}" "DELETE" + "${SemanticAttributes.DB_SQL_TABLE.key}" "JpaCustomer" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomer.java new file mode 100644 index 000000000..de81f08ab --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomer.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import java.util.Objects; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class JpaCustomer { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String firstName; + private String lastName; + + protected JpaCustomer() {} + + public JpaCustomer(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public String toString() { + return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof JpaCustomer)) { + return false; + } + JpaCustomer other = (JpaCustomer) obj; + return Objects.equals(id, other.id) + && Objects.equals(firstName, other.firstName) + && Objects.equals(lastName, other.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomerRepository.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomerRepository.java new file mode 100644 index 000000000..0aba55ef6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomerRepository.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaCustomerRepository extends JpaRepository { + List findByLastName(String lastName); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaPersistenceConfig.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaPersistenceConfig.java new file mode 100644 index 000000000..88d031184 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaPersistenceConfig.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import javax.sql.DataSource; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; + +@EnableJpaRepositories(basePackages = "spring.jpa") +public class JpaPersistenceConfig { + + @Bean + public PlatformTransactionManager transactionManager() { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); + return transactionManager; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setDatabase(Database.HSQL); + vendorAdapter.setGenerateDdl(true); + + LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); + em.setDataSource(dataSource()); + em.setPackagesToScan("spring.jpa"); + em.setJpaVendorAdapter(vendorAdapter); + return em; + } + + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:test"); + dataSource.setUsername("sa"); + dataSource.setPassword("1"); + return dataSource; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/spring-integration-4.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/spring-integration-4.1-javaagent.gradle new file mode 100644 index 000000000..3de1a83e4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/spring-integration-4.1-javaagent.gradle @@ -0,0 +1,51 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +ext { + // context "leak" here is intentional: spring-integration instrumentation will always override + // "local" span context with one extracted from the incoming message when it decides to start a + // CONSUMER span + failOnContextLeak = false +} + +muzzle { + pass { + group = "org.springframework.integration" + module = "spring-integration-core" + versions = "[4.1.0.RELEASE,)" + assertInverse = true + } +} + +dependencies { + implementation project(':instrumentation:spring:spring-integration-4.1:library') + + library 'org.springframework.integration:spring-integration-core:4.1.0.RELEASE' + + testInstrumentation project(':instrumentation:rabbitmq-2.7:javaagent') + + testImplementation project(':instrumentation:spring:spring-integration-4.1:testing') + + testLibrary "org.springframework.boot:spring-boot-starter-test:1.5.22.RELEASE" + testLibrary "org.springframework.boot:spring-boot-starter:1.5.22.RELEASE" + testLibrary "org.springframework.cloud:spring-cloud-stream:2.2.1.RELEASE" + testLibrary "org.springframework.cloud:spring-cloud-stream-binder-rabbit:2.2.1.RELEASE" + + testImplementation "javax.servlet:javax.servlet-api:3.1.0" +} + +test { + filter { + excludeTestsMatching 'SpringIntegrationAndRabbitTest' + } + jvmArgs "-Dotel.instrumentation.rabbitmq.enabled=false" +} +test.finalizedBy(tasks.register("testWithRabbitInstrumentation", Test) { + filter { + includeTestsMatching 'SpringIntegrationAndRabbitTest' + } + jvmArgs "-Dotel.instrumentation.rabbitmq.enabled=true" +}) + +tasks.withType(Test).configureEach { + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/ApplicationContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/ApplicationContextInstrumentation.java new file mode 100644 index 000000000..62ba10148 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/ApplicationContextInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.integration.channel.interceptor.GlobalChannelInterceptorWrapper; + +public class ApplicationContextInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.context.support.AbstractApplicationContext"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("org.springframework.context.support.AbstractApplicationContext")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("postProcessBeanFactory")) + .and( + takesArgument( + 0, + named( + "org.springframework.beans.factory.config.ConfigurableListableBeanFactory"))), + ApplicationContextInstrumentation.class.getName() + "$PostProcessBeanFactoryAdvice"); + } + + public static class PostProcessBeanFactoryAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) ConfigurableListableBeanFactory beanFactory) { + if (beanFactory instanceof BeanDefinitionRegistry + && !beanFactory.containsBean("otelGlobalChannelInterceptor")) { + + BeanDefinition globalChannelInterceptorBean = + genericBeanDefinition(GlobalChannelInterceptorWrapper.class) + .addConstructorArgValue(SpringIntegrationSingletons.interceptor()) + .addPropertyValue("patterns", SpringIntegrationSingletons.patterns()) + .getBeanDefinition(); + + ((BeanDefinitionRegistry) beanFactory) + .registerBeanDefinition("otelGlobalChannelInterceptor", globalChannelInterceptorBean); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/SpringIntegrationInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/SpringIntegrationInstrumentationModule.java new file mode 100644 index 000000000..642c6c210 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/SpringIntegrationInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class SpringIntegrationInstrumentationModule extends InstrumentationModule { + public SpringIntegrationInstrumentationModule() { + super("spring-integration", "spring-integration-4.1"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ApplicationContextInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/SpringIntegrationSingletons.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/SpringIntegrationSingletons.java new file mode 100644 index 000000000..b0aab16d8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/SpringIntegrationSingletons.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration; + +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.spring.integration.SpringIntegrationTracing; +import java.util.List; +import org.springframework.messaging.support.ChannelInterceptor; + +public final class SpringIntegrationSingletons { + + private static final List PATTERNS = + Config.get() + .getList( + "otel.instrumentation.spring-integration.global-channel-interceptor-patterns", + singletonList("*")); + + private static final ChannelInterceptor INTERCEPTOR = + SpringIntegrationTracing.create(GlobalOpenTelemetry.get()).newChannelInterceptor(); + + public static String[] patterns() { + return PATTERNS.toArray(new String[0]); + } + + public static ChannelInterceptor interceptor() { + return INTERCEPTOR; + } + + private SpringIntegrationSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/ComplexPropagationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/ComplexPropagationTest.groovy new file mode 100644 index 000000000..c5c32311b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/ComplexPropagationTest.groovy @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class ComplexPropagationTest extends AbstractComplexPropagationTest implements AgentTestTrait { + @Override + Class additionalContextClass() { + null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamRabbitTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamRabbitTest.groovy new file mode 100644 index 000000000..fa7447ea8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamRabbitTest.groovy @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class SpringCloudStreamRabbitTest extends AbstractSpringCloudStreamRabbitTest implements AgentTestTrait { + @Override + Class additionalContextClass() { + null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationAndRabbitTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationAndRabbitTest.groovy new file mode 100644 index 000000000..67d3be49b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationAndRabbitTest.groovy @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.server.ServerTraceUtils.runUnderServerTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +class SpringIntegrationAndRabbitTest extends AgentInstrumentationSpecification implements WithRabbitProducerConsumerTrait { + def setupSpec() { + startRabbit() + } + + def cleanupSpec() { + stopRabbit() + } + + def "should cooperate with existing RabbitMQ instrumentation"() { + when: + // simulate the workflow being triggered by HTTP request + runUnderServerTrace("HTTP GET") { + producerContext.getBean("producer", Runnable).run() + } + + then: + assertTraces(2) { + trace(0, 7) { + span(0) { + name "HTTP GET" + kind SERVER + attributes {} + } + span(1) { + name "producer" + childOf span(0) + attributes {} + } + span(2) { + // span created by rabbitmq instrumentation + name "exchange.declare" + childOf span(1) + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" { it == null || it == "localhost" } + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" { it == null || it instanceof Long } + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq" + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue" + } + } + span(3) { + // span created by rabbitmq instrumentation + name "testTopic -> testTopic send" + childOf span(1) + kind PRODUCER + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" "testTopic" + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + } + } + // spring-cloud-stream-binder-rabbit listener puts all messages into a BlockingQueue immediately after receiving + // that's why the rabbitmq CONSUMER span will never have any child span (and propagate context, actually) + // and that's why spring-integration creates another CONSUMER span + span(4) { + // span created by rabbitmq instrumentation + name ~/testTopic.anonymous.[-\w]+ process/ + childOf span(3) + kind CONSUMER + attributes { + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq" + "${SemanticAttributes.MESSAGING_DESTINATION.key}" "testTopic" + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue" + "${SemanticAttributes.MESSAGING_OPERATION.key}" "process" + "${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long + } + } + span(5) { + // span created by spring-integration instrumentation + name "testConsumer.input process" + childOf span(3) + kind CONSUMER + attributes {} + } + span(6) { + name "consumer" + childOf span(5) + attributes {} + } + } + + trace(1, 1) { + span(0) { + // span created by rabbitmq instrumentation + name "basic.ack" + kind CLIENT + attributes { + "${SemanticAttributes.NET_PEER_NAME.key}" "localhost" + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq" + "${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue" + } + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationTracingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationTracingTest.groovy new file mode 100644 index 000000000..24f69f2a8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationTracingTest.groovy @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait + +class SpringIntegrationTracingTest extends AbstractSpringIntegrationTracingTest implements AgentTestTrait { + @Override + Class additionalContextClass() { + null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/spring-integration-4.1-library.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/spring-integration-4.1-library.gradle new file mode 100644 index 000000000..8e3c16e15 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/spring-integration-4.1-library.gradle @@ -0,0 +1,19 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + library 'org.springframework.integration:spring-integration-core:4.1.0.RELEASE' + + testImplementation project(':instrumentation:spring:spring-integration-4.1:testing') + + testLibrary "org.springframework.boot:spring-boot-starter-test:1.5.22.RELEASE" + testLibrary "org.springframework.boot:spring-boot-starter:1.5.22.RELEASE" + testLibrary "org.springframework.cloud:spring-cloud-stream:2.2.1.RELEASE" + testLibrary "org.springframework.cloud:spring-cloud-stream-binder-rabbit:2.2.1.RELEASE" +} + +test { + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/ContextAndScope.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/ContextAndScope.java new file mode 100644 index 000000000..4f18b1ef0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/ContextAndScope.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.integration; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.checkerframework.checker.nullness.qual.Nullable; + +@AutoValue +abstract class ContextAndScope { + + @Nullable + abstract Context getContext(); + + abstract Scope getScope(); + + void close() { + getScope().close(); + } + + static ContextAndScope create(Context context, Scope scope) { + return new AutoValue_ContextAndScope(context, scope); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageChannelSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageChannelSpanNameExtractor.java new file mode 100644 index 000000000..8a87d9f65 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageChannelSpanNameExtractor.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.integration; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import org.springframework.integration.channel.AbstractMessageChannel; +import org.springframework.messaging.MessageChannel; + +final class MessageChannelSpanNameExtractor implements SpanNameExtractor { + @Override + public String extract(MessageWithChannel messageWithChannel) { + final String channelName; + MessageChannel channel = messageWithChannel.getMessageChannel(); + if (channel instanceof AbstractMessageChannel) { + channelName = ((AbstractMessageChannel) channel).getFullChannelName(); + } else if (channel instanceof org.springframework.messaging.support.AbstractMessageChannel) { + channelName = + ((org.springframework.messaging.support.AbstractMessageChannel) channel).getBeanName(); + } else { + channelName = channel.getClass().getSimpleName(); + } + return channelName + " process"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageHeadersGetter.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageHeadersGetter.java new file mode 100644 index 000000000..8df012e88 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageHeadersGetter.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.integration; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; + +// Reading native headers is required by some protocols, e.g. STOMP +// see https://github.com/spring-cloud/spring-cloud-sleuth/issues/716 for more details +// Native headers logic inspired by +// https://github.com/spring-cloud/spring-cloud-sleuth/blob/main/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/MessageHeaderPropagatorGetter.java +enum MessageHeadersGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(MessageWithChannel carrier) { + MessageHeaders headers = carrier.getMessage().getHeaders(); + Map> nativeHeaders = + headers.get(NativeMessageHeaderAccessor.NATIVE_HEADERS, Map.class); + if (nativeHeaders != null) { + return nativeHeaders.keySet(); + } + return headers.keySet(); + } + + @Override + public String get(MessageWithChannel carrier, String key) { + MessageHeaders headers = carrier.getMessage().getHeaders(); + String nativeHeaderValue = getNativeHeader(headers, key); + if (nativeHeaderValue != null) { + return nativeHeaderValue; + } + Object headerValue = headers.get(key); + if (headerValue == null) { + return null; + } + if (headerValue instanceof byte[]) { + return new String((byte[]) headerValue, StandardCharsets.UTF_8); + } + return headerValue.toString(); + } + + @Nullable + private static String getNativeHeader(MessageHeaders carrier, String key) { + Map> nativeMap = + carrier.get(NativeMessageHeaderAccessor.NATIVE_HEADERS, Map.class); + if (nativeMap == null) { + return null; + } + List values = nativeMap.get(key); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(0); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageHeadersSetter.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageHeadersSetter.java new file mode 100644 index 000000000..1564b5013 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageHeadersSetter.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.integration; + +import static java.util.Collections.singletonList; + +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.List; +import java.util.Map; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; + +// Setting native headers is required by some protocols, e.g. STOMP +// see https://github.com/spring-cloud/spring-cloud-sleuth/issues/716 for more details +// Native headers logic inspired by +// https://github.com/spring-cloud/spring-cloud-sleuth/blob/main/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/MessageHeaderPropagatorSetter.java +enum MessageHeadersSetter implements TextMapSetter { + INSTANCE; + + @Override + public void set(MessageHeaderAccessor carrier, String key, String value) { + carrier.setHeader(key, value); + setNativeHeader(carrier, key, value); + } + + private static void setNativeHeader(MessageHeaderAccessor carrier, String key, String value) { + Object nativeMap = carrier.getHeader(NativeMessageHeaderAccessor.NATIVE_HEADERS); + if (nativeMap instanceof Map) { + ((Map>) nativeMap).put(key, singletonList(value)); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageWithChannel.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageWithChannel.java new file mode 100644 index 000000000..ee959799e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/MessageWithChannel.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.integration; + +import com.google.auto.value.AutoValue; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; + +@AutoValue +public abstract class MessageWithChannel { + + public abstract Message getMessage(); + + public abstract MessageChannel getMessageChannel(); + + static MessageWithChannel create(Message message, MessageChannel messageChannel) { + return new AutoValue_MessageWithChannel(message, messageChannel); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/SpringIntegrationTracing.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/SpringIntegrationTracing.java new file mode 100644 index 000000000..44277be10 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/SpringIntegrationTracing.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.integration; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.ChannelInterceptor; + +/** Entrypoint for instrumenting Spring Integration {@link MessageChannel}s. */ +public final class SpringIntegrationTracing { + + /** + * Returns a new {@link SpringIntegrationTracing} configured with the given {@link OpenTelemetry}. + */ + public static SpringIntegrationTracing create(OpenTelemetry openTelemetry) { + return newBuilder(openTelemetry).build(); + } + + /** + * Returns a new {@link SpringIntegrationTracingBuilder} configured with the given {@link + * OpenTelemetry}. + */ + public static SpringIntegrationTracingBuilder newBuilder(OpenTelemetry openTelemetry) { + return new SpringIntegrationTracingBuilder(openTelemetry); + } + + private final ContextPropagators propagators; + private final Instrumenter instrumenter; + + SpringIntegrationTracing( + ContextPropagators propagators, Instrumenter instrumenter) { + this.propagators = propagators; + this.instrumenter = instrumenter; + } + + /** + * Returns a new {@link ChannelInterceptor} that propagates context through {@link Message}s and + * when no other messaging instrumentation is detected, traces {@link + * MessageChannel#send(Message)} calls. + * + * @see org.springframework.integration.channel.ChannelInterceptorAware + * @see org.springframework.messaging.support.InterceptableChannel + * @see org.springframework.integration.config.GlobalChannelInterceptor + */ + public ChannelInterceptor newChannelInterceptor() { + return new TracingChannelInterceptor(propagators, instrumenter); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/SpringIntegrationTracingBuilder.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/SpringIntegrationTracingBuilder.java new file mode 100644 index 000000000..627374f12 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/SpringIntegrationTracingBuilder.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.integration; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.ArrayList; +import java.util.List; + +/** A builder of {@link SpringIntegrationTracing}. */ +public final class SpringIntegrationTracingBuilder { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-integration-core-4.1"; + + private final OpenTelemetry openTelemetry; + private final List> additionalAttributeExtractors = + new ArrayList<>(); + + SpringIntegrationTracingBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. + */ + public SpringIntegrationTracingBuilder addAttributesExtractor( + AttributesExtractor attributesExtractor) { + additionalAttributeExtractors.add(attributesExtractor); + return this; + } + + /** + * Returns a new {@link SpringIntegrationTracing} with the settings of this {@link + * SpringIntegrationTracingBuilder}. + */ + public SpringIntegrationTracing build() { + Instrumenter instrumenter = + Instrumenter.newBuilder( + openTelemetry, INSTRUMENTATION_NAME, new MessageChannelSpanNameExtractor()) + .addAttributesExtractors(additionalAttributeExtractors) + .newConsumerInstrumenter(MessageHeadersGetter.INSTANCE); + return new SpringIntegrationTracing(openTelemetry.getPropagators(), instrumenter); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/TracingChannelInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/TracingChannelInterceptor.java new file mode 100644 index 000000000..966394af5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/TracingChannelInterceptor.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.integration; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.List; +import java.util.Map; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.support.ExecutorChannelInterceptor; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; +import org.springframework.util.LinkedMultiValueMap; + +final class TracingChannelInterceptor implements ExecutorChannelInterceptor { + private static final String CONTEXT_AND_SCOPE_KEY = ContextAndScope.class.getName(); + private static final String SCOPE_KEY = TracingChannelInterceptor.class.getName() + ".scope"; + + private final ContextPropagators propagators; + private final Instrumenter instrumenter; + + TracingChannelInterceptor( + ContextPropagators propagators, Instrumenter instrumenter) { + this.propagators = propagators; + this.instrumenter = instrumenter; + } + + @Override + public Message preSend(Message message, MessageChannel messageChannel) { + Context parentContext = Context.current(); + MessageWithChannel messageWithChannel = MessageWithChannel.create(message, messageChannel); + + final Context context; + MessageHeaderAccessor messageHeaderAccessor = createMutableHeaderAccessor(message); + + // when there's no CONSUMER span created by another instrumentation, start it; there's no other + // messaging instrumentation that can do this, so spring-integration must ensure proper context + // propagation + // the new CONSUMER span will use the span context extracted from the incoming message as the + // parent + if (instrumenter.shouldStart(parentContext, messageWithChannel)) { + context = instrumenter.start(parentContext, messageWithChannel); + messageHeaderAccessor.setHeader( + CONTEXT_AND_SCOPE_KEY, ContextAndScope.create(context, context.makeCurrent())); + } else { + // if there was a top-level span detected it means that there's another messaging + // instrumentation that creates CONSUMER/PRODUCER spans; in that case, back off and just + // inject the current context into the message + context = parentContext; + messageHeaderAccessor.setHeader( + CONTEXT_AND_SCOPE_KEY, ContextAndScope.create(null, context.makeCurrent())); + } + + propagators + .getTextMapPropagator() + .inject(context, messageHeaderAccessor, MessageHeadersSetter.INSTANCE); + return createMessageWithHeaders(message, messageHeaderAccessor); + } + + @Override + public void postSend(Message message, MessageChannel messageChannel, boolean sent) {} + + @Override + public void afterSendCompletion( + Message message, MessageChannel messageChannel, boolean sent, Exception e) { + Object contextAndScope = message.getHeaders().get(CONTEXT_AND_SCOPE_KEY); + if (contextAndScope instanceof ContextAndScope) { + ContextAndScope cas = (ContextAndScope) contextAndScope; + cas.close(); + Context context = cas.getContext(); + + if (context != null) { + MessageWithChannel messageWithChannel = MessageWithChannel.create(message, messageChannel); + instrumenter.end(context, messageWithChannel, null, e); + } + } + } + + @Override + public boolean preReceive(MessageChannel messageChannel) { + return true; + } + + @Override + public Message postReceive(Message message, MessageChannel messageChannel) { + return message; + } + + @Override + public void afterReceiveCompletion( + Message message, MessageChannel messageChannel, Exception e) {} + + @Override + public Message beforeHandle( + Message message, MessageChannel channel, MessageHandler handler) { + MessageWithChannel messageWithChannel = MessageWithChannel.create(message, channel); + Context context = + propagators + .getTextMapPropagator() + .extract(Context.current(), messageWithChannel, MessageHeadersGetter.INSTANCE); + MessageHeaderAccessor messageHeaderAccessor = MessageHeaderAccessor.getMutableAccessor(message); + messageHeaderAccessor.setHeader(SCOPE_KEY, context.makeCurrent()); + return createMessageWithHeaders(message, messageHeaderAccessor); + } + + @Override + public void afterMessageHandled( + Message message, MessageChannel channel, MessageHandler handler, Exception ex) { + Object scope = message.getHeaders().get(SCOPE_KEY); + if (scope instanceof Scope) { + ((Scope) scope).close(); + } + } + + private static MessageHeaderAccessor createMutableHeaderAccessor(Message message) { + MessageHeaderAccessor headerAccessor = MessageHeaderAccessor.getMutableAccessor(message); + headerAccessor.setLeaveMutable(true); + ensureNativeHeadersAreMutable(headerAccessor); + return headerAccessor; + } + + private static void ensureNativeHeadersAreMutable(MessageHeaderAccessor headerAccessor) { + Object nativeMap = headerAccessor.getHeader(NativeMessageHeaderAccessor.NATIVE_HEADERS); + if (nativeMap != null && !(nativeMap instanceof LinkedMultiValueMap)) { + headerAccessor.setHeader( + NativeMessageHeaderAccessor.NATIVE_HEADERS, + new LinkedMultiValueMap<>((Map>) nativeMap)); + } + } + + private static Message createMessageWithHeaders( + Message message, MessageHeaderAccessor messageHeaderAccessor) { + return MessageBuilder.fromMessage(message) + .copyHeaders(messageHeaderAccessor.toMessageHeaders()) + .build(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/ComplexPropagationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/ComplexPropagationTest.groovy new file mode 100644 index 000000000..ff40c296c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/ComplexPropagationTest.groovy @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class ComplexPropagationTest extends AbstractComplexPropagationTest implements LibraryTestTrait { + @Override + Class additionalContextClass() { + GlobalInterceptorSpringConfig + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorSpringConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorSpringConfig.groovy new file mode 100644 index 000000000..685cf3bd3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorSpringConfig.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.instrumentation.spring.integration.SpringIntegrationTracing +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.integration.config.GlobalChannelInterceptor +import org.springframework.messaging.support.ChannelInterceptor + +@Configuration +class GlobalInterceptorSpringConfig { + + @GlobalChannelInterceptor + @Bean + ChannelInterceptor otelInterceptor() { + SpringIntegrationTracing.create(GlobalOpenTelemetry.get()).newChannelInterceptor() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamRabbitTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamRabbitTest.groovy new file mode 100644 index 000000000..e9d659f21 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamRabbitTest.groovy @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class SpringCloudStreamRabbitTest extends AbstractSpringCloudStreamRabbitTest implements LibraryTestTrait { + @Override + Class additionalContextClass() { + GlobalInterceptorSpringConfig + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringIntegrationTracingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringIntegrationTracingTest.groovy new file mode 100644 index 000000000..67c42521d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringIntegrationTracingTest.groovy @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.LibraryTestTrait + +class SpringIntegrationTracingTest extends AbstractSpringIntegrationTracingTest implements LibraryTestTrait { + @Override + Class additionalContextClass() { + GlobalInterceptorSpringConfig + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/spring-integration-4.1-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/spring-integration-4.1-testing.gradle new file mode 100644 index 000000000..71ae108e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/spring-integration-4.1-testing.gradle @@ -0,0 +1,13 @@ +apply plugin: "otel.java-conventions" + +dependencies { + api project(':testing-common') + + api "org.testcontainers:testcontainers" + + compileOnly 'org.springframework.integration:spring-integration-core:4.1.0.RELEASE' + compileOnly "org.springframework.boot:spring-boot-starter-test:1.5.22.RELEASE" + compileOnly "org.springframework.boot:spring-boot-starter:1.5.22.RELEASE" + compileOnly "org.springframework.cloud:spring-cloud-stream:2.2.1.RELEASE" + compileOnly "org.springframework.cloud:spring-cloud-stream-binder-rabbit:2.2.1.RELEASE" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractComplexPropagationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractComplexPropagationTest.groovy new file mode 100644 index 000000000..66ba177ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractComplexPropagationTest.groovy @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import java.util.concurrent.BlockingQueue +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.LinkedBlockingQueue +import java.util.stream.Collectors +import org.springframework.boot.SpringApplication +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.event.EventListener +import org.springframework.integration.channel.DirectChannel +import org.springframework.integration.channel.ExecutorChannel +import org.springframework.messaging.Message +import org.springframework.messaging.SubscribableChannel +import org.springframework.messaging.support.MessageBuilder +import spock.lang.Shared + +abstract class AbstractComplexPropagationTest extends InstrumentationSpecification { + + abstract Class additionalContextClass() + + @Shared + ConfigurableApplicationContext applicationContext + + def setupSpec() { + def contextClasses = [ExternalQueueConfig] + if (additionalContextClass() != null) { + contextClasses += additionalContextClass() + } + + def app = new SpringApplication(contextClasses as Class[]) + app.setDefaultProperties([ + "spring.main.web-application-type": "none" + ]) + applicationContext = app.run() + } + + def cleanupSpec() { + applicationContext?.close() + } + + def "should propagate context through a custom message queue"() { + given: + def sendChannel = applicationContext.getBean("sendChannel", SubscribableChannel) + def receiveChannel = applicationContext.getBean("receiveChannel", SubscribableChannel) + + def messageHandler = new CapturingMessageHandler() + receiveChannel.subscribe(messageHandler) + + when: + runUnderTrace("parent") { + sendChannel.send(MessageBuilder.withPayload("test") + .setHeader("theAnswer", "42") + .build()) + } + + then: + messageHandler.join() + + assertTraces(1) { + trace(0, 4) { + span(0) { + name "parent" + } + // there's no top-level SERVER or CONSUMER span, so spring-integration adds a CONSUMER one + span(1) { + name "application.sendChannel process" + childOf span(0) + kind CONSUMER + } + // message is received in a separate thread without any context, so a CONSUMER span with parent + // extracted from the incoming message is created + span(2) { + name "application.receiveChannel process" + childOf span(1) + kind CONSUMER + } + span(3) { + name "handler" + childOf span(2) + } + } + } + + cleanup: + receiveChannel.unsubscribe(messageHandler) + } + + // this setup simulates separate producer/consumer and some "external" message queue in between + @SpringBootConfiguration + @EnableAutoConfiguration + static class ExternalQueueConfig { + @Bean + SubscribableChannel sendChannel() { + new ExecutorChannel(Executors.newSingleThreadExecutor()) + } + + @Bean + SubscribableChannel receiveChannel() { + new DirectChannel() + } + + @Bean + BlockingQueue externalQueue() { + new LinkedBlockingQueue() + } + + @Bean + ExecutorService consumerThread() { + Executors.newSingleThreadExecutor() + } + + @EventListener(ApplicationReadyEvent) + void initialize() { + sendChannel().subscribe { message -> + externalQueue().offer(Payload.from(message)) + } + + consumerThread().execute({ + while (!Thread.interrupted()) { + def payload = externalQueue().take() + receiveChannel().send(payload.toMessage()) + } + }) + } + } + + static class Payload { + String body + Map headers + + static Payload from(Message message) { + def body = message.payload as String + Map headers = message.headers.entrySet().stream() + .filter({ kv -> kv.value instanceof String }) + .collect(Collectors.toMap({ it.key }, { it.value })) + new Payload(body: body, headers: headers) + } + + Message toMessage() { + MessageBuilder.withPayload(body) + .copyHeaders(headers) + .build() + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamRabbitTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamRabbitTest.groovy new file mode 100644 index 000000000..f7eb32490 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamRabbitTest.groovy @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER + +import io.opentelemetry.instrumentation.test.InstrumentationSpecification + +abstract class AbstractSpringCloudStreamRabbitTest extends InstrumentationSpecification implements WithRabbitProducerConsumerTrait { + + abstract Class additionalContextClass() + + def setupSpec() { + startRabbit(additionalContextClass()) + } + + def cleanupSpec() { + stopRabbit() + } + + def "should propagate context through RabbitMQ"() { + when: + producerContext.getBean("producer", Runnable).run() + + then: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "producer" + } + span(1) { + name "testProducer.output process" + childOf span(0) + kind CONSUMER + } + span(2) { + name "testConsumer.input process" + childOf span(1) + kind CONSUMER + } + span(3) { + name "consumer" + childOf span(2) + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringIntegrationTracingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringIntegrationTracingTest.groovy new file mode 100644 index 000000000..69da4168e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringIntegrationTracingTest.groovy @@ -0,0 +1,181 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.Executors +import org.springframework.boot.SpringApplication +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.event.EventListener +import org.springframework.integration.channel.DirectChannel +import org.springframework.integration.channel.interceptor.GlobalChannelInterceptorWrapper +import org.springframework.messaging.Message +import org.springframework.messaging.SubscribableChannel +import org.springframework.messaging.support.ExecutorSubscribableChannel +import org.springframework.messaging.support.MessageBuilder +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class AbstractSpringIntegrationTracingTest extends InstrumentationSpecification { + + abstract Class additionalContextClass() + + @Shared + ConfigurableApplicationContext applicationContext + + def setupSpec() { + def contextClasses = [MessageChannelsConfig] + if (additionalContextClass() != null) { + contextClasses += additionalContextClass() + } + + def app = new SpringApplication(contextClasses as Class[]) + app.setDefaultProperties([ + "spring.main.web-application-type": "none" + ]) + applicationContext = app.run() + } + + def cleanupSpec() { + applicationContext?.close() + } + + def "should propagate context (#channelName)"() { + given: + def channel = applicationContext.getBean(channelName, SubscribableChannel) + + def messageHandler = new CapturingMessageHandler() + channel.subscribe(messageHandler) + + when: + runUnderTrace("parent") { + channel.send(MessageBuilder.withPayload("test") + .build()) + } + + then: + def capturedMessage = messageHandler.join() + + assertTraces(1) { + trace(0, 3) { + span(0) { + name "parent" + } + span(1) { + name interceptorSpanName + childOf span(0) + kind CONSUMER + } + span(2) { + name "handler" + childOf span(1) + } + + def interceptorSpan = span(1) + verifyCorrectSpanWasPropagated(capturedMessage, interceptorSpan) + } + } + + cleanup: + channel.unsubscribe(messageHandler) + + where: + channelName | interceptorSpanName + "directChannel" | "application.directChannel process" + "executorChannel" | "executorChannel process" + } + + def "should handle multiple message channels in a chain"() { + given: + def channel1 = applicationContext.getBean("linkedChannel1", SubscribableChannel) + def channel2 = applicationContext.getBean("linkedChannel2", SubscribableChannel) + + def messageHandler = new CapturingMessageHandler() + channel2.subscribe(messageHandler) + + when: + runUnderTrace("parent") { + channel1.send(MessageBuilder.withPayload("test") + .build()) + } + + then: + def capturedMessage = messageHandler.join() + + assertTraces(1) { + trace(0, 3) { + span(0) { + name "parent" + } + span(1) { + name "application.linkedChannel1 process" + childOf span(0) + kind CONSUMER + } + span(2) { + name "handler" + childOf span(1) + } + + def lastChannelSpan = span(1) + verifyCorrectSpanWasPropagated(capturedMessage, lastChannelSpan) + } + } + + cleanup: + channel2.unsubscribe(messageHandler) + } + + static void verifyCorrectSpanWasPropagated(Message capturedMessage, SpanData parentSpan) { + def propagatedSpan = capturedMessage.headers.get("traceparent") as String + assert propagatedSpan.contains(parentSpan.traceId), "wrong trace id" + assert propagatedSpan.contains(parentSpan.spanId), "wrong span id" + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class MessageChannelsConfig { + @Bean + SubscribableChannel directChannel() { + new DirectChannel() + } + + @Bean + SubscribableChannel executorChannel(GlobalChannelInterceptorWrapper otelInterceptor) { + def channel = new ExecutorSubscribableChannel(Executors.newSingleThreadExecutor()) + if (!Boolean.getBoolean("testLatestDeps")) { + // spring does not inject the interceptor in 4.1 because ExecutorSubscribableChannel isn't ChannelInterceptorAware + // in later versions spring injects the global interceptor into InterceptableChannel (which ExecutorSubscribableChannel is) + channel.addInterceptor(otelInterceptor.channelInterceptor) + } + channel + } + + @Bean + SubscribableChannel linkedChannel1() { + new DirectChannel() + } + + @Bean + SubscribableChannel linkedChannel2() { + new DirectChannel() + } + + @EventListener(ApplicationReadyEvent) + void initialize() { + linkedChannel1().subscribe { message -> + linkedChannel2().send(message) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/CapturingMessageHandler.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/CapturingMessageHandler.groovy new file mode 100644 index 000000000..5fa03d05c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/CapturingMessageHandler.groovy @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import java.util.concurrent.CompletableFuture +import org.springframework.messaging.Message +import org.springframework.messaging.MessageHandler +import org.springframework.messaging.MessagingException + +class CapturingMessageHandler implements MessageHandler { + final CompletableFuture> captured = new CompletableFuture<>() + + @Override + void handleMessage(Message message) throws MessagingException { + runUnderTrace("handler") { + captured.complete(message) + } + } + + Message join() { + captured.join() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/WithRabbitProducerConsumerTrait.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/WithRabbitProducerConsumerTrait.groovy new file mode 100644 index 000000000..ace2c6a3b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/WithRabbitProducerConsumerTrait.groovy @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runInternalSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import java.time.Duration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.SpringApplication +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.cloud.stream.annotation.EnableBinding +import org.springframework.cloud.stream.annotation.StreamListener +import org.springframework.cloud.stream.messaging.Sink +import org.springframework.cloud.stream.messaging.Source +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.messaging.support.MessageBuilder +import org.testcontainers.containers.GenericContainer + +trait WithRabbitProducerConsumerTrait { + + static GenericContainer rabbitMqContainer + static ConfigurableApplicationContext producerContext + static ConfigurableApplicationContext consumerContext + + def startRabbit(Class additionalContext = null) { + rabbitMqContainer = new GenericContainer('rabbitmq:latest') + .withExposedPorts(5672) + .withStartupTimeout(Duration.ofSeconds(120)) + rabbitMqContainer.start() + + def producerApp = new SpringApplication(getContextClasses(ProducerConfig, additionalContext)) + producerApp.setDefaultProperties([ + "spring.application.name" : "testProducer", + "spring.jmx.enabled" : false, + "spring.main.web-application-type" : "none", + "spring.rabbitmq.host" : rabbitMqContainer.containerIpAddress, + "spring.rabbitmq.port" : rabbitMqContainer.getMappedPort(5672), + "spring.cloud.stream.bindings.output.destination": "testTopic" + ]) + producerContext = producerApp.run() + + def consumerApp = new SpringApplication(getContextClasses(ConsumerConfig, additionalContext)) + consumerApp.setDefaultProperties([ + "spring.application.name" : "testConsumer", + "spring.jmx.enabled" : false, + "spring.main.web-application-type" : "none", + "spring.rabbitmq.host" : rabbitMqContainer.containerIpAddress, + "spring.rabbitmq.port" : rabbitMqContainer.getMappedPort(5672), + "spring.cloud.stream.bindings.input.destination": "testTopic" + ]) + consumerContext = consumerApp.run() + } + + private Class[] getContextClasses(Class mainContext, Class additionalContext) { + def contextClasses = [mainContext] + if (additionalContext != null) { + contextClasses += additionalContext + } + contextClasses + } + + def stopRabbit() { + rabbitMqContainer?.stop() + rabbitMqContainer = null + producerContext?.close() + producerContext = null + consumerContext?.close() + consumerContext = null + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @EnableBinding(Source) + static class ProducerConfig { + @Autowired + Source source + + @Bean + Runnable producer() { + return { + runUnderTrace("producer") { + source.output().send(MessageBuilder.withPayload("test").build()) + } + } + } + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @EnableBinding(Sink) + static class ConsumerConfig { + @StreamListener(Sink.INPUT) + void consume(String ignored) { + runInternalSpan("consumer") + } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/spring-scheduling-3.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/spring-scheduling-3.1-javaagent.gradle new file mode 100644 index 000000000..e38b2abd0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/spring-scheduling-3.1-javaagent.gradle @@ -0,0 +1,17 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'org.springframework' + module = 'spring-context' + versions = "[3.1.0.RELEASE,]" + assertInverse = true + } +} + +dependencies { + // 3.2.3 is the first version with which the tests will run. Lower versions require other + // classes and packages to be imported. Versions 3.1.0+ work with the instrumentation. + library "org.springframework:spring-context:3.1.0.RELEASE" + testLibrary "org.springframework:spring-context:3.2.3.RELEASE" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingInstrumentationModule.java new file mode 100644 index 000000000..9b7902236 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.scheduling; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class SpringSchedulingInstrumentationModule extends InstrumentationModule { + + public SpringSchedulingInstrumentationModule() { + super("spring-scheduling", "spring-scheduling-3.1"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new TaskInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingRunnableWrapper.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingRunnableWrapper.java new file mode 100644 index 000000000..8958fbd72 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingRunnableWrapper.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.scheduling; + +import static io.opentelemetry.javaagent.instrumentation.spring.scheduling.SpringSchedulingTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public class SpringSchedulingRunnableWrapper implements Runnable { + private final Runnable runnable; + + private SpringSchedulingRunnableWrapper(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void run() { + if (runnable == null) { + return; + } + + Context context = tracer().startSpan(runnable); + try (Scope ignored = context.makeCurrent()) { + runnable.run(); + tracer().end(context); + } catch (Throwable throwable) { + tracer().endExceptionally(context, throwable); + throw throwable; + } + } + + public static Runnable wrapIfNeeded(Runnable task) { + // We wrap only lambdas' anonymous classes and if given object has not already been wrapped. + // Anonymous classes have '/' in class name which is not allowed in 'normal' classes. + if (task instanceof SpringSchedulingRunnableWrapper) { + return task; + } + return new SpringSchedulingRunnableWrapper(task); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingTracer.java new file mode 100644 index 000000000..44c4f7d29 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/SpringSchedulingTracer.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.scheduling; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import org.springframework.scheduling.support.ScheduledMethodRunnable; + +public class SpringSchedulingTracer extends BaseTracer { + private static final SpringSchedulingTracer TRACER = new SpringSchedulingTracer(); + + public static SpringSchedulingTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-scheduling-3.1"; + } + + public Context startSpan(Runnable runnable) { + return startSpan(spanNameOnRun(runnable), SpanKind.INTERNAL); + } + + private static String spanNameOnRun(Runnable runnable) { + if (runnable instanceof ScheduledMethodRunnable) { + ScheduledMethodRunnable scheduledMethodRunnable = (ScheduledMethodRunnable) runnable; + return SpanNames.fromMethod(scheduledMethodRunnable.getMethod()); + } else { + return SpanNames.fromMethod(runnable.getClass(), "run"); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/TaskInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/TaskInstrumentation.java new file mode 100644 index 000000000..83c1942c7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/TaskInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.scheduling; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class TaskInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.scheduling.config.Task"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(0, Runnable.class)), + this.getClass().getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onConstruction( + @Advice.Argument(value = 0, readOnly = false) Runnable runnable) { + runnable = SpringSchedulingRunnableWrapper.wrapIfNeeded(runnable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/groovy/SpringSchedulingTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/groovy/SpringSchedulingTest.groovy new file mode 100644 index 000000000..1692a8327 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/groovy/SpringSchedulingTest.groovy @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.TimeUnit +import org.springframework.context.annotation.AnnotationConfigApplicationContext + +class SpringSchedulingTest extends AgentInstrumentationSpecification { + + def "schedule trigger test according to cron expression"() { + setup: + def context = new AnnotationConfigApplicationContext(TriggerTaskConfig) + def task = context.getBean(TriggerTask) + + task.blockUntilExecute() + + expect: + assert task != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "TriggerTask.run" + hasNoParent() + attributes { + } + } + } + } + } + + def "schedule interval test"() { + setup: + def context = new AnnotationConfigApplicationContext(IntervalTaskConfig) + def task = context.getBean(IntervalTask) + + task.blockUntilExecute() + + expect: + assert task != null + assertTraces(1) { + trace(0, 1) { + span(0) { + name "IntervalTask.run" + hasNoParent() + attributes { + } + } + } + } + + } + + def "schedule lambda test"() { + setup: + def context = new AnnotationConfigApplicationContext(LambdaTaskConfig) + def configurer = context.getBean(LambdaTaskConfigurer) + + configurer.singleUseLatch.await(2000, TimeUnit.MILLISECONDS) + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + nameContains "LambdaTaskConfigurer\$\$Lambda\$" + hasNoParent() + attributes { + } + } + } + } + + cleanup: + context.close() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTask.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTask.java new file mode 100644 index 000000000..d7143a0fd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTask.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class IntervalTask implements Runnable { + + private final CountDownLatch latch = new CountDownLatch(1); + + @Scheduled(fixedRate = 5000) + @Override + public void run() { + latch.countDown(); + } + + public void blockUntilExecute() throws InterruptedException { + latch.await(5, TimeUnit.SECONDS); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTaskConfig.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTaskConfig.java new file mode 100644 index 000000000..62fd7dbbc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTaskConfig.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class IntervalTaskConfig { + @Bean + public IntervalTask scheduledTasks() { + return new IntervalTask(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfig.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfig.java new file mode 100644 index 000000000..4d7ec3a34 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfig.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class LambdaTaskConfig { + + @Bean + LambdaTaskConfigurer lambdaTaskConfigurer() { + return new LambdaTaskConfigurer(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfigurer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfigurer.java new file mode 100644 index 000000000..59051a534 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfigurer.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.concurrent.CountDownLatch; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.stereotype.Service; + +@Service +public class LambdaTaskConfigurer implements SchedulingConfigurer { + + public final CountDownLatch singleUseLatch = new CountDownLatch(1); + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.addFixedDelayTask(singleUseLatch::countDown, 500); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTask.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTask.java new file mode 100644 index 000000000..87003be13 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTask.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class TriggerTask implements Runnable { + + private final CountDownLatch latch = new CountDownLatch(1); + + @Scheduled(cron = "0/5 * * * * *") + @Override + public void run() { + latch.countDown(); + } + + public void blockUntilExecute() throws InterruptedException { + latch.await(5, TimeUnit.SECONDS); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTaskConfig.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTaskConfig.java new file mode 100644 index 000000000..c8fa9dc79 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTaskConfig.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class TriggerTaskConfig { + @Bean + public TriggerTask triggerTasks() { + return new TriggerTask(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/README.md new file mode 100644 index 000000000..298b376a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/README.md @@ -0,0 +1,89 @@ +# Manual Instrumentation for Spring-Web + +Provides OpenTelemetry instrumentation for Spring's RestTemplate. + +## Quickstart + +### Add these dependencies to your project. + +Replace `SPRING_VERSION` with the version of spring you're using. +`Minimum version: 3.1` + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://mvnrepository.com/artifact/io.opentelemetry). +`Minimum version: 0.17.0` + +For Maven add to your `pom.xml`: +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-spring-web-3.1 + OPENTELEMETRY_VERSION + + + + + io.opentelemetry + opentelemetry-exporters-logging + OPENTELEMETRY_VERSION + + + + + + org.springframework + spring-web + SPRING_VERSION + + + +``` + +For Gradle add to your dependencies: +```groovy +implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-web-3.1:OPENTELEMETRY_VERSION' +implementation 'io.opentelemetry:opentelemetry-exporters-logging:OPENTELEMETRY_VERSION' + +//this artifact should already be present in your application +implementation 'org.springframework:spring-web:SPRING_VERSION' +``` + +### Features + +#### RestTemplateInterceptor + +RestTemplateInterceptor adds OpenTelemetry client spans to requests sent using RestTemplate by implementing the [ClientHttpRequestInterceptor](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/client/ClientHttpRequestInterceptor.html) +interface. An example is shown below: + +##### Usage + +```java + +import io.opentelemetry.instrumentation.spring.httpclients.RestTemplateInterceptor; +import io.opentelemetry.api.OpenTelemetry; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(OpenTelemetry openTelemetry) { + + RestTemplate restTemplate = new RestTemplate(); + RestTemplateInterceptor restTemplateInterceptor = new RestTemplateInterceptor(openTelemetry); + restTemplate.getInterceptors().add(restTemplateInterceptor); + + return restTemplate; + } +} +``` + +### Starter Guide + +Check out the opentelemetry [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation. diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/spring-web-3.1-library.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/spring-web-3.1-library.gradle new file mode 100644 index 000000000..8fbc6c16f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/spring-web-3.1-library.gradle @@ -0,0 +1,13 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "org.springframework:spring-web:3.1.0.RELEASE" + + testImplementation "org.springframework:spring-web:3.1.0.RELEASE" + + testImplementation project(':testing-common') + testImplementation "org.assertj:assertj-core" + testImplementation "org.mockito:mockito-core" + testImplementation "org.mockito:mockito-junit-jupiter" + testImplementation "io.opentelemetry:opentelemetry-sdk-testing" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/HttpHeadersInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/HttpHeadersInjectAdapter.java new file mode 100644 index 000000000..557b9a957 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/HttpHeadersInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.httpclients; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.springframework.http.HttpHeaders; + +class HttpHeadersInjectAdapter implements TextMapSetter { + + public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); + + @Override + public void set(HttpHeaders carrier, String key, String value) { + carrier.set(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateInterceptor.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateInterceptor.java new file mode 100644 index 000000000..731aad453 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.httpclients; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +/** Wraps RestTemplate requests in a span. Adds the current span context to request headers. */ +public final class RestTemplateInterceptor implements ClientHttpRequestInterceptor { + + private final RestTemplateTracer tracer; + + // TODO: create a SpringWebTracing class that follows the new library instrumentation pattern + public RestTemplateInterceptor(OpenTelemetry openTelemetry) { + this.tracer = new RestTemplateTracer(openTelemetry); + } + + @Override + public ClientHttpResponse intercept( + HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + Context parentContext = Context.current(); + if (!tracer.shouldStartSpan(parentContext)) { + return execution.execute(request, body); + } + + Context context = tracer.startSpan(parentContext, request, request.getHeaders()); + try (Scope ignored = context.makeCurrent()) { + ClientHttpResponse response = execution.execute(request, body); + tracer.end(context, response); + return response; + } catch (Throwable t) { + tracer.endExceptionally(context, t); + throw t; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateTracer.java new file mode 100644 index 000000000..c63902ecd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateTracer.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.httpclients; + +import static io.opentelemetry.instrumentation.spring.httpclients.HttpHeadersInjectAdapter.SETTER; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.io.IOException; +import java.net.URI; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; + +class RestTemplateTracer extends HttpClientTracer { + RestTemplateTracer(OpenTelemetry openTelemetry) { + super(openTelemetry, new NetPeerAttributes()); + } + + @Override + protected String method(HttpRequest httpRequest) { + return httpRequest.getMethod().name(); + } + + @Override + protected URI url(HttpRequest request) { + return request.getURI(); + } + + @Override + protected Integer status(ClientHttpResponse response) { + try { + return response.getStatusCode().value(); + } catch (IOException e) { + return HttpStatus.INTERNAL_SERVER_ERROR.value(); + } + } + + @Override + protected String requestHeader(HttpRequest request, String name) { + return request.getHeaders().getFirst(name); + } + + @Override + protected String responseHeader(ClientHttpResponse response, String name) { + return response.getHeaders().getFirst(name); + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.spring-web-3.1"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/test/groovy/RestTemplateInstrumentationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/test/groovy/RestTemplateInstrumentationTest.groovy new file mode 100644 index 000000000..0f7847e9c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/test/groovy/RestTemplateInstrumentationTest.groovy @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.spring.httpclients.RestTemplateInterceptor +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.web.client.ResourceAccessException +import org.springframework.web.client.RestTemplate +import spock.lang.Shared + +class RestTemplateInstrumentationTest extends HttpClientTest> implements LibraryTestTrait { + @Shared + RestTemplate restTemplate + + def setupSpec() { + if (restTemplate == null) { + restTemplate = new RestTemplate() + restTemplate.getInterceptors().add(new RestTemplateInterceptor(getOpenTelemetry())) + } + } + + @Override + HttpEntity buildRequest(String method, URI uri, Map headers) { + def httpHeaders = new HttpHeaders() + headers.each { httpHeaders.put(it.key, [it.value]) } + return new HttpEntity(httpHeaders) + } + + @Override + int sendRequest(HttpEntity request, String method, URI uri, Map headers) { + try { + return restTemplate.exchange(uri, HttpMethod.valueOf(method), request, String) + .statusCode + .value() + } catch (ResourceAccessException exception) { + throw exception.getCause() + } + } + + @Override + void sendRequestWithCallback(HttpEntity request, String method, URI uri, Map headers, RequestResult requestResult) { + try { + restTemplate.execute(uri, HttpMethod.valueOf(method), { req -> + headers.forEach(req.getHeaders().&add) + }, { response -> + requestResult.complete(response.statusCode.value()) + }) + } catch (ResourceAccessException exception) { + requestResult.complete(exception.getCause()) + } + } + + @Override + boolean testCircularRedirects() { + false + } + + // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet + @Override + boolean testWithClientParent() { + false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/test/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateInterceptorTest.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/test/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateInterceptorTest.java new file mode 100644 index 000000000..147b99674 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-web-3.1/library/src/test/java/io/opentelemetry/instrumentation/spring/httpclients/RestTemplateInterceptorTest.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.httpclients; + +import static io.opentelemetry.instrumentation.testing.util.TraceUtils.withClientSpan; +import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat; +import static org.mockito.BDDMockito.then; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; + +@ExtendWith(MockitoExtension.class) +class RestTemplateInterceptorTest { + @RegisterExtension + static final LibraryInstrumentationExtension instrumentation = + LibraryInstrumentationExtension.create(); + + @Mock HttpRequest httpRequestMock; + @Mock ClientHttpRequestExecution requestExecutionMock; + static final byte[] requestBody = new byte[0]; + + @Test + void shouldSkipWhenContextHasClientSpan() throws Exception { + // given + RestTemplateInterceptor interceptor = + new RestTemplateInterceptor(instrumentation.getOpenTelemetry()); + + // when + withClientSpan( + "parent", + () -> { + interceptor.intercept(httpRequestMock, requestBody, requestExecutionMock); + }); + + // then + then(requestExecutionMock).should().execute(httpRequestMock, requestBody); + + assertThat(instrumentation.waitForTraces(1)) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.CLIENT))); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/spring-webflux-5.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/spring-webflux-5.0-javaagent.gradle new file mode 100644 index 000000000..d3a5abea0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/spring-webflux-5.0-javaagent.gradle @@ -0,0 +1,69 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + name = "webflux_5.0.0+_with_netty_0.8.0" + group = "org.springframework" + module = "spring-webflux" + versions = "[5.0.0.RELEASE,)" + assertInverse = true + extraDependency "io.projectreactor.netty:reactor-netty:0.8.0.RELEASE" + } + + pass { + name = "webflux_5.0.0_with_ipc_0.7.0" + group = "org.springframework" + module = "spring-webflux" + versions = "[5.0.0.RELEASE,)" + assertInverse = true + extraDependency "io.projectreactor.ipc:reactor-netty:0.7.0.RELEASE" + } + + pass { + name = "netty_0.8.0+_with_spring-webflux:5.1.0" + group = "io.projectreactor.netty" + module = "reactor-netty" + versions = "[0.8.0.RELEASE,)" + extraDependency "org.springframework:spring-webflux:5.1.0.RELEASE" + } + + pass { + name = "ipc_0.7.0+_with_spring-webflux:5.0.0" + group = "io.projectreactor.ipc" + module = "reactor-netty" + versions = "[0.7.0.RELEASE,)" + extraDependency "org.springframework:spring-webflux:5.0.0.RELEASE" + } +} + +dependencies { + implementation project(':instrumentation:spring:spring-webflux-5.0:library') + compileOnly "org.springframework:spring-webflux:5.0.0.RELEASE" + compileOnly "io.projectreactor.ipc:reactor-netty:0.7.0.RELEASE" + + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:reactor-3.1:javaagent') + testInstrumentation project(':instrumentation:reactor-netty:reactor-netty-1.0:javaagent') + + // Compile with both old and new netty packages since our test references both for old and + // latest dep tests. + testCompileOnly "io.projectreactor.ipc:reactor-netty:0.7.0.RELEASE" + testCompileOnly "io.projectreactor.netty:reactor-netty-http:1.0.7" + + testLibrary "org.springframework.boot:spring-boot-starter-webflux:2.0.0.RELEASE" + testLibrary "org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE" + testLibrary "org.springframework.boot:spring-boot-starter-reactor-netty:2.0.0.RELEASE" + testImplementation "org.spockframework:spock-spring:1.1-groovy-2.4" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs '-Dotel.instrumentation.spring-webflux.experimental-span-attributes=true' + // TODO(anuraaga): There is no actual context leak - it just seems that the server-side does not + // fully complete processing before the test cases finish, which is when we check for context + // leaks. Adding Thread.sleep(1000) just before checking for leaks allows it to pass but is not + // a good approach. Come up with a better one and enable this. + jvmArgs "-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=false" + + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/SpringWebfluxConfig.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/SpringWebfluxConfig.java new file mode 100644 index 000000000..02dfe9950 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/SpringWebfluxConfig.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux; + +import io.opentelemetry.instrumentation.api.config.Config; + +public class SpringWebfluxConfig { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBoolean( + "otel.instrumentation.spring-webflux.experimental-span-attributes", false); + + public static boolean captureExperimentalSpanAttributes() { + return CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebClientBuilderInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebClientBuilderInstrumentation.java new file mode 100644 index 000000000..d315049f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebClientBuilderInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.client; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.web.reactive.function.client.WebClient; + +public class WebClientBuilderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.web.reactive.function.client.WebClient"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface( + named("org.springframework.web.reactive.function.client.WebClient$Builder")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")), this.getClass().getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onBuild(@Advice.This WebClient.Builder builder) { + builder.filters(WebClientHelper::addFilter); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebClientHelper.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebClientHelper.java new file mode 100644 index 000000000..fdb095e9a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebClientHelper.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.client; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.spring.webflux.client.WebClientTracingFilter; +import java.util.List; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; + +public class WebClientHelper { + + public static void addFilter(List exchangeFilterFunctions) { + WebClientTracingFilter.addFilter(GlobalOpenTelemetry.get(), exchangeFilterFunctions); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebfluxClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebfluxClientInstrumentationModule.java new file mode 100644 index 000000000..0e6eb9aa7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/client/WebfluxClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.client; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class WebfluxClientInstrumentationModule extends InstrumentationModule { + + public WebfluxClientInstrumentationModule() { + super("spring-webflux", "spring-webflux-5.0", "spring-webflux-client"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new WebClientBuilderInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/AdviceUtils.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/AdviceUtils.java new file mode 100644 index 000000000..a5ed49c96 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/AdviceUtils.java @@ -0,0 +1,148 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.server; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.ClassNames; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.util.context.Context; + +public class AdviceUtils { + + public static final String CONTEXT_ATTRIBUTE = AdviceUtils.class.getName() + ".Context"; + + public static String spanNameForHandler(Object handler) { + String className = ClassNames.simpleName(handler.getClass()); + int lambdaIdx = className.indexOf("$$Lambda$"); + + if (lambdaIdx > -1) { + return className.substring(0, lambdaIdx) + ".lambda"; + } + return className + ".handle"; + } + + public static Mono setPublisherSpan( + Mono mono, io.opentelemetry.context.Context context) { + return mono.transform(finishSpanNextOrError(context)); + } + + /** + * Idea for this has been lifted from https://github.com/reactor/reactor-core/issues/947. Newer + * versions of reactor-core have easier way to access context but we want to support older + * versions. + */ + public static Function, ? extends Publisher> finishSpanNextOrError( + io.opentelemetry.context.Context context) { + return Operators.lift( + (scannable, subscriber) -> new SpanFinishingSubscriber<>(subscriber, context)); + } + + public static void finishSpanIfPresent(ServerWebExchange exchange, Throwable throwable) { + if (exchange != null) { + finishSpanIfPresentInAttributes(exchange.getAttributes(), throwable); + } + } + + public static void finishSpanIfPresent(ServerRequest serverRequest, Throwable throwable) { + if (serverRequest != null) { + finishSpanIfPresentInAttributes(serverRequest.attributes(), throwable); + } + } + + static void finishSpanIfPresent(io.opentelemetry.context.Context context, Throwable throwable) { + if (context != null) { + Span span = Span.fromContext(context); + if (throwable != null) { + span.setStatus(StatusCode.ERROR); + span.recordException(throwable); + } + span.end(); + } + } + + private static void finishSpanIfPresentInAttributes( + Map attributes, Throwable throwable) { + io.opentelemetry.context.Context context = + (io.opentelemetry.context.Context) attributes.remove(CONTEXT_ATTRIBUTE); + finishSpanIfPresent(context, throwable); + } + + public static class SpanFinishingSubscriber implements CoreSubscriber, Subscription { + + private final CoreSubscriber subscriber; + private final io.opentelemetry.context.Context otelContext; + private final Context context; + private final AtomicBoolean completed = new AtomicBoolean(); + private Subscription subscription; + + public SpanFinishingSubscriber( + CoreSubscriber subscriber, io.opentelemetry.context.Context otelContext) { + this.subscriber = subscriber; + this.otelContext = otelContext; + context = subscriber.currentContext().put(Span.class, otelContext); + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + try (Scope ignored = otelContext.makeCurrent()) { + subscriber.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + try (Scope ignored = otelContext.makeCurrent()) { + subscriber.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + if (completed.compareAndSet(false, true)) { + finishSpanIfPresent(otelContext, t); + } + subscriber.onError(t); + } + + @Override + public void onComplete() { + if (completed.compareAndSet(false, true)) { + finishSpanIfPresent(otelContext, null); + } + subscriber.onComplete(); + } + + @Override + public Context currentContext() { + return context; + } + + @Override + public void request(long n) { + subscription.request(n); + } + + @Override + public void cancel() { + if (completed.compareAndSet(false, true)) { + finishSpanIfPresent(otelContext, null); + } + subscription.cancel(); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/DispatcherHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/DispatcherHandlerInstrumentation.java new file mode 100644 index 000000000..b2e0981fa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/DispatcherHandlerInstrumentation.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.server; + +import static io.opentelemetry.javaagent.instrumentation.spring.webflux.server.SpringWebfluxHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +public class DispatcherHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.web.reactive.DispatcherHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("handle")) + .and(takesArgument(0, named("org.springframework.web.server.ServerWebExchange"))) + .and(takesArguments(1)), + this.getClass().getName() + "$HandleAdvice"); + } + + /** + * This is 'top level' advice for Webflux instrumentation. This handles creating and finishing + * Webflux span. + */ + @SuppressWarnings("unused") + public static class HandleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) ServerWebExchange exchange, + @Advice.Local("otelScope") Scope otelScope, + @Advice.Local("otelContext") Context otelContext) { + + otelContext = tracer().startSpan("DispatcherHandler.handle", SpanKind.INTERNAL); + // Unfortunately Netty EventLoop is not instrumented well enough to attribute all work to the + // right things so we have to store the context in request itself. + exchange.getAttributes().put(AdviceUtils.CONTEXT_ATTRIBUTE, otelContext); + + otelScope = otelContext.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Thrown Throwable throwable, + @Advice.Argument(0) ServerWebExchange exchange, + @Advice.Return(readOnly = false) Mono mono, + @Advice.Local("otelScope") Scope otelScope, + @Advice.Local("otelContext") Context otelContext) { + if (throwable == null && mono != null) { + mono = AdviceUtils.setPublisherSpan(mono, otelContext); + } else if (throwable != null) { + AdviceUtils.finishSpanIfPresent(exchange, throwable); + } + otelScope.close(); + // span finished in SpanFinishingSubscriber + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/HandlerAdapterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/HandlerAdapterInstrumentation.java new file mode 100644 index 000000000..c62125269 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/HandlerAdapterInstrumentation.java @@ -0,0 +1,119 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.server; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.spring.webflux.SpringWebfluxConfig; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.pattern.PathPattern; + +public class HandlerAdapterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.web.reactive.HandlerAdapter"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isAbstract()) + .and(implementsInterface(named("org.springframework.web.reactive.HandlerAdapter"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("handle")) + .and(takesArgument(0, named("org.springframework.web.server.ServerWebExchange"))) + .and(takesArgument(1, Object.class)) + .and(takesArguments(2)), + this.getClass().getName() + "$HandleAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) ServerWebExchange exchange, + @Advice.Argument(1) Object handler, + @Advice.Local("otelScope") Scope scope) { + + Context context = exchange.getAttribute(AdviceUtils.CONTEXT_ATTRIBUTE); + if (handler != null && context != null) { + Span span = Span.fromContext(context); + String handlerType; + String spanName; + + if (handler instanceof HandlerMethod) { + // Special case for requests mapped with annotations + HandlerMethod handlerMethod = (HandlerMethod) handler; + spanName = SpanNames.fromMethod(handlerMethod.getMethod()); + handlerType = handlerMethod.getMethod().getDeclaringClass().getName(); + } else { + spanName = AdviceUtils.spanNameForHandler(handler); + handlerType = handler.getClass().getName(); + } + + span.updateName(spanName); + if (SpringWebfluxConfig.captureExperimentalSpanAttributes()) { + span.setAttribute("spring-webflux.handler.type", handlerType); + } + + scope = context.makeCurrent(); + } + + if (context != null) { + Span serverSpan = ServerSpan.fromContextOrNull(context); + + PathPattern bestPattern = + exchange.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + if (serverSpan != null && bestPattern != null) { + serverSpan.updateName( + ServletContextPath.prepend(Context.current(), bestPattern.toString())); + } + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) ServerWebExchange exchange, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelScope") Scope scope) { + if (throwable != null) { + AdviceUtils.finishSpanIfPresent(exchange, throwable); + } + if (scope != null) { + scope.close(); + // span finished in SpanFinishingSubscriber + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/RouteOnSuccessOrError.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/RouteOnSuccessOrError.java new file mode 100644 index 000000000..d5b2ebcc4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/RouteOnSuccessOrError.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.server; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.instrumentation.spring.webflux.SpringWebfluxConfig; +import java.util.function.BiConsumer; +import java.util.regex.Pattern; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; + +public class RouteOnSuccessOrError implements BiConsumer, Throwable> { + + private static final Pattern SPECIAL_CHARACTERS_REGEX = Pattern.compile("[()&|]"); + private static final Pattern SPACES_REGEX = Pattern.compile("[ \\t]+"); + private static final Pattern METHOD_REGEX = + Pattern.compile("^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) "); + + private final RouterFunction routerFunction; + private final ServerRequest serverRequest; + + public RouteOnSuccessOrError(RouterFunction routerFunction, ServerRequest serverRequest) { + this.routerFunction = routerFunction; + this.serverRequest = serverRequest; + } + + @Override + public void accept(HandlerFunction handler, Throwable throwable) { + if (handler != null) { + String predicateString = parsePredicateString(); + if (predicateString != null) { + Context context = (Context) serverRequest.attributes().get(AdviceUtils.CONTEXT_ATTRIBUTE); + if (context != null) { + if (SpringWebfluxConfig.captureExperimentalSpanAttributes()) { + Span span = Span.fromContext(context); + span.setAttribute("spring-webflux.request.predicate", predicateString); + } + + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan != null) { + serverSpan.updateName(ServletContextPath.prepend(context, parseRoute(predicateString))); + } + } + } + } + } + + private String parsePredicateString() { + String routerFunctionString = routerFunction.toString(); + // Router functions containing lambda predicates should not end up in span tags since they are + // confusing + if (routerFunctionString.startsWith( + "org.springframework.web.reactive.function.server.RequestPredicates$$Lambda$")) { + return null; + } else { + return routerFunctionString.replaceFirst("\\s*->.*$", ""); + } + } + + private static String parseRoute(String routerString) { + return METHOD_REGEX + .matcher( + SPACES_REGEX + .matcher(SPECIAL_CHARACTERS_REGEX.matcher(routerString).replaceAll("")) + .replaceAll(" ") + .trim()) + .replaceAll(""); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/RouterFunctionInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/RouterFunctionInstrumentation.java new file mode 100644 index 000000000..66e2eecfd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/RouterFunctionInstrumentation.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.server; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import reactor.core.publisher.Mono; + +public class RouterFunctionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.web.reactive.function.server.ServerRequest"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isAbstract()) + .and( + extendsClass( + // TODO: this doesn't handle nested routes (DefaultNestedRouterFunction) + named( + "org.springframework.web.reactive.function.server.RouterFunctions$DefaultRouterFunction"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("route")) + .and( + takesArgument( + 0, named("org.springframework.web.reactive.function.server.ServerRequest"))) + .and(takesArguments(1)), + this.getClass().getName() + "$RouteAdvice"); + } + + /** + * This advice is responsible for setting additional span parameters for routes implemented with + * functional interface. + */ + @SuppressWarnings("unused") + public static class RouteAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.This RouterFunction thiz, + @Advice.Argument(0) ServerRequest serverRequest, + @Advice.Return(readOnly = false) Mono> result, + @Advice.Thrown Throwable throwable) { + if (throwable == null) { + result = result.doOnSuccessOrError(new RouteOnSuccessOrError(thiz, serverRequest)); + } else { + AdviceUtils.finishSpanIfPresent(serverRequest, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/SpringWebfluxHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/SpringWebfluxHttpServerTracer.java new file mode 100644 index 000000000..4329ce9e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/SpringWebfluxHttpServerTracer.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.server; + +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; + +public class SpringWebfluxHttpServerTracer extends BaseTracer { + private static final SpringWebfluxHttpServerTracer TRACER = new SpringWebfluxHttpServerTracer(); + + public static SpringWebfluxHttpServerTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-webflux-5.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/WebfluxServerInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/WebfluxServerInstrumentationModule.java new file mode 100644 index 000000000..ca5d9f743 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/server/WebfluxServerInstrumentationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.server; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class WebfluxServerInstrumentationModule extends InstrumentationModule { + + public WebfluxServerInstrumentationModule() { + super("spring-webflux", "spring-webflux-5.0", "spring-webflux-server"); + } + + @Override + public List typeInstrumentations() { + return asList( + new DispatcherHandlerInstrumentation(), + new HandlerAdapterInstrumentation(), + new RouterFunctionInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/SingleThreadedSpringWebfluxTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/SingleThreadedSpringWebfluxTest.groovy new file mode 100644 index 000000000..3d3799dd2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/SingleThreadedSpringWebfluxTest.groovy @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory +import org.springframework.boot.web.embedded.netty.NettyServerCustomizer +import org.springframework.context.annotation.Bean +import server.SpringWebFluxTestApplication +/** + * Run all Webflux tests under netty event loop having only 1 thread. + * Some of the bugs are better visible in this setup because same thread is reused + * for different requests. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = [SpringWebFluxTestApplication, ForceSingleThreadedNettyAutoConfiguration]) +class SingleThreadedSpringWebfluxTest extends SpringWebfluxTest { + + @TestConfiguration + static class ForceSingleThreadedNettyAutoConfiguration { + @Bean + NettyReactiveWebServerFactory nettyFactory() { + def factory = new NettyReactiveWebServerFactory() + factory.addServerCustomizers(customizer()) + return factory + } + } + + static NettyServerCustomizer customizer() { + if (Boolean.getBoolean("testLatestDeps")) { + return { builder -> builder.runOn(reactor.netty.resources.LoopResources.create("my-http", 1, true)) } + } + return { builder -> builder.loopResources(reactor.ipc.netty.resources.LoopResources.create("my-http", 1, true)) } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/SpringWebfluxTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/SpringWebfluxTest.groovy new file mode 100644 index 000000000..1715c07b3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/SpringWebfluxTest.groovy @@ -0,0 +1,555 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.client.ClientRequestContext +import io.opentelemetry.testing.internal.armeria.client.DecoratingHttpClientFunction +import io.opentelemetry.testing.internal.armeria.client.HttpClient +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames +import io.opentelemetry.testing.internal.armeria.common.HttpRequest +import io.opentelemetry.testing.internal.armeria.common.HttpResponse +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory +import org.springframework.boot.web.server.LocalServerPort +import org.springframework.context.annotation.Bean +import org.springframework.web.server.ResponseStatusException +import server.EchoHandlerFunction +import server.FooModel +import server.SpringWebFluxTestApplication +import server.TestController +import spock.lang.Unroll + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = [SpringWebFluxTestApplication, ForceNettyAutoConfiguration]) +class SpringWebfluxTest extends AgentInstrumentationSpecification { + @TestConfiguration + static class ForceNettyAutoConfiguration { + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory() + } + } + + static final String INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX = SpringWebFluxTestApplication.getName() + "\$" + static final String SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX = SpringWebFluxTestApplication.getSimpleName() + "\$" + + @LocalServerPort + int port + + WebClient client + + def setup() { + client = WebClient.builder("h1c://localhost:$port") + .decorator(new DecoratingHttpClientFunction() { + // https://github.com/line/armeria/issues/2489 + @Override + HttpResponse execute(HttpClient delegate, ClientRequestContext ctx, HttpRequest req) throws Exception { + return HttpResponse.from(delegate.execute(ctx, req).aggregate().thenApply {resp -> + if (resp.status().isRedirection()) { + return delegate.execute(ctx, HttpRequest.of(req.method(), resp.headers().get(HttpHeaderNames.LOCATION))) + } + return resp.toHttpResponse() + }) + } + }) + .build() + } + + @Unroll + def "Basic GET test #testName"() { + when: + def response = client.get(urlPath).aggregate().join() + + then: + response.status().code() == 200 + response.contentUtf8() == expectedResponseBody + assertTraces(1) { + trace(0, 2) { + span(0) { + name urlPathWithVariables + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$urlPath" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + if (annotatedMethod == null) { + // Functional API + nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") + } else { + // Annotation API + name TestController.getSimpleName() + "." + annotatedMethod + } + kind INTERNAL + childOf span(0) + attributes { + if (annotatedMethod == null) { + // Functional API + "spring-webflux.request.predicate" "(GET && $urlPathWithVariables)" + "spring-webflux.handler.type" { String tagVal -> + return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) + } + } else { + // Annotation API + "spring-webflux.handler.type" TestController.getName() + } + } + } + } + } + + where: + testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody + "functional API without parameters" | "/greet" | "/greet" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + "functional API with one parameter" | "/greet/WORLD" | "/greet/{name}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " WORLD" + "functional API with two parameters" | "/greet/World/Test1" | "/greet/{name}/{word}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " World Test1" + "functional API delayed response" | "/greet-delayed" | "/greet-delayed" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + + "annotation API without parameters" | "/foo" | "/foo" | "getFooModel" | new FooModel(0L, "DEFAULT").toString() + "annotation API with one parameter" | "/foo/1" | "/foo/{id}" | "getFooModel" | new FooModel(1L, "pass").toString() + "annotation API with two parameters" | "/foo/2/world" | "/foo/{id}/{name}" | "getFooModel" | new FooModel(2L, "world").toString() + "annotation API delayed response" | "/foo-delayed" | "/foo-delayed" | "getFooDelayed" | new FooModel(3L, "delayed").toString() + } + + @Unroll + def "GET test with async response #testName"() { + when: + def response = client.get(urlPath).aggregate().join() + + then: + response.status().code() == 200 + response.contentUtf8() == expectedResponseBody + assertTraces(1) { + trace(0, 3) { + span(0) { + name urlPathWithVariables + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$urlPath" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + if (annotatedMethod == null) { + // Functional API + nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") + } else { + // Annotation API + name TestController.getSimpleName() + "." + annotatedMethod + } + kind INTERNAL + childOf span(0) + attributes { + if (annotatedMethod == null) { + // Functional API + "spring-webflux.request.predicate" "(GET && $urlPathWithVariables)" + "spring-webflux.handler.type" { String tagVal -> + return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) + } + } else { + // Annotation API + "spring-webflux.handler.type" TestController.getName() + } + } + } + span(2) { + name "tracedMethod" + childOf span(0) + attributes { + } + } + } + } + + where: + testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody + "functional API traced method from mono" | "/greet-mono-from-callable/4" | "/greet-mono-from-callable/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 4" + "functional API traced method with delay" | "/greet-delayed-mono/6" | "/greet-delayed-mono/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 6" + + "annotation API traced method from mono" | "/foo-mono-from-callable/7" | "/foo-mono-from-callable/{id}" | "getMonoFromCallable" | new FooModel(7L, "tracedMethod").toString() + "annotation API traced method with delay" | "/foo-delayed-mono/9" | "/foo-delayed-mono/{id}" | "getFooDelayedMono" | new FooModel(9L, "tracedMethod").toString() + } + + /* + This test differs from the previous in one important aspect. + The test above calls endpoints which does not create any spans during their invocation. + They merely assemble reactive pipeline where some steps create spans. + Thus all those spans are created when WebFlux span created by DispatcherHandlerInstrumentation + has already finished. Therefore, they have `SERVER` span as their parent. + + This test below calls endpoints which do create spans right inside endpoint handler. + Therefore, in theory, those spans should have INTERNAL span created by DispatcherHandlerInstrumentation + as their parent. But there is a difference how Spring WebFlux handles functional endpoints + (created in server.SpringWebFluxTestApplication.greetRouterFunction) and annotated endpoints + (created in server.TestController). + In the former case org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter.handle + calls handler function directly. Thus "tracedMethod" span below has INTERNAL handler span as its parent. + In the latter case org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.handle + merely wraps handler call into Mono and thus actual invocation of handler function happens later, + when INTERNAL handler span has already finished. Thus, "tracedMethod" has SERVER Netty span as its parent. + */ + def "Create span during handler function"() { + when: + def response = client.get(urlPath).aggregate().join() + + then: + response.status().code() == 200 + response.contentUtf8() == expectedResponseBody + assertTraces(1) { + trace(0, 3) { + span(0) { + name urlPathWithVariables + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$urlPath" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + if (annotatedMethod == null) { + // Functional API + nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") + } else { + // Annotation API + name TestController.getSimpleName() + "." + annotatedMethod + } + kind INTERNAL + childOf span(0) + attributes { + if (annotatedMethod == null) { + // Functional API + "spring-webflux.request.predicate" "(GET && $urlPathWithVariables)" + "spring-webflux.handler.type" { String tagVal -> + return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) + } + } else { + // Annotation API + "spring-webflux.handler.type" TestController.getName() + } + } + } + span(2) { + name "tracedMethod" + childOf span(annotatedMethod ? 0 : 1) + attributes { + } + } + } + } + + where: + testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody + "functional API traced method" | "/greet-traced-method/5" | "/greet-traced-method/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 5" + "annotation API traced method" | "/foo-traced-method/8" | "/foo-traced-method/{id}" | "getTracedMethod" | new FooModel(8L, "tracedMethod").toString() + } + + def "404 GET test"() { + when: + def response = client.get("/notfoundgreet").aggregate().join() + + then: + response.status().code() == 404 + assertTraces(1) { + trace(0, 2) { + span(0) { + name "/**" + kind SERVER + hasNoParent() + status ERROR + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/notfoundgreet" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 404 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + name "ResourceWebHandler.handle" + kind INTERNAL + childOf span(0) + status ERROR + errorEvent(ResponseStatusException, String) + attributes { + "spring-webflux.handler.type" "org.springframework.web.reactive.resource.ResourceWebHandler" + } + } + } + } + } + + def "Basic POST test"() { + setup: + String echoString = "TEST" + when: + def response = client.post("/echo", echoString).aggregate().join() + + then: + response.status().code() == 202 + response.contentUtf8() == echoString + assertTraces(1) { + trace(0, 3) { + span(0) { + name "/echo" + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/echo" + "${SemanticAttributes.HTTP_METHOD.key}" "POST" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 202 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + name EchoHandlerFunction.getSimpleName() + ".handle" + kind INTERNAL + childOf span(0) + attributes { + "spring-webflux.request.predicate" "(POST && /echo)" + "spring-webflux.handler.type" { String tagVal -> + return tagVal.contains(EchoHandlerFunction.getName()) + } + } + } + span(2) { + name "echo" + childOf span(1) + attributes { + } + } + } + } + } + + @Unroll + def "GET to bad endpoint #testName"() { + when: + def response = client.get(urlPath).aggregate().join() + + then: + response.status().code() == 500 + assertTraces(1) { + trace(0, 2) { + span(0) { + name urlPathWithVariables + kind SERVER + status ERROR + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$urlPath" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 500 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + if (annotatedMethod == null) { + // Functional API + nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") + } else { + // Annotation API + name TestController.getSimpleName() + "." + annotatedMethod + } + kind INTERNAL + childOf span(0) + status ERROR + errorEvent(IllegalStateException, "bad things happen") + attributes { + if (annotatedMethod == null) { + // Functional API + "spring-webflux.request.predicate" "(GET && $urlPathWithVariables)" + "spring-webflux.handler.type" { String tagVal -> + return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) + } + } else { + // Annotation API + "spring-webflux.handler.type" TestController.getName() + } + } + } + } + } + + where: + testName | urlPath | urlPathWithVariables | annotatedMethod + "functional API fail fast" | "/greet-failfast/1" | "/greet-failfast/{id}" | null + "functional API fail Mono" | "/greet-failmono/1" | "/greet-failmono/{id}" | null + + "annotation API fail fast" | "/foo-failfast/1" | "/foo-failfast/{id}" | "getFooFailFast" + "annotation API fail Mono" | "/foo-failmono/1" | "/foo-failmono/{id}" | "getFooFailMono" + } + + def "Redirect test"() { + setup: + String finalUrl = "http://localhost:$port/double-greet" + + when: + def response = client.get("/double-greet-redirect").aggregate().join() + + then: + response.status().code() == 200 + assertTraces(2) { + // TODO: why order of spans is different in these traces? + trace(0, 2) { + span(0) { + name "/double-greet-redirect" + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/double-greet-redirect" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 307 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + name "RedirectComponent.lambda" + kind INTERNAL + childOf span(0) + attributes { + "spring-webflux.request.predicate" "(GET && /double-greet-redirect)" + "spring-webflux.handler.type" { String tagVal -> + return (tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) + || tagVal.contains("Lambda")) + } + } + } + } + trace(1, 2) { + span(0) { + name "/double-greet" + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" finalUrl + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + nameContains(SpringWebFluxTestApplication.getSimpleName() + "\$", ".handle") + kind INTERNAL + childOf span(0) + attributes { + "spring-webflux.request.predicate" "(GET && /double-greet)" + "spring-webflux.handler.type" { String tagVal -> + return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) + } + } + } + } + } + } + + @Unroll + def "Multiple GETs to delaying route #testName"() { + setup: + def requestsCount = 50 // Should be more than 2x CPUs to fish out some bugs + def url = "http://localhost:$port$urlPath" + when: + def responses = (0..requestsCount - 1).collect { client.get(urlPath).aggregate().join() } + + then: + responses.every { it.status().code() == 200 } + responses.every { it.contentUtf8() == expectedResponseBody } + assertTraces(responses.size()) { + responses.eachWithIndex { def response, int i -> + trace(i, 2) { + span(0) { + name urlPathWithVariables + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.HTTP_URL.key}" url + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + span(1) { + if (annotatedMethod == null) { + // Functional API + nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle") + } else { + // Annotation API + name TestController.getSimpleName() + "." + annotatedMethod + } + kind INTERNAL + childOf span(0) + attributes { + if (annotatedMethod == null) { + // Functional API + "spring-webflux.request.predicate" "(GET && $urlPathWithVariables)" + "spring-webflux.handler.type" { String tagVal -> + return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX) + } + } else { + // Annotation API + "spring-webflux.handler.type" TestController.getName() + } + } + } + } + } + } + + where: + testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody + "functional API delayed response" | "/greet-delayed" | "/greet-delayed" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + "annotation API delayed response" | "/foo-delayed" | "/foo-delayed" | "getFooDelayed" | new FooModel(3L, "delayed").toString() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebFluxSingleConnection.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebFluxSingleConnection.groovy new file mode 100644 index 000000000..9fc91c0ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebFluxSingleConnection.groovy @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import io.netty.channel.ChannelOption +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import org.springframework.http.HttpMethod +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.web.reactive.function.client.WebClient +import reactor.ipc.netty.http.client.HttpClientOptions +import reactor.ipc.netty.resources.PoolResources +import reactor.netty.http.client.HttpClient +import reactor.netty.resources.LoopResources + +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeoutException + +class SpringWebFluxSingleConnection implements SingleConnection { + private final ReactorClientHttpConnector connector + private final String host + private final int port + + SpringWebFluxSingleConnection(boolean isOldVersion, String host, int port) { + if (isOldVersion) { + connector = new ReactorClientHttpConnector({ HttpClientOptions.Builder clientOptions -> + clientOptions.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, HttpClientTest.CONNECT_TIMEOUT_MS) + clientOptions.poolResources(PoolResources.fixed("pool", 1, HttpClientTest.CONNECT_TIMEOUT_MS)) + }) + } else { + def httpClient = HttpClient.create().tcpConfiguration({ tcpClient -> + tcpClient.runOn(LoopResources.create("pool", 1, true)) + tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, HttpClientTest.CONNECT_TIMEOUT_MS) + }) + connector = new ReactorClientHttpConnector(httpClient) + } + + this.host = host + this.port = port + } + + @Override + int doRequest(String path, Map headers) throws ExecutionException, InterruptedException, TimeoutException { + String requestId = Objects.requireNonNull(headers.get(REQUEST_ID_HEADER)) + + URI uri + try { + uri = new URL("http", host, port, path).toURI() + } catch (MalformedURLException e) { + throw new ExecutionException(e) + } + + def request = WebClient.builder().clientConnector(connector).build().method(HttpMethod.GET) + .uri(uri) + .headers { h -> headers.forEach({ key, value -> h.add(key, value) }) } + + def response = request.exchange().block() + + String responseId = response.headers().asHttpHeaders().getFirst(REQUEST_ID_HEADER) + if (requestId != responseId) { + throw new IllegalStateException( + String.format("Received response with id %s, expected %s", responseId, requestId)) + } + + return response.statusCode().value() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebfluxHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebfluxHttpClientTest.groovy new file mode 100644 index 000000000..4fe8e643b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebfluxHttpClientTest.groovy @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import io.netty.channel.ChannelOption +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.SpanAssert +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import org.springframework.http.HttpMethod +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.web.reactive.function.client.WebClient + +class SpringWebfluxHttpClientTest extends HttpClientTest implements AgentTestTrait { + + @Override + WebClient.RequestBodySpec buildRequest(String method, URI uri, Map headers) { + def connector + if (isOldVersion()) { + connector = new ReactorClientHttpConnector({ clientOptions -> + clientOptions.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + }) + } else { + def httpClient = reactor.netty.http.client.HttpClient.create().tcpConfiguration({ tcpClient -> + tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + }) + connector = new ReactorClientHttpConnector(httpClient) + } + return WebClient.builder().clientConnector(connector).build().method(HttpMethod.resolve(method)) + .uri(uri) + .headers { h -> headers.forEach({ key, value -> h.add(key, value) }) } + } + + private static boolean isOldVersion() { + try { + Class.forName("reactor.netty.http.client.HttpClient") + return false + } catch (ClassNotFoundException exception) { + return true + } + } + + @Override + int sendRequest(WebClient.RequestBodySpec request, String method, URI uri, Map headers) { + return request.exchange().block().statusCode().value() + } + + @Override + void sendRequestWithCallback(WebClient.RequestBodySpec request, String method, URI uri, Map headers, RequestResult requestResult) { + request.exchange().subscribe({ + requestResult.complete(it.statusCode().value()) + }, { + requestResult.complete(it) + }) + } + + @Override + void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + if (!exception.getClass().getName().endsWith("WebClientRequestException")) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + if (!exception.getClass().getName().endsWith("AnnotatedConnectException")) { + exception = exception.getCause() + } + break + case "https://192.0.2.1/": // non routable address + exception = exception.getCause() + } + } + super.assertClientSpanErrorEvent(spanAssert, uri, exception) + } + + @Override + boolean testRedirects() { + false + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + return new SpringWebFluxSingleConnection(isOldVersion(), host, port) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/EchoHandler.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/EchoHandler.groovy new file mode 100644 index 000000000..808fbd3c4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/EchoHandler.groovy @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Tracer +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import reactor.core.publisher.Mono + +@Component +class EchoHandler { + + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test") + + Mono echo(ServerRequest request) { + tracer.spanBuilder("echo").startSpan().end() + return ServerResponse.accepted().contentType(MediaType.TEXT_PLAIN) + .body(request.bodyToMono(String), String) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/EchoHandlerFunction.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/EchoHandlerFunction.groovy new file mode 100644 index 000000000..b1ca5c3ca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/EchoHandlerFunction.groovy @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + + +import org.springframework.web.reactive.function.server.HandlerFunction +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import reactor.core.publisher.Mono + +class EchoHandlerFunction implements HandlerFunction { + + EchoHandler echoHandler + + EchoHandlerFunction(EchoHandler echoHandler) { + this.echoHandler = echoHandler + } + + @Override + Mono handle(ServerRequest request) { + return echoHandler.echo(request) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/FooModel.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/FooModel.groovy new file mode 100644 index 000000000..43c47f2a1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/FooModel.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +class FooModel { + public long id + public String name + + FooModel(long id, String name) { + this.id = id + this.name = name + } + + @Override + String toString() { + return "{\"id\":" + id + ",\"name\":\"" + name + "\"}" + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/SpringWebFluxTestApplication.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/SpringWebFluxTestApplication.groovy new file mode 100644 index 000000000..855d0c092 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/SpringWebFluxTestApplication.groovy @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET +import static org.springframework.web.reactive.function.server.RequestPredicates.POST +import static org.springframework.web.reactive.function.server.RouterFunctions.route + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Tracer +import java.time.Duration +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.FilterType +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.server.HandlerFunction +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import reactor.core.publisher.Mono + +@SpringBootApplication +@ComponentScan(basePackages = ["server"], excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "server.base.*")) +class SpringWebFluxTestApplication { + + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test") + + @Bean + RouterFunction echoRouterFunction(EchoHandler echoHandler) { + return route(POST("/echo"), new EchoHandlerFunction(echoHandler)) + } + + @Bean + RouterFunction greetRouterFunction(GreetingHandler greetingHandler) { + return route(GET("/greet"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return greetingHandler.defaultGreet() + } + }).andRoute(GET("/greet/{name}"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return greetingHandler.customGreet(request) + } + }).andRoute(GET("/greet/{name}/{word}"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return greetingHandler.customGreetWithWord(request) + } + }).andRoute(GET("/double-greet"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return greetingHandler.doubleGreet() + } + }).andRoute(GET("/greet-delayed"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return greetingHandler.defaultGreet().delayElement(Duration.ofMillis(100)) + } + }).andRoute(GET("/greet-failfast/{id}"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + throw new IllegalStateException("bad things happen") + } + }).andRoute(GET("/greet-failmono/{id}"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return Mono.error(new IllegalStateException("bad things happen")) + } + }).andRoute(GET("/greet-traced-method/{id}"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return greetingHandler.intResponse(Mono.just(tracedMethod(request.pathVariable("id").toInteger()))) + } + }).andRoute(GET("/greet-mono-from-callable/{id}"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return greetingHandler.intResponse(Mono.fromCallable { + return tracedMethod(request.pathVariable("id").toInteger()) + }) + } + }).andRoute(GET("/greet-delayed-mono/{id}"), new HandlerFunction() { + @Override + Mono handle(ServerRequest request) { + return greetingHandler.intResponse(Mono.just(request.pathVariable("id").toInteger()).delayElement(Duration.ofMillis(100)).map { i -> tracedMethod(i) }) + } + }) + } + + @Component + static class GreetingHandler { + static final String DEFAULT_RESPONSE = "HELLO" + + Mono defaultGreet() { + return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN) + .body(BodyInserters.fromObject(DEFAULT_RESPONSE)) + } + + Mono doubleGreet() { + return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN) + .body(BodyInserters.fromObject(DEFAULT_RESPONSE + DEFAULT_RESPONSE)) + } + + Mono customGreet(ServerRequest request) { + return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN) + .body(BodyInserters.fromObject(DEFAULT_RESPONSE + " " + request.pathVariable("name"))) + } + + Mono customGreetWithWord(ServerRequest request) { + return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN) + .body(BodyInserters.fromObject(DEFAULT_RESPONSE + " " + request.pathVariable("name") + " " + request.pathVariable("word"))) + } + + Mono intResponse(Mono mono) { + return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN) + .body(BodyInserters.fromPublisher(mono.map { i -> DEFAULT_RESPONSE + " " + i.id }, String)) + + } + } + + private static FooModel tracedMethod(long id) { + tracer.spanBuilder("tracedMethod").startSpan().end() + return new FooModel(id, "tracedMethod") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/TestController.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/TestController.groovy new file mode 100644 index 000000000..cd21cbcaa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/TestController.groovy @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Tracer +import java.time.Duration +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Mono + +@RestController +class TestController { + + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test") + + @GetMapping("/foo") + Mono getFooModel() { + return Mono.just(new FooModel(0L, "DEFAULT")) + } + + @GetMapping("/foo/{id}") + Mono getFooModel(@PathVariable("id") long id) { + return Mono.just(new FooModel(id, "pass")) + } + + @GetMapping("/foo/{id}/{name}") + Mono getFooModel(@PathVariable("id") long id, @PathVariable("name") String name) { + return Mono.just(new FooModel(id, name)) + } + + @GetMapping("/foo-delayed") + Mono getFooDelayed() { + return Mono.just(new FooModel(3L, "delayed")).delayElement(Duration.ofMillis(100)) + } + + @GetMapping("/foo-failfast/{id}") + Mono getFooFailFast(@PathVariable("id") long id) { + throw new IllegalStateException("bad things happen") + } + + @GetMapping("/foo-failmono/{id}") + Mono getFooFailMono(@PathVariable("id") long id) { + return Mono.error(new IllegalStateException("bad things happen")) + } + + @GetMapping("/foo-traced-method/{id}") + Mono getTracedMethod(@PathVariable("id") long id) { + return Mono.just(tracedMethod(id)) + } + + @GetMapping("/foo-mono-from-callable/{id}") + Mono getMonoFromCallable(@PathVariable("id") long id) { + return Mono.fromCallable { return tracedMethod(id) } + } + + @GetMapping("/foo-delayed-mono/{id}") + Mono getFooDelayedMono(@PathVariable("id") long id) { + return Mono.just(id).delayElement(Duration.ofMillis(100)).map { i -> tracedMethod(i) } + } + + private FooModel tracedMethod(long id) { + tracer.spanBuilder("tracedMethod").startSpan().end() + return new FooModel(id, "tracedMethod") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ControllerSpringWebFluxServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ControllerSpringWebFluxServerTest.groovy new file mode 100644 index 000000000..501d142ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ControllerSpringWebFluxServerTest.groovy @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND + +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import org.springframework.web.server.ResponseStatusException + +abstract class ControllerSpringWebFluxServerTest extends SpringWebFluxServerTest { + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method, HttpServerTest.ServerEndpoint endpoint) { + def handlerSpanName = "${ServerTestController.simpleName}.${endpoint.name().toLowerCase()}" + if (endpoint == NOT_FOUND) { + handlerSpanName = "ResourceWebHandler.handle" + } + trace.span(index) { + name handlerSpanName + kind INTERNAL + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(IllegalStateException, EXCEPTION.body) + } else if (endpoint == NOT_FOUND) { + status StatusCode.ERROR + if (Boolean.getBoolean("testLatestDeps")) { + errorEvent(ResponseStatusException, "404 NOT_FOUND") + } else { + errorEvent(ResponseStatusException, "Response status 404") + } + } + childOf((SpanData) parent) + } + } + + @Override + boolean hasHandlerAsControllerParentSpan(HttpServerTest.ServerEndpoint endpoint) { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/DelayedControllerSpringWebFluxServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/DelayedControllerSpringWebFluxServerTest.groovy new file mode 100644 index 000000000..d57dc6d67 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/DelayedControllerSpringWebFluxServerTest.groovy @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import java.time.Duration +import java.util.concurrent.Callable +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Mono + +/** + * Tests the case which uses annotated controller methods, and where "controller" span is created + * within a Mono map step, which follows a delay step. For exception endpoint, the exception + * is thrown within the last map step. + */ +class DelayedControllerSpringWebFluxServerTest extends ControllerSpringWebFluxServerTest { + @Override + protected Class getApplicationClass() { + return Application + } + + @Configuration + @EnableAutoConfiguration + static class Application { + @Bean + Controller controller() { + return new Controller() + } + + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory() + } + } + + @RestController + static class Controller extends ServerTestController { + @Override + protected Mono wrapControllerMethod( + HttpServerTest.ServerEndpoint endpoint, Callable handler) { + + return Mono.just("") + .delayElement(Duration.ofMillis(10)) + .map({ controller(endpoint, handler) }) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/DelayedHandlerSpringWebFluxServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/DelayedHandlerSpringWebFluxServerTest.groovy new file mode 100644 index 000000000..af6383564 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/DelayedHandlerSpringWebFluxServerTest.groovy @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import java.time.Duration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerResponse +import reactor.core.publisher.Mono + +/** + * Tests the case which uses route handlers, and where "controller" span is created within a Mono + * map step, which follows a delay step. For exception endpoint, the exception is thrown within the + * last map step. + */ +class DelayedHandlerSpringWebFluxServerTest extends HandlerSpringWebFluxServerTest { + @Override + protected Class getApplicationClass() { + return Application + } + + @Configuration + @EnableAutoConfiguration + static class Application { + @Bean + RouterFunction router() { + return new RouteFactory().createRoutes() + } + + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory() + } + } + + static class RouteFactory extends ServerTestRouteFactory { + + @Override + protected Mono wrapResponse(HttpServerTest.ServerEndpoint endpoint, Mono response, Runnable spanAction) { + return response.delayElement(Duration.ofMillis(10)).map({ original -> + return controller(endpoint, { + spanAction.run() + return original + }) + }) + } + } + + @Override + boolean hasHandlerAsControllerParentSpan(HttpServerTest.ServerEndpoint endpoint) { + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/HandlerSpringWebFluxServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/HandlerSpringWebFluxServerTest.groovy new file mode 100644 index 000000000..4c2b1f123 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/HandlerSpringWebFluxServerTest.groovy @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND + +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import org.springframework.web.server.ResponseStatusException + +abstract class HandlerSpringWebFluxServerTest extends SpringWebFluxServerTest { + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method, HttpServerTest.ServerEndpoint endpoint) { + def handlerSpanName = "${ServerTestRouteFactory.simpleName}.lambda" + if (endpoint == NOT_FOUND) { + handlerSpanName = "ResourceWebHandler.handle" + } + trace.span(index) { + name handlerSpanName + kind INTERNAL + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(IllegalStateException, EXCEPTION.body) + } else if (endpoint == NOT_FOUND) { + status StatusCode.ERROR + if (Boolean.getBoolean("testLatestDeps")) { + errorEvent(ResponseStatusException, "404 NOT_FOUND") + } else { + errorEvent(ResponseStatusException, "Response status 404") + } + } + childOf((SpanData) parent) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ImmediateControllerSpringWebFluxServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ImmediateControllerSpringWebFluxServerTest.groovy new file mode 100644 index 000000000..7ea4599b3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ImmediateControllerSpringWebFluxServerTest.groovy @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import java.util.concurrent.Callable +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Mono + +/** + * Tests the case where "controller" span is created within the controller method scope, and the + * Mono from a handler is already a fully constructed response with no deferred actions. + * For exception endpoint, the exception is thrown within controller method scope. + */ +class ImmediateControllerSpringWebFluxServerTest extends ControllerSpringWebFluxServerTest { + @Override + protected Class getApplicationClass() { + return Application + } + + @Configuration + @EnableAutoConfiguration + static class Application { + @Bean + Controller controller() { + return new Controller() + } + + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory() + } + } + + @RestController + static class Controller extends ServerTestController { + @Override + protected Mono wrapControllerMethod(HttpServerTest.ServerEndpoint endpoint, Callable controllerMethod) { + return Mono.just(controller(endpoint, controllerMethod)) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ImmediateHandlerSpringWebFluxServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ImmediateHandlerSpringWebFluxServerTest.groovy new file mode 100644 index 000000000..a1433ea24 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/ImmediateHandlerSpringWebFluxServerTest.groovy @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerResponse +import reactor.core.publisher.Mono + +/** + * Tests the case where "controller" span is created within the route handler method scope, and + * the Mono from a handler is already a fully constructed response with no deferred + * actions. For exception endpoint, the exception is thrown within route handler method scope. + */ +class ImmediateHandlerSpringWebFluxServerTest extends HandlerSpringWebFluxServerTest { + @Override + protected Class getApplicationClass() { + return Application + } + + @Configuration + @EnableAutoConfiguration + static class Application { + @Bean + RouterFunction router() { + return new RouteFactory().createRoutes() + } + + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory() + } + } + + static class RouteFactory extends ServerTestRouteFactory { + + @Override + protected Mono wrapResponse(HttpServerTest.ServerEndpoint endpoint, Mono response, Runnable spanAction) { + return controller(endpoint, { + spanAction.run() + return response + }) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/SpringWebFluxServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/SpringWebFluxServerTest.groovy new file mode 100644 index 000000000..6e976ee5c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/SpringWebFluxServerTest.groovy @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext + +abstract class SpringWebFluxServerTest extends HttpServerTest implements AgentTestTrait { + protected abstract Class getApplicationClass(); + + @Override + ConfigurableApplicationContext startServer(int port) { + def app = new SpringApplication(getApplicationClass()) + app.setDefaultProperties([ + "server.port" : port, + "server.context-path" : getContextPath(), + "server.servlet.contextPath" : getContextPath(), + "server.error.include-message": "always"]) + def context = app.run() + return context + } + + @Override + void stopServer(ConfigurableApplicationContext ctx) { + ctx.close() + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return getContextPath() + "/path/{id}/param" + case NOT_FOUND: + return "/**" + default: + return super.expectedServerSpanName(endpoint) + } + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + return true + } + + @Override + boolean testPathParam() { + return true + } + + @Override + boolean testConcurrency() { + return true + } + + @Override + Class expectedExceptionClass() { + return IllegalStateException + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/package-info.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/package-info.groovy new file mode 100644 index 000000000..38a003ec0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/server/base/package-info.groovy @@ -0,0 +1,5 @@ +/** + * The classes in this package are specific to tests that extend + * {@link io.opentelemetry.instrumentation.test.base.HttpServerTest}. + */ +package server.base diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/RedirectComponent.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/RedirectComponent.java new file mode 100644 index 000000000..fef1801cf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/RedirectComponent.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import java.net.URI; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +// Need to keep this in Java because groovy creates crazy proxies around lambdas +@Component +public class RedirectComponent { + @Bean + public RouterFunction redirectRouterFunction() { + return route( + GET("/double-greet-redirect"), + req -> ServerResponse.temporaryRedirect(URI.create("/double-greet")).build()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestController.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestController.java new file mode 100644 index 000000000..577827046 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestController.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base; + +import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint; +import java.net.URI; +import java.util.concurrent.Callable; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import reactor.core.publisher.Mono; + +public abstract class ServerTestController { + @GetMapping("/success") + public Mono success(ServerHttpResponse response) { + ServerEndpoint endpoint = ServerEndpoint.SUCCESS; + + return wrapControllerMethod( + endpoint, + () -> { + setStatus(response, endpoint); + return endpoint.getBody(); + }); + } + + @GetMapping("/query") + public Mono query_param(ServerHttpRequest request, ServerHttpResponse response) { + ServerEndpoint endpoint = ServerEndpoint.QUERY_PARAM; + + return wrapControllerMethod( + endpoint, + () -> { + setStatus(response, endpoint); + return request.getURI().getRawQuery(); + }); + } + + @GetMapping("/redirect") + public Mono redirect(ServerHttpResponse response) { + ServerEndpoint endpoint = ServerEndpoint.REDIRECT; + + return wrapControllerMethod( + endpoint, + () -> { + setStatus(response, endpoint); + response.getHeaders().setLocation(URI.create(endpoint.getBody())); + return ""; + }); + } + + @GetMapping("/error-status") + Mono error(ServerHttpResponse response) { + ServerEndpoint endpoint = ServerEndpoint.ERROR; + + return wrapControllerMethod( + endpoint, + () -> { + setStatus(response, endpoint); + return endpoint.getBody(); + }); + } + + @GetMapping("/exception") + Mono exception() { + ServerEndpoint endpoint = ServerEndpoint.EXCEPTION; + + return wrapControllerMethod( + endpoint, + () -> { + throw new IllegalStateException(endpoint.getBody()); + }); + } + + @GetMapping("/path/{id}/param") + Mono path_param(ServerHttpResponse response, @PathVariable("id") String id) { + ServerEndpoint endpoint = ServerEndpoint.PATH_PARAM; + + return wrapControllerMethod( + endpoint, + () -> { + setStatus(response, endpoint); + return id; + }); + } + + @GetMapping("/child") + Mono indexed_child(ServerHttpRequest request, ServerHttpResponse response) { + ServerEndpoint endpoint = ServerEndpoint.INDEXED_CHILD; + + return wrapControllerMethod( + endpoint, + () -> { + endpoint.collectSpanAttributes(it -> request.getQueryParams().getFirst(it)); + setStatus(response, endpoint); + return ""; + }); + } + + protected abstract Mono wrapControllerMethod(ServerEndpoint endpoint, Callable handler); + + private static void setStatus(ServerHttpResponse response, ServerEndpoint endpoint) { + response.setStatusCode(HttpStatus.resolve(endpoint.getStatus())); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestRouteFactory.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestRouteFactory.java new file mode 100644 index 000000000..fb2028a13 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestRouteFactory.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server.base; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder; +import reactor.core.publisher.Mono; + +public abstract class ServerTestRouteFactory { + public RouterFunction createRoutes() { + return route( + GET("/success"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.SUCCESS; + + return respond(endpoint, null, null, null); + }) + .andRoute( + GET("/query"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.QUERY_PARAM; + + return respond(endpoint, null, request.uri().getRawQuery(), null); + }) + .andRoute( + GET("/redirect"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.REDIRECT; + + return respond( + endpoint, + ServerResponse.status(endpoint.getStatus()) + .header(HttpHeaders.LOCATION, endpoint.getBody()), + "", + null); + }) + .andRoute( + GET("/error-status"), + redirect -> { + ServerEndpoint endpoint = ServerEndpoint.ERROR; + + return respond(endpoint, null, null, null); + }) + .andRoute( + GET("/exception"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.EXCEPTION; + + return respond( + endpoint, + ServerResponse.ok(), + "", + () -> { + throw new IllegalStateException(endpoint.getBody()); + }); + }) + .andRoute( + GET("/path/{id}/param"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.PATH_PARAM; + + return respond(endpoint, null, request.pathVariable("id"), null); + }) + .andRoute( + GET("/child"), + request -> { + ServerEndpoint endpoint = ServerEndpoint.INDEXED_CHILD; + + return respond( + endpoint, + null, + null, + () -> + Span.current() + .setAttribute( + "test.request.id", Long.parseLong(request.queryParam("id").get()))); + }); + } + + protected Mono respond( + ServerEndpoint endpoint, BodyBuilder bodyBuilder, String body, Runnable spanAction) { + if (bodyBuilder == null) { + bodyBuilder = ServerResponse.status(endpoint.getStatus()); + } + if (body == null) { + body = endpoint.getBody() != null ? endpoint.getBody() : ""; + } + if (spanAction == null) { + spanAction = () -> {}; + } + + return wrapResponse(endpoint, bodyBuilder.syncBody(body), spanAction); + } + + protected abstract Mono wrapResponse( + ServerEndpoint endpoint, Mono response, Runnable spanAction); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/resources/logback.xml new file mode 100644 index 000000000..7f2406629 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/NOTICE.txt b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/NOTICE.txt new file mode 100644 index 000000000..83bc71784 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/NOTICE.txt @@ -0,0 +1,19 @@ +This product contains a modified part of Spring Cloud Sleuth: + + * License: + + Copyright 2013-2020 the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + * Homepage: https://github.com/spring-cloud/spring-cloud-sleuth \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/README.md new file mode 100644 index 000000000..d63208c62 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/README.md @@ -0,0 +1,96 @@ +# Manual Instrumentation for Spring Webflux + +Provides OpenTelemetry instrumentation for Spring's WebClient. + +## Quickstart + +### Add these dependencies to your project. + +Replace `SPRING_VERSION` with the version of spring you're using. +`Minimum version: 5.0` + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://mvnrepository.com/artifact/io.opentelemetry). +`Minimum version: 0.8.0` + + +For Maven add to your `pom.xml`: + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-spring-webflux-5.0 + OPENTELEMETRY_VERSION + + + + + + io.opentelemetry + opentelemetry-exporters-logging + OPENTELEMETRY_VERSION + + + + + + org.springframework + spring-webflux + SPRING_VERSION + + + +``` + +For Gradle add to your dependencies: + +```groovy +// opentelemetry instrumentation +implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-webflux-5.0:OPENTELEMETRY_VERSION' + +// opentelemetry exporter +// replace this default exporter with your opentelemetry exporter (ex. otlp/zipkin/jaeger/..) +implementation 'io.opentelemetry:opentelemetry-exporters-logging:OPENTELEMETRY_VERSION' + +// required to instrument spring-webmvc +// this artifact should already be present in your application +implementation 'org.springframework:spring-webflux:SPRING_VERSION' +``` + +### Features + +#### WebClientTracingFilter + +WebClientTracingFilter adds OpenTelemetry client spans to requests sent using WebClient by implementing the [ExchangeFilterFunction](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ExchangeFilterFunction.html) +interface. An example is shown below: + +##### Usage + +```java + +import io.opentelemetry.instrumentation.spring.webflux.client.WebClientTracingFilter +import io.opentelemetry.api.trace.Tracer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient.Builder webClient(Tracer tracer) { + + WebClient webClient = WebClient.create(); + WebClientTracingFilter webClientTracingFilter = new WebClientTracingFilter(tracer); + + return webClient.mutate().filter(webClientTracingFilter); + } +} +``` + +### Starter Guide + +Check out the opentelemetry [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation. diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/spring-webflux-5.0-library.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/spring-webflux-5.0-library.gradle new file mode 100644 index 000000000..fcc408c12 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/spring-webflux-5.0-library.gradle @@ -0,0 +1,6 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + compileOnly "org.springframework:spring-webflux:5.0.0.RELEASE" + compileOnly "io.projectreactor.ipc:reactor-netty:0.7.0.RELEASE" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/HttpHeadersInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/HttpHeadersInjectAdapter.java new file mode 100644 index 000000000..518b7d4bb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/HttpHeadersInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.client; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.springframework.web.reactive.function.client.ClientRequest; + +class HttpHeadersInjectAdapter implements TextMapSetter { + + static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); + + @Override + public void set(ClientRequest.Builder carrier, String key, String value) { + carrier.headers(httpHeaders -> httpHeaders.set(key, value)); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/SpringWebfluxHttpClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/SpringWebfluxHttpClientTracer.java new file mode 100644 index 000000000..fa422d8b4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/SpringWebfluxHttpClientTracer.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.client; + +import static io.opentelemetry.instrumentation.spring.webflux.client.HttpHeadersInjectAdapter.SETTER; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.net.URI; +import java.util.List; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; + +class SpringWebfluxHttpClientTracer + extends HttpClientTracer { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBoolean( + "otel.instrumentation.spring-webflux.experimental-span-attributes", false); + + SpringWebfluxHttpClientTracer(OpenTelemetry openTelemetry) { + super(openTelemetry, new NetPeerAttributes()); + } + + private static final MethodHandle RAW_STATUS_CODE = findRawStatusCode(); + + void onCancel(Context context) { + if (captureExperimentalSpanAttributes()) { + Span span = Span.fromContext(context); + span.setAttribute("spring-webflux.event", "cancelled"); + span.setAttribute("spring-webflux.message", "The subscription was cancelled"); + } + } + + @Override + protected String method(ClientRequest httpRequest) { + return httpRequest.method().name(); + } + + @Override + protected URI url(ClientRequest httpRequest) { + return httpRequest.url(); + } + + @Override + protected Integer status(ClientResponse httpResponse) { + if (RAW_STATUS_CODE != null) { + // rawStatusCode() method was introduced in webflux 5.1 + try { + return (int) RAW_STATUS_CODE.invokeExact(httpResponse); + } catch (Throwable ignored) { + // Ignore + } + } + // prior to webflux 5.1, the best we can get is HttpStatus enum, which only covers standard + // status codes + return httpResponse.statusCode().value(); + } + + @Override + protected String requestHeader(ClientRequest clientRequest, String name) { + return clientRequest.headers().getFirst(name); + } + + @Override + protected String responseHeader(ClientResponse clientResponse, String name) { + List headers = clientResponse.headers().header(name); + return !headers.isEmpty() ? headers.get(0) : null; + } + + @Override + protected TextMapSetter getSetter() { + return SETTER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-webflux-5.0"; + } + + // rawStatusCode() method was introduced in webflux 5.1 + // prior to this method, the best we can get is HttpStatus enum, which only covers standard status + // codes (see usage above) + private static MethodHandle findRawStatusCode() { + try { + return MethodHandles.publicLookup() + .findVirtual(ClientResponse.class, "rawStatusCode", MethodType.methodType(int.class)); + } catch (IllegalAccessException | NoSuchMethodException e) { + return null; + } + } + + private static boolean captureExperimentalSpanAttributes() { + return CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/TraceWebClientSubscriber.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/TraceWebClientSubscriber.java new file mode 100644 index 000000000..a20c0be7c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/TraceWebClientSubscriber.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.client; + +import io.opentelemetry.context.Scope; +import org.reactivestreams.Subscription; +import org.springframework.web.reactive.function.client.ClientResponse; +import reactor.core.CoreSubscriber; + +/** + * Based on Spring Sleuth's Reactor instrumentation. + * https://github.com/spring-cloud/spring-cloud-sleuth/blob/master/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceWebClientBeanPostProcessor.java + */ +final class TraceWebClientSubscriber implements CoreSubscriber { + + private final SpringWebfluxHttpClientTracer tracer; + + private final CoreSubscriber actual; + + private final reactor.util.context.Context context; + + private final io.opentelemetry.context.Context tracingContext; + + TraceWebClientSubscriber( + SpringWebfluxHttpClientTracer tracer, + CoreSubscriber actual, + io.opentelemetry.context.Context tracingContext) { + this.tracer = tracer; + this.actual = actual; + this.tracingContext = tracingContext; + this.context = actual.currentContext(); + } + + @Override + public void onSubscribe(Subscription subscription) { + this.actual.onSubscribe(subscription); + } + + @Override + public void onNext(ClientResponse response) { + try (Scope ignored = tracingContext.makeCurrent()) { + this.actual.onNext(response); + } finally { + tracer.end(tracingContext, response); + } + } + + @Override + public void onError(Throwable t) { + try (Scope ignored = tracingContext.makeCurrent()) { + this.actual.onError(t); + } finally { + tracer.endExceptionally(tracingContext, t); + } + } + + @Override + public void onComplete() { + try (Scope ignored = tracingContext.makeCurrent()) { + this.actual.onComplete(); + } + } + + @Override + public reactor.util.context.Context currentContext() { + return this.context; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/WebClientTracingFilter.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/WebClientTracingFilter.java new file mode 100644 index 000000000..902caf3ae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webflux-5.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/WebClientTracingFilter.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.client; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.List; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Mono; + +/** + * Based on Spring Sleuth's Reactor instrumentation. + * https://github.com/spring-cloud/spring-cloud-sleuth/blob/master/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceWebClientBeanPostProcessor.java + */ +public class WebClientTracingFilter implements ExchangeFilterFunction { + + private final SpringWebfluxHttpClientTracer tracer; + + private WebClientTracingFilter(SpringWebfluxHttpClientTracer tracer) { + this.tracer = tracer; + } + + public static void addFilter( + OpenTelemetry openTelemetry, List exchangeFilterFunctions) { + for (ExchangeFilterFunction filterFunction : exchangeFilterFunctions) { + if (filterFunction instanceof WebClientTracingFilter) { + return; + } + } + exchangeFilterFunctions.add( + 0, new WebClientTracingFilter(new SpringWebfluxHttpClientTracer(openTelemetry))); + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + return new MonoWebClientTrace(tracer, request, next); + } + + private static final class MonoWebClientTrace extends Mono { + + private final SpringWebfluxHttpClientTracer tracer; + private final ExchangeFunction next; + private final ClientRequest request; + + private MonoWebClientTrace( + SpringWebfluxHttpClientTracer tracer, ClientRequest request, ExchangeFunction next) { + this.tracer = tracer; + this.next = next; + this.request = request; + } + + @Override + public void subscribe(CoreSubscriber subscriber) { + Context parentContext = Context.current(); + if (!tracer.shouldStartSpan(parentContext)) { + next.exchange(request).subscribe(subscriber); + return; + } + + ClientRequest.Builder builder = ClientRequest.from(request); + Context context = tracer.startSpan(parentContext, request, builder); + try (Scope ignored = context.makeCurrent()) { + this.next + .exchange(builder.build()) + .doOnCancel( + () -> { + tracer.onCancel(context); + tracer.end(context); + }) + .subscribe(new TraceWebClientSubscriber(tracer, subscriber, context)); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/spring-webmvc-3.1-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/spring-webmvc-3.1-javaagent.gradle new file mode 100644 index 000000000..2ecf26bd8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/spring-webmvc-3.1-javaagent.gradle @@ -0,0 +1,63 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'org.springframework' + module = 'spring-webmvc' + versions = "[3.1.0.RELEASE,]" + // these versions depend on org.springframework:spring-web which has a bad dependency on + // javax.faces:jsf-api:1.1 which was released as pom only + skip('1.2.1', '1.2.2', '1.2.3', '1.2.4') + // 3.2.1.RELEASE has transitive dependencies like spring-web as "provided" instead of "compile" + skip('3.2.1.RELEASE') + extraDependency "javax.servlet:javax.servlet-api:3.0.1" + assertInverse = true + } + + // FIXME: webmvc depends on web, so we need a separate instrumentation for spring-web specifically. + fail { + group = 'org.springframework' + module = 'spring-web' + versions = "[,]" + // these versions depend on org.springframework:spring-web which has a bad dependency on + // javax.faces:jsf-api:1.1 which was released as pom only + skip('1.2.1', '1.2.2', '1.2.3', '1.2.4') + extraDependency "javax.servlet:javax.servlet-api:3.0.1" + } +} + +dependencies { + compileOnly "org.springframework:spring-webmvc:3.1.0.RELEASE" + compileOnly "javax.servlet:javax.servlet-api:3.1.0" +// compileOnly "org.springframework:spring-webmvc:2.5.6" +// compileOnly "javax.servlet:servlet-api:2.4" + + testImplementation(project(':testing-common')) { + exclude(module: 'jetty-server') // incompatible servlet api + } + + // Include servlet instrumentation for verifying the tomcat requests + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + testInstrumentation project(':instrumentation:tomcat:tomcat-7.0:javaagent') + + testImplementation "javax.validation:validation-api:1.1.0.Final" + testImplementation "org.hibernate:hibernate-validator:5.4.2.Final" + + testImplementation "org.spockframework:spock-spring:${versions["org.spockframework"]}" + + testLibrary "org.springframework.boot:spring-boot-starter-test:1.5.17.RELEASE" + testLibrary "org.springframework.boot:spring-boot-starter-web:1.5.17.RELEASE" + testLibrary "org.springframework.boot:spring-boot-starter-security:1.5.17.RELEASE" + + testImplementation "org.springframework.security.oauth:spring-security-oauth2:2.0.16.RELEASE" + + // For spring security + testImplementation "jakarta.xml.bind:jakarta.xml.bind-api:2.3.2" + testImplementation "org.glassfish.jaxb:jaxb-runtime:2.3.2" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs '-Dotel.instrumentation.spring-webmvc.experimental-span-attributes=true' +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/DispatcherServletInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/DispatcherServletInstrumentation.java new file mode 100644 index 000000000..a06a90cbd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/DispatcherServletInstrumentation.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.springwebmvc.SpringWebMvcSingletons.modelAndViewInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.context.ApplicationContext; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.ModelAndView; + +public class DispatcherServletInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.web.servlet.DispatcherServlet"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isProtected()) + .and(named("onRefresh")) + .and(takesArgument(0, named("org.springframework.context.ApplicationContext"))) + .and(takesArguments(1)), + DispatcherServletInstrumentation.class.getName() + "$HandlerMappingAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(isProtected()) + .and(named("render")) + .and(takesArgument(0, named("org.springframework.web.servlet.ModelAndView"))), + DispatcherServletInstrumentation.class.getName() + "$RenderAdvice"); + } + + /** + * This advice creates a filter that has reference to the handlerMappings from DispatcherServlet + * which allows the mappings to be evaluated at the beginning of the filter chain. This evaluation + * is done inside the Servlet3Decorator.onContext method. + */ + @SuppressWarnings("unused") + public static class HandlerMappingAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void afterRefresh( + @Advice.Argument(0) ApplicationContext springCtx, + @Advice.FieldValue("handlerMappings") List handlerMappings) { + if (springCtx.containsBean("otelAutoDispatcherFilter")) { + HandlerMappingResourceNameFilter filter = + (HandlerMappingResourceNameFilter) springCtx.getBean("otelAutoDispatcherFilter"); + if (handlerMappings != null && filter != null) { + filter.setHandlerMappings(handlerMappings); + } + } + } + } + + @SuppressWarnings("unused") + public static class RenderAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) ModelAndView mv, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (modelAndViewInstrumenter().shouldStart(parentContext, mv)) { + context = modelAndViewInstrumenter().start(parentContext, mv); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(0) ModelAndView mv, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + modelAndViewInstrumenter().end(context, mv, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerAdapterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerAdapterInstrumentation.java new file mode 100644 index 000000000..2556ee3dc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerAdapterInstrumentation.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.springwebmvc.SpringWebMvcSingletons.handlerInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import javax.servlet.http.HttpServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HandlerAdapterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.web.servlet.HandlerAdapter"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.springframework.web.servlet.HandlerAdapter")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(nameStartsWith("handle")) + .and(takesArgument(0, named("javax.servlet.http.HttpServletRequest"))) + .and(takesArguments(3)), + HandlerAdapterInstrumentation.class.getName() + "$ControllerAdvice"); + } + + @SuppressWarnings("unused") + public static class ControllerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void nameResourceAndStartSpan( + @Advice.Argument(0) HttpServletRequest request, + @Advice.Argument(2) Object handler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + // TODO (trask) should there be a way to customize Instrumenter.shouldStart()? + if (handler.getClass().getName().startsWith("org.grails.")) { + // skip creating handler span for grails, grails instrumentation will take care of it + return; + } + Context parentContext = Java8BytecodeBridge.currentContext(); + Span serverSpan = ServerSpan.fromContextOrNull(parentContext); + // TODO (trask) is it important to check serverSpan != null here? + if (serverSpan != null) { + // Name the parent span based on the matching pattern + ServerNameUpdater.updateServerSpanName(parentContext, request); + // Now create a span for handler/controller execution. + context = handlerInstrumenter().start(parentContext, handler); + if (context != null) { + scope = context.makeCurrent(); + } + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(2) Object handler, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + handlerInstrumenter().end(context, handler, null, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerMappingResourceNameFilter.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerMappingResourceNameFilter.java new file mode 100644 index 000000000..7a333d31d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerMappingResourceNameFilter.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import io.opentelemetry.context.Context; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.Ordered; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +public class HandlerMappingResourceNameFilter implements Filter, Ordered { + private volatile List handlerMappings; + + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + filterChain.doFilter(request, response); + return; + } + + Context context = Context.current(); + + if (handlerMappings != null) { + try { + if (findMapping((HttpServletRequest) request)) { + + // Name the parent span based on the matching pattern + // Let the parent span resource name be set with the attribute set in findMapping. + ServerNameUpdater.updateServerSpanName(context, (HttpServletRequest) request); + } + } catch (Exception ignored) { + // mapping.getHandler() threw exception. Ignore + } + } + + filterChain.doFilter(request, response); + } + + @Override + public void destroy() {} + + /** + * When a HandlerMapping matches a request, it sets HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE + * as an attribute on the request. This attribute is read by SpringWebMvcDecorator.onRequest and + * set as the resource name. + */ + private boolean findMapping(HttpServletRequest request) throws Exception { + for (HandlerMapping mapping : handlerMappings) { + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return true; + } + } + return false; + } + + public void setHandlerMappings(List mappings) { + List handlerMappings = new ArrayList<>(); + for (HandlerMapping mapping : mappings) { + // it may be enticing to add all HandlerMapping classes here, but DO NOT + // + // because we call getHandler() on them above, at the very beginning of the request + // and this can be a very invasive call with application-crashing side-effects + // + // for example: org.grails.web.mapping.mvc.UrlMappingsHandlerMapping.getHandler() + // 1. uses GrailsWebRequest.lookup() to get GrailsWebRequest bound to thread local + // 2. and populates the servlet request attribute "org.grails.url.match.info" + // with GrailsControllerUrlMappingInfo + // + // which causes big problems if GrailsWebRequest thread local is leaked from prior request + // (which has been observed to happen in Grails 3.0.17 at least), because then our call to + // UrlMappingsHandlerMapping.getHandler() at the very beginning of the request: + // 1. GrailsWebRequest.lookup() gets the leaked GrailsWebRequest + // 2. servlet request attribute "org.grails.url.match.info" is populated based on this leaked + // GrailsWebRequest (so in other words, most likely the wrong route is matched) + // + // and then GrailsWebRequestFilter creates a new GrailsWebRequest and binds it to the thread + // + // and then the application calls UrlMappingsHandlerMapping.getHandler() to route the request + // but it finds servlet request attribute "org.grails.url.match.info" already populated (by + // above) and so it short cuts the matching process and uses the wrong route that the agent + // populated caused to be populated into the request attribute above + if (mapping instanceof RequestMappingHandlerMapping) { + handlerMappings.add(mapping); + } + } + if (!handlerMappings.isEmpty()) { + this.handlerMappings = handlerMappings; + } + } + + @Override + public int getOrder() { + // Run after all HIGHEST_PRECEDENCE items + return Ordered.HIGHEST_PRECEDENCE + 1; + } + + public static class BeanDefinition extends GenericBeanDefinition { + public BeanDefinition() { + setScope(SCOPE_SINGLETON); + setBeanClass(HandlerMappingResourceNameFilter.class); + setBeanClassName(HandlerMappingResourceNameFilter.class.getName()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerMethodReturnValueInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerMethodReturnValueInstrumentation.java new file mode 100644 index 000000000..6be4ea414 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerMethodReturnValueInstrumentation.java @@ -0,0 +1,69 @@ +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +import java.lang.reflect.Field; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +@SuppressWarnings({"SystemOut","CatchAndPrintStackTrace"}) +public class HandlerMethodReturnValueInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.web.method.support.HandlerMethodReturnValueHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.springframework.web.method.support.HandlerMethodReturnValueHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("handleReturnValue")), + HandlerMethodReturnValueInstrumentation.class.getName() + "$HandleReturnValueAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleReturnValueAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void nameResourceAndStartSpan( + @Advice.Argument(0) Object returnValue, + @Advice.Argument(3) NativeWebRequest request) { + if(returnValue != null && "run.mone.common.Result".equals(returnValue.getClass().getName())){ + if(request instanceof ServletWebRequest){ + ServletWebRequest servletRequst = (ServletWebRequest)request; + Class returnValueClass = returnValue.getClass(); + try { + Field codeFiled = returnValueClass.getDeclaredField("code"); + codeFiled.setAccessible(true); + Object code = codeFiled.get(returnValue); + servletRequst.getResponse().addHeader("X-BUSSINESS-CODE",String.valueOf(code)); + Field messageFiled = returnValueClass.getDeclaredField("message"); + messageFiled.setAccessible(true); + Object message = messageFiled.get(returnValue); + servletRequst.getResponse().addHeader("X-BUSSINESS-MESSAGE",String.valueOf(message)); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerSpanNameExtractor.java new file mode 100644 index 000000000..3e75809d0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/HandlerSpanNameExtractor.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import java.lang.reflect.Method; +import javax.servlet.Servlet; +import org.springframework.web.HttpRequestHandler; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.Controller; + +public class HandlerSpanNameExtractor implements SpanNameExtractor { + @Override + public String extract(Object handler) { + Class clazz; + String methodName; + + if (handler instanceof HandlerMethod) { + // name span based on the class and method name defined in the handler + Method method = ((HandlerMethod) handler).getMethod(); + clazz = method.getDeclaringClass(); + methodName = method.getName(); + } else if (handler instanceof HttpRequestHandler) { + // org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter + clazz = handler.getClass(); + methodName = "handleRequest"; + } else if (handler instanceof Controller) { + // org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter + clazz = handler.getClass(); + methodName = "handleRequest"; + } else if (handler instanceof Servlet) { + // org.springframework.web.servlet.handler.SimpleServletHandlerAdapter + clazz = handler.getClass(); + methodName = "service"; + } else { + // perhaps org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + clazz = handler.getClass(); + methodName = ""; + } + + return SpanNames.fromMethod(clazz, methodName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ModelAndViewAttributesExtractor.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ModelAndViewAttributesExtractor.java new file mode 100644 index 000000000..c44ef7b6f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ModelAndViewAttributesExtractor.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.tracer.ClassNames; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.View; + +public class ModelAndViewAttributesExtractor extends AttributesExtractor { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBoolean( + "otel.instrumentation.spring-webmvc.experimental-span-attributes", false); + + @Override + protected void onStart(AttributesBuilder attributes, ModelAndView modelAndView) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + attributes.put("spring-webmvc.view.name", modelAndView.getViewName()); + View view = modelAndView.getView(); + if (view != null) { + attributes.put("spring-webmvc.view.type", ClassNames.simpleName(view.getClass())); + } + } + } + + @Override + protected void onEnd( + AttributesBuilder attributes, ModelAndView modelAndView, @Nullable Void unused) {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ModelAndViewSpanNameExtractor.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ModelAndViewSpanNameExtractor.java new file mode 100644 index 000000000..b26d31636 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ModelAndViewSpanNameExtractor.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.View; + +public class ModelAndViewSpanNameExtractor implements SpanNameExtractor { + @Override + public String extract(ModelAndView modelAndView) { + String viewName = modelAndView.getViewName(); + if (viewName != null) { + return "Render " + viewName; + } + View view = modelAndView.getView(); + if (view != null) { + return "Render " + view.getClass().getSimpleName(); + } + // either viewName or view should be non-null, but just in case + return "Render "; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ServerNameUpdater.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ServerNameUpdater.java new file mode 100644 index 000000000..06421eec9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/ServerNameUpdater.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTROLLER; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import javax.servlet.http.HttpServletRequest; +import org.springframework.web.servlet.HandlerMapping; + +public class ServerNameUpdater { + + public static void updateServerSpanName(Context context, HttpServletRequest request) { + if (request != null) { + Object bestMatchingPattern = + request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + if (bestMatchingPattern != null) { + ServerSpanNaming.updateServerSpanName( + context, + CONTROLLER, + () -> ServletContextPath.prepend(context, bestMatchingPattern.toString())); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/SpringWebMvcInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/SpringWebMvcInstrumentationModule.java new file mode 100644 index 000000000..652387a0f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/SpringWebMvcInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; + +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class SpringWebMvcInstrumentationModule extends InstrumentationModule { + public SpringWebMvcInstrumentationModule() { + super("spring-webmvc", "spring-webmvc-3.1"); + } + + @Override + public List typeInstrumentations() { + return asList( + new WebApplicationContextInstrumentation(), + new DispatcherServletInstrumentation(), + new HandlerAdapterInstrumentation(), + new HandlerMethodReturnValueInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/SpringWebMvcSingletons.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/SpringWebMvcSingletons.java new file mode 100644 index 000000000..3ac2419d2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/SpringWebMvcSingletons.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import org.springframework.web.servlet.ModelAndView; + +public final class SpringWebMvcSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.spring-webmvc-3.1"; + + private static final Instrumenter HANDLER_INSTRUMENTER; + + private static final Instrumenter MODEL_AND_VIEW_INSTRUMENTER; + + static { + HANDLER_INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, new HandlerSpanNameExtractor()) + .newInstrumenter(); + + MODEL_AND_VIEW_INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), + INSTRUMENTATION_NAME, + new ModelAndViewSpanNameExtractor()) + .addAttributesExtractor(new ModelAndViewAttributesExtractor()) + .newInstrumenter(); + } + + public static Instrumenter handlerInstrumenter() { + return HANDLER_INSTRUMENTER; + } + + public static Instrumenter modelAndViewInstrumenter() { + return MODEL_AND_VIEW_INSTRUMENTER; + } + + private SpringWebMvcSingletons() {} +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/WebApplicationContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/WebApplicationContextInstrumentation.java new file mode 100644 index 000000000..378c6d917 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/springwebmvc/WebApplicationContextInstrumentation.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.springwebmvc; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; + +/** + * This instrumentation adds the HandlerMappingResourceNameFilter definition to the spring context + * When the context is created, the filter will be added to the beginning of the filter chain. + */ +public class WebApplicationContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed( + "org.springframework.context.support.AbstractApplicationContext", + "org.springframework.web.context.WebApplicationContext"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("org.springframework.context.support.AbstractApplicationContext")) + .and(implementsInterface(named("org.springframework.web.context.WebApplicationContext"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("postProcessBeanFactory")) + .and( + takesArgument( + 0, + named( + "org.springframework.beans.factory.config.ConfigurableListableBeanFactory"))), + WebApplicationContextInstrumentation.class.getName() + "$FilterInjectingAdvice"); + } + + @SuppressWarnings("unused") + public static class FilterInjectingAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) ConfigurableListableBeanFactory beanFactory) { + if (beanFactory instanceof BeanDefinitionRegistry + && !beanFactory.containsBean("otelAutoDispatcherFilter")) { + + ((BeanDefinitionRegistry) beanFactory) + .registerBeanDefinition( + "otelAutoDispatcherFilter", new HandlerMappingResourceNameFilter.BeanDefinition()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/AppConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/AppConfig.groovy new file mode 100644 index 000000000..99fa2366a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/AppConfig.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter + +@SpringBootApplication +class AppConfig extends WebMvcConfigurerAdapter { + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/AuthServerConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/AuthServerConfig.groovy new file mode 100644 index 000000000..51c71ccfa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/AuthServerConfig.groovy @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import org.springframework.context.annotation.Configuration +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer + +@Configuration +@EnableAuthorizationServer +class AuthServerConfig extends AuthorizationServerConfigurerAdapter { +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SavingAuthenticationProvider.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SavingAuthenticationProvider.groovy new file mode 100644 index 000000000..2c87e396c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SavingAuthenticationProvider.groovy @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider +import org.springframework.security.core.AuthenticationException +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +class SavingAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { + List latestAuthentications = new ArrayList<>() + + @Override + protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { + // none + } + + @Override + protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { + def details = new TestUserDetails(username, authentication.credentials.toString()) + + latestAuthentications.add(details) + + return details + } +} + +class TestUserDetails implements UserDetails { + private final String username + private final String password + + TestUserDetails(String username, String password) { + this.username = username + this.password = password + } + + @Override + Collection getAuthorities() { + return Collections.emptySet() + } + + @Override + String getPassword() { + return password + } + + @Override + String getUsername() { + return username + } + + @Override + boolean isAccountNonExpired() { + return true + } + + @Override + boolean isAccountNonLocked() { + return true + } + + @Override + boolean isCredentialsNonExpired() { + return true + } + + @Override + boolean isEnabled() { + return true + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SecurityConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SecurityConfig.groovy new file mode 100644 index 000000000..08858f6c5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SecurityConfig.groovy @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter + +@Configuration +@EnableWebSecurity +class SecurityConfig { + + @Bean + SavingAuthenticationProvider savingAuthenticationProvider() { + return new SavingAuthenticationProvider() + } + + /** + * Following configuration is required for unauthorised call tests (form would redirect, we need 401) + */ + @Configuration + @Order(1) + static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { + + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .antMatcher("/basicsecured/**") + .authorizeRequests() + .antMatchers("/basicsecured/**").authenticated() + .and() + .httpBasic() + .and().authenticationProvider(applicationContext.getBean(SavingAuthenticationProvider)) + } + } + + /** + * Following configuration is required in order to get form login, needed by password tests + */ + @Configuration + static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .authorizeRequests() + .antMatchers("/formsecured/**").authenticated() + .and() + .formLogin() + .and().authenticationProvider(applicationContext.getBean(SavingAuthenticationProvider)) + } + } +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SpringBootBasedTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SpringBootBasedTest.groovy new file mode 100644 index 000000000..7d08e1ec3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SpringBootBasedTest.groovy @@ -0,0 +1,192 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.AUTH_ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.LOGIN +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest +import io.opentelemetry.testing.internal.armeria.common.HttpData +import io.opentelemetry.testing.internal.armeria.common.MediaType +import io.opentelemetry.testing.internal.armeria.common.QueryParams +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.web.servlet.view.RedirectView + +class SpringBootBasedTest extends HttpServerTest implements AgentTestTrait { + + @Override + ConfigurableApplicationContext startServer(int port) { + def app = new SpringApplication(AppConfig, SecurityConfig, AuthServerConfig) + app.setDefaultProperties([ + "server.port" : port, + "server.context-path" : getContextPath(), + "server.servlet.contextPath" : getContextPath(), + "server.error.include-message": "always"]) + def context = app.run() + return context + } + + @Override + void stopServer(ConfigurableApplicationContext ctx) { + ctx.close() + } + + @Override + String getContextPath() { + return "/xyz" + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + true + } + + @Override + boolean hasRenderSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == NOT_FOUND + } + + @Override + boolean testPathParam() { + true + } + + @Override + boolean hasErrorPageSpans(ServerEndpoint endpoint) { + endpoint == NOT_FOUND + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return getContextPath() + "/path/{id}/param" + case NOT_FOUND: + return getContextPath() + "/**" + case LOGIN: + return getContextPath() + "/*" + default: + return super.expectedServerSpanName(endpoint) + } + } + + def "test spans with auth error"() { + setup: + def authProvider = server.getBean(SavingAuthenticationProvider) + def request = request(AUTH_ERROR, "GET") + + when: + authProvider.latestAuthentications.clear() + def response = client.execute(request).aggregate().join() + + then: + response.status().code() == 401 // not secured + + and: + assertTraces(1) { + trace(0, 3) { + serverSpan(it, 0, null, null, "GET", null, AUTH_ERROR) + sendErrorSpan(it, 1, span(0)) + errorPageSpans(it, 2, null) + } + } + } + + def "test character encoding of #testPassword"() { + setup: + def authProvider = server.getBean(SavingAuthenticationProvider) + + QueryParams form = QueryParams.of("username", "test", "password", testPassword) + def request = AggregatedHttpRequest.of( + request(LOGIN, "POST").headers().toBuilder().contentType(MediaType.FORM_DATA).build(), + HttpData.ofUtf8(form.toQueryString())) + + when: + authProvider.latestAuthentications.clear() + def response = client.execute(request).aggregate().join() + + then: + response.status().code() == 302 // redirect after success + authProvider.latestAuthentications.get(0).password == testPassword + + and: + assertTraces(1) { + trace(0, 2) { + serverSpan(it, 0, null, null, "POST", response.contentUtf8().length(), LOGIN) + redirectSpan(it, 1, span(0)) + } + } + + where: + testPassword << ["password", "dfsdföääöüüä", "🤓"] + } + + @Override + void errorPageSpans(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name "BasicErrorController.error" + kind INTERNAL + attributes { + } + } + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + def responseSpanName = endpoint == NOT_FOUND ? "OnCommittedResponseWrapper.sendError" : "OnCommittedResponseWrapper.sendRedirect" + trace.span(index) { + name responseSpanName + kind INTERNAL + attributes { + } + } + } + + @Override + void renderSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name "Render RedirectView" + kind INTERNAL + attributes { + "spring-webmvc.view.type" RedirectView.simpleName + } + } + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + def handlerSpanName = "TestController.${endpoint.name().toLowerCase()}" + if (endpoint == NOT_FOUND) { + handlerSpanName = "ResourceHttpRequestHandler.handleRequest" + } + trace.span(index) { + name handlerSpanName + kind INTERNAL + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(Exception, EXCEPTION.body) + } + childOf((SpanData) parent) + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/TestController.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/TestController.groovy new file mode 100644 index 000000000..616fbce18 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/TestController.groovy @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseBody +import org.springframework.web.servlet.view.RedirectView + +@Controller +class TestController { + + @RequestMapping("/basicsecured/endpoint") + @ResponseBody + String secureEndpoint() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } + + @RequestMapping("/success") + @ResponseBody + String success() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } + + @RequestMapping("/query") + @ResponseBody + String query_param(@RequestParam("some") String param) { + HttpServerTest.controller(QUERY_PARAM) { + "some=$param" + } + } + + @RequestMapping("/redirect") + @ResponseBody + RedirectView redirect() { + HttpServerTest.controller(REDIRECT) { + new RedirectView(REDIRECT.body) + } + } + + @RequestMapping("/error-status") + ResponseEntity error() { + HttpServerTest.controller(ERROR) { + new ResponseEntity(ERROR.body, HttpStatus.valueOf(ERROR.status)) + } + } + + @RequestMapping("/exception") + ResponseEntity exception() { + HttpServerTest.controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + + @RequestMapping("/path/{id}/param") + @ResponseBody + String path_param(@PathVariable("id") int id) { + HttpServerTest.controller(PATH_PARAM) { + id + } + } + + @ExceptionHandler + ResponseEntity handleException(Throwable throwable) { + new ResponseEntity(throwable.message, HttpStatus.INTERNAL_SERVER_ERROR) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/FilteredAppConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/FilteredAppConfig.groovy new file mode 100644 index 000000000..184596d2a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/FilteredAppConfig.groovy @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.filter + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import java.nio.charset.StandardCharsets +import javax.servlet.Filter +import javax.servlet.FilterChain +import javax.servlet.FilterConfig +import javax.servlet.ServletException +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpInputMessage +import org.springframework.http.HttpOutputMessage +import org.springframework.http.MediaType +import org.springframework.http.converter.AbstractHttpMessageConverter +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.http.converter.HttpMessageNotWritableException +import org.springframework.util.StreamUtils +import org.springframework.web.HttpMediaTypeNotAcceptableException +import org.springframework.web.accept.ContentNegotiationStrategy +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter + +@SpringBootApplication +class FilteredAppConfig extends WebMvcConfigurerAdapter { + + @Override + void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.favorPathExtension(false) + .favorParameter(true) + .ignoreAcceptHeader(true) + .useJaf(false) + .defaultContentTypeStrategy(new ContentNegotiationStrategy() { + @Override + List resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { + return [MediaType.TEXT_PLAIN] + } + }) + } + + @Bean + HttpMessageConverter> createPlainMapMessageConverter() { + return new AbstractHttpMessageConverter>(MediaType.TEXT_PLAIN) { + + @Override + protected boolean supports(Class clazz) { + return Map.isAssignableFrom(clazz) + } + + @Override + protected Map readInternal(Class> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + return null + } + + @Override + protected void writeInternal(Map stringObjectMap, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + StreamUtils.copy(stringObjectMap.get("message"), StandardCharsets.UTF_8, outputMessage.getBody()) + } + } + } + + @Bean + Filter servletFilter() { + return new Filter() { + + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request + HttpServletResponse resp = (HttpServletResponse) response + HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath) + HttpServerTest.controller(endpoint) { + resp.contentType = "text/plain" + switch (endpoint) { + case SUCCESS: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + break + case QUERY_PARAM: + resp.status = endpoint.status + resp.writer.print(req.queryString) + break + case PATH_PARAM: + resp.status = endpoint.status + resp.writer.print(endpoint.body) + break + case REDIRECT: + resp.sendRedirect(endpoint.body) + break + case ERROR: + resp.sendError(endpoint.status, endpoint.body) + break + case EXCEPTION: + throw new Exception(endpoint.body) + default: + chain.doFilter(request, response) + } + } + } + + @Override + void destroy() { + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterTest.groovy new file mode 100644 index 000000000..e7c3e0d02 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterTest.groovy @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.filter + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import test.boot.SecurityConfig + +class ServletFilterTest extends HttpServerTest implements AgentTestTrait { + + @Override + ConfigurableApplicationContext startServer(int port) { + def app = new SpringApplication(FilteredAppConfig, SecurityConfig) + app.setDefaultProperties(["server.port": port, "server.error.include-message": "always"]) + def context = app.run() + return context + } + + @Override + void stopServer(ConfigurableApplicationContext ctx) { + ctx.close() + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + endpoint == NOT_FOUND + } + + @Override + boolean hasErrorPageSpans(ServerEndpoint endpoint) { + endpoint == ERROR || endpoint == EXCEPTION || endpoint == NOT_FOUND + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == ERROR || endpoint == NOT_FOUND + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case REDIRECT: + redirectSpan(trace, index, parent) + break + case ERROR: + case NOT_FOUND: + sendErrorSpan(trace, index, parent) + break + } + } + + @Override + boolean testPathParam() { + true + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint) { + trace.span(index) { + name "ResourceHttpRequestHandler.handleRequest" + kind INTERNAL + childOf((SpanData) parent) + } + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return getContextPath() + "/path/{id}/param" + case NOT_FOUND: + return getContextPath() + "/**" + default: + return super.expectedServerSpanName(endpoint) + } + } + + @Override + void errorPageSpans(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + trace.span(index) { + name "BasicErrorController.error" + kind INTERNAL + childOf((SpanData) parent) + attributes { + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/TestController.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/TestController.groovy new file mode 100644 index 000000000..2540c65b0 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/TestController.groovy @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.filter + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseBody +import org.springframework.web.servlet.view.RedirectView + +/** + * None of the methods in this controller should be called because they are intercepted + * by the filter + */ +@Controller +class TestController { + + @RequestMapping("/success") + @ResponseBody + String success() { + throw new Exception("This should not be called") + } + + @RequestMapping("/query") + @ResponseBody + String query_param(@RequestParam("some") String param) { + throw new Exception("This should not be called") + } + + @RequestMapping("/path/{id}/param") + @ResponseBody + String path_param(@PathVariable Integer id) { + throw new Exception("This should not be called") + } + + @RequestMapping("/redirect") + @ResponseBody + RedirectView redirect() { + throw new Exception("This should not be called") + } + + @RequestMapping("/error-status") + ResponseEntity error() { + throw new Exception("This should not be called") + } + + @RequestMapping("/exception") + ResponseEntity exception() { + throw new Exception("This should not be called") + } + + @ExceptionHandler + ResponseEntity handleException(Throwable throwable) { + new ResponseEntity(throwable.message, HttpStatus.INTERNAL_SERVER_ERROR) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/resources/logback.xml new file mode 100644 index 000000000..7f2406629 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/javaagent/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/README.md new file mode 100644 index 000000000..cd4a2f4ff --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/README.md @@ -0,0 +1,90 @@ +# Manual Instrumentation for Spring Web MVC + +Provides OpenTelemetry tracing for spring-webmvc RestControllers by leveraging spring-webmvc [filters](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter). + +## Quickstart + +### Add these dependencies to your project. + +Replace `SPRING_VERSION` with the version of spring you're using. + - `Minimum version: 3.1` + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://mvnrepository.com/artifact/io.opentelemetry). + - `Minimum version: 0.8.0` + +For Maven add to your `pom.xml`: + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-spring-webmvc-3.1 + OPENTELEMETRY_VERSION + + + + + + io.opentelemetry + opentelemetry-exporters-logging + OPENTELEMETRY_VERSION + + + + + + org.springframework + spring-webmvc + SPRING_VERSION + + + +``` + +For Gradle add to your dependencies: + +```groovy + +// opentelemetry instrumentation +implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-webmvc-3.1:OPENTELEMETRY_VERSION' + +// opentelemetry exporter +// replace this default exporter with your opentelemetry exporter (ex. otlp/zipkin/jaeger/..) +implementation 'io.opentelemetry:opentelemetry-exporters-logging:OPENTELEMETRY_VERSION' + +// required to instrument spring-webmvc +// this artifact should already be present in your application +implementation 'org.springframework:spring-webmvc:SPRING_VERSION' +``` + +### Features + +#### WebMvcTracingFilter + +WebMvcTracingFilter adds OpenTelemetry server spans to requests processed by request dispatch, on any spring servlet container. An example is shown below: + +##### Usage + +```java +import io.opentelemetry.instrumentation.spring.webmvc.WebMvcTracingFilter +import io.opentelemetry.api.trace.Tracer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class WebMvcTracingFilterConfig { + + @Bean + public WebMvcTracingFilter webMvcTracingFilter(Tracer tracer) { + return new WebMvcTracingFilter(tracer); + } +} +``` + +### Starter Guide + +Check out the opentelemetry [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation. diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/spring-webmvc-3.1-library.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/spring-webmvc-3.1-library.gradle new file mode 100644 index 000000000..655bf50dd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/spring-webmvc-3.1-library.gradle @@ -0,0 +1,9 @@ +apply plugin: "otel.library-instrumentation" + +dependencies { + implementation project(':instrumentation:servlet:servlet-common:library') + implementation project(':instrumentation:servlet:servlet-javax-common:library') + + compileOnly "org.springframework:spring-webmvc:3.1.0.RELEASE" + compileOnly "javax.servlet:javax.servlet-api:3.1.0" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/SpringWebMvcServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/SpringWebMvcServerTracer.java new file mode 100644 index 000000000..7d927e61e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/SpringWebMvcServerTracer.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import io.opentelemetry.instrumentation.servlet.javax.JavaxHttpServletRequestGetter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +class SpringWebMvcServerTracer + extends HttpServerTracer< + HttpServletRequest, HttpServletResponse, HttpServletRequest, HttpServletRequest> { + + SpringWebMvcServerTracer(OpenTelemetry openTelemetry) { + super(openTelemetry); + } + + @Override + protected Integer peerPort(HttpServletRequest request) { + return request.getRemotePort(); + } + + @Override + protected String peerHostIP(HttpServletRequest request) { + return request.getRemoteAddr(); + } + + @Override + protected TextMapGetter getGetter() { + return JavaxHttpServletRequestGetter.GETTER; + } + + @Override + protected String url(HttpServletRequest request) { + return request.getRequestURI(); + } + + @Override + protected String method(HttpServletRequest request) { + return request.getMethod(); + } + + @Override + protected String requestHeader(HttpServletRequest httpServletRequest, String name) { + return httpServletRequest.getHeader(name); + } + + @Override + protected int responseStatus(HttpServletResponse httpServletResponse) { + return httpServletResponse.getStatus(); + } + + @Override + protected String bussinessStatus(HttpServletResponse response) { + return null; + } + + @Override + protected String bussinessMessage(HttpServletResponse response) { + return null; + } + + @Override + protected void attachServerContext(Context context, HttpServletRequest request) { + request.setAttribute(CONTEXT_ATTRIBUTE, context); + } + + @Override + protected String flavor(HttpServletRequest connection, HttpServletRequest request) { + return connection.getProtocol(); + } + + @Override + public Context getServerContext(HttpServletRequest request) { + Object context = request.getAttribute(CONTEXT_ATTRIBUTE); + return context instanceof Context ? (Context) context : null; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.spring-webmvc-3.1"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/WebMvcTracingFilter.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/WebMvcTracingFilter.java new file mode 100644 index 000000000..2bc078184 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-webmvc-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/WebMvcTracingFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.web.filter.OncePerRequestFilter; + +public class WebMvcTracingFilter extends OncePerRequestFilter implements Ordered { + + private static final String FILTER_CLASS = "WebMVCTracingFilter"; + private static final String FILTER_METHOD = "doFilterInternal"; + private final SpringWebMvcServerTracer tracer; + + public WebMvcTracingFilter(OpenTelemetry openTelemetry) { + this.tracer = new SpringWebMvcServerTracer(openTelemetry); + } + + @Override + public void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + Context ctx = tracer.startSpan(request, request, request, FILTER_CLASS + "." + FILTER_METHOD); + try (Scope ignored = ctx.makeCurrent()) { + filterChain.doFilter(request, response); + tracer.end(ctx, response); + } catch (Throwable t) { + tracer.endExceptionally(ctx, t, response); + throw t; + } + } + + @Override + public void destroy() {} + + @Override + public int getOrder() { + // Run after all HIGHEST_PRECEDENCE items + return Ordered.HIGHEST_PRECEDENCE + 1; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/spring-ws-2.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/spring-ws-2.0-javaagent.gradle new file mode 100644 index 000000000..0331ed58b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/spring-ws-2.0-javaagent.gradle @@ -0,0 +1,42 @@ +plugins { + id "org.unbroken-dome.xjc" version "2.0.0" +} + +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'org.springframework.ws' + module = 'spring-ws-core' + versions = "[2.0.0.RELEASE,]" + // broken version, jar doesn't contain classes + skip('3.1.0') + assertInverse = true + } +} + +sourceSets { + test { + resources.srcDirs "src/test/schema" + } +} + +checkstyle { + // exclude generated web service classes + checkstyleTest.exclude "**/hello_web_service/**" +} + +dependencies { + compileOnly "org.springframework.ws:spring-ws-core:2.0.0.RELEASE" + + testLibrary "org.springframework.boot:spring-boot-starter-web-services:2.0.0.RELEASE" + testLibrary "org.springframework.boot:spring-boot-starter-web:2.0.0.RELEASE" + + testImplementation "wsdl4j:wsdl4j:1.6.3" + testImplementation "com.sun.xml.messaging.saaj:saaj-impl:1.5.2" + testImplementation "javax.xml.bind:jaxb-api:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-core:2.2.11" + testImplementation "com.sun.xml.bind:jaxb-impl:2.2.11" + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/AnnotatedMethodInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/AnnotatedMethodInstrumentation.java new file mode 100644 index 000000000..b1896b468 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/AnnotatedMethodInstrumentation.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.ws; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.spring.ws.SpringWsTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.springframework.ws.server.endpoint.annotation.PayloadRoot; + +public class AnnotatedMethodInstrumentation implements TypeInstrumentation { + private static final String[] ANNOTATION_CLASSES = + new String[] { + "org.springframework.ws.server.endpoint.annotation.PayloadRoot", + "org.springframework.ws.soap.server.endpoint.annotation.SoapAction", + "org.springframework.ws.soap.addressing.server.annotation.Action" + }; + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.ws.server.endpoint.annotation.PayloadRoot"); + } + + @Override + public ElementMatcher typeMatcher() { + return declaresMethod(isAnnotatedWith(namedOneOf(ANNOTATION_CLASSES))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isAnnotatedWith(namedOneOf(ANNOTATION_CLASSES))), + AnnotatedMethodInstrumentation.class.getName() + "$AnnotatedMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class AnnotatedMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void startSpan( + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (CallDepthThreadLocalMap.incrementCallDepth(PayloadRoot.class) > 0) { + return; + } + context = tracer().startSpan(method); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + CallDepthThreadLocalMap.reset(PayloadRoot.class); + + scope.close(); + if (throwable == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/SpringWsInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/SpringWsInstrumentationModule.java new file mode 100644 index 000000000..298919830 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/SpringWsInstrumentationModule.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.ws; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class SpringWsInstrumentationModule extends InstrumentationModule { + public SpringWsInstrumentationModule() { + super("spring-ws", "spring-ws-2.0"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new AnnotatedMethodInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/SpringWsTracer.java b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/SpringWsTracer.java new file mode 100644 index 000000000..f7dec5a6e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/SpringWsTracer.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.ws; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.lang.reflect.Method; + +public class SpringWsTracer extends BaseTracer { + + private static final SpringWsTracer TRACER = new SpringWsTracer(); + + public static SpringWsTracer tracer() { + return TRACER; + } + + public Context startSpan(Method method) { + Context parentContext = Context.current(); + Span span = + spanBuilder(parentContext, SpanNames.fromMethod(method), SpanKind.INTERNAL) + .setAttribute(SemanticAttributes.CODE_NAMESPACE, method.getDeclaringClass().getName()) + .setAttribute(SemanticAttributes.CODE_FUNCTION, method.getName()) + .startSpan(); + return parentContext.with(span); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spring-ws-2.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/AppConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/AppConfig.groovy new file mode 100644 index 000000000..e9ddd37cd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/AppConfig.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@SpringBootApplication +class AppConfig implements WebMvcConfigurer { + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/HelloEndpoint.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/HelloEndpoint.groovy new file mode 100644 index 000000000..0c040cc85 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/HelloEndpoint.groovy @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import io.opentelemetry.test.hello_web_service.HelloRequest +import io.opentelemetry.test.hello_web_service.HelloRequestSoapAction +import io.opentelemetry.test.hello_web_service.HelloRequestWsAction +import io.opentelemetry.test.hello_web_service.HelloResponse +import org.springframework.ws.server.endpoint.annotation.Endpoint +import org.springframework.ws.server.endpoint.annotation.PayloadRoot +import org.springframework.ws.server.endpoint.annotation.RequestPayload +import org.springframework.ws.server.endpoint.annotation.ResponsePayload +import org.springframework.ws.soap.addressing.server.annotation.Action +import org.springframework.ws.soap.server.endpoint.annotation.SoapAction + +@Endpoint +class HelloEndpoint { + private static final String NAMESPACE_URI = "http://opentelemetry.io/test/hello-web-service" + + @PayloadRoot(namespace = NAMESPACE_URI, localPart = "helloRequest") + @ResponsePayload + HelloResponse hello(@RequestPayload HelloRequest request) { + return handleHello(request.getName()) + } + + @SoapAction(value = "http://opentelemetry.io/test/hello-soap-action") + @ResponsePayload + HelloResponse helloSoapAction(@RequestPayload HelloRequestSoapAction request) { + return handleHello(request.getName()) + } + + @Action(value = "http://opentelemetry.io/test/hello-ws-action") + @ResponsePayload + HelloResponse helloWsAction(@RequestPayload HelloRequestWsAction request) { + return handleHello(request.getName()) + } + + private HelloResponse handleHello(String name) { + if ("exception" == name) { + throw new Exception("hello exception") + } + HelloResponse response = new HelloResponse() + response.setMessage("Hello " + name) + + return response + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/SpringWsTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/SpringWsTest.groovy new file mode 100644 index 000000000..d8ed37809 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/SpringWsTest.groovy @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.StatusCode.ERROR + +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.test.hello_web_service.HelloRequest +import io.opentelemetry.test.hello_web_service.HelloRequestSoapAction +import io.opentelemetry.test.hello_web_service.HelloRequestWsAction +import io.opentelemetry.test.hello_web_service.HelloResponse +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.oxm.jaxb.Jaxb2Marshaller +import org.springframework.util.ClassUtils +import org.springframework.ws.client.core.WebServiceMessageCallback +import org.springframework.ws.client.core.WebServiceTemplate +import org.springframework.ws.soap.addressing.client.ActionCallback +import org.springframework.ws.soap.client.SoapFaultClientException +import org.springframework.ws.soap.client.core.SoapActionCallback +import spock.lang.Shared + +class SpringWsTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + @Shared + private Jaxb2Marshaller marshaller = new Jaxb2Marshaller() + + def setupSpec() { + marshaller.setPackagesToScan(ClassUtils.getPackageName(HelloRequest)) + marshaller.afterPropertiesSet() + } + + @Override + ConfigurableApplicationContext startServer(int port) { + def app = new SpringApplication(AppConfig, WebServiceConfig) + app.setDefaultProperties([ + "server.port" : port, + "server.context-path" : getContextPath(), + "server.servlet.contextPath" : getContextPath(), + "server.error.include-message": "always"]) + def context = app.run() + return context + } + + @Override + void stopServer(ConfigurableApplicationContext ctx) { + ctx.close() + } + + @Override + String getContextPath() { + return "/xyz" + } + + HelloResponse makeRequest(methodName, name) { + WebServiceTemplate webServiceTemplate = new WebServiceTemplate(marshaller) + + Object request = null + WebServiceMessageCallback callback = null + if ("hello" == methodName) { + request = new HelloRequest(name: name) + } else if ("helloSoapAction" == methodName) { + request = new HelloRequestSoapAction(name: name) + callback = new SoapActionCallback("http://opentelemetry.io/test/hello-soap-action") + } else if ("helloWsAction" == methodName) { + request = new HelloRequestWsAction(name: name) + callback = new ActionCallback("http://opentelemetry.io/test/hello-ws-action") + } else { + throw new IllegalArgumentException(methodName) + } + + return (HelloResponse) webServiceTemplate.marshalSendAndReceive(address.resolve("ws").toString(), request, callback) + } + + def "test #methodName"(methodName) { + setup: + HelloResponse response = makeRequest(methodName, "Test") + + expect: + response.getMessage() == "Hello Test" + + and: + assertTraces(1) { + trace(0, 2) { + serverSpan(it, 0, getContextPath() + "/ws/*") + handlerSpan(it, 1, methodName, span(0)) + } + } + + where: + methodName << ["hello", "helloSoapAction", "helloWsAction"] + } + + def "test #methodName exception"(methodName) { + when: + makeRequest(methodName, "exception") + + then: + def error = thrown(SoapFaultClientException) + error.getMessage() == "hello exception" + + and: + def expectedException = new Exception("hello exception") + assertTraces(1) { + trace(0, 2) { + serverSpan(it, 0, getContextPath() + "/ws/*", expectedException) + handlerSpan(it, 1, methodName, span(0), expectedException) + } + } + + where: + methodName << ["hello", "helloSoapAction", "helloWsAction"] + } + + static serverSpan(TraceAssert trace, int index, String operation, Throwable exception = null) { + trace.span(index) { + hasNoParent() + name operation + kind SpanKind.SERVER + if (exception != null) { + status ERROR + } + } + } + + static handlerSpan(TraceAssert trace, int index, String methodName, Object parentSpan = null, Throwable exception = null) { + trace.span(index) { + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + name "HelloEndpoint." + methodName + kind INTERNAL + if (exception) { + status ERROR + errorEvent(exception.class, exception.message) + } + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "test.boot.HelloEndpoint" + "${SemanticAttributes.CODE_FUNCTION.key}" methodName + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/WebServiceConfig.groovy b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/WebServiceConfig.groovy new file mode 100644 index 000000000..89f8cc426 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/WebServiceConfig.groovy @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.boot + +import org.springframework.context.ApplicationContext +import org.springframework.boot.web.servlet.ServletRegistrationBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.io.ClassPathResource +import org.springframework.ws.config.annotation.EnableWs +import org.springframework.ws.config.annotation.WsConfigurerAdapter +import org.springframework.ws.transport.http.MessageDispatcherServlet +import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition +import org.springframework.xml.xsd.SimpleXsdSchema +import org.springframework.xml.xsd.XsdSchema + +@EnableWs +@Configuration +class WebServiceConfig extends WsConfigurerAdapter { + @Bean + ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) { + MessageDispatcherServlet servlet = new MessageDispatcherServlet() + servlet.setApplicationContext(applicationContext) + servlet.setTransformWsdlLocations(true) + return new ServletRegistrationBean<>(servlet, "/ws/*") + } + + @Bean(name = "hello") + DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) { + DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition() + wsdl11Definition.setPortTypeName("HelloPort") + wsdl11Definition.setLocationUri("/ws") + wsdl11Definition.setTargetNamespace("http://opentelemetry.io/test/hello-web-service") + wsdl11Definition.setSchema(countriesSchema) + return wsdl11Definition + } + + @Bean + XsdSchema helloSchema() { + return new SimpleXsdSchema(new ClassPathResource("hello.xsd")) + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/schema/hello.xsd b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/schema/hello.xsd new file mode 100644 index 000000000..4f1beb638 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/spring-ws-2.0/javaagent/src/test/schema/hello.xsd @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/starters/jaeger-exporter-starter/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/starters/jaeger-exporter-starter/README.md new file mode 100644 index 000000000..f091f6293 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/starters/jaeger-exporter-starter/README.md @@ -0,0 +1,36 @@ +# OpenTelemetry Jaeger Exporter Starter + +OpenTelemetry Jaeger Exporter Starter is a starter package that includes the opentelemetry-api, opentelemetry-sdk, opentelemetry-extension-annotations, opentelmetry-logging-exporter, opentelemetry-spring-boot-autoconfigurations and spring framework starters required to setup distributed tracing. It also provides the [opentelemetry-exporters-jaeger](https://github.com/open-telemetry/opentelemetry-java/tree/master/exporters/jaeger) artifact and corresponding exporter auto-configuration. Check out [opentelemetry-spring-boot-autoconfigure](../../spring-boot-autoconfigure/README.md#features) for the list of supported libraries and features. + +## Quickstart + +### Add these dependencies to your project. + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://search.maven.org/search?q=g:io.opentelemetry). + - Minimum version: `1.1.0` + - Note: You may need to include our bintray maven repository to your build file: `https://dl.bintray.com/open-telemetry/maven/`. As of August 2020 the latest opentelemetry-java-instrumentation artifacts are not published to maven-central. Please check the [releasing](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/master/RELEASING.md) doc for updates to this process. + + +#### Maven + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-jaeger-exporter-starter + OPENTELEMETRY_VERSION + + + +``` + +#### Gradle + +```groovy +implementation 'io.opentelemetry.instrumentation:opentelemetry-jaeger-exporter-starter:OPENTELEMETRY_VERSION' +``` + +### Starter Guide + +Check out the opentelemetry-api [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation. diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/starters/jaeger-exporter-starter/jaeger-exporter-starter.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/starters/jaeger-exporter-starter/jaeger-exporter-starter.gradle new file mode 100644 index 000000000..0456bacd6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/starters/jaeger-exporter-starter/jaeger-exporter-starter.gradle @@ -0,0 +1,18 @@ +ext { + springbootVersion = "2.3.1.RELEASE" +} + +group = 'io.opentelemetry.instrumentation' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +sourceCompatibility = '8' + +dependencies { + api group: "org.springframework.boot", name: "spring-boot-starter", version: versions["org.springframework.boot"] + api project(':instrumentation:spring:starters:spring-starter') + api "run.mone:opentelemetry-exporter-jaeger" + implementation "io.grpc:grpc-netty-shaded:1.30.2" +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/starters/otlp-exporter-starter/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/starters/otlp-exporter-starter/README.md new file mode 100644 index 000000000..11ec3c894 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/starters/otlp-exporter-starter/README.md @@ -0,0 +1,36 @@ +# OpenTelemetry OTLP Exporter Starter + +OpenTelemetry OTLP Exporter Starter is a starter package that includes the opentelemetry-api, opentelemetry-sdk, opentelemetry-extension-annotations, opentelmetry-logging-exporter, opentelemetry-spring-boot-autoconfigurations and spring framework starters required to setup distributed tracing. It also provides the [opentelemetry-exporters-otlp](https://github.com/open-telemetry/opentelemetry-java/tree/master/exporters/otlp) artifact and corresponding exporter auto-configuration. Check out [opentelemetry-spring-boot-autoconfigure](../../spring-boot-autoconfigure/README.md#features) for the list of supported libraries and features. + +## Quickstart + +### Add these dependencies to your project. + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://search.maven.org/search?q=g:io.opentelemetry). + - Minimum version: `1.1.0` + - Note: You may need to include our bintray maven repository to your build file: `https://dl.bintray.com/open-telemetry/maven/`. As of August 2020 the latest opentelemetry-java-instrumentation artifacts are not published to maven-central. Please check the [releasing](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/master/RELEASING.md) doc for updates to this process. + + +#### Maven + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-otlp-exporter-starter + OPENTELEMETRY_VERSION + + + +``` + +#### Gradle + +```groovy +implementation 'io.opentelemetry.instrumentation:opentelemetry-otlp-exporter-starter:OPENTELEMETRY_VERSION' +``` + +### Starter Guide + +Check out the opentelemetry-api [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation. diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/starters/otlp-exporter-starter/otlp-exporter-starter.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/starters/otlp-exporter-starter/otlp-exporter-starter.gradle new file mode 100644 index 000000000..497d64a18 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/starters/otlp-exporter-starter/otlp-exporter-starter.gradle @@ -0,0 +1,14 @@ +group = 'io.opentelemetry.instrumentation' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +sourceCompatibility = '8' + +dependencies { + api group: "org.springframework.boot", name: "spring-boot-starter", version: versions["org.springframework.boot"] + api project(':instrumentation:spring:starters:spring-starter') + api "io.opentelemetry:opentelemetry-exporter-otlp" + implementation "io.grpc:grpc-netty-shaded:1.30.2" +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/starters/spring-starter/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/starters/spring-starter/README.md new file mode 100644 index 000000000..2956024d3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/starters/spring-starter/README.md @@ -0,0 +1,40 @@ +# OpenTelemetry Spring Starter + +OpenTelemetry Spring Starter is a starter package that includes the opentelemetry-api, opentelemetry-sdk, opentelemetry-extension-annotations, opentelmetry-logging-exporter, opentelemetry-spring-boot-autoconfigurations and spring framework starters required to setup distributed tracing. Check out [opentelemetry-spring-boot-autoconfigure](../../spring-boot-autoconfigure/README.md#features) for the full list of supported libraries and features. + +This version is compatible with Spring Boot 2.0. + +## Quickstart + +### Add these dependencies to your project. + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://search.maven.org/search?q=g:io.opentelemetry). + - Minimum version: `1.1.0` + - Note: You may need to include our bintray maven repository to your build file: `https://dl.bintray.com/open-telemetry/maven/`. As of August 2020 the latest opentelemetry-java-instrumentation artifacts are not published to maven-central. Please check the [releasing](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/master/RELEASING.md) doc for updates to this process. + + +### Maven +Add the following dependencies to your `pom.xml` file: + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-spring-starter + OPENTELEMETRY_VERSION + + + +``` + +### Gradle +Add the following dependencies to your gradle.build file: + +```groovy +implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-starter:OPENTELEMETRY_VERSION' +``` + +### Starter Guide + +Check out the opentelemetry-api [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation. diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/starters/spring-starter/spring-starter.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/starters/spring-starter/spring-starter.gradle new file mode 100644 index 000000000..eff248ba8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/starters/spring-starter/spring-starter.gradle @@ -0,0 +1,16 @@ +group = 'io.opentelemetry.instrumentation' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +sourceCompatibility = '8' + +dependencies { + api group: "org.springframework.boot", name: "spring-boot-starter", version: versions["org.springframework.boot"] + api "org.springframework.boot:spring-boot-starter-aop:${versions["org.springframework.boot"]}" + api project(':instrumentation:spring:spring-boot-autoconfigure') + api "run.mone:opentelemetry-extension-annotations" + api "run.mone:opentelemetry-api" + api "run.mone:opentelemetry-exporter-logging" + api "run.mone:opentelemetry-sdk" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/starters/zipkin-exporter-starter/README.md b/opentelemetry-java-instrumentation/instrumentation/spring/starters/zipkin-exporter-starter/README.md new file mode 100644 index 000000000..82e416ebf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/starters/zipkin-exporter-starter/README.md @@ -0,0 +1,38 @@ +# OpenTelemetry Zipkin Exporter Starter + +The OpenTelemetry Exporter Starter for Java is a starter package that includes packages required to enable tracing using OpenTelemetry. It also provides the dependency and corresponding auto-configuration. Check out [opentelemetry-spring-boot-autoconfigure](../../spring-boot-autoconfigure/README.md#features) for the list of supported libraries and features. + +OpenTelemetry Zipkin Exporter Starter is a starter package that includes the opentelemetry-api, opentelemetry-sdk, opentelemetry-extension-annotations, opentelmetry-logging-exporter, opentelemetry-spring-boot-autoconfigurations and spring framework starters required to setup distributed tracing. It also provides the [opentelemetry-exporters-zipkin](https://github.com/open-telemetry/opentelemetry-java/tree/master/exporters/zipkin) artifact and corresponding exporter auto-configuration. Check out [opentelemetry-spring-boot-autoconfigure](../../spring-boot-autoconfigure/README.md#features) for the list of supported libraries and features. + +## Quickstart + +### Add these dependencies to your project. + +Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://search.maven.org/search?q=g:io.opentelemetry). + - Minimum version: `1.1.0` + - Note: You may need to include our bintray maven repository to your build file: `https://dl.bintray.com/open-telemetry/maven/`. As of August 2020 the latest opentelemetry-java-instrumentation artifacts are not published to maven-central. Please check the [releasing](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/master/RELEASING.md) doc for updates to this process. + + +#### Maven + +```xml + + + + io.opentelemetry.instrumentation + opentelemetry-zipkin-exporter-starter + OPENTELEMETRY_VERSION + + + +``` + +#### Gradle + +```groovy +implementation 'io.opentelemetry.instrumentation:opentelemetry-zipkin-exporter-starter:OPENTELEMETRY_VERSION' +``` + +### Starter Guide + +Check out the opentelemetry-api [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation. diff --git a/opentelemetry-java-instrumentation/instrumentation/spring/starters/zipkin-exporter-starter/zipkin-exporter-starter.gradle b/opentelemetry-java-instrumentation/instrumentation/spring/starters/zipkin-exporter-starter/zipkin-exporter-starter.gradle new file mode 100644 index 000000000..db4d44384 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spring/starters/zipkin-exporter-starter/zipkin-exporter-starter.gradle @@ -0,0 +1,13 @@ +group = 'io.opentelemetry.instrumentation' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +sourceCompatibility = '8' + +dependencies { + api group: "org.springframework.boot", name: "spring-boot-starter", version: versions["org.springframework.boot"] + api project(':instrumentation:spring:starters:spring-starter') + api "io.opentelemetry:opentelemetry-exporter-zipkin" +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/spymemcached-2.12-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/spymemcached-2.12-javaagent.gradle new file mode 100644 index 000000000..064deaa46 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/spymemcached-2.12-javaagent.gradle @@ -0,0 +1,21 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "net.spy" + module = 'spymemcached' + versions = "[2.12.0,)" + assertInverse = true + } +} + +dependencies { + library "net.spy:spymemcached:2.12.0" + + testImplementation "com.google.guava:guava" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.spymemcached.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/BulkGetCompletionListener.java b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/BulkGetCompletionListener.java new file mode 100644 index 000000000..768894d88 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/BulkGetCompletionListener.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spymemcached; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import java.util.concurrent.ExecutionException; +import net.spy.memcached.MemcachedConnection; +import net.spy.memcached.internal.BulkGetFuture; + +public class BulkGetCompletionListener extends CompletionListener> + implements net.spy.memcached.internal.BulkGetCompletionListener { + + public BulkGetCompletionListener( + Context parentContext, MemcachedConnection connection, String methodName) { + super(parentContext, connection, methodName); + } + + @Override + public void onComplete(BulkGetFuture future) { + closeAsyncSpan(future); + } + + @Override + protected void processResult(Span span, BulkGetFuture future) + throws ExecutionException, InterruptedException { + /* + Note: for now we do not have an affective way of representing results of bulk operations, + i.e. we cannot say that we got 4 hits out of 10. So we will just ignore results for now. + */ + future.get(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/CompletionListener.java b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/CompletionListener.java new file mode 100644 index 000000000..162508dca --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/CompletionListener.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spymemcached; + +import static io.opentelemetry.javaagent.instrumentation.spymemcached.MemcacheClientTracer.tracer; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import net.spy.memcached.MemcachedConnection; + +public abstract class CompletionListener { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty( + "otel.instrumentation.spymemcached.experimental-span-attributes", false); + + private static final String DB_COMMAND_CANCELLED = "spymemcached.command.cancelled"; + private static final String MEMCACHED_RESULT = "spymemcached.result"; + private static final String HIT = "hit"; + private static final String MISS = "miss"; + + private final Context context; + + protected CompletionListener( + Context parentContext, MemcachedConnection connection, String methodName) { + context = tracer().startSpan(parentContext, connection, methodName); + } + + protected void closeAsyncSpan(T future) { + Span span = Span.fromContext(context); + try { + processResult(span, future); + } catch (CancellationException e) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute(DB_COMMAND_CANCELLED, true); + } + } catch (ExecutionException e) { + if (e.getCause() instanceof CancellationException) { + // Looks like underlying OperationFuture wraps CancellationException into + // ExecutionException + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute(DB_COMMAND_CANCELLED, true); + } + } else { + tracer().endExceptionally(context, e); + } + } catch (InterruptedException e) { + // Avoid swallowing InterruptedException + tracer().endExceptionally(context, e); + Thread.currentThread().interrupt(); + } catch (Throwable t) { + // This should never happen, just in case to make sure we cover all unexpected exceptions + tracer().endExceptionally(context, t); + } finally { + tracer().end(context); + } + } + + protected void closeSyncSpan(Throwable thrown) { + if (thrown == null) { + tracer().end(context); + } else { + tracer().endExceptionally(context, thrown); + } + } + + protected abstract void processResult(Span span, T future) + throws ExecutionException, InterruptedException; + + protected void setResultTag(Span span, boolean hit) { + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + span.setAttribute(MEMCACHED_RESULT, hit ? HIT : MISS); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/GetCompletionListener.java b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/GetCompletionListener.java new file mode 100644 index 000000000..f44c11694 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/GetCompletionListener.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spymemcached; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import java.util.concurrent.ExecutionException; +import net.spy.memcached.MemcachedConnection; +import net.spy.memcached.internal.GetFuture; + +public class GetCompletionListener extends CompletionListener> + implements net.spy.memcached.internal.GetCompletionListener { + public GetCompletionListener( + Context parentContext, MemcachedConnection connection, String methodName) { + super(parentContext, connection, methodName); + } + + @Override + public void onComplete(GetFuture future) { + closeAsyncSpan(future); + } + + @Override + protected void processResult(Span span, GetFuture future) + throws ExecutionException, InterruptedException { + Object result = future.get(); + setResultTag(span, result != null); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/MemcacheClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/MemcacheClientTracer.java new file mode 100644 index 000000000..02f1833a1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/MemcacheClientTracer.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spymemcached; + +import io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import java.net.InetSocketAddress; +import net.spy.memcached.MemcachedConnection; + +public class MemcacheClientTracer + extends DatabaseClientTracer { + private static final MemcacheClientTracer TRACER = new MemcacheClientTracer(); + + private MemcacheClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + public static MemcacheClientTracer tracer() { + return TRACER; + } + + @Override + protected String sanitizeStatement(String methodName) { + char[] chars = + methodName + .replaceFirst("^async", "") + // 'CAS' name is special, we have to lowercase whole name + .replaceFirst("^CAS", "cas") + .toCharArray(); + + // Lowercase first letter + chars[0] = Character.toLowerCase(chars[0]); + + return new String(chars); + } + + @Override + protected String dbSystem(MemcachedConnection memcachedConnection) { + return "memcached"; + } + + @Override + protected InetSocketAddress peerAddress(MemcachedConnection memcachedConnection) { + return null; + } + + @Override + protected String dbOperation( + MemcachedConnection connection, String methodName, String operation) { + return operation; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.spymemcached-2.12"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/MemcachedClientInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/MemcachedClientInstrumentation.java new file mode 100644 index 000000000..7829f8d87 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/MemcachedClientInstrumentation.java @@ -0,0 +1,164 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spymemcached; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.CallDepth; +import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.spy.memcached.MemcachedClient; +import net.spy.memcached.internal.BulkFuture; +import net.spy.memcached.internal.GetFuture; +import net.spy.memcached.internal.OperationFuture; + +public class MemcachedClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("net.spy.memcached.MemcachedClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(returns(named("net.spy.memcached.internal.OperationFuture"))) + // Flush seems to have a bug when listeners may not be always called. + // Also tracing flush is probably of a very limited value. + .and(not(named("flush"))), + this.getClass().getName() + "$AsyncOperationAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(returns(named("net.spy.memcached.internal.GetFuture"))), + this.getClass().getName() + "$AsyncGetAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(returns(named("net.spy.memcached.internal.BulkFuture"))), + this.getClass().getName() + "$AsyncBulkAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(namedOneOf("incr", "decr")), + this.getClass().getName() + "$SyncOperationAdvice"); + } + + @SuppressWarnings("unused") + public static class AsyncOperationAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void trackCallDepth(@Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepthThreadLocalMap.getCallDepth(MemcachedClient.class); + callDepth.getAndIncrement(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.This MemcachedClient client, + @Advice.Origin("#m") String methodName, + @Advice.Return OperationFuture future, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { + return; + } + + if (future != null) { + OperationCompletionListener listener = + new OperationCompletionListener(currentContext(), client.getConnection(), methodName); + future.addListener(listener); + } + } + } + + @SuppressWarnings("unused") + public static class AsyncGetAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void trackCallDepth(@Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepthThreadLocalMap.getCallDepth(MemcachedClient.class); + callDepth.getAndIncrement(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.This MemcachedClient client, + @Advice.Origin("#m") String methodName, + @Advice.Return GetFuture future, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { + return; + } + + if (future != null) { + GetCompletionListener listener = + new GetCompletionListener(currentContext(), client.getConnection(), methodName); + future.addListener(listener); + } + } + } + + @SuppressWarnings("unused") + public static class AsyncBulkAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void trackCallDepth(@Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepthThreadLocalMap.getCallDepth(MemcachedClient.class); + callDepth.getAndIncrement(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.This MemcachedClient client, + @Advice.Origin("#m") String methodName, + @Advice.Return BulkFuture future, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { + return; + } + + if (future != null) { + BulkGetCompletionListener listener = + new BulkGetCompletionListener(currentContext(), client.getConnection(), methodName); + future.addListener(listener); + } + } + } + + @SuppressWarnings("unused") + public static class SyncOperationAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static SyncCompletionListener methodEnter( + @Advice.This MemcachedClient client, + @Advice.Origin("#m") String methodName, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepthThreadLocalMap.getCallDepth(MemcachedClient.class); + if (callDepth.getAndIncrement() > 0) { + return null; + } + + return new SyncCompletionListener(currentContext(), client.getConnection(), methodName); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Enter SyncCompletionListener listener, + @Advice.Thrown Throwable thrown, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { + return; + } + + listener.done(thrown); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/OperationCompletionListener.java b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/OperationCompletionListener.java new file mode 100644 index 000000000..eb5c406c1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/OperationCompletionListener.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spymemcached; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import java.util.concurrent.ExecutionException; +import net.spy.memcached.MemcachedConnection; +import net.spy.memcached.internal.OperationFuture; + +public class OperationCompletionListener extends CompletionListener> + implements net.spy.memcached.internal.OperationCompletionListener { + public OperationCompletionListener( + Context parentContext, MemcachedConnection connection, String methodName) { + super(parentContext, connection, methodName); + } + + @Override + public void onComplete(OperationFuture future) { + closeAsyncSpan(future); + } + + @Override + protected void processResult(Span span, OperationFuture future) + throws ExecutionException, InterruptedException { + future.get(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedInstrumentationModule.java new file mode 100644 index 000000000..d7a6b20a6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spymemcached; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class SpymemcachedInstrumentationModule extends InstrumentationModule { + + public SpymemcachedInstrumentationModule() { + super("spymemcached", "spymemcached-2.12"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new MemcachedClientInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SyncCompletionListener.java b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SyncCompletionListener.java new file mode 100644 index 000000000..4715af2e2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SyncCompletionListener.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spymemcached; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import net.spy.memcached.MemcachedConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SyncCompletionListener extends CompletionListener { + + private static final Logger log = LoggerFactory.getLogger(SyncCompletionListener.class); + + public SyncCompletionListener( + Context parentContext, MemcachedConnection connection, String methodName) { + super(parentContext, connection, methodName); + } + + @Override + protected void processResult(Span span, Void future) { + log.error("processResult was called on SyncCompletionListener. This should never happen. "); + } + + public void done(Throwable thrown) { + closeSyncSpan(thrown); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/test/groovy/SpymemcachedTest.groovy b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/test/groovy/SpymemcachedTest.groovy new file mode 100644 index 000000000..2cb269135 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/spymemcached-2.12/javaagent/src/test/groovy/SpymemcachedTest.groovy @@ -0,0 +1,643 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static net.spy.memcached.ConnectionFactoryBuilder.Protocol.BINARY + +import com.google.common.util.concurrent.MoreExecutors +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.javaagent.instrumentation.spymemcached.CompletionListener +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.time.Duration +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.BlockingQueue +import java.util.concurrent.ExecutorService +import java.util.concurrent.locks.ReentrantLock +import net.spy.memcached.CASResponse +import net.spy.memcached.ConnectionFactory +import net.spy.memcached.ConnectionFactoryBuilder +import net.spy.memcached.DefaultConnectionFactory +import net.spy.memcached.MemcachedClient +import net.spy.memcached.internal.CheckedOperationTimeoutException +import net.spy.memcached.ops.Operation +import net.spy.memcached.ops.OperationQueueFactory +import org.testcontainers.containers.GenericContainer +import spock.lang.Shared + +class SpymemcachedTest extends AgentInstrumentationSpecification { + + @Shared + def parentOperation = "parent-span" + @Shared + def expiration = 3600 + @Shared + def keyPrefix = "SpymemcachedTest-" + (Math.abs(new Random().nextInt())) + "-" + @Shared + def timingOutMemcachedOpTimeout = 1000 + + @Shared + def memcachedContainer + @Shared + InetSocketAddress memcachedAddress + + def setupSpec() { + memcachedContainer = new GenericContainer('memcached:latest') + .withExposedPorts(11211) + .withStartupTimeout(Duration.ofSeconds(120)) + memcachedContainer.start() + memcachedAddress = new InetSocketAddress( + memcachedContainer.containerIpAddress, + memcachedContainer.getMappedPort(11211) + ) + } + + def cleanupSpec() { + if (memcachedContainer) { + memcachedContainer.stop() + } + } + + ReentrantLock queueLock + MemcachedClient memcached + MemcachedClient lockableMemcached + MemcachedClient timingoutMemcached + + def setup() { + queueLock = new ReentrantLock() + + // Use direct executor service so our listeners finish in deterministic order + ExecutorService listenerExecutorService = MoreExecutors.newDirectExecutorService() + + ConnectionFactory connectionFactory = (new ConnectionFactoryBuilder()) + .setListenerExecutorService(listenerExecutorService) + .setProtocol(BINARY) + .build() + memcached = new MemcachedClient(connectionFactory, Arrays.asList(memcachedAddress)) + + def lockableQueueFactory = new OperationQueueFactory() { + @Override + BlockingQueue create() { + return getLockableQueue(queueLock) + } + } + + ConnectionFactory lockableConnectionFactory = (new ConnectionFactoryBuilder()) + .setListenerExecutorService(listenerExecutorService) + .setProtocol(BINARY) + .setOpQueueFactory(lockableQueueFactory) + .build() + lockableMemcached = new MemcachedClient(lockableConnectionFactory, Arrays.asList(memcachedAddress)) + + ConnectionFactory timingoutConnectionFactory = (new ConnectionFactoryBuilder()) + .setListenerExecutorService(listenerExecutorService) + .setProtocol(BINARY) + .setOpQueueFactory(lockableQueueFactory) + .setOpTimeout(timingOutMemcachedOpTimeout) + .build() + timingoutMemcached = new MemcachedClient(timingoutConnectionFactory, Arrays.asList(memcachedAddress)) + + // Add some keys to test on later: + def valuesToSet = [ + "test-get" : "get test", + "test-get-2" : "get test 2", + "test-append" : "append test", + "test-prepend": "prepend test", + "test-delete" : "delete test", + "test-replace": "replace test", + "test-touch" : "touch test", + "test-cas" : "cas test", + "test-decr" : "200", + "test-incr" : "100" + ] + runUnderTrace("setup") { + valuesToSet.each { k, v -> assert memcached.set(key(k), expiration, v).get() } + } + ignoreTracesAndClear(1) + } + + def "test get hit"() { + when: + runUnderTrace(parentOperation) { + assert "get test" == memcached.get(key("test-get")) + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "get", null, "hit") + } + } + } + + def "test get miss"() { + when: + runUnderTrace(parentOperation) { + assert null == memcached.get(key("test-get-key-that-doesn't-exist")) + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "get", null, "miss") + } + } + } + + def "test get cancel"() { + when: + runUnderTrace(parentOperation) { + queueLock.lock() + lockableMemcached.asyncGet(key("test-get")).cancel(true) + queueLock.unlock() + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "get", "canceled") + } + } + } + + def "test get timeout"() { + when: + /* + Not using runUnderTrace since timeouts happen in separate thread + and direct executor doesn't help to make sure that parent span finishes last. + Instead run without parent span to have only 1 span to test with. + */ + try { + queueLock.lock() + timingoutMemcached.asyncGet(key("test-get")) + Thread.sleep(timingOutMemcachedOpTimeout + 1000) + } finally { + queueLock.unlock() + } + + then: + assertTraces(1) { + trace(0, 1) { + getSpan(it, 0, "get", "timeout") + } + } + } + + def "test bulk get"() { + when: + runUnderTrace(parentOperation) { + def expected = [(key("test-get")): "get test", (key("test-get-2")): "get test 2"] + assert expected == memcached.getBulk(key("test-get"), key("test-get-2")) + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "getBulk", null, null) + } + } + } + + def "test set"() { + when: + runUnderTrace(parentOperation) { + assert memcached.set(key("test-set"), expiration, "bar").get() + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "set") + } + } + } + + def "test set cancel"() { + when: + runUnderTrace(parentOperation) { + queueLock.lock() + assert lockableMemcached.set(key("test-set-cancel"), expiration, "bar").cancel() + queueLock.unlock() + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "set", "canceled") + } + } + } + + def "test add"() { + when: + runUnderTrace(parentOperation) { + assert memcached.add(key("test-add"), expiration, "add bar").get() + assert "add bar" == memcached.get(key("test-add")) + } + + then: + assertTraces(1) { + trace(0, 3) { + getParentSpan(it, 0) + getSpan(it, 1, "add") + getSpan(it, 2, "get", null, "hit") + } + } + } + + def "test second add"() { + when: + runUnderTrace(parentOperation) { + assert memcached.add(key("test-add-2"), expiration, "add bar").get() + assert !memcached.add(key("test-add-2"), expiration, "add bar 123").get() + } + + then: + assertTraces(1) { + trace(0, 3) { + getParentSpan(it, 0) + getSpan(it, 1, "add") + getSpan(it, 2, "add") + } + } + } + + def "test delete"() { + when: + runUnderTrace(parentOperation) { + assert memcached.delete(key("test-delete")).get() + assert null == memcached.get(key("test-delete")) + } + + then: + assertTraces(1) { + trace(0, 3) { + getParentSpan(it, 0) + getSpan(it, 1, "delete") + getSpan(it, 2, "get", null, "miss") + } + } + } + + def "test delete non existent"() { + when: + runUnderTrace(parentOperation) { + assert !memcached.delete(key("test-delete-non-existent")).get() + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "delete") + } + } + } + + def "test replace"() { + when: + runUnderTrace(parentOperation) { + assert memcached.replace(key("test-replace"), expiration, "new value").get() + assert "new value" == memcached.get(key("test-replace")) + } + + then: + assertTraces(1) { + trace(0, 3) { + getParentSpan(it, 0) + getSpan(it, 1, "replace") + getSpan(it, 2, "get", null, "hit") + } + } + } + + def "test replace non existent"() { + when: + runUnderTrace(parentOperation) { + assert !memcached.replace(key("test-replace-non-existent"), expiration, "new value").get() + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "replace") + } + } + } + + def "test append"() { + when: + runUnderTrace(parentOperation) { + def cas = memcached.gets(key("test-append")) + assert memcached.append(cas.cas, key("test-append"), " appended").get() + assert "append test appended" == memcached.get(key("test-append")) + } + + then: + assertTraces(1) { + trace(0, 4) { + getParentSpan(it, 0) + getSpan(it, 1, "gets") + getSpan(it, 2, "append") + getSpan(it, 3, "get", null, "hit") + } + } + } + + def "test prepend"() { + when: + runUnderTrace(parentOperation) { + def cas = memcached.gets(key("test-prepend")) + assert memcached.prepend(cas.cas, key("test-prepend"), "prepended ").get() + assert "prepended prepend test" == memcached.get(key("test-prepend")) + } + + then: + assertTraces(1) { + trace(0, 4) { + getParentSpan(it, 0) + getSpan(it, 1, "gets") + getSpan(it, 2, "prepend") + getSpan(it, 3, "get", null, "hit") + } + } + } + + def "test cas"() { + when: + runUnderTrace(parentOperation) { + def cas = memcached.gets(key("test-cas")) + assert CASResponse.OK == memcached.cas(key("test-cas"), cas.cas, expiration, "cas bar") + } + + then: + assertTraces(1) { + trace(0, 3) { + getParentSpan(it, 0) + getSpan(it, 1, "gets") + getSpan(it, 2, "cas") + } + } + } + + def "test cas not found"() { + when: + runUnderTrace(parentOperation) { + assert CASResponse.NOT_FOUND == memcached.cas(key("test-cas-doesnt-exist"), 1234, expiration, "cas bar") + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "cas") + } + } + } + + def "test touch"() { + when: + runUnderTrace(parentOperation) { + assert memcached.touch(key("test-touch"), expiration).get() + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "touch") + } + } + } + + def "test touch non existent"() { + when: + runUnderTrace(parentOperation) { + assert !memcached.touch(key("test-touch-non-existent"), expiration).get() + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "touch") + } + } + } + + def "test get and touch"() { + when: + runUnderTrace(parentOperation) { + assert "touch test" == memcached.getAndTouch(key("test-touch"), expiration).value + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "getAndTouch") + } + } + } + + def "test get and touch non existent"() { + when: + runUnderTrace(parentOperation) { + assert null == memcached.getAndTouch(key("test-touch-non-existent"), expiration) + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "getAndTouch") + } + } + } + + def "test decr"() { + when: + runUnderTrace(parentOperation) { + /* + Memcached is funny in the way it handles incr/decr operations: + it needs values to be strings (with digits in them) and it returns actual long from decr/incr + */ + assert 195 == memcached.decr(key("test-decr"), 5) + assert "195" == memcached.get(key("test-decr")) + } + + then: + assertTraces(1) { + trace(0, 3) { + getParentSpan(it, 0) + getSpan(it, 1, "decr") + getSpan(it, 2, "get", null, "hit") + } + } + } + + def "test decr non existent"() { + when: + runUnderTrace(parentOperation) { + assert -1 == memcached.decr(key("test-decr-non-existent"), 5) + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "decr") + } + } + } + + def "test decr exception"() { + when: + memcached.decr(key("long key: " + longString()), 5) + + then: + thrown IllegalArgumentException + assertTraces(1) { + trace(0, 1) { + getSpan(it, 0, "decr", "long key") + } + } + } + + def "test incr"() { + when: + runUnderTrace(parentOperation) { + /* + Memcached is funny in the way it handles incr/decr operations: + it needs values to be strings (with digits in them) and it returns actual long from decr/incr + */ + assert 105 == memcached.incr(key("test-incr"), 5) + assert "105" == memcached.get(key("test-incr")) + } + + then: + assertTraces(1) { + trace(0, 3) { + getParentSpan(it, 0) + getSpan(it, 1, "incr") + getSpan(it, 2, "get", null, "hit") + } + } + } + + def "test incr non existent"() { + when: + runUnderTrace(parentOperation) { + assert -1 == memcached.incr(key("test-incr-non-existent"), 5) + } + + then: + assertTraces(1) { + trace(0, 2) { + getParentSpan(it, 0) + getSpan(it, 1, "incr") + } + } + } + + def "test incr exception"() { + when: + memcached.incr(key("long key: " + longString()), 5) + + then: + thrown IllegalArgumentException + assertTraces(1) { + trace(0, 1) { + getSpan(it, 0, "incr", "long key") + } + } + } + + def key(String k) { + keyPrefix + k + } + + def longString(char c = 's' as char) { + char[] chars = new char[250] + Arrays.fill(chars, 's' as char) + return new String(chars) + } + + def getLockableQueue(ReentrantLock queueLock) { + return new ArrayBlockingQueue(DefaultConnectionFactory.DEFAULT_OP_QUEUE_LEN) { + + @Override + int drainTo(Collection c, int maxElements) { + try { + queueLock.lock() + return super.drainTo(c, maxElements) + } finally { + queueLock.unlock() + } + } + } + } + + def getParentSpan(TraceAssert trace, int index) { + return trace.span(index) { + name parentOperation + hasNoParent() + attributes { + } + } + } + + def getSpan(TraceAssert trace, int index, String operation, String error = null, String result = null) { + return trace.span(index) { + if (index > 0) { + childOf(trace.span(0)) + } + + name operation + kind CLIENT + if (error != null && error != "canceled") { + status ERROR + } + + if (error == "timeout") { + errorEvent( + CheckedOperationTimeoutException, + "Operation timed out. - failing node: ${memcachedAddress.address}:${memcachedAddress.port}") + } + + if (error == "long key") { + errorEvent( + IllegalArgumentException, + "Key is too long (maxlen = 250)") + } + + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "memcached" + "${SemanticAttributes.DB_OPERATION.key}" operation + + if (error == "canceled") { + "${CompletionListener.DB_COMMAND_CANCELLED}" true + } + + if (result == "hit") { + "${CompletionListener.MEMCACHED_RESULT}" CompletionListener.HIT + } + + if (result == "miss") { + "${CompletionListener.MEMCACHED_RESULT}" CompletionListener.MISS + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java new file mode 100644 index 000000000..71b1a6e97 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.struts2; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.struts2.Struts2Tracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.opensymphony.xwork2.ActionInvocation; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ActionInvocationInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.opensymphony.xwork2.ActionInvocation"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("com.opensymphony.xwork2.ActionInvocation")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("invokeActionOnly")), + this.getClass().getName() + "$InvokeActionOnlyAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeActionOnlyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This ActionInvocation actionInvocation, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = Java8BytecodeBridge.currentContext(); + + context = tracer().startSpan(parentContext, actionInvocation); + scope = context.makeCurrent(); + + tracer().updateServerSpanName(parentContext, actionInvocation.getProxy()); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2InstrumentationModule.java new file mode 100644 index 000000000..82d00204f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2InstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.struts2; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class Struts2InstrumentationModule extends InstrumentationModule { + + public Struts2InstrumentationModule() { + super("struts", "struts-2.3"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ActionInvocationInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2Tracer.java b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2Tracer.java new file mode 100644 index 000000000..8cc99ead3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2Tracer.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.struts2; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTROLLER; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.ActionProxy; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +public class Struts2Tracer extends BaseTracer { + + private static final Struts2Tracer TRACER = new Struts2Tracer(); + + public static Struts2Tracer tracer() { + return TRACER; + } + + public Context startSpan(Context parentContext, ActionInvocation actionInvocation) { + Object action = actionInvocation.getAction(); + Class actionClass = action.getClass(); + + String method = actionInvocation.getProxy().getMethod(); + String spanName = SpanNames.fromMethod(actionClass, method); + + SpanBuilder strutsSpan = spanBuilder(parentContext, spanName, INTERNAL); + + strutsSpan.setAttribute(SemanticAttributes.CODE_NAMESPACE, actionClass.getName()); + if (method != null) { + strutsSpan.setAttribute(SemanticAttributes.CODE_FUNCTION, method); + } + + return parentContext.with(strutsSpan.startSpan()); + } + + // Handle cases where action parameters are encoded into URL path + public void updateServerSpanName(Context context, ActionProxy actionProxy) { + ServerSpanNaming.updateServerSpanName( + context, CONTROLLER, () -> getServerSpanName(context, actionProxy)); + } + + private static String getServerSpanName(Context context, ActionProxy actionProxy) { + // We take name from the config, because it contains the path pattern from the + // configuration. + String result = actionProxy.getConfig().getName(); + + String actionNamespace = actionProxy.getNamespace(); + if (actionNamespace != null && !actionNamespace.isEmpty()) { + if (actionNamespace.endsWith("/") || result.startsWith("/")) { + result = actionNamespace + result; + } else { + result = actionNamespace + "/" + result; + } + } + + if (!result.startsWith("/")) { + result = "/" + result; + } + + return ServletContextPath.prepend(context, result); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.struts-2.3"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/groovy/Struts2ActionSpanTest.groovy b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/groovy/Struts2ActionSpanTest.groovy new file mode 100644 index 000000000..7e1afa5f1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/groovy/Struts2ActionSpanTest.groovy @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicServerSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan + +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.struts.GreetingServlet +import javax.servlet.DispatcherType +import org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.DefaultServlet +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.util.resource.FileResource + +class Struts2ActionSpanTest extends HttpServerTest implements AgentTestTrait { + + @Override + boolean testPathParam() { + return true + } + + @Override + boolean testErrorBody() { + return false + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + return endpoint != NOT_FOUND + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == ERROR || endpoint == EXCEPTION || endpoint == NOT_FOUND + } + + @Override + void responseSpan(TraceAssert trace, int index, Object controllerSpan, Object handlerSpan, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case REDIRECT: + redirectSpan(trace, index, handlerSpan) + break + case ERROR: + case EXCEPTION: + case NOT_FOUND: + sendErrorSpan(trace, index, handlerSpan) + break + } + } + + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return getContextPath() + "/path/{id}/param" + case NOT_FOUND: + return getContextPath() + "/*" + default: + return endpoint.resolvePath(address).path + } + } + + @Override + void handlerSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + trace.span(index) { + name "GreetingAction.${endpoint.name().toLowerCase()}" + kind INTERNAL + if (endpoint == EXCEPTION) { + status StatusCode.ERROR + errorEvent(Exception, EXCEPTION.body) + } + def expectedMethodName = endpoint.name().toLowerCase() + attributes { + "${SemanticAttributes.CODE_NAMESPACE.key}" "io.opentelemetry.struts.GreetingAction" + "${SemanticAttributes.CODE_FUNCTION.key}" expectedMethodName + } + childOf((SpanData) parent) + } + } + + @Override + String getContextPath() { + return "/context" + } + + @Override + Server startServer(int port) { + def server = new Server(port) + ServletContextHandler context = new ServletContextHandler(0) + context.setContextPath(getContextPath()) + def resource = new FileResource(getClass().getResource("/")) + context.setBaseResource(resource) + server.setHandler(context) + + context.addServlet(DefaultServlet, "/") + context.addServlet(GreetingServlet, "/greetingServlet") + context.addFilter(StrutsPrepareAndExecuteFilter, "/*", EnumSet.of(DispatcherType.REQUEST)) + + server.start() + + return server + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + // Struts runs from a servlet filter. Test that dispatching from struts action to a servlet + // does not overwrite server span name given by struts instrumentation. + def "test dispatch to servlet"() { + setup: + def response = client.get(address.resolve("dispatch").toString()).aggregate().join() + + expect: + response.status().code() == 200 + response.contentUtf8() == "greeting" + + and: + assertTraces(1) { + trace(0, 2) { + basicServerSpan(it, 0, getContextPath() + "/dispatch", null) + basicSpan(it, 1, "GreetingAction.dispatch_servlet", span(0)) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingAction.java b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingAction.java new file mode 100644 index 000000000..d3a217d76 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingAction.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.struts; + +import com.opensymphony.xwork2.ActionSupport; +import io.opentelemetry.instrumentation.test.base.HttpServerTest; + +public class GreetingAction extends ActionSupport { + + String responseBody = "default"; + + public String success() { + responseBody = + HttpServerTest.controller( + HttpServerTest.ServerEndpoint.SUCCESS, HttpServerTest.ServerEndpoint.SUCCESS::getBody); + + return "greeting"; + } + + public String redirect() { + responseBody = + HttpServerTest.controller( + HttpServerTest.ServerEndpoint.REDIRECT, + HttpServerTest.ServerEndpoint.REDIRECT::getBody); + return "redirect"; + } + + public String query_param() { + responseBody = + HttpServerTest.controller( + HttpServerTest.ServerEndpoint.QUERY_PARAM, + HttpServerTest.ServerEndpoint.QUERY_PARAM::getBody); + return "greeting"; + } + + public String error() { + HttpServerTest.controller( + HttpServerTest.ServerEndpoint.ERROR, HttpServerTest.ServerEndpoint.ERROR::getBody); + return "error"; + } + + public String exception() { + HttpServerTest.controller( + HttpServerTest.ServerEndpoint.EXCEPTION, + () -> { + throw new Exception(HttpServerTest.ServerEndpoint.EXCEPTION.getBody()); + }); + throw new AssertionError(); // should not reach here + } + + public String path_param() { + HttpServerTest.controller( + HttpServerTest.ServerEndpoint.PATH_PARAM, + () -> + "this does nothing, as responseBody is set in setId, but we need this controller span nevertheless"); + return "greeting"; + } + + public String dispatch_servlet() { + return "greetingServlet"; + } + + public void setId(String id) { + responseBody = id; + } + + public String getResponseBody() { + return responseBody; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingServlet.java b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingServlet.java new file mode 100644 index 000000000..5d8e491c5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingServlet.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.struts; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class GreetingServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.getWriter().write("greeting"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/resources/greeting.ftl b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/resources/greeting.ftl new file mode 100644 index 000000000..b833223d9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/resources/greeting.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="responseBody" type="java.lang.String" --> +${responseBody} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml new file mode 100644 index 000000000..11a08fb2e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + /redirected + false + + + 500 + + /greeting.ftl + /greetingServlet + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/struts-2.3-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/struts-2.3-javaagent.gradle new file mode 100644 index 000000000..b84079df7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/struts-2.3/javaagent/struts-2.3-javaagent.gradle @@ -0,0 +1,30 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.struts" + module = "struts2-core" + versions = "[2.3.1,)" + } +} + +dependencies { + library "org.apache.struts:struts2-core:2.3.1" + + // There was no 2.4 version at all. + // In version 2.5 Struts Servlet Filter entry point was relocated. + // This Servlet Filter is relevant only in setting up the test app and it is not used in + // instrumentation. So fixing Struts library version for the test. + latestDepTestLibrary "org.apache.struts:struts2-core:2.3.+" + + testImplementation(project(':testing-common')) + testImplementation "org.eclipse.jetty:jetty-server:8.0.0.v20110901" + testImplementation "org.eclipse.jetty:jetty-servlet:8.0.0.v20110901" + testRuntimeOnly "javax.servlet:javax.servlet-api:3.0.1" + testRuntimeOnly "javax.servlet:jsp-api:2.0" + + testInstrumentation project(":instrumentation:servlet:servlet-3.0:javaagent") + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') +} + diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/ComponentPageElementImplInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/ComponentPageElementImplInstrumentation.java new file mode 100644 index 000000000..87531a397 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/ComponentPageElementImplInstrumentation.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tapestry; + +import static io.opentelemetry.javaagent.instrumentation.tapestry.TapestryTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.tapestry5.internal.structure.ComponentPageElementImpl; + +public class ComponentPageElementImplInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.tapestry5.internal.structure.ComponentPageElementImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("processEventTriggering")) + .and(takesArguments(3)) + .and(takesArgument(0, String.class)) + .and(takesArgument(1, named("org.apache.tapestry5.EventContext"))) + .and(takesArgument(2, named("org.apache.tapestry5.ComponentEventCallback"))), + this.getClass().getName() + "$EventAdvice"); + } + + @SuppressWarnings("unused") + public static class EventAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This ComponentPageElementImpl componentPageElementImpl, + @Advice.Argument(0) String eventType, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startEventSpan(eventType, componentPageElementImpl.getCompleteId()); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/InitializeActivePageNameInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/InitializeActivePageNameInstrumentation.java new file mode 100644 index 000000000..f3eb9e8ed --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/InitializeActivePageNameInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tapestry; + +import static io.opentelemetry.javaagent.instrumentation.tapestry.TapestryTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.tapestry5.services.ComponentEventRequestParameters; +import org.apache.tapestry5.services.PageRenderRequestParameters; + +public class InitializeActivePageNameInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.tapestry5.services.InitializeActivePageName"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("handleComponentEvent")) + .and(takesArguments(2)) + .and( + takesArgument( + 0, named("org.apache.tapestry5.services.ComponentEventRequestParameters"))) + .and(takesArgument(1, named("org.apache.tapestry5.services.ComponentRequestHandler"))), + this.getClass().getName() + "$HandleComponentEventAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("handlePageRender")) + .and(takesArguments(2)) + .and( + takesArgument( + 0, named("org.apache.tapestry5.services.PageRenderRequestParameters"))) + .and(takesArgument(1, named("org.apache.tapestry5.services.ComponentRequestHandler"))), + this.getClass().getName() + "$HandlePageRenderAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleComponentEventAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) ComponentEventRequestParameters parameters) { + tracer().updateServerSpanName(parameters.getActivePageName()); + } + } + + @SuppressWarnings("unused") + public static class HandlePageRenderAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) PageRenderRequestParameters parameters) { + tracer().updateServerSpanName(parameters.getLogicalPageName()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryInstrumentationModule.java new file mode 100644 index 000000000..bfd4cff33 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryInstrumentationModule.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tapestry; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class TapestryInstrumentationModule extends InstrumentationModule { + + public TapestryInstrumentationModule() { + super("tapestry", "tapestry-5.4"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // class added in tapestry 5.4.0 + return hasClassesNamed("org.apache.tapestry5.Binding2"); + } + + @Override + public List typeInstrumentations() { + return asList( + new InitializeActivePageNameInstrumentation(), + new ComponentPageElementImplInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryTracer.java b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryTracer.java new file mode 100644 index 000000000..730fccc31 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryTracer.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tapestry; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import org.apache.tapestry5.runtime.ComponentEventException; + +public class TapestryTracer extends BaseTracer { + + private static final TapestryTracer TRACER = new TapestryTracer(); + + public static TapestryTracer tracer() { + return TRACER; + } + + private TapestryTracer() { + super(GlobalOpenTelemetry.get()); + } + + public void updateServerSpanName(String pageName) { + if (pageName == null) { + return; + } + Context context = Context.current(); + Span span = ServerSpan.fromContextOrNull(context); + if (span != null) { + if (!pageName.isEmpty()) { + pageName = "/" + pageName; + } + span.updateName(ServletContextPath.prepend(context, pageName)); + } + } + + public Context startEventSpan(String eventType, String componentId) { + return super.startSpan(eventType + "/" + componentId); + } + + @Override + protected Throwable unwrapThrowable(Throwable throwable) { + if (throwable instanceof ComponentEventException) { + throwable = throwable.getCause(); + } + return super.unwrapThrowable(throwable); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.tapestry-5.4"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/groovy/TapestryTest.groovy b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/groovy/TapestryTest.groovy new file mode 100644 index 000000000..734670c6b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/groovy/TapestryTest.groovy @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan + +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.testing.internal.armeria.client.ClientRequestContext +import io.opentelemetry.testing.internal.armeria.client.DecoratingHttpClientFunction +import io.opentelemetry.testing.internal.armeria.client.HttpClient +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames +import io.opentelemetry.testing.internal.armeria.common.HttpRequest +import io.opentelemetry.testing.internal.armeria.common.HttpResponse +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.util.resource.Resource +import org.eclipse.jetty.webapp.WebAppContext +import org.jsoup.Jsoup + +class TapestryTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + @Override + Server startServer(int port) { + WebAppContext webAppContext = new WebAppContext() + webAppContext.setContextPath(getContextPath()) + // set up test application + webAppContext.setBaseResource(Resource.newResource("src/test/webapp")) + + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + jettyServer.setHandler(webAppContext) + jettyServer.start() + + return jettyServer + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/jetty-context" + } + + WebClient client + + def setup() { + client = WebClient.builder(address) + .decorator(new DecoratingHttpClientFunction() { + // https://github.com/line/armeria/issues/2489 + @Override + HttpResponse execute(HttpClient delegate, ClientRequestContext ctx, HttpRequest req) throws Exception { + return HttpResponse.from(delegate.execute(ctx, req).aggregate().thenApply {resp -> + if (resp.status().isRedirection()) { + return delegate.execute(ctx, HttpRequest.of(req.method(), URI.create(resp.headers().get(HttpHeaderNames.LOCATION)).path)) + } + return resp.toHttpResponse() + }) + } + }) + .build() + } + + static serverSpan(TraceAssert trace, int index, String spanName) { + trace.span(index) { + hasNoParent() + + name spanName + kind SpanKind.SERVER + } + } + + def "test index page"() { + setup: + AggregatedHttpResponse response = client.get("/").aggregate().join() + def doc = Jsoup.parse(response.contentUtf8()) + + expect: + response.status().code() == 200 + doc.selectFirst("title").text() == "Index page" + + assertTraces(1) { + trace(0, 2) { + serverSpan(it, 0, getContextPath() + "/Index") + basicSpan(it, 1, "activate/Index", span(0)) + } + } + } + + def "test start action"() { + setup: + // index.start triggers an action named "start" on index page + AggregatedHttpResponse response = client.get("/index.start").aggregate().join() + def doc = Jsoup.parse(response.contentUtf8()) + + expect: + response.status().code() == 200 + doc.selectFirst("title").text() == "Other page" + + assertTraces(2) { + trace(0, 4) { + serverSpan(it, 0, getContextPath() + "/Index") + basicSpan(it, 1, "activate/Index", span(0)) + basicSpan(it, 2, "action/Index:start", span(0)) + basicSpan(it, 3, "Response.sendRedirect", span(2)) + } + trace(1, 2) { + serverSpan(it, 0, getContextPath() + "/Other") + basicSpan(it, 1, "activate/Other", span(0)) + } + } + } + + def "test exception action"() { + setup: + // index.exception triggers an action named "exception" on index page + AggregatedHttpResponse response = client.get("/index.exception").aggregate().join() + + expect: + response.status().code() == 500 + + assertTraces(1) { + trace(0, 3) { + span(0) { + hasNoParent() + kind SpanKind.SERVER + name getContextPath() + "/Index" + status ERROR + } + basicSpan(it, 1, "activate/Index", span(0)) + basicSpan(it, 2, "action/Index:exception", span(0), new IllegalStateException("expected")) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/components/Layout.java b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/components/Layout.java new file mode 100644 index 000000000..52be1368d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/components/Layout.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.tapestry.components; + +import org.apache.tapestry5.BindingConstants; +import org.apache.tapestry5.ComponentResources; +import org.apache.tapestry5.annotations.Parameter; +import org.apache.tapestry5.annotations.Property; +import org.apache.tapestry5.ioc.annotations.Inject; + +public class Layout { + @Inject private ComponentResources resources; + + @Property + @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL) + private String title; +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/pages/Index.java b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/pages/Index.java new file mode 100644 index 000000000..6692fa9fc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/pages/Index.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.tapestry.pages; + +import org.apache.tapestry5.annotations.InjectPage; + +public class Index { + + @InjectPage private Other other; + + Object onActionFromStart() { + return other; + } + + Object onActionFromException() { + throw new IllegalStateException("expected"); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/pages/Other.java b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/pages/Other.java new file mode 100644 index 000000000..8fb39e1bc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/java/test/tapestry/pages/Other.java @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.tapestry.pages; + +public class Other {} diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/components/Layout.tml b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/components/Layout.tml new file mode 100644 index 000000000..1940e7a41 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/components/Layout.tml @@ -0,0 +1,12 @@ + + + + + ${title} + + + + + + diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/pages/Index.tml b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/pages/Index.tml new file mode 100644 index 000000000..7cc3c3bc3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/pages/Index.tml @@ -0,0 +1,7 @@ + + + start + exception + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/pages/Other.tml b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/pages/Other.tml new file mode 100644 index 000000000..e9f0ff732 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/resources/test/tapestry/pages/Other.tml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/webapp/WEB-INF/web.xml b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..dd6c9074c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,30 @@ + + + + + + + tapestry.app-package + test.tapestry + + + + + app + org.apache.tapestry5.TapestryFilter + + + + app + /* + REQUEST + ERROR + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/tapestry-5.4-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/tapestry-5.4-javaagent.gradle new file mode 100644 index 000000000..80c20578d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tapestry-5.4/javaagent/tapestry-5.4-javaagent.gradle @@ -0,0 +1,26 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.tapestry" + module = "tapestry-core" + versions = "[5.4.0,)" + assertInverse = true + } +} + +otelJava { + maxJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +dependencies { + library 'org.apache.tapestry:tapestry-core:5.4.0' + + testImplementation "org.eclipse.jetty:jetty-webapp:8.0.0.v20110901" + testImplementation "org.jsoup:jsoup:1.13.1" + testImplementation 'javax.annotation:javax.annotation-api:1.3.2' + + testInstrumentation project(':instrumentation:jetty:jetty-8.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/FilterContextInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/FilterContextInstrumentation.java new file mode 100644 index 000000000..2ad3adbce --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/FilterContextInstrumentation.java @@ -0,0 +1,127 @@ +package io.opentelemetry.javaagent.instrumentation.tesla; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import java.util.Map; + +public class FilterContextInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return ElementMatchers.named("com.xiaomi.youpin.gateway.filter.FilterContext"); + + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + ElementMatchers.named("addTraceEvent") + .and(ElementMatchers.isPublic()) + .and(ElementMatchers.takesArguments(2)) + .and(ElementMatchers.takesArgument(0, String.class)) + .and(ElementMatchers.takesArgument(1, Long.class)), + this.getClass().getName() + "$AddTraceEventAdvice1"); + transformer.applyAdviceToMethod( + ElementMatchers.named("addTraceEvent") + .and(ElementMatchers.isPublic()) + .and(ElementMatchers.takesArguments(2)) + .and(ElementMatchers.takesArgument(0, String.class)) + .and(ElementMatchers.takesArgument(1, Map.class)), + this.getClass().getName() + "$AddTraceEventAdvice2"); + transformer.applyAdviceToMethod( + ElementMatchers.named("addBusErrorEvent") + .and(ElementMatchers.isPublic()), + this.getClass().getName() + "$AddBusErrorEventAdvice"); + } + + + public static class AddTraceEventAdvice1 { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(0) String name, + @Advice.Argument(1) long time) { + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(0) String name, + @Advice.Argument(1) Long time) { + context = Context.current(); + Span.fromContext(context).addEvent(name, Attributes.builder().put("time", time + "ms").build()); + return; + } + } + + public static class AddTraceEventAdvice2 { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(0) String name, + @Advice.Argument(1) Map events) { + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(0) String name, + @Advice.Argument(1) Map events) { + if(events != null) { + context = Context.current(); + AttributesBuilder builder = Attributes.builder(); + for (String key : events.keySet()) { + builder.put(key,events.get(key)); + } + Span.fromContext(context).addEvent(name, builder.build()); + } + return; + } + } + + public static class AddBusErrorEventAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(0) String eventName, + @Advice.Argument(1) int errorCode, + @Advice.Argument(2) String errorMsg) { + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(0) String eventName, + @Advice.Argument(1) int errorCode, + @Advice.Argument(2) String errorMsg) { + context = Context.current(); + Span span = Span.fromContext(context); + AttributesBuilder builder = Attributes.builder(); + builder.put("errorCode", errorCode); + if (null != errorMsg && !"".equals(errorMsg)) { + if(errorMsg.length() > 4000) { + builder.put("result", errorMsg.substring(0, 4000) + "......"); + }else { + builder.put("result", errorMsg); + } + } + span.addEvent(eventName, builder.build()); + span.setStatus(StatusCode.ERROR); + return; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/RequestFilterChainInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/RequestFilterChainInstrumentation.java new file mode 100644 index 000000000..af0d05f90 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/RequestFilterChainInstrumentation.java @@ -0,0 +1,89 @@ +package io.opentelemetry.javaagent.instrumentation.tesla;/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.xiaomi.youpin.gateway.filter.RequestContext; +import com.youpin.xiaomi.tesla.bo.ApiInfo; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; + +public class RequestFilterChainInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return ElementMatchers.named("com.xiaomi.youpin.gateway.netty.filter.RequestFilterChain"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + ElementMatchers.named("doFilter") + .and(ElementMatchers.isPublic()), + this.getClass().getName() + "$InvokeAdvice"); + } + + + public static class InvokeAdvice { + + @SuppressWarnings({"SystemOut","CatchAndPrintStackTrace"}) + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(0) ApiInfo apiInfo, + @Advice.Argument(2) RequestContext reuqestContext + ) { + try { + Context parentContext = currentContext(); + TeslaRequest teslaRequest = new TeslaRequest(); + if (apiInfo == null) { + teslaRequest.setApiInfoIsNull(true); + String ctxUri = reuqestContext.getUri(); + if (ctxUri == null || ctxUri.isEmpty() || !ctxUri.startsWith(TeslaTraceHelper.URI_PREFIX)) { + teslaRequest.setUri(TeslaTraceHelper.UNKNOW_URI_SPANNAME); + } else { + teslaRequest.setUri(ctxUri); + } + } else { + teslaRequest.setApiInfoIsNull(false); + teslaRequest.setUri(apiInfo.getUrl()); + } + context = TeslaSingletons.instrumenter().start(parentContext, teslaRequest); + scope = context.makeCurrent(); + }catch(Throwable t){ + t.printStackTrace(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if(scope == null){ + return; + } + scope.close(); + TeslaSingletons.instrumenter().end(context,null,null,throwable); + return; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaInstrumentationModule.java new file mode 100644 index 000000000..b85859e95 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaInstrumentationModule.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tesla; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.tesla.sidecar.ExecuteProcessorInstrumentation; +import io.opentelemetry.javaagent.instrumentation.tesla.sidecar.SidecarServiceInstrumentation; + +import java.util.List; + +import static java.util.Arrays.asList; + +@AutoService(InstrumentationModule.class) +public class TeslaInstrumentationModule extends InstrumentationModule { + + public TeslaInstrumentationModule() { + super("tesla", "tesla"); + } + + @Override + public List typeInstrumentations() { + return asList( + new RequestFilterChainInstrumentation(), + new FilterContextInstrumentation(), + new SidecarServiceInstrumentation(), + new ExecuteProcessorInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaRequest.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaRequest.java new file mode 100644 index 000000000..c13b334cf --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaRequest.java @@ -0,0 +1,24 @@ +package io.opentelemetry.javaagent.instrumentation.tesla; + +public class TeslaRequest { + + private String uri; + + private boolean apiInfoIsNull; + + public boolean isApiInfoIsNull() { + return apiInfoIsNull; + } + + public void setApiInfoIsNull(boolean apiInfoIsNull) { + this.apiInfoIsNull = apiInfoIsNull; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaSingletons.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaSingletons.java new file mode 100644 index 000000000..6b1886a99 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaSingletons.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tesla; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +public final class TeslaSingletons { + + private static final String INSTRUMENTATION_NAME = "run.mone.tesla"; + + private static final Instrumenter INSTRUMENTER; + + static { + SpanNameExtractor spanName = request -> request.getUri(); + + AttributesExtractor extractor = new AttributesExtractor() { + @Override + protected void onStart(AttributesBuilder attributes, TeslaRequest request) { + if(request.isApiInfoIsNull()) { + String uri = request.getUri(); + if (uri == null || uri.isEmpty()) { + attributes.put("http.unknow.uri", uri); + } else { + attributes.put("http.apiinfo.isnull", true); + } + } + } + + @Override + protected void onEnd(AttributesBuilder attributes, TeslaRequest request, Void o2) { + + } + }; + INSTRUMENTER = + Instrumenter.newBuilder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName) + .addAttributesExtractor(extractor) + .newInstrumenter(SpanKindExtractor.alwaysInternal()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private TeslaSingletons() { + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaTraceHelper.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaTraceHelper.java new file mode 100644 index 000000000..8a6e564f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/TeslaTraceHelper.java @@ -0,0 +1,6 @@ +package io.opentelemetry.javaagent.instrumentation.tesla; + +public class TeslaTraceHelper { + public static final String URI_PREFIX = "/mtop"; + public static final String UNKNOW_URI_SPANNAME = "unknow-uri"; +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/ExecuteProcessorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/ExecuteProcessorInstrumentation.java new file mode 100644 index 000000000..8ba12cad5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/ExecuteProcessorInstrumentation.java @@ -0,0 +1,69 @@ +package io.opentelemetry.javaagent.instrumentation.tesla.sidecar; + +import com.xiaomi.data.push.uds.po.RpcCommand; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import java.util.HashMap; +import java.util.Map; + +import static io.opentelemetry.javaagent.instrumentation.tesla.sidecar.SidecarTracer.sidecarTracer; + +@SuppressWarnings({"CatchAndPrintStackTrace", "SystemOut"}) +public class ExecuteProcessorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return ElementMatchers.nameEndsWith("ExecuteProcessor"); + + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + ElementMatchers.named("processRequest") + .and(ElementMatchers.isPublic()) + .and(ElementMatchers.takesArguments(1)) + .and(ElementMatchers.takesArgument(0, ElementMatchers.named("com.xiaomi.data.push.uds.po.RpcCommand"))), + this.getClass().getName() + "$ProcessRequestAdvice"); + + } + + + public static class ProcessRequestAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Argument(0) RpcCommand rpcCommand) { + if (rpcCommand != null) { + try { + Map attachments = rpcCommand.getAttachments(); + if (attachments == null) { + attachments = new HashMap<>(); + } + context = sidecarTracer().extract(rpcCommand, SidecarExtractAdapter.GETTER); + scope = context.makeCurrent(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if(scope == null){ + return; + } + scope.close(); + return; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarExtractAdapter.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarExtractAdapter.java new file mode 100644 index 000000000..db185854a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarExtractAdapter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tesla.sidecar; + +import com.xiaomi.data.push.uds.po.RpcCommand; +import io.opentelemetry.context.propagation.TextMapGetter; + +public class SidecarExtractAdapter implements TextMapGetter { + + public static final SidecarExtractAdapter GETTER = new SidecarExtractAdapter(); + + @Override + public Iterable keys(RpcCommand rpcCommand) { + return rpcCommand.getAttachments().keySet(); + } + + @Override + public String get(RpcCommand carrier, String key) { + return carrier.getAttachments().get(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarInjectAdapter.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarInjectAdapter.java new file mode 100644 index 000000000..5bdf90526 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tesla.sidecar; + +import com.xiaomi.data.push.uds.po.RpcCommand; +import io.opentelemetry.context.propagation.TextMapSetter; + +public class SidecarInjectAdapter implements TextMapSetter { + + public static final SidecarInjectAdapter SETTER = new SidecarInjectAdapter(); + + @Override + public void set(RpcCommand rpcInvocation, String key, String value) { + rpcInvocation.getAttachments().put(key, value); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarServiceInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarServiceInstrumentation.java new file mode 100644 index 000000000..a93cd2277 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarServiceInstrumentation.java @@ -0,0 +1,71 @@ +package io.opentelemetry.javaagent.instrumentation.tesla.sidecar; + +import com.xiaomi.data.push.uds.po.RpcCommand; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import java.util.HashMap; +import java.util.Map; + +import static io.opentelemetry.javaagent.instrumentation.tesla.sidecar.SidecarTracer.sidecarTracer; + +@SuppressWarnings({"CatchAndPrintStackTrace","SystemOut"}) +public class SidecarServiceInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return ElementMatchers.named("com.xiaomi.youpin.gateway.sidecar.SidecarService"); + + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + ElementMatchers.named("call") + .and(ElementMatchers.isPublic()) + .and(ElementMatchers.takesArguments(1)) + .and(ElementMatchers.takesArgument(0, ElementMatchers.named("com.xiaomi.data.push.uds.po.RpcCommand"))), + this.getClass().getName() + "$CallAdvice"); + + } + + + public static class CallAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.Local("otelContext") Context context, + @Advice.Local("sidecarStartTime") long startTime, + @Advice.Argument(0) RpcCommand rpcCommand) { + startTime = System.currentTimeMillis(); + if(rpcCommand != null){ + try { + Map attachments = rpcCommand.getAttachments(); + if (attachments == null) { + attachments = new HashMap<>(); + } + context = Context.current(); + sidecarTracer().inject(context, rpcCommand, SidecarInjectAdapter.SETTER); + }catch (Throwable t){ + t.printStackTrace(); + } + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("sidecarStartTime") long startTime, + @Advice.Argument(0) RpcCommand rpcCommand) { + context = Context.current(); + long time = System.currentTimeMillis() - startTime; + Span.fromContext(context).addEvent(rpcCommand.getApp()+".client", Attributes.builder().put("time", time + "ms").build()); + return; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarTracer.java b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarTracer.java new file mode 100644 index 000000000..c61999360 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tesla/sidecar/SidecarTracer.java @@ -0,0 +1,28 @@ +package io.opentelemetry.javaagent.instrumentation.tesla.sidecar; + +import com.xiaomi.data.push.uds.po.RpcCommand; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.tracer.RpcServerTracer; + +public class SidecarTracer extends RpcServerTracer { + + private static final SidecarTracer TRACER; + static{ + TRACER = new SidecarTracer(); + } + + public static SidecarTracer sidecarTracer(){ + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "run.mone.tesla.sidecar.filter"; + } + + @Override + protected TextMapGetter getGetter() { + return SidecarExtractAdapter.GETTER; + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/tesla-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/tesla-javaagent.gradle new file mode 100644 index 000000000..b4e4db69e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tesla/javaagent/tesla-javaagent.gradle @@ -0,0 +1,18 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly("io.netty:netty-all:4.1.48.Final") + compileOnly ("run.mone:api:1.4-SNAPSHOT") + compileOnly ("run.mone:tesla-gateway:1.0.0-SNAPSHOT"){ + transitive = false + } + compileOnly ("run.mone:tesla-filter-api:1.0.0-SNAPSHOT"){ + transitive = false + } + compileOnly ("io.github.tesla:tesla-api:1.0.4-SNAPSHOT"){ + transitive = false + } + compileOnly ("io.github.tesla:tesla-common:1.0.0-SNAPSHOT"){ + transitive = false + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/README.md b/opentelemetry-java-instrumentation/instrumentation/tomcat/README.md new file mode 100644 index 000000000..800aa1be1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/README.md @@ -0,0 +1,11 @@ +# Instrumentation for Tomcat request handlers + +Tomcat support is divided into the following sub-modules: +- `tomcat-common:javaagent` contains common type instrumentation, advice helper classes and abstract + tracer used by the `javaagent` modules of all supported Tomcat versions +- `tomcat-7.0:javaagent` applies Tomcat request handler instrumentation for versions `[7, 10)` +- `tomcat-10.0:javaagent` applies Tomcat request handler instrumentation for versions `[10,)` + +Instrumentations in `tomcat-7.0` and `tomcat-10.0` are mutually exclusive, this is guaranteed by +`tomcat-10.0` instrumentation checking that its `Request` class uses `jakarta.servlet` classes, and +the `tomcat-7.0` module doing the opposite check. diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10InstrumentationModule.java new file mode 100644 index 000000000..7ab3809b9 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10InstrumentationModule.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatServerHandlerInstrumentation; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class Tomcat10InstrumentationModule extends InstrumentationModule { + + public Tomcat10InstrumentationModule() { + super("tomcat", "tomcat-10.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // only matches tomcat 10.0+ + return hasClassesNamed("jakarta.servlet.ReadListener"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList( + new TomcatServerHandlerInstrumentation( + Tomcat10InstrumentationModule.class.getPackage().getName() + + ".Tomcat10ServerHandlerAdvice")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10ServerHandlerAdvice.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10ServerHandlerAdvice.java new file mode 100644 index 000000000..e3c0b8fe1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10ServerHandlerAdvice.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import static io.opentelemetry.javaagent.instrumentation.tomcat.v10_0.Tomcat10Tracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.servlet.jakarta.v5_0.JakartaServletHttpServerTracer; +import io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatServerHandlerAdviceHelper; +import net.bytebuddy.asm.Advice; +import org.apache.coyote.Request; +import org.apache.coyote.Response; + +@SuppressWarnings("unused") +public class Tomcat10ServerHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(1) Response response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (!tracer().shouldStartSpan(request)) { + return; + } + + context = tracer().startServerSpan(request); + + scope = context.makeCurrent(); + + TomcatServerHandlerAdviceHelper.attachResponseToRequest( + Tomcat10ServletEntityProvider.INSTANCE, + JakartaServletHttpServerTracer.tracer(), + request, + response); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(0) Request request, + @Advice.Argument(1) Response response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + TomcatServerHandlerAdviceHelper.stopSpan( + tracer(), + Tomcat10ServletEntityProvider.INSTANCE, + JakartaServletHttpServerTracer.tracer(), + request, + response, + throwable, + context, + scope); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10ServletEntityProvider.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10ServletEntityProvider.java new file mode 100644 index 000000000..6ef1fad4b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10ServletEntityProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatServletEntityProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.coyote.Request; +import org.apache.coyote.Response; + +public class Tomcat10ServletEntityProvider + implements TomcatServletEntityProvider { + public static final Tomcat10ServletEntityProvider INSTANCE = new Tomcat10ServletEntityProvider(); + + private Tomcat10ServletEntityProvider() {} + + @Override + public HttpServletRequest getServletRequest(Request request) { + Object note = request.getNote(1); + + if (note instanceof HttpServletRequest) { + return (HttpServletRequest) note; + } else { + return null; + } + } + + @Override + public HttpServletResponse getServletResponse(Response response) { + Object note = response.getNote(1); + + if (note instanceof HttpServletResponse) { + return (HttpServletResponse) note; + } else { + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10Tracer.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10Tracer.java new file mode 100644 index 000000000..36b977df5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10Tracer.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatTracer; + +public class Tomcat10Tracer extends TomcatTracer { + private static final Tomcat10Tracer TRACER = new Tomcat10Tracer(); + + public static TomcatTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.tomcat-10.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java new file mode 100644 index 000000000..7748f532f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import io.opentelemetry.instrumentation.test.base.HttpServerTest; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class TestServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String path = req.getServletPath(); + + HttpServerTest.ServerEndpoint serverEndpoint = HttpServerTest.ServerEndpoint.forPath(path); + if (serverEndpoint != null) { + HttpServerTest.controller( + serverEndpoint, + () -> { + if (serverEndpoint == HttpServerTest.ServerEndpoint.EXCEPTION) { + throw new Exception(serverEndpoint.getBody()); + } + resp.getWriter().print(serverEndpoint.getBody()); + if (serverEndpoint == HttpServerTest.ServerEndpoint.REDIRECT) { + resp.sendRedirect(serverEndpoint.getBody()); + } else if (serverEndpoint == HttpServerTest.ServerEndpoint.ERROR) { + resp.sendError(serverEndpoint.getStatus(), serverEndpoint.getBody()); + } else { + resp.setStatus(serverEndpoint.getStatus()); + } + return null; + }); + } else { + resp.getWriter().println("No cookie for you: " + path); + resp.setStatus(400); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.groovy new file mode 100644 index 000000000..b219226e5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.groovy @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0 + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import org.apache.catalina.Context +import org.apache.catalina.connector.Request +import org.apache.catalina.connector.Response +import org.apache.catalina.core.StandardHost +import org.apache.catalina.startup.Tomcat +import org.apache.catalina.valves.ErrorReportValve + +class TomcatHandlerTest extends HttpServerTest implements AgentTestTrait { + + def "Tomcat starts"() { + expect: + getServer() != null + } + + @Override + String getContextPath() { + return "/app" + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case NOT_FOUND: + return "HTTP GET" + default: + return endpoint.resolvePath(address).path + } + } + + @Override + Tomcat startServer(int port) { + Tomcat tomcat = new Tomcat() + tomcat.setBaseDir(File.createTempDir().absolutePath) + tomcat.setPort(port) + tomcat.getConnector() + + Context ctx = tomcat.addContext(getContextPath(), new File(".").getAbsolutePath()) + + Tomcat.addServlet(ctx, "testServlet", new TestServlet()) + + // Mapping servlet to /* will result in all requests have a name of just a context. + ServerEndpoint.values().toList().stream() + .filter { it != NOT_FOUND } + .forEach { + ctx.addServletMappingDecoded(it.path, "testServlet") + } + + (tomcat.host as StandardHost).errorReportValveClass = ErrorHandlerValve.name + + tomcat.start() + + return tomcat + } + + @Override + void stopServer(Tomcat tomcat) { + tomcat.getServer().stop() + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == ERROR || endpoint == NOT_FOUND + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case REDIRECT: + redirectSpan(trace, index, parent) + break + case ERROR: + case NOT_FOUND: + sendErrorSpan(trace, index, parent) + break + } + } +} + +class ErrorHandlerValve extends ErrorReportValve { + @Override + protected void report(Request request, Response response, Throwable t) { + if (response.getStatus() < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) { + return + } + try { + response.writer.print(t ? t.cause.message : response.message) + } catch (IOException ignored) { + // Ignore exception when writing exception message to response fails on IO - same as is done + // by the superclass itself and by other built-in ErrorReportValve implementations. + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/tomcat-10.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/tomcat-10.0-javaagent.gradle new file mode 100644 index 000000000..46652d99b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-10.0/javaagent/tomcat-10.0-javaagent.gradle @@ -0,0 +1,17 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.tomcat.embed" + module = "tomcat-embed-core" + versions = "[10,)" + } +} + +dependencies { + library "org.apache.tomcat.embed:tomcat-embed-core:10.0.0" + implementation project(':instrumentation:tomcat:tomcat-common:javaagent') + implementation project(':instrumentation:servlet:servlet-5.0:javaagent') + // Make sure nothing breaks due to both 7.0 and 10.0 modules being present together + testInstrumentation project(':instrumentation:tomcat:tomcat-7.0:javaagent') +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7InstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7InstrumentationModule.java new file mode 100644 index 000000000..b27e5a779 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7InstrumentationModule.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatServerHandlerInstrumentation; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class Tomcat7InstrumentationModule extends InstrumentationModule { + + public Tomcat7InstrumentationModule() { + super("tomcat", "tomcat-7.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // does not match tomcat 10.0+ + return not(hasClassesNamed("jakarta.servlet.ReadListener")); + } + + @Override + public List typeInstrumentations() { + // Tomcat 10+ is excluded by making sure Request does not have any methods returning + // jakarta.servlet.ReadListener which is returned by getReadListener method on Tomcat 10+ + return Collections.singletonList( + new TomcatServerHandlerInstrumentation( + Tomcat7InstrumentationModule.class.getPackage().getName() + + ".Tomcat7ServerHandlerAdvice")); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7ServerHandlerAdvice.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7ServerHandlerAdvice.java new file mode 100644 index 000000000..5ca7f2c15 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7ServerHandlerAdvice.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import static io.opentelemetry.javaagent.instrumentation.tomcat.v7_0.Tomcat7Tracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.servlet.v3_0.Servlet3HttpServerTracer; +import io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatServerHandlerAdviceHelper; +import net.bytebuddy.asm.Advice; +import org.apache.coyote.Request; +import org.apache.coyote.Response; + +@SuppressWarnings({"unused"}) +public class Tomcat7ServerHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Request request, + @Advice.Argument(1) Response response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (!tracer().shouldStartSpan(request)) { + return; + } + + context = tracer().startServerSpan(request); + scope = context.makeCurrent(); + + TomcatServerHandlerAdviceHelper.attachResponseToRequest( + Tomcat7ServletEntityProvider.INSTANCE, + Servlet3HttpServerTracer.tracer(), + request, + response); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Argument(0) Request request, + @Advice.Argument(1) Response response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + TomcatServerHandlerAdviceHelper.stopSpan( + tracer(), + Tomcat7ServletEntityProvider.INSTANCE, + Servlet3HttpServerTracer.tracer(), + request, + response, + throwable, + context, + scope); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7ServletEntityProvider.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7ServletEntityProvider.java new file mode 100644 index 000000000..644e8471a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7ServletEntityProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatServletEntityProvider; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.coyote.Request; +import org.apache.coyote.Response; + +public class Tomcat7ServletEntityProvider + implements TomcatServletEntityProvider { + public static final Tomcat7ServletEntityProvider INSTANCE = new Tomcat7ServletEntityProvider(); + + private Tomcat7ServletEntityProvider() {} + + @Override + public HttpServletRequest getServletRequest(Request request) { + Object note = request.getNote(1); + + if (note instanceof HttpServletRequest) { + return (HttpServletRequest) note; + } else { + return null; + } + } + + @Override + public HttpServletResponse getServletResponse(Response response) { + Object note = response.getNote(1); + + if (note instanceof HttpServletResponse) { + return (HttpServletResponse) note; + } else { + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7Tracer.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7Tracer.java new file mode 100644 index 000000000..342eb8925 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7Tracer.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatTracer; + +public class Tomcat7Tracer extends TomcatTracer { + private static final Tomcat7Tracer TRACER = new Tomcat7Tracer(); + + public static TomcatTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.tomcat-7.0"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java new file mode 100644 index 000000000..46273a1f2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import io.opentelemetry.instrumentation.test.base.HttpServerTest; +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class TestServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String path = req.getServletPath(); + + HttpServerTest.ServerEndpoint serverEndpoint = HttpServerTest.ServerEndpoint.forPath(path); + if (serverEndpoint != null) { + HttpServerTest.controller( + serverEndpoint, + () -> { + if (serverEndpoint == HttpServerTest.ServerEndpoint.EXCEPTION) { + throw new Exception(serverEndpoint.getBody()); + } + resp.getWriter().print(serverEndpoint.getBody()); + if (serverEndpoint == HttpServerTest.ServerEndpoint.REDIRECT) { + resp.sendRedirect(serverEndpoint.getBody()); + } else if (serverEndpoint == HttpServerTest.ServerEndpoint.ERROR) { + resp.sendError(serverEndpoint.getStatus(), serverEndpoint.getBody()); + } else { + resp.setStatus(serverEndpoint.getStatus()); + } + return null; + }); + } else { + resp.getWriter().println("No cookie for you: " + path); + resp.setStatus(400); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.groovy new file mode 100644 index 000000000..d7af9bfc8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.groovy @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0 + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import org.apache.catalina.Context +import org.apache.catalina.connector.Request +import org.apache.catalina.connector.Response +import org.apache.catalina.core.StandardHost +import org.apache.catalina.startup.Tomcat +import org.apache.catalina.valves.ErrorReportValve + +class TomcatHandlerTest extends HttpServerTest implements AgentTestTrait { + + def "Tomcat starts"() { + expect: + getServer() != null + } + + @Override + String getContextPath() { + return "/app" + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case NOT_FOUND: + return "HTTP GET" + default: + return endpoint.resolvePath(address).path + } + } + + @Override + Tomcat startServer(int port) { + Tomcat tomcat = new Tomcat() + tomcat.setBaseDir(File.createTempDir().absolutePath) + tomcat.setPort(port) + tomcat.getConnector() + + Context ctx = tomcat.addContext(getContextPath(), new File(".").getAbsolutePath()) + + Tomcat.addServlet(ctx, "testServlet", new TestServlet()) + + // Mapping servlet to /* will result in all requests have a name of just a context. + ServerEndpoint.values().toList().stream() + .filter { it != NOT_FOUND } + .forEach { + ctx.addServletMappingDecoded(it.path, "testServlet") + } + + (tomcat.host as StandardHost).errorReportValveClass = ErrorHandlerValve.name + + tomcat.start() + + return tomcat + } + + @Override + void stopServer(Tomcat tomcat) { + tomcat.getServer().stop() + } + + @Override + boolean hasResponseSpan(ServerEndpoint endpoint) { + endpoint == REDIRECT || endpoint == ERROR || endpoint == NOT_FOUND + } + + @Override + void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { + switch (endpoint) { + case REDIRECT: + redirectSpan(trace, index, parent) + break + case ERROR: + case NOT_FOUND: + sendErrorSpan(trace, index, parent) + break + } + } +} + +class ErrorHandlerValve extends ErrorReportValve { + @Override + protected void report(Request request, Response response, Throwable t) { + if (response.getStatus() < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) { + return + } + try { + response.writer.print(t ? t.cause.message : response.message) + } catch (IOException ignored) { + // Ignore exception when writing exception message to response fails on IO - same as is done + // by the superclass itself and by other built-in ErrorReportValve implementations. + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/tomcat-7.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/tomcat-7.0-javaagent.gradle new file mode 100644 index 000000000..0bc6bb3de --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-7.0/javaagent/tomcat-7.0-javaagent.gradle @@ -0,0 +1,25 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.tomcat.embed" + module = "tomcat-embed-core" + // Tomcat 10 is about servlet 5.0 + // 7.0.4 added Request.isAsync, which is needed + versions = "[7.0.4, 10)" + } +} + +dependencies { + library "org.apache.tomcat.embed:tomcat-embed-core:7.0.4" + implementation project(':instrumentation:tomcat:tomcat-common:javaagent') + implementation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + // Make sure nothing breaks due to both 7.0 and 10.0 modules being present together + testInstrumentation project(':instrumentation:tomcat:tomcat-10.0:javaagent') + + // Tests need at least version 9 to have necessary classes to configure the embedded tomcat... + // ... but not newer that version 10, because its servlet 5. + testLibrary "org.apache.tomcat.embed:tomcat-embed-core:[9.+, 10)" + latestDepTestLibrary "org.apache.tomcat.embed:tomcat-embed-core:[9.+, 10)" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServerHandlerAdviceHelper.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServerHandlerAdviceHelper.java new file mode 100644 index 000000000..5583e137f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServerHandlerAdviceHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.common; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.servlet.ServletHttpServerTracer; +import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterAdviceHelper; +import org.apache.coyote.Request; +import org.apache.coyote.Response; + +public class TomcatServerHandlerAdviceHelper { + /** + * Shared stop method used by advices for different Tomcat versions. + * + * @param tracer Tracer for non-async path (uses Tomcat Coyote request/response) + * @param servletTracer Tracer for async path (uses servlet request/response) + * @param request Tomcat Coyote request object + * @param response Tomcat Coyote request object + * @param HttpServletRequest class + * @param HttpServletResponse class + */ + public static void stopSpan( + TomcatTracer tracer, + TomcatServletEntityProvider servletEntityProvider, + ServletHttpServerTracer servletTracer, + Request request, + Response response, + Throwable throwable, + Context context, + Scope scope) { + if (scope != null) { + scope.close(); + } + + if (context == null) { + return; + } + + if (throwable != null) { + if (response.isCommitted()) { + tracer.endExceptionally(context, throwable, response); + } else { + // If the response is not committed, then response headers, including response code, are + // not yet written to the output stream. + tracer.endExceptionally(context, throwable); + } + return; + } + + if (response.isCommitted()) { + tracer.end(context, response); + return; + } + + REQUEST servletRequest = servletEntityProvider.getServletRequest(request); + + if (servletRequest != null + && ServletAndFilterAdviceHelper.mustEndOnHandlerMethodExit(servletTracer, servletRequest)) { + tracer.end(context, response); + } + } + + /** + * Must be attached in Tomcat instrumentations since Tomcat valves can use startAsync outside of + * servlet scope. + */ + public static void attachResponseToRequest( + TomcatServletEntityProvider servletEntityProvider, + ServletHttpServerTracer servletTracer, + Request request, + Response response) { + + REQUEST servletRequest = servletEntityProvider.getServletRequest(request); + RESPONSE servletResponse = servletEntityProvider.getServletResponse(response); + + if (servletRequest != null && servletResponse != null) { + servletTracer.setAsyncListenerResponse(servletRequest, servletResponse); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServerHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServerHandlerInstrumentation.java new file mode 100644 index 000000000..863e7071b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServerHandlerInstrumentation.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.common; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class TomcatServerHandlerInstrumentation implements TypeInstrumentation { + private final String adviceClassName; + + public TomcatServerHandlerInstrumentation(String adviceClassName) { + this.adviceClassName = adviceClassName; + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.apache.coyote.Adapter")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("service")) + .and(takesArgument(0, named("org.apache.coyote.Request"))) + .and(takesArgument(1, named("org.apache.coyote.Response"))), + adviceClassName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServletEntityProvider.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServletEntityProvider.java new file mode 100644 index 000000000..fe0523831 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatServletEntityProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.common; + +import org.apache.coyote.Request; +import org.apache.coyote.Response; + +/** + * Used to access servlet request/response classes from their Apache Coyote counterparts. As the + * Coyote classes are the same for all Tomcat versions, but newer Tomcat uses jakarta.servlet + * instead of javax.servlet, this allows accessing the servlet entities without unchecked casts in + * shared code where HttpServletRequest and/or HttpServletResponse are used as generic parameters. + * + * @param HttpServletRequest instance + * @param HttpServletResponse instance + */ +public interface TomcatServletEntityProvider { + REQUEST getServletRequest(Request request); + + RESPONSE getServletResponse(Response response); +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatTracer.java b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatTracer.java new file mode 100644 index 000000000..a1fd2846a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatTracer.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.common; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTAINER; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import java.net.URI; +import java.util.Collections; +import org.apache.coyote.ActionCode; +import org.apache.coyote.Request; +import org.apache.coyote.Response; +import org.apache.tomcat.util.buf.MessageBytes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletResponse; + +/** + * Abstract tracer for all Tomcat versions. This class must not access any methods of fields of + * Tomcat classes which have the javax.servlet/jakarta.servlet packages in their + * signature - these must only be accessed by the version-specific subclasses. + */ +@SuppressWarnings("SystemOut") +public abstract class TomcatTracer extends HttpServerTracer + implements TextMapGetter { + + private static final Logger log = LoggerFactory.getLogger(TomcatTracer.class); + + public boolean shouldStartSpan(Request request) { + Context attachedContext = getServerContext(request); + if (attachedContext == null) { + return true; + } + log.debug("Unexpected context found before server handler even started: {}", attachedContext); + return false; + } + + public Context startServerSpan(Request request) { + return startSpan(request, request, request, "HTTP " + request.method().toString()); + } + + @Override + protected Context customizeContext(Context context, Request request) { + context = ServerSpanNaming.init(context, CONTAINER); + return AppServerBridge.init(context); + } + + @Override + public Context getServerContext(Request storage) { + Object attribute = storage.getAttribute(CONTEXT_ATTRIBUTE); + return attribute instanceof Context ? (Context) attribute : null; + } + + @Override + protected Integer peerPort(Request connection) { + connection.action(ActionCode.REQ_REMOTEPORT_ATTRIBUTE, connection); + return connection.getRemotePort(); + } + + @Override + protected String peerHostIP(Request connection) { + connection.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, connection); + return connection.remoteAddr().toString(); + } + + @Override + protected String flavor(Request connection, Request request) { + return request.protocol().toString(); + } + + @Override + protected TextMapGetter getGetter() { + return this; + } + + @Override + protected String url(Request request) { + MessageBytes schemeMB = request.scheme(); + String scheme = schemeMB.isNull() ? "http" : schemeMB.toString(); + String host = request.serverName().toString(); + int serverPort = request.getServerPort(); + String path = request.requestURI().toString(); + String query = request.queryString().toString(); + + try { + return new URI(scheme, null, host, serverPort, path, query, null).toString(); + } catch (Exception e) { + log.warn( + "Malformed url? scheme: {}, host: {}, port: {}, path: {}, query: {}", + scheme, + host, + serverPort, + path, + query, + e); + } + return null; + } + + @Override + protected String method(Request request) { + return request.method().toString(); + } + + @Override + protected String requestHeader(Request request, String name) { + return request.getHeader(name); + } + + @Override + protected int responseStatus(Response response) { + return response.getStatus(); + } + + @Override + protected String bussinessStatus(Response response) { + Object note = response.getNote(1); + if(note instanceof HttpServletResponse){ + HttpServletResponse httpServletResponse = (HttpServletResponse)note; + String header = httpServletResponse.getHeader("X-BUSSINESS-CODE"); + return header; + } + return null; + } + + @Override + protected String bussinessMessage(Response response) { + Object note = response.getNote(1); + if(note instanceof HttpServletResponse){ + HttpServletResponse httpServletResponse = (HttpServletResponse)note; + String header = httpServletResponse.getHeader("X-BUSSINESS-MESSAGE"); + return header; + } + return null; + } + + @Override + protected void attachServerContext(Context context, Request storage) { + storage.setAttribute(CONTEXT_ATTRIBUTE, context); + } + + @Override + public Iterable keys(Request request) { + return Collections.list(request.getMimeHeaders().names()); + } + + @Override + public String get(Request request, String key) { + return request.getHeader(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/tomcat-common-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/tomcat-common-javaagent.gradle new file mode 100644 index 000000000..6776fe89b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/tomcat/tomcat-common/javaagent/tomcat-common-javaagent.gradle @@ -0,0 +1,7 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + api(project(':instrumentation:servlet:servlet-common:library')) + implementation(project(':instrumentation:servlet:servlet-common:javaagent')) + compileOnly "org.apache.tomcat.embed:tomcat-embed-core:7.0.4" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioAsyncInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioAsyncInstrumentation.java new file mode 100644 index 000000000..b06e2c8ac --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioAsyncInstrumentation.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.twilio; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.twilio.TwilioTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.twilio.Twilio; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** Instrument the Twilio SDK to identify calls as a separate service. */ +public class TwilioAsyncInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.twilio.Twilio"); + } + + /** Match any child class of the base Twilio service classes. */ + @Override + public ElementMatcher typeMatcher() { + return extendsClass( + namedOneOf( + "com.twilio.base.Creator", + "com.twilio.base.Deleter", + "com.twilio.base.Fetcher", + "com.twilio.base.Reader", + "com.twilio.base.Updater")); + } + + @Override + public void transform(TypeTransformer transformer) { + /* + We are listing out the main service calls on the Creator, Deleter, Fetcher, Reader, and + Updater abstract classes. The isDeclaredBy() matcher did not work in the unit tests and + we found that there were certain methods declared on the base class (particularly Reader), + which we weren't interested in annotating. + */ + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("createAsync", "deleteAsync", "readAsync", "fetchAsync", "updateAsync")) + .and(isPublic()) + .and(not(isAbstract())) + .and(returns(named("com.google.common.util.concurrent.ListenableFuture"))), + TwilioAsyncInstrumentation.class.getName() + "$TwilioClientAsyncAdvice"); + } + + /** Advice for instrumenting Twilio service classes. */ + @SuppressWarnings("unused") + public static class TwilioClientAsyncAdvice { + + /** Method entry instrumentation. */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.This Object that, + @Advice.Origin("#m") String methodName, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, that, methodName); + scope = context.makeCurrent(); + } + + /** Method exit instrumentation. */ + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Thrown Throwable throwable, + @Advice.Return ListenableFuture response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable != null) { + // There was an synchronous error, + // which means we shouldn't wait for a callback to close the span. + tracer().endExceptionally(context, throwable); + } else { + // We're calling an async operation, we still need to finish the span when it's + // complete and report the results; set an appropriate callback + Futures.addCallback( + response, new SpanFinishingCallback<>(context), Twilio.getExecutorService()); + } + } + } + + /** + * FutureCallback, which automatically finishes the span and annotates with any appropriate + * metadata on a potential failure. + */ + public static class SpanFinishingCallback implements FutureCallback { + + /** Span that we should finish and annotate when the future is complete. */ + private final Context context; + + public SpanFinishingCallback(Context context) { + this.context = context; + } + + @Override + public void onSuccess(Object result) { + tracer().end(context, result); + } + + @Override + public void onFailure(Throwable t) { + tracer().endExceptionally(context, t); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioInstrumentationModule.java new file mode 100644 index 000000000..f96604d97 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.twilio; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class TwilioInstrumentationModule extends InstrumentationModule { + public TwilioInstrumentationModule() { + super("twilio", "twilio-6.6"); + } + + @Override + public List typeInstrumentations() { + return asList(new TwilioAsyncInstrumentation(), new TwilioSyncInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioSyncInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioSyncInstrumentation.java new file mode 100644 index 000000000..414af9630 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioSyncInstrumentation.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.twilio; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.twilio.TwilioTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** Instrument the Twilio SDK to identify calls as a separate service. */ +public class TwilioSyncInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.twilio.Twilio"); + } + + /** Match any child class of the base Twilio service classes. */ + @Override + public ElementMatcher typeMatcher() { + return extendsClass( + namedOneOf( + "com.twilio.base.Creator", + "com.twilio.base.Deleter", + "com.twilio.base.Fetcher", + "com.twilio.base.Reader", + "com.twilio.base.Updater")); + } + + @Override + public void transform(TypeTransformer transformer) { + /* + We are listing out the main service calls on the Creator, Deleter, Fetcher, Reader, and + Updater abstract classes. The isDeclaredBy() matcher did not work in the unit tests and + we found that there were certain methods declared on the base class (particularly Reader), + which we weren't interested in annotating. + */ + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(not(isAbstract())) + .and(namedOneOf("create", "delete", "read", "fetch", "update")), + TwilioSyncInstrumentation.class.getName() + "$TwilioClientAdvice"); + } + + /** Advice for instrumenting Twilio service classes. */ + @SuppressWarnings("unused") + public static class TwilioClientAdvice { + + /** Method entry instrumentation. */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.This Object that, + @Advice.Origin("#m") String methodName, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, that, methodName); + scope = context.makeCurrent(); + } + + /** Method exit instrumentation. */ + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Thrown Throwable throwable, + @Advice.Return Object response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else { + tracer().end(context, response); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioTracer.java b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioTracer.java new file mode 100644 index 000000000..02d687915 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioTracer.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.twilio; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.Uninterruptibles; +import com.twilio.rest.api.v2010.account.Call; +import com.twilio.rest.api.v2010.account.Message; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TwilioTracer extends BaseTracer { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + Config.get() + .getBooleanProperty("otel.instrumentation.twilio.experimental-span-attributes", false); + + private static final Logger log = LoggerFactory.getLogger(TwilioTracer.class); + + public static final TwilioTracer TRACER = new TwilioTracer(); + + public static TwilioTracer tracer() { + return TRACER; + } + + public boolean shouldStartSpan(Context parentContext) { + return shouldStartSpan(parentContext, CLIENT); + } + + public Context startSpan(Context parentContext, Object serviceExecutor, String methodName) { + String spanName = spanNameOnServiceExecution(serviceExecutor, methodName); + Span span = spanBuilder(parentContext, spanName, CLIENT).startSpan(); + return withClientSpan(parentContext, span); + } + + /** Decorate trace based on service execution metadata. */ + private static String spanNameOnServiceExecution(Object serviceExecutor, String methodName) { + return SpanNames.fromMethod(serviceExecutor.getClass(), methodName); + } + + /** Annotate the span with the results of the operation. */ + public void end(Context context, Object result) { + + // Unwrap ListenableFuture (if present) + if (result instanceof ListenableFuture) { + try { + result = + Uninterruptibles.getUninterruptibly( + (ListenableFuture) result, 0, TimeUnit.MICROSECONDS); + } catch (Exception e) { + log.debug("Error unwrapping result", e); + } + } + + Span span = Span.fromContext(context); + + // Nothing to do here, so return + if (result == null) { + span.end(); + return; + } + + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + // Provide helpful metadata for some of the more common response types + span.setAttribute("twilio.type", result.getClass().getCanonicalName()); + + // Instrument the most popular resource types directly + if (result instanceof Message) { + Message message = (Message) result; + span.setAttribute("twilio.account", message.getAccountSid()); + span.setAttribute("twilio.sid", message.getSid()); + Message.Status status = message.getStatus(); + if (status != null) { + span.setAttribute("twilio.status", status.toString()); + } + } else if (result instanceof Call) { + Call call = (Call) result; + span.setAttribute("twilio.account", call.getAccountSid()); + span.setAttribute("twilio.sid", call.getSid()); + span.setAttribute("twilio.parentSid", call.getParentCallSid()); + Call.Status status = call.getStatus(); + if (status != null) { + span.setAttribute("twilio.status", status.toString()); + } + } else { + // Use reflection to gather insight from other types; note that Twilio requests take close + // to + // 1 second, so the added hit from reflection here is relatively minimal in the grand scheme + // of things + setTagIfPresent(span, result, "twilio.sid", "getSid"); + setTagIfPresent(span, result, "twilio.account", "getAccountSid"); + setTagIfPresent(span, result, "twilio.status", "getStatus"); + } + } + + super.end(context); + } + + /** + * Helper method for calling a getter using reflection. This will be slow, so only use when + * required. + */ + private static void setTagIfPresent(Span span, Object result, String tag, String getter) { + try { + Method method = result.getClass().getMethod(getter); + Object value = method.invoke(result); + + if (value != null) { + span.setAttribute(tag, value.toString()); + } + + } catch (Exception e) { + // Expected that this won't work for all result types + } + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.twilio-6.6"; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy new file mode 100644 index 000000000..87e61c46a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy @@ -0,0 +1,611 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.common.util.concurrent.ListenableFuture +import com.twilio.Twilio +import com.twilio.exception.ApiException +import com.twilio.http.NetworkHttpClient +import com.twilio.http.Response +import com.twilio.http.TwilioRestClient +import com.twilio.rest.api.v2010.account.Call +import com.twilio.rest.api.v2010.account.Message +import com.twilio.type.PhoneNumber +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import org.apache.http.HttpEntity +import org.apache.http.HttpStatus +import org.apache.http.StatusLine +import org.apache.http.client.methods.CloseableHttpResponse +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClientBuilder + +class TwilioClientTest extends AgentInstrumentationSpecification { + final static String ACCOUNT_SID = "abc" + final static String AUTH_TOKEN = "efg" + + final static String MESSAGE_RESPONSE_BODY = """ + { + "account_sid": "AC14984e09e497506cf0d5eb59b1f6ace7", + "api_version": "2010-04-01", + "body": "Hello, World!", + "date_created": "Thu, 30 Jul 2015 20:12:31 +0000", + "date_sent": "Thu, 30 Jul 2015 20:12:33 +0000", + "date_updated": "Thu, 30 Jul 2015 20:12:33 +0000", + "direction": "outbound-api", + "from": "+14155552345", + "messaging_service_sid": "MGXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "num_media": "0", + "num_segments": "1", + "price": -0.00750, + "price_unit": "USD", + "sid": "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "status": "sent", + "subresource_uris": { + "media": "/2010-04-01/Accounts/AC14984e09e497506cf0d5eb59b1f6ace7/Messages/SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Media.json" + }, + "to": "+14155552345", + "uri": "/2010-04-01/Accounts/AC14984e09e497506cf0d5eb59b1f6ace7/Messages/SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json" + } + """ + + final static String CALL_RESPONSE_BODY = """ + { + "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "annotation": null, + "answered_by": null, + "api_version": "2010-04-01", + "caller_name": null, + "date_created": "Tue, 31 Aug 2010 20:36:28 +0000", + "date_updated": "Tue, 31 Aug 2010 20:36:44 +0000", + "direction": "inbound", + "duration": "15", + "end_time": "Tue, 31 Aug 2010 20:36:44 +0000", + "forwarded_from": "+141586753093", + "from": "+15017122661", + "from_formatted": "(501) 712-2661", + "group_sid": null, + "parent_call_sid": null, + "phone_number_sid": "PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "price": -0.03000, + "price_unit": "USD", + "sid": "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "start_time": "Tue, 31 Aug 2010 20:36:29 +0000", + "status": "completed", + "subresource_uris": { + "notifications": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Notifications.json", + "recordings": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Recordings.json", + "feedback": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Feedback.json", + "feedback_summaries": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/FeedbackSummary.json" + }, + "to": "+15558675310", + "to_formatted": "(555) 867-5310", + "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json" + } + """ + + final static String ERROR_RESPONSE_BODY = """ + { + "code": 123, + "message": "Testing Failure", + "code": 567, + "more_info": "Testing" + } + """ + + TwilioRestClient twilioRestClient = Mock() + + def setupSpec() { + Twilio.init(ACCOUNT_SID, AUTH_TOKEN) + } + + def cleanup() { + Twilio.getExecutorService().shutdown() + Twilio.setExecutorService(null) + Twilio.setRestClient(null) + } + + def "synchronous message"() { + setup: + twilioRestClient.getObjectMapper() >> new ObjectMapper() + + 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()), 200) + + Message message = runUnderTrace("test") { + Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).create(twilioRestClient) + } + + expect: + + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "test" + hasNoParent() + attributes { + } + } + span(1) { + name "MessageCreator.create" + kind CLIENT + attributes { + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + } + } + } + } + } + + def "synchronous call"() { + setup: + twilioRestClient.getObjectMapper() >> new ObjectMapper() + + 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(CALL_RESPONSE_BODY.getBytes()), 200) + + Call call = runUnderTrace("test") { + Call.creator( + new PhoneNumber("+15558881234"), // To number + new PhoneNumber("+15559994321"), // From number + + // Read TwiML at this URL when a call connects (hold music) + new URI("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient") + ).create(twilioRestClient) + } + + expect: + + call.status == Call.Status.COMPLETED + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "test" + hasNoParent() + attributes { + } + } + span(1) { + name "CallCreator.create" + kind CLIENT + attributes { + "twilio.type" "com.twilio.rest.api.v2010.account.Call" + "twilio.account" "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.sid" "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "completed" + } + } + } + } + } + + + def "http client"() { + setup: + HttpClientBuilder clientBuilder = Mock() + CloseableHttpClient httpClient = Mock() + CloseableHttpResponse httpResponse = Mock() + HttpEntity httpEntity = Mock() + StatusLine statusLine = Mock() + + clientBuilder.build() >> httpClient + + httpClient.execute(_) >> httpResponse + + httpResponse.getEntity() >> httpEntity + httpResponse.getStatusLine() >> statusLine + + httpEntity.getContent() >> { new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()) } + httpEntity.isRepeatable() >> true + httpEntity.getContentLength() >> MESSAGE_RESPONSE_BODY.length() + + statusLine.getStatusCode() >> HttpStatus.SC_OK + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build() + + Message message = runUnderTrace("test") { + Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).create(realTwilioRestClient) + } + + expect: + + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "test" + hasNoParent() + attributes { + } + } + span(1) { + name "MessageCreator.create" + kind CLIENT + childOf(span(0)) + attributes { + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + } + } + } + } + } + + def "http client retry"() { + setup: + HttpClientBuilder clientBuilder = Mock() + CloseableHttpClient httpClient = Mock() + CloseableHttpResponse httpResponse1 = Mock() + CloseableHttpResponse httpResponse2 = Mock() + HttpEntity httpEntity1 = Mock() + HttpEntity httpEntity2 = Mock() + StatusLine statusLine1 = Mock() + StatusLine statusLine2 = Mock() + + clientBuilder.build() >> httpClient + + httpClient.execute(_) >>> [httpResponse1, httpResponse2] + + // First response is an HTTP/500 error, which should drive a retry + httpResponse1.getEntity() >> httpEntity1 + httpResponse1.getStatusLine() >> statusLine1 + + httpEntity1.getContent() >> { new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()) } + + httpEntity1.isRepeatable() >> true + httpEntity1.getContentLength() >> ERROR_RESPONSE_BODY.length() + + statusLine1.getStatusCode() >> HttpStatus.SC_INTERNAL_SERVER_ERROR + + // Second response is HTTP/200 success + httpResponse2.getEntity() >> httpEntity2 + httpResponse2.getStatusLine() >> statusLine2 + + httpEntity2.getContent() >> { + new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()) + } + httpEntity2.isRepeatable() >> true + httpEntity2.getContentLength() >> MESSAGE_RESPONSE_BODY.length() + + statusLine2.getStatusCode() >> HttpStatus.SC_OK + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build() + + Message message = runUnderTrace("test") { + Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).create(realTwilioRestClient) + } + + expect: + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "test" + hasNoParent() + attributes { + } + } + span(1) { + name "MessageCreator.create" + kind CLIENT + childOf(span(0)) + attributes { + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + } + } + } + } + } + + def "http client retry async"() { + setup: + HttpClientBuilder clientBuilder = Mock() + CloseableHttpClient httpClient = Mock() + CloseableHttpResponse httpResponse1 = Mock() + CloseableHttpResponse httpResponse2 = Mock() + HttpEntity httpEntity1 = Mock() + HttpEntity httpEntity2 = Mock() + StatusLine statusLine1 = Mock() + StatusLine statusLine2 = Mock() + + clientBuilder.build() >> httpClient + + httpClient.execute(_) >>> [httpResponse1, httpResponse2] + + // First response is an HTTP/500 error, which should drive a retry + httpResponse1.getEntity() >> httpEntity1 + httpResponse1.getStatusLine() >> statusLine1 + + httpEntity1.getContent() >> { new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()) } + + httpEntity1.isRepeatable() >> true + httpEntity1.getContentLength() >> ERROR_RESPONSE_BODY.length() + + statusLine1.getStatusCode() >> HttpStatus.SC_INTERNAL_SERVER_ERROR + + // Second response is HTTP/200 success + httpResponse2.getEntity() >> httpEntity2 + httpResponse2.getStatusLine() >> statusLine2 + + httpEntity2.getContent() >> { + new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()) + } + httpEntity2.isRepeatable() >> true + httpEntity2.getContentLength() >> MESSAGE_RESPONSE_BODY.length() + + statusLine2.getStatusCode() >> HttpStatus.SC_OK + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build() + + Message message = runUnderTrace("test") { + ListenableFuture future = Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).createAsync(realTwilioRestClient) + + try { + return future.get(10, TimeUnit.SECONDS) + } finally { + // Give the future callback a chance to run + Thread.sleep(1000) + } + } + + expect: + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "test" + hasNoParent() + attributes { + } + } + span(1) { + name "MessageCreator.createAsync" + kind CLIENT + childOf(span(0)) + attributes { + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + } + } + } + } + + cleanup: + Twilio.getExecutorService().shutdown() + Twilio.setExecutorService(null) + Twilio.setRestClient(null) + } + + def "Sync Failure"() { + setup: + + twilioRestClient.getObjectMapper() >> new ObjectMapper() + + 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()), 500) + + when: + runUnderTrace("test") { + Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).create(twilioRestClient) + } + + then: + thrown(ApiException) + + expect: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "test" + status ERROR + errorEvent(ApiException, "Testing Failure") + hasNoParent() + } + span(1) { + name "MessageCreator.create" + kind CLIENT + status ERROR + errorEvent(ApiException, "Testing Failure") + } + } + } + } + + def "root span"() { + setup: + twilioRestClient.getObjectMapper() >> new ObjectMapper() + + 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()), 200) + + Message message = Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).create(twilioRestClient) + + expect: + + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 1) { + span(0) { + name "MessageCreator.create" + kind CLIENT + hasNoParent() + attributes { + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + } + } + } + } + } + + def "asynchronous call"(a) { + setup: + twilioRestClient.getObjectMapper() >> new ObjectMapper() + + 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()), 200) + + when: + + Message message = runUnderTrace("test") { + + ListenableFuture future = Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).createAsync(twilioRestClient) + + try { + return future.get(10, TimeUnit.SECONDS) + } finally { + // Give the future callback a chance to run + Thread.sleep(1000) + } + } + + then: + + message != null + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "test" + hasNoParent() + attributes { + } + } + span(1) { + name "MessageCreator.createAsync" + kind CLIENT + attributes { + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + } + } + } + } + + cleanup: + Twilio.getExecutorService().shutdown() + Twilio.setExecutorService(null) + Twilio.setRestClient(null) + + where: + a | _ + 1 | _ + 2 | _ + } + + def "asynchronous error"() { + setup: + twilioRestClient.getObjectMapper() >> new ObjectMapper() + + 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()), 500) + + when: + runUnderTrace("test") { + ListenableFuture future = Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).createAsync(twilioRestClient) + + try { + return future.get(10, TimeUnit.SECONDS) + } finally { + Thread.sleep(1000) + } + } + + then: + thrown(ExecutionException) + + expect: + + assertTraces(1) { + trace(0, 2) { + span(0) { + name "test" + status ERROR + errorEvent(ApiException, "Testing Failure") + hasNoParent() + } + span(1) { + name "MessageCreator.createAsync" + kind CLIENT + status ERROR + errorEvent(ApiException, "Testing Failure") + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/twilio-6.6-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/twilio-6.6-javaagent.gradle new file mode 100644 index 000000000..04b659534 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/twilio-6.6/javaagent/twilio-6.6-javaagent.gradle @@ -0,0 +1,24 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'com.twilio.sdk' + module = 'twilio' + // this is first version in maven central (there's a 0.0.1 but that is really 7.14.4) + versions = "[6.6.9,8.0.0)" + } +} + +dependencies { + library "com.twilio.sdk:twilio:6.6.9" + + // included to make sure the apache httpclient nested spans are suppressed + testInstrumentation project(':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent') + + latestDepTestLibrary "com.twilio.sdk:twilio:7.+" +} + +tasks.withType(Test).configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs "-Dotel.instrumentation.twilio.experimental-span-attributes=true" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/EndSpanListener.java b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/EndSpanListener.java new file mode 100644 index 000000000..1b8ce9889 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/EndSpanListener.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.undertow; + +import static io.opentelemetry.javaagent.instrumentation.undertow.UndertowHttpServerTracer.tracer; + +import io.opentelemetry.context.Context; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpServerExchange; + +public class EndSpanListener implements ExchangeCompletionListener { + private final Context context; + + public EndSpanListener(Context context) { + this.context = context; + } + + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + tracer().exchangeCompleted(context, exchange); + nextListener.proceed(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java new file mode 100644 index 000000000..1fff1cc1f --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.undertow; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.undertow.UndertowHttpServerTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.undertow.server.HttpServerExchange; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher.Junction classLoaderOptimization() { + return hasClassesNamed("io.undertow.server.HttpHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.undertow.server.HttpHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handleRequest") + .and(takesArgument(0, named("io.undertow.server.HttpServerExchange"))) + .and(isPublic()), + this.getClass().getName() + "$HandleRequestAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) HttpServerExchange exchange, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context attachedContext = tracer().getServerContext(exchange); + if (attachedContext != null) { + if (!Java8BytecodeBridge.currentContext().equals(attachedContext)) { + // request processing is dispatched to another thread + scope = attachedContext.makeCurrent(); + context = attachedContext; + tracer().handlerStarted(attachedContext); + } + return; + } + + context = tracer().startServerSpan(exchange); + scope = context.makeCurrent(); + + exchange.addExchangeCompleteListener(new EndSpanListener(context)); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) HttpServerExchange exchange, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + + tracer().handlerCompleted(context, throwable, exchange); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowExchangeGetter.java b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowExchangeGetter.java new file mode 100644 index 000000000..19f27151c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowExchangeGetter.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.undertow; + +import io.opentelemetry.context.propagation.TextMapGetter; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; +import java.util.stream.Collectors; + +public class UndertowExchangeGetter implements TextMapGetter { + + public static final UndertowExchangeGetter GETTER = new UndertowExchangeGetter(); + + @Override + public Iterable keys(HttpServerExchange carrier) { + return carrier.getRequestHeaders().getHeaderNames().stream() + .map(HttpString::toString) + .collect(Collectors.toList()); + } + + @Override + public String get(HttpServerExchange carrier, String key) { + return carrier.getRequestHeaders().getFirst(key); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpServerTracer.java b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpServerTracer.java new file mode 100644 index 000000000..a001f812e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpServerTracer.java @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.undertow; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTAINER; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.servlet.AppServerBridge; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.tracer.HttpServerTracer; +import io.opentelemetry.javaagent.instrumentation.api.undertow.KeyHolder; +import io.opentelemetry.javaagent.instrumentation.api.undertow.UndertowActiveHandlers; +import io.undertow.server.DefaultResponseListener; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; +import java.net.InetSocketAddress; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class UndertowHttpServerTracer + extends HttpServerTracer< + HttpServerExchange, HttpServerExchange, HttpServerExchange, HttpServerExchange> { + private static final UndertowHttpServerTracer TRACER = new UndertowHttpServerTracer(); + + public static UndertowHttpServerTracer tracer() { + return TRACER; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.undertow"; + } + + public Context startServerSpan(HttpServerExchange exchange) { + return startSpan( + exchange, exchange, exchange, "HTTP " + exchange.getRequestMethod().toString()); + } + + @Override + protected Context customizeContext(Context context, HttpServerExchange exchange) { + context = ServerSpanNaming.init(context, CONTAINER); + // span is ended when counter reaches 0, we start from 2 which accounts for the + // handler that started the span and exchange completion listener + context = UndertowActiveHandlers.init(context, 2); + return AppServerBridge.init(context); + } + + public void handlerStarted(Context context) { + // request was dispatched to a new thread, handler on the original thread + // may exit before this one so we need to wait for this handler to complete + // before ending span + UndertowActiveHandlers.increment(context); + } + + public void handlerCompleted(Context context, Throwable throwable, HttpServerExchange exchange) { + // end the span when this is the last handler to complete and exchange has + // been completed + if (UndertowActiveHandlers.decrementAndGet(context) == 0) { + endSpan(context, throwable, exchange); + } + } + + public void exchangeCompleted(Context context, HttpServerExchange exchange) { + // after exchange is completed we can read response status + // if all handlers have completed we can end the span, if there are running + // handlers we'll end the span when last handler exits + if (UndertowActiveHandlers.decrementAndGet(context) == 0) { + Throwable throwable = exchange.getAttachment(DefaultResponseListener.EXCEPTION); + endSpan(context, throwable, exchange); + } + } + + private static void endSpan(Context context, Throwable throwable, HttpServerExchange exchange) { + if (throwable != null) { + tracer().endExceptionally(context, throwable, exchange); + } else { + tracer().end(context, exchange); + } + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public Context getServerContext(HttpServerExchange exchange) { + AttachmentKey contextKey = + (AttachmentKey) KeyHolder.contextKeys.get(AttachmentKey.class); + if (contextKey == null) { + return null; + } + return exchange.getAttachment(contextKey); + } + + @Override + @Nullable + protected Integer peerPort(HttpServerExchange exchange) { + InetSocketAddress peerAddress = + exchange.getConnection().getPeerAddress(InetSocketAddress.class); + return peerAddress.getPort(); + } + + @Override + @Nullable + protected String peerHostIP(HttpServerExchange exchange) { + InetSocketAddress peerAddress = + exchange.getConnection().getPeerAddress(InetSocketAddress.class); + return peerAddress.getHostString(); + } + + @Override + protected String flavor(HttpServerExchange exchange, HttpServerExchange exchange2) { + return exchange.getProtocol().toString(); + } + + @Override + protected TextMapGetter getGetter() { + return UndertowExchangeGetter.GETTER; + } + + @Override + protected String url(HttpServerExchange exchange) { + String result = exchange.getRequestURL(); + if (exchange.getQueryString() == null || exchange.getQueryString().isEmpty()) { + return result; + } else { + return result + "?" + exchange.getQueryString(); + } + } + + @Override + protected String method(HttpServerExchange exchange) { + return exchange.getRequestMethod().toString(); + } + + @Override + @Nullable + protected String requestHeader(HttpServerExchange exchange, String name) { + return exchange.getRequestHeaders().getFirst(name); + } + + @Override + protected int responseStatus(HttpServerExchange exchange) { + return exchange.getStatusCode(); + } + + @SuppressWarnings("unchecked") + @Override + protected void attachServerContext(Context context, HttpServerExchange exchange) { + AttachmentKey contextKey = + (AttachmentKey) + KeyHolder.contextKeys.computeIfAbsent( + AttachmentKey.class, key -> AttachmentKey.create(Context.class)); + exchange.putAttachment(contextKey, context); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowInstrumentationModule.java new file mode 100644 index 000000000..878dd6ab3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.undertow; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class UndertowInstrumentationModule extends InstrumentationModule { + + public UndertowInstrumentationModule() { + super("undertow", "undertow-1.4"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HandlerInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy new file mode 100644 index 000000000..b3fb1cb8c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy @@ -0,0 +1,178 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import io.undertow.Handlers +import io.undertow.Undertow +import io.undertow.util.Headers +import io.undertow.util.StatusCodes +//TODO make test which mixes handlers and servlets +class UndertowServerTest extends HttpServerTest implements AgentTestTrait { + + @Override + Undertow startServer(int port) { + Undertow server = Undertow.builder() + .addHttpListener(port, "localhost") + .setHandler(Handlers.path() + .addExactPath(SUCCESS.rawPath()) { exchange -> + controller(SUCCESS) { + exchange.getResponseSender().send(SUCCESS.body) + } + } + .addExactPath(QUERY_PARAM.rawPath()) { exchange -> + controller(QUERY_PARAM) { + exchange.getResponseSender().send(exchange.getQueryString()) + } + } + .addExactPath(REDIRECT.rawPath()) { exchange -> + controller(REDIRECT) { + exchange.setStatusCode(StatusCodes.FOUND) + exchange.getResponseHeaders().put(Headers.LOCATION, REDIRECT.body) + exchange.endExchange() + } + } + .addExactPath(ERROR.rawPath()) { exchange -> + controller(ERROR) { + exchange.setStatusCode(ERROR.status) + exchange.getResponseSender().send(ERROR.body) + } + } + .addExactPath(EXCEPTION.rawPath()) { exchange -> + controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + .addExactPath("sendResponse") { exchange -> + Span.current().addEvent("before-event") + runUnderTrace("sendResponse") { + exchange.setStatusCode(StatusCodes.OK) + exchange.getResponseSender().send("sendResponse") + } + // event is added only when server span has not been ended + // we need to make sure that sending response does not end server span + Span.current().addEvent("after-event") + } + .addExactPath("sendResponseWithException") { exchange -> + Span.current().addEvent("before-event") + runUnderTrace("sendResponseWithException") { + exchange.setStatusCode(StatusCodes.OK) + exchange.getResponseSender().send("sendResponseWithException") + } + // event is added only when server span has not been ended + // we need to make sure that sending response does not end server span + Span.current().addEvent("after-event") + throw new Exception("exception after sending response") + } + ).build() + server.start() + return server + } + + @Override + void stopServer(Undertow undertow) { + undertow.stop() + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + return "HTTP GET" + } + + def "test send response"() { + setup: + def uri = address.resolve("sendResponse") + AggregatedHttpResponse response = client.get(uri.toString()).aggregate().join() + + expect: + response.status().code() == 200 + response.contentUtf8().trim() == "sendResponse" + + and: + assertTraces(1) { + trace(0, 2) { + it.span(0) { + hasNoParent() + name "HTTP GET" + kind SpanKind.SERVER + + event(0) { + eventName "before-event" + } + event(1) { + eventName "after-event" + } + + attributes { + "${SemanticAttributes.NET_PEER_PORT.key}" { it instanceof Long } + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.HTTP_CLIENT_IP.key}" TEST_CLIENT_IP + "${SemanticAttributes.HTTP_URL.key}" uri.toString() + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT + } + } + basicSpan(it, 1, "sendResponse", span(0)) + } + } + } + + def "test send response with exception"() { + setup: + def uri = address.resolve("sendResponseWithException") + AggregatedHttpResponse response = client.get(uri.toString()).aggregate().join() + + expect: + response.status().code() == 200 + response.contentUtf8().trim() == "sendResponseWithException" + + and: + assertTraces(1) { + trace(0, 2) { + it.span(0) { + hasNoParent() + name "HTTP GET" + kind SpanKind.SERVER + status StatusCode.ERROR + + event(0) { + eventName "before-event" + } + event(1) { + eventName "after-event" + } + errorEvent(Exception, "exception after sending response", 2) + + attributes { + "${SemanticAttributes.NET_PEER_PORT.key}" { it instanceof Long } + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.HTTP_CLIENT_IP.key}" TEST_CLIENT_IP + "${SemanticAttributes.HTTP_URL.key}" uri.toString() + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT + } + } + basicSpan(it, 1, "sendResponseWithException", span(0)) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/undertow-1.4-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/undertow-1.4-javaagent.gradle new file mode 100644 index 000000000..1feadd804 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/undertow-1.4/javaagent/undertow-1.4-javaagent.gradle @@ -0,0 +1,14 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "io.undertow" + module = 'undertow-core' + versions = "[1.4.0.Final,)" + assertInverse = true + } +} + +dependencies { + library "io.undertow:undertow-core:2.0.0.Final" +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/ClientCallableRpcInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/ClientCallableRpcInstrumentation.java new file mode 100644 index 000000000..022a2c71c --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/ClientCallableRpcInstrumentation.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import static io.opentelemetry.javaagent.instrumentation.vaadin.VaadinTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// add spans around calls to methods with @ClientCallable annotation +public class ClientCallableRpcInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.vaadin.flow.server.communication.rpc.PublishedServerEventHandlerRpcHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invokeMethod") + .and(takesArgument(0, named("com.vaadin.flow.component.Component"))) + .and(takesArgument(1, named(Class.class.getName()))) + .and(takesArgument(2, named(String.class.getName()))) + .and(takesArgument(3, named("elemental.json.JsonArray"))) + .and(takesArgument(4, named(int.class.getName()))), + this.getClass().getName() + "$InvokeMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeMethodAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(1) Class componentClass, + @Advice.Argument(2) String methodName, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = tracer().startClientCallableSpan(componentClass, methodName); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + tracer().endSpan(context, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/JavaScriptBootstrapUiInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/JavaScriptBootstrapUiInstrumentation.java new file mode 100644 index 000000000..8426097bb --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/JavaScriptBootstrapUiInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import static io.opentelemetry.javaagent.instrumentation.vaadin.VaadinTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.vaadin.flow.component.UI; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// set server span name on initial page load, vaadin 15+ +public class JavaScriptBootstrapUiInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.vaadin.flow.component.internal.JavaScriptBootstrapUI"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("connectClient"), this.getClass().getName() + "$ConnectClientAdvice"); + } + + @SuppressWarnings("unused") + public static class ConnectClientAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This UI ui) { + tracer().updateServerSpanName(ui); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RequestHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RequestHandlerInstrumentation.java new file mode 100644 index 000000000..ccf40d709 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RequestHandlerInstrumentation.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.vaadin.VaadinTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.vaadin.flow.server.RequestHandler; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// add spans around vaadin request handlers +public class RequestHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.vaadin.flow.server.RequestHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("com.vaadin.flow.server.RequestHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handleRequest") + .and(takesArgument(0, named("com.vaadin.flow.server.VaadinSession"))) + .and(takesArgument(1, named("com.vaadin.flow.server.VaadinRequest"))) + .and(takesArgument(2, named("com.vaadin.flow.server.VaadinResponse"))), + this.getClass().getName() + "$HandleRequestAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This RequestHandler requestHandler, + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = tracer().startRequestHandlerSpan(requestHandler, method); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Return boolean handled, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + + tracer().endRequestHandlerSpan(context, throwable, handled); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RouterInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RouterInstrumentation.java new file mode 100644 index 000000000..8c06ea16e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RouterInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import static io.opentelemetry.javaagent.instrumentation.vaadin.VaadinTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.vaadin.flow.router.Location; +import com.vaadin.flow.router.NavigationTrigger; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// set server span name on initial page load +public class RouterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.vaadin.flow.router.Router"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("navigate") + .and(takesArguments(4)) + .and(takesArgument(1, named("com.vaadin.flow.router.Location"))) + .and(takesArgument(2, named("com.vaadin.flow.router.NavigationTrigger"))), + this.getClass().getName() + "$NavigateAdvice"); + } + + @SuppressWarnings("unused") + public static class NavigateAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(1) Location location, + @Advice.Argument(2) NavigationTrigger navigationTrigger) { + if (navigationTrigger == NavigationTrigger.PAGE_LOAD) { + tracer().updateServerSpanName(location); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RpcInvocationHandlerInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RpcInvocationHandlerInstrumentation.java new file mode 100644 index 000000000..ad3d28d5a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/RpcInvocationHandlerInstrumentation.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.vaadin.VaadinTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.vaadin.flow.server.communication.rpc.RpcInvocationHandler; +import elemental.json.JsonObject; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// add span around rpc calls from javascript +public class RpcInvocationHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.vaadin.flow.server.communication.rpc.RpcInvocationHandler"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface( + named("com.vaadin.flow.server.communication.rpc.RpcInvocationHandler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handle") + .and(takesArgument(0, named("com.vaadin.flow.component.UI"))) + .and(takesArgument(1, named("elemental.json.JsonObject"))), + this.getClass().getName() + "$HandleAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This RpcInvocationHandler rpcInvocationHandler, + @Advice.Origin Method method, + @Advice.Argument(1) JsonObject jsonObject, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + context = tracer().startRpcInvocationHandlerSpan(rpcInvocationHandler, method, jsonObject); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + tracer().endSpan(context, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/UiInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/UiInstrumentation.java new file mode 100644 index 000000000..d6ebe2963 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/UiInstrumentation.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import static io.opentelemetry.javaagent.instrumentation.vaadin.VaadinTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.vaadin.flow.component.UI; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// update server span name to route of current view +public class UiInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.vaadin.flow.component.UI"); + } + + @Override + public void transform(TypeTransformer transformer) { + // setCurrent is called by some request handler when they have accepted the request + // we can get the path of currently active route from ui + transformer.applyAdviceToMethod( + named("setCurrent").and(takesArgument(0, named("com.vaadin.flow.component.UI"))), + this.getClass().getName() + "$SetCurrentAdvice"); + } + + @SuppressWarnings("unused") + public static class SetCurrentAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) UI ui) { + tracer().updateServerSpanName(ui); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinInstrumentationModule.java new file mode 100644 index 000000000..5355b8590 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinInstrumentationModule.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class VaadinInstrumentationModule extends InstrumentationModule { + + public VaadinInstrumentationModule() { + super("vaadin", "vaadin-14.2"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // class added in vaadin 14.2 + return hasClassesNamed("com.vaadin.flow.server.frontend.installer.NodeInstaller"); + } + + @Override + public List typeInstrumentations() { + return asList( + new VaadinServiceInstrumentation(), + new RequestHandlerInstrumentation(), + new UiInstrumentation(), + new RouterInstrumentation(), + new JavaScriptBootstrapUiInstrumentation(), + new RpcInvocationHandlerInstrumentation(), + new ClientCallableRpcInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinServiceInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinServiceInstrumentation.java new file mode 100644 index 000000000..84e3bd4b5 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinServiceInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import static io.opentelemetry.javaagent.instrumentation.vaadin.VaadinTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.vaadin.flow.server.VaadinService; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// add span around vaadin request processing code +public class VaadinServiceInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.vaadin.flow.server.VaadinService"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handleRequest") + .and(takesArgument(0, named("com.vaadin.flow.server.VaadinRequest"))) + .and(takesArgument(1, named("com.vaadin.flow.server.VaadinResponse"))), + VaadinServiceInstrumentation.class.getName() + "$HandleRequestAdvice"); + } + + @SuppressWarnings("unused") + public static class HandleRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This VaadinService vaadinService, + @Advice.Origin Method method, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = tracer().startVaadinServiceSpan(vaadinService, method); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + scope.close(); + + tracer().endVaadinServiceSpan(context, throwable); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinTracer.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinTracer.java new file mode 100644 index 000000000..1467a0aae --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinTracer.java @@ -0,0 +1,166 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vaadin; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.router.Location; +import com.vaadin.flow.server.RequestHandler; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.communication.rpc.RpcInvocationHandler; +import elemental.json.JsonObject; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; +import java.lang.reflect.Method; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class VaadinTracer extends BaseTracer { + private static final ContextKey SERVICE_CONTEXT_KEY = + ContextKey.named("opentelemetry-vaadin-service-context-key"); + private static final ContextKey REQUEST_HANDLER_CONTEXT_KEY = + ContextKey.named("opentelemetry-vaadin-request-handler-context-key"); + + private static final VaadinTracer TRACER = new VaadinTracer(); + + public static VaadinTracer tracer() { + return TRACER; + } + + private VaadinTracer() { + super(GlobalOpenTelemetry.get()); + } + + public Context startVaadinServiceSpan(VaadinService vaadinService, Method method) { + String spanName = SpanNames.fromMethod(vaadinService.getClass(), method); + Context context = super.startSpan(spanName); + return context.with(SERVICE_CONTEXT_KEY, new VaadinServiceContext(spanName)); + } + + public void endSpan(Context context, Throwable throwable) { + if (throwable != null) { + endExceptionally(context, throwable); + } else { + end(context); + } + } + + public void endVaadinServiceSpan(Context context, Throwable throwable) { + endSpan(context, throwable); + + VaadinServiceContext vaadinServiceContext = context.get(SERVICE_CONTEXT_KEY); + if (!vaadinServiceContext.isRequestHandled()) { + // none of the request handlers processed the request + // as we update server span name on call to each request handler currently server span name + // is set based on the last request handler even when it didn't process the request, set + // server span name to main request processing method name + Span span = ServerSpan.fromContextOrNull(context); + if (span != null) { + span.updateName(vaadinServiceContext.vaadinServiceSpanName); + } + } + } + + @Nullable + public Context startRequestHandlerSpan(RequestHandler requestHandler, Method method) { + Context current = Context.current(); + // ignore nested request handlers + if (current.get(REQUEST_HANDLER_CONTEXT_KEY) != null) { + return null; + } + + String spanName = SpanNames.fromMethod(requestHandler.getClass(), method); + VaadinServiceContext vaadinServiceContext = current.get(SERVICE_CONTEXT_KEY); + if (vaadinServiceContext != null && !vaadinServiceContext.isRequestHandled()) { + Span span = ServerSpan.fromContextOrNull(current); + if (span != null) { + // set server span name to request handler name + // we don't really know whether this request handler is going to be the one + // that process the request, if it isn't then next handler will also update + // server span name + span.updateName(spanName); + } + } + + Context context = super.startSpan(spanName); + return context.with(REQUEST_HANDLER_CONTEXT_KEY, Boolean.TRUE); + } + + public void endRequestHandlerSpan(Context context, Throwable throwable, boolean handled) { + endSpan(context, throwable); + + // request handler returns true when it processes the request, if that is the case then + // mark request as handled + if (handled) { + VaadinServiceContext vaadinServiceContext = context.get(SERVICE_CONTEXT_KEY); + if (vaadinServiceContext != null) { + vaadinServiceContext.setRequestHandled(); + } + } + } + + public void updateServerSpanName(UI ui) { + if (ui != null) { + Location location = ui.getInternals().getActiveViewLocation(); + updateServerSpanName(location); + } + } + + public void updateServerSpanName(Location location) { + Context context = Context.current(); + Span span = ServerSpan.fromContextOrNull(context); + if (span != null) { + String path = location.getPath(); + if (!path.isEmpty()) { + path = "/" + path; + } + span.updateName(ServletContextPath.prepend(context, path)); + } + } + + public Context startClientCallableSpan(Class componentClass, String methodName) { + return super.startSpan(SpanNames.fromMethod(componentClass, methodName)); + } + + public Context startRpcInvocationHandlerSpan( + RpcInvocationHandler rpcInvocationHandler, Method method, JsonObject jsonObject) { + String spanName = SpanNames.fromMethod(rpcInvocationHandler.getClass(), method); + if ("event".equals(rpcInvocationHandler.getRpcType())) { + String eventType = jsonObject.getString("event"); + if (eventType != null) { + // append event type to make span name more descriptive + spanName += "/" + eventType; + } + } + return super.startSpan(spanName); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.vaadin-14.2"; + } + + private static class VaadinServiceContext { + final String vaadinServiceSpanName; + boolean requestHandled; + + VaadinServiceContext(String vaadinServiceSpanName) { + this.vaadinServiceSpanName = vaadinServiceSpanName; + } + + void setRequestHandled() { + requestHandled = true; + } + + boolean isRequestHandled() { + return requestHandled; + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin142Test/groovy/test/vaadin/Vaadin142Test.groovy b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin142Test/groovy/test/vaadin/Vaadin142Test.groovy new file mode 100644 index 000000000..b9fe95cf3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin142Test/groovy/test/vaadin/Vaadin142Test.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin + +class Vaadin142Test extends AbstractVaadin14Test { + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin14LatestTest/groovy/test/vaadin/Vaadin14LatestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin14LatestTest/groovy/test/vaadin/Vaadin14LatestTest.groovy new file mode 100644 index 000000000..bf8503a03 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin14LatestTest/groovy/test/vaadin/Vaadin14LatestTest.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin + +class Vaadin14LatestTest extends AbstractVaadin14Test { + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin16Test/groovy/test/vaadin/Vaadin16Test.groovy b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin16Test/groovy/test/vaadin/Vaadin16Test.groovy new file mode 100644 index 000000000..bea052478 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadin16Test/groovy/test/vaadin/Vaadin16Test.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin + +class Vaadin16Test extends AbstractVaadin16Test { + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadinLatestTest/groovy/test/vaadin/VaadinLatestTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadinLatestTest/groovy/test/vaadin/VaadinLatestTest.groovy new file mode 100644 index 000000000..b8f1bb26b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/src/vaadinLatestTest/groovy/test/vaadin/VaadinLatestTest.groovy @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin + +class VaadinLatestTest extends AbstractVaadin16Test { + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/vaadin-14.2-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/vaadin-14.2-javaagent.gradle new file mode 100644 index 000000000..01f3a5818 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/vaadin-14.2-javaagent.gradle @@ -0,0 +1,59 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +muzzle { + fail { + group = "com.vaadin" + module = "flow-server" + versions = "[,2.2.0)" + } + pass { + group = "com.vaadin" + module = "flow-server" + versions = "[2.2.0,3)" + } + fail { + group = "com.vaadin" + module = "flow-server" + versions = "[3.0.0,3.1.0)" + } + pass { + group = "com.vaadin" + module = "flow-server" + versions = "[3.1.0,)" + } +} + + +testSets { + vaadin142Test + vaadin14LatestTest + vaadin16Test + latestDepTest { + dirName = 'vaadinLatestTest' + } +} + +test.dependsOn vaadin142Test, vaadin16Test +if (findProperty('testLatestDeps')) { + test.dependsOn vaadin14LatestTest +} + +dependencies { + compileOnly "com.vaadin:flow-server:2.2.0" + + vaadin16TestImplementation 'com.vaadin:vaadin-spring-boot-starter:16.0.0' + vaadin142TestImplementation 'com.vaadin:vaadin-spring-boot-starter:14.2.0' + + testImplementation project(':instrumentation:vaadin-14.2:testing') + testImplementation(project(':testing-common')) { + exclude(module: 'jetty-server') + } + + testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent') + testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent') + testInstrumentation project(':instrumentation:tomcat:tomcat-7.0:javaagent') + + vaadin14LatestTestImplementation 'com.vaadin:vaadin-spring-boot-starter:14.+' + latestDepTestImplementation 'com.vaadin:vaadin-spring-boot-starter:+' +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/webpack.config.js b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/webpack.config.js new file mode 100644 index 000000000..48e402402 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/javaagent/webpack.config.js @@ -0,0 +1,4 @@ +// Some vaadin versions need webpack.config.js to be present in project root +// this webpack.config.js has to contains reference to ./webpack.generated.js +// to pass check in FrontendUtils.isWebpackConfigFile. This file is just a +// placeholder real webpack.config.js is generated under build/vaadin-* diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadin14Test.groovy b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadin14Test.groovy new file mode 100644 index 000000000..ad2c27e9a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadin14Test.groovy @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan + +import com.vaadin.flow.server.Version + +abstract class AbstractVaadin14Test extends AbstractVaadinTest { + static final boolean VAADIN_14_4 = Version.majorVersion >= 2 && Version.minorVersion >= 4 + + List getRequestHandlers() { + List handlers = [ + "PushRequestHandler" + ] + if (VAADIN_14_4) { + handlers.add("DevModeHandler") + } + handlers.addAll([ + "StreamRequestHandler", "UnsupportedBrowserHandler", "UidlRequestHandler", + "HeartbeatHandler", "SessionRequestHandler", "FaviconHandler", "BootstrapHandler" + ]) + + return handlers + } + + @Override + void assertFirstRequest() { + assertTraces(VAADIN_14_4 ? 5 : 4) { + def handlers = getRequestHandlers("BootstrapHandler") + trace(0, 2 + handlers.size()) { + serverSpan(it, 0, getContextPath() + "/main") + basicSpan(it, 1, "SpringVaadinServletService.handleRequest", span(0)) + + int spanIndex = 2 + handlers.each { handler -> + basicSpan(it, spanIndex++, handler + ".handleRequest", span(1)) + } + } + // following traces are for javascript files used on page + trace(1, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + trace(2, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + trace(3, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + if (VAADIN_14_4) { + trace(4, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + } + } + } + + @Override + void assertButtonClick() { + assertTraces(1) { + def handlers = getRequestHandlers("UidlRequestHandler") + trace(0, 2 + handlers.size() + 1) { + serverSpan(it, 0, getContextPath() + "/main") + basicSpan(it, 1, "SpringVaadinServletService.handleRequest", span(0)) + + int spanIndex = 2 + handlers.each { handler -> + basicSpan(it, spanIndex++, handler + ".handleRequest", span(1)) + } + basicSpan(it, spanIndex, "EventRpcHandler.handle/click", span(spanIndex - 1)) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadin16Test.groovy b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadin16Test.groovy new file mode 100644 index 000000000..bc9de5ee6 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadin16Test.groovy @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan + +import com.vaadin.flow.server.Version + +abstract class AbstractVaadin16Test extends AbstractVaadinTest { + static final boolean VAADIN_17 = Version.majorVersion >= 4 + static final boolean VAADIN_19 = Version.majorVersion >= 6 + + @Override + List getRequestHandlers() { + List handlers = [ + "PushRequestHandler" + ] + if (VAADIN_19) { + handlers.addAll("WebComponentBootstrapHandler", "WebComponentProvider", "PwaHandler") + } + handlers.addAll([ + "StreamRequestHandler", "UnsupportedBrowserHandler", "UidlRequestHandler", + "HeartbeatHandler", "SessionRequestHandler", "JavaScriptBootstrapHandler", "FaviconHandler", + "DevModeHandler", "IndexHtmlRequestHandler" + ]) + + return handlers + } + + @Override + void assertFirstRequest() { + assertTraces(VAADIN_17 ? 9 : 8) { + def handlers = getRequestHandlers("IndexHtmlRequestHandler") + trace(0, 2 + handlers.size()) { + serverSpan(it, 0, "IndexHtmlRequestHandler.handleRequest") + basicSpan(it, 1, "SpringVaadinServletService.handleRequest", span(0)) + int spanIndex = 2 + handlers.each { handler -> + basicSpan(it, spanIndex++, handler + ".handleRequest", span(1)) + } + } + // /xyz/VAADIN/build/vaadin-bundle-*.cache.js + trace(1, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + if (VAADIN_17) { + // /xyz/VAADIN/build/vaadin-devmodeGizmo-*.cache.js + trace(2, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + } + int traceIndex = VAADIN_17 ? 3 : 2 + handlers = getRequestHandlers("JavaScriptBootstrapHandler") + trace(traceIndex, 2 + handlers.size()) { + serverSpan(it, 0, getContextPath()) + basicSpan(it, 1, "SpringVaadinServletService.handleRequest", span(0)) + int spanIndex = 2 + handlers.each { handler -> + basicSpan(it, spanIndex++, handler + ".handleRequest", span(1)) + } + } + // /xyz/VAADIN/build/vaadin-?-*.cache.js + trace(traceIndex + 1, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + // /xyz/VAADIN/build/vaadin-?-*.cache.js + trace(traceIndex + 2, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + // /xyz/VAADIN/build/vaadin-?-*.cache.js + trace(traceIndex + 3, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + // /xyz/VAADIN/build/vaadin-?-*.cache.js + trace(traceIndex + 4, 1) { + serverSpan(it, 0, getContextPath() + "/*") + } + handlers = getRequestHandlers("UidlRequestHandler") + trace(traceIndex + 5, 2 + handlers.size() + 2) { + serverSpan(it, 0, getContextPath() + "/main") + basicSpan(it, 1, "SpringVaadinServletService.handleRequest", span(0)) + + int spanIndex = 2 + handlers.each { handler -> + basicSpan(it, spanIndex++, handler + ".handleRequest", span(1)) + } + + basicSpan(it, spanIndex, "PublishedServerEventHandlerRpcHandler.handle", span(spanIndex - 1)) + basicSpan(it, spanIndex + 1, "JavaScriptBootstrapUI.connectClient", span(spanIndex)) + } + } + } + + @Override + void assertButtonClick() { + assertTraces(1) { + def handlers = getRequestHandlers("UidlRequestHandler") + trace(0, 2 + handlers.size() + 1) { + serverSpan(it, 0, getContextPath() + "/main") + basicSpan(it, 1, "SpringVaadinServletService.handleRequest", span(0)) + + int spanIndex = 2 + handlers.each { handler -> + basicSpan(it, spanIndex++, handler + ".handleRequest", span(1)) + } + + basicSpan(it, spanIndex, "EventRpcHandler.handle/click", span(spanIndex - 1)) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadinTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadinTest.groovy new file mode 100644 index 000000000..60dba944b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/AbstractVaadinTest.groovy @@ -0,0 +1,148 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin + +import com.vaadin.flow.server.Version +import com.vaadin.flow.spring.annotation.EnableVaadin +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import java.util.concurrent.TimeUnit +import org.openqa.selenium.chrome.ChromeOptions +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.ConfigurableApplicationContext +import org.testcontainers.Testcontainers +import org.testcontainers.containers.BrowserWebDriverContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import spock.lang.Shared + +abstract class AbstractVaadinTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + private static final Logger logger = LoggerFactory.getLogger(AbstractVaadinTest) + + @Shared + BrowserWebDriverContainer chrome + + @SpringBootApplication + @EnableVaadin("test.vaadin") + static class TestApplication { + static ConfigurableApplicationContext start(int port, String contextPath) { + def app = new SpringApplication(TestApplication) + app.setDefaultProperties([ + "server.port" : port, + "server.servlet.contextPath" : contextPath, + "server.error.include-message" : "always"]) + def context = app.run() + return context + } + } + + def setupSpec() { + Testcontainers.exposeHostPorts(port) + + chrome = new BrowserWebDriverContainer<>() + .withCapabilities(new ChromeOptions()) + .withLogConsumer(new Slf4jLogConsumer(logger)) + chrome.start() + + address = new URI("http://host.testcontainers.internal:$port" + getContextPath() + "/") + } + + def cleanupSpec() { + chrome?.stop() + } + + @Override + ConfigurableApplicationContext startServer(int port) { + // set directory for files generated by vaadin development mode + // by default these go to project root + System.setProperty("vaadin.project.basedir", new File("build/vaadin-" + Version.getFullVersion()).getAbsolutePath()) + return TestApplication.start(port, getContextPath()) + } + + @Override + void stopServer(ConfigurableApplicationContext ctx) { + ctx.close() + } + + @Override + String getContextPath() { + return "/xyz" + } + + def waitForStart(driver) { + // In development mode ui javascript is compiled when application starts + // this involves downloading and installing npm and a bunch of packages + // and running webpack. Wait until all of this is done before starting test. + driver.manage().timeouts().implicitlyWait(3, TimeUnit.MINUTES) + driver.get(address.resolve("main").toString()) + // wait for page to load + driver.findElementById("main.label") + // clear traces so test would start from clean state + clearExportedData() + + driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS) + } + + def getWebDriver() { + return chrome.getWebDriver() + } + + abstract List getRequestHandlers() + + def getRequestHandlers(String lastHandler) { + def handlers = getRequestHandlers() + int index = handlers.indexOf(lastHandler) + if (index == -1) { + throw new IllegalStateException("unexpected handler " + lastHandler) + } + return handlers.subList(0, index + 1) + } + + abstract void assertFirstRequest() + + abstract void assertButtonClick() + + static serverSpan(TraceAssert trace, int index, String spanName) { + trace.span(index) { + hasNoParent() + + name spanName + kind SpanKind.SERVER + } + } + + def "test vaadin"() { + setup: + def driver = getWebDriver() + waitForStart(driver) + + // fetch the test page + driver.get(address.resolve("main").toString()) + + expect: + // wait for page to load + "Main view" == driver.findElementById("main.label").getText() + assertFirstRequest() + + clearExportedData() + + when: + // click a button to trigger calling java code in MainView + driver.findElementById("main.button").click() + + then: + // wait for page to load + "Other view" == driver.findElementById("other.label").getText() + assertButtonClick() + + cleanup: + driver.close() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/MainView.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/MainView.java new file mode 100644 index 000000000..381c2f2fa --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/MainView.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Label; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; + +@Route("main") +public class MainView extends VerticalLayout { + + public MainView() { + Label label = new Label("Main view"); + label.setId("main.label"); + Button button = new Button("To other view", e -> UI.getCurrent().navigate(OtherView.class)); + button.setId("main.button"); + add(label, button); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/OtherView.java b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/OtherView.java new file mode 100644 index 000000000..23fe3a398 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/groovy/test/vaadin/OtherView.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test.vaadin; + +import com.vaadin.flow.component.html.Label; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; + +@Route("other") +public class OtherView extends VerticalLayout { + + public OtherView() { + Label label = new Label("Other view"); + label.setId("other.label"); + add(label); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/resources/application.properties b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/resources/application.properties new file mode 100644 index 000000000..3c04ab695 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/src/main/resources/application.properties @@ -0,0 +1,3 @@ +vaadin.whitelisted-packages=test/vaadin +vaadin.devmode.liveReload.enabled=false +vaadin.pnpm.enable=true diff --git a/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/vaadin-14.2-testing.gradle b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/vaadin-14.2-testing.gradle new file mode 100644 index 000000000..2c1f53b11 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vaadin-14.2/testing/vaadin-14.2-testing.gradle @@ -0,0 +1,15 @@ +ext { + skipPublish = true +} + +apply plugin: "otel.java-conventions" + +dependencies { + compileOnly 'com.vaadin:vaadin-spring-boot-starter:14.2.0' + + api "org.testcontainers:selenium:${versions["org.testcontainers"]}" + implementation(project(':testing-common')) { + exclude(module: 'jetty-server') + } + implementation 'org.seleniumhq.selenium:selenium-java:3.141.59' +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultConsumerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultConsumerWrapper.java new file mode 100644 index 000000000..69b7dc66b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultConsumerWrapper.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.reactive; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AsyncResultConsumerWrapper implements Consumer>> { + + private static final Logger log = LoggerFactory.getLogger(AsyncResultConsumerWrapper.class); + + private final Consumer>> delegate; + private final Context executionContext; + + public AsyncResultConsumerWrapper( + Consumer>> delegate, Context executionContext) { + this.delegate = delegate; + this.executionContext = executionContext; + } + + @Override + public void accept(Handler> asyncResultHandler) { + if (executionContext != null) { + try (Scope ignored = executionContext.makeCurrent()) { + delegate.accept(asyncResultHandler); + } + } else { + delegate.accept(asyncResultHandler); + } + } + + public static Consumer>> wrapIfNeeded( + Consumer>> delegate, Context executionContext) { + if (!(delegate instanceof AsyncResultConsumerWrapper)) { + log.debug("Wrapping consumer {}", delegate); + return new AsyncResultConsumerWrapper(delegate, executionContext); + } + return delegate; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultHandlerWrapper.java new file mode 100644 index 000000000..6aa789ce2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultHandlerWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.reactive; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AsyncResultHandlerWrapper implements Handler>> { + + private static final Logger log = LoggerFactory.getLogger(AsyncResultHandlerWrapper.class); + + private final Handler>> delegate; + private final Context executionContext; + + public AsyncResultHandlerWrapper( + Handler>> delegate, Context executionContext) { + this.delegate = delegate; + this.executionContext = executionContext; + } + + @Override + public void handle(Handler> asyncResultHandler) { + if (executionContext != null) { + try (Scope ignored = executionContext.makeCurrent()) { + delegate.handle(asyncResultHandler); + } + } else { + delegate.handle(asyncResultHandler); + } + } + + public static Handler>> wrapIfNeeded( + Handler>> delegate, Context executionContext) { + if (!(delegate instanceof AsyncResultHandlerWrapper)) { + log.debug("Wrapping handler {}", delegate); + return new AsyncResultHandlerWrapper(delegate, executionContext); + } + return delegate; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultSingleInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultSingleInstrumentation.java new file mode 100644 index 000000000..afaeb828a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/AsyncResultSingleInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.reactive; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import java.util.function.Consumer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** This instrumentation allows span context propagation across Vert.x reactive executions. */ +public class AsyncResultSingleInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + // Different versions of Vert.x has this class in different packages + return hasClassesNamed("io.vertx.reactivex.core.impl.AsyncResultSingle") + .or(hasClassesNamed("io.vertx.reactivex.impl.AsyncResultSingle")); + } + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "io.vertx.reactivex.core.impl.AsyncResultSingle", + "io.vertx.reactivex.impl.AsyncResultSingle"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(0, named("io.vertx.core.Handler"))), + this.getClass().getName() + "$ConstructorWithHandlerAdvice"); + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(0, Consumer.class)), + this.getClass().getName() + "$ConstructorWithConsumerAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorWithHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.Argument(value = 0, readOnly = false) Handler>> handler) { + handler = + AsyncResultHandlerWrapper.wrapIfNeeded(handler, Java8BytecodeBridge.currentContext()); + } + } + + @SuppressWarnings("unused") + public static class ConstructorWithConsumerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.Argument(value = 0, readOnly = false) Consumer>> handler) { + handler = + AsyncResultConsumerWrapper.wrapIfNeeded(handler, Java8BytecodeBridge.currentContext()); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/VertxRxInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/VertxRxInstrumentationModule.java new file mode 100644 index 000000000..6ac5bb38a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/VertxRxInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.reactive; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class VertxRxInstrumentationModule extends InstrumentationModule { + + public VertxRxInstrumentationModule() { + super("vertx-reactive", "vertx-reactive-3.5", "vertx"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new AsyncResultSingleInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/package-info.java b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/package-info.java new file mode 100644 index 000000000..c87927db2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/package-info.java @@ -0,0 +1,9 @@ +/** + * The majority of monitoring needs of Vert.x application is covered by generic instrumentations. + * Such as those of netty or JDBC. + * + *

{@link io.opentelemetry.javaagent.instrumentation.vertx.reactive.VertxRxInstrumentationModule} + * wraps {code AsyncResultSingle} classes from Vert.x RxJava library to ensure proper span context + * propagation in reactive Vert.x applications. + */ +package io.opentelemetry.javaagent.instrumentation.vertx.reactive; diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/VertxReactivePropagationTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/VertxReactivePropagationTest.groovy new file mode 100644 index 000000000..d4ab4733e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/VertxReactivePropagationTest.groovy @@ -0,0 +1,169 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static VertxReactiveWebServer.TEST_REQUEST_ID_ATTRIBUTE +import static VertxReactiveWebServer.TEST_REQUEST_ID_PARAMETER +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicClientSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicServerSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.HttpRequest +import io.opentelemetry.testing.internal.armeria.common.HttpRequestBuilder +import io.vertx.reactivex.core.Vertx +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import spock.lang.Shared + +class VertxReactivePropagationTest extends AgentInstrumentationSpecification { + @Shared + WebClient client + + @Shared + int port + + @Shared + Vertx server + + def setupSpec() { + port = PortUtils.findOpenPort() + server = VertxReactiveWebServer.start(port) + client = WebClient.of("h1c://localhost:${port}") + } + + def cleanupSpec() { + server.close() + } + + //Verifies that context is correctly propagated and sql query span has correct parent. + //Tests io.opentelemetry.javaagent.instrumentation.vertx.reactive.VertxRxInstrumentation + def "should propagate context over vert.x rx-java framework"() { + setup: + def response = client.get("/listProducts").aggregate().join() + + expect: + response.status().code() == SUCCESS.status + + and: + assertTraces(1) { + trace(0, 4) { + span(0) { + name "/listProducts" + kind SERVER + hasNoParent() + attributes { + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:${port}/listProducts" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + } + } + basicSpan(it, 1, "handleListProducts", span(0)) + basicSpan(it, 2, "listProducts", span(1)) + span(3) { + name "SELECT test.products" + kind CLIENT + childOf span(2) + attributes { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "SA" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" "SELECT id, name, price, weight FROM products" + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "products" + } + } + } + } + } + + def "should propagate context correctly over vert.x rx-java framework with high concurrency"() { + setup: + int count = 100 + def baseUrl = "/listProducts" + def latch = new CountDownLatch(1) + + def pool = Executors.newFixedThreadPool(8) + def propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator() + def setter = { HttpRequestBuilder carrier, String name, String value -> + carrier.header(name, value) + } + + when: + count.times { index -> + def job = { + latch.await() + runUnderTrace("client " + index) { + HttpRequestBuilder builder = HttpRequest.builder() + .get("${baseUrl}?${TEST_REQUEST_ID_PARAMETER}=${index}") + Span.current().setAttribute(TEST_REQUEST_ID_ATTRIBUTE, index) + propagator.inject(Context.current(), builder, setter) + client.execute(builder.build()).aggregate().join() + } + } + pool.submit(job) + } + + latch.countDown() + + then: + assertTraces(count) { + (0..count - 1).each { + trace(it, 5) { + def rootSpan = it.span(0) + def requestId = Long.valueOf(rootSpan.name.substring("client ".length())) + + basicSpan(it, 0, "client $requestId", null, null) { + "${TEST_REQUEST_ID_ATTRIBUTE}" requestId + } + basicServerSpan(it, 1, "/listProducts", span(0), null) { + "${SemanticAttributes.NET_PEER_PORT.key}" Long + "${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1" + "${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$baseUrl?$TEST_REQUEST_ID_PARAMETER=$requestId" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" String + "${SemanticAttributes.HTTP_CLIENT_IP.key}" "127.0.0.1" + "${TEST_REQUEST_ID_ATTRIBUTE}" requestId + } + basicSpan(it, 2, "handleListProducts", span(1), null) { + "${TEST_REQUEST_ID_ATTRIBUTE}" requestId + } + basicSpan(it, 3, "listProducts", span(2), null) { + "${TEST_REQUEST_ID_ATTRIBUTE}" requestId + } + basicClientSpan(it, 4, "SELECT test.products", span(3), null) { + "${SemanticAttributes.DB_SYSTEM.key}" "hsqldb" + "${SemanticAttributes.DB_NAME.key}" "test" + "${SemanticAttributes.DB_USER.key}" "SA" + "${SemanticAttributes.DB_CONNECTION_STRING.key}" "hsqldb:mem:" + "${SemanticAttributes.DB_STATEMENT.key}" "SELECT id AS request$requestId, name, price, weight FROM products" + "${SemanticAttributes.DB_OPERATION.key}" "SELECT" + "${SemanticAttributes.DB_SQL_TABLE.key}" "products" + } + } + } + } + + cleanup: + pool.shutdownNow() + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy new file mode 100644 index 000000000..3f6f5c993 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.vertx.circuitbreaker.CircuitBreakerOptions +import io.vertx.core.AsyncResult +import io.vertx.core.VertxOptions +import io.vertx.core.http.HttpMethod +import io.vertx.ext.web.client.WebClientOptions +import io.vertx.reactivex.circuitbreaker.CircuitBreaker +import io.vertx.reactivex.core.Vertx +import io.vertx.reactivex.ext.web.client.HttpRequest +import io.vertx.reactivex.ext.web.client.WebClient +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer +import spock.lang.Shared + +class VertxRxCircuitBreakerWebClientTest extends HttpClientTest> implements AgentTestTrait { + + @Shared + Vertx vertx = Vertx.vertx(new VertxOptions()) + @Shared + def clientOptions = new WebClientOptions().setConnectTimeout(CONNECT_TIMEOUT_MS) + @Shared + WebClient client = WebClient.create(vertx, clientOptions) + @Shared + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, + new CircuitBreakerOptions() + .setTimeout(-1) // Disable the timeout otherwise it makes each test take this long. + ) + + @Override + HttpRequest buildRequest(String method, URI uri, Map headers) { + def request = client.request(HttpMethod.valueOf(method), getPort(uri), uri.host, "$uri") + headers.each { request.putHeader(it.key, it.value) } + return request + } + + @Override + int sendRequest(HttpRequest request, String method, URI uri, Map headers) { + // VertxRx doesn't seem to provide a synchronous API at all for circuit breaker. Bridge through + // a callback. + CompletableFuture future = new CompletableFuture<>() + sendRequestWithCallback(request) { + if (it.succeeded()) { + future.complete(it.result().statusCode()) + } else { + future.completeExceptionally(it.cause()) + } + } + return future.get() + } + + void sendRequestWithCallback(HttpRequest request, Consumer consumer) { + breaker.executeCommand({ command -> + request.rxSend().doOnSuccess { + command.complete(it) + }.doOnError { + command.fail(it) + }.subscribe() + }, { + consumer.accept(it) + }) + } + + @Override + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + sendRequestWithCallback(request) { + if (it.succeeded()) { + requestResult.complete(it.result().statusCode()) + } else { + requestResult.complete(it.cause()) + } + } + } + + @Override + String userAgent() { + return "Vert.x-WebClient" + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testHttps() { + false + } + + @Override + boolean testCausality() { + true + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + return new VertxRxCircuitBreakerSingleConnection(host, port, breaker) + } + + @Override + boolean testCallbackWithParent() { + //Make rxjava2 instrumentation work with vert.x reactive in order to fix this test + return false + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy new file mode 100644 index 000000000..0eab797f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.SpanAssert +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.vertx.core.VertxOptions +import io.vertx.core.http.HttpMethod +import io.vertx.ext.web.client.WebClientOptions +import io.vertx.reactivex.core.Vertx +import io.vertx.reactivex.core.buffer.Buffer +import io.vertx.reactivex.ext.web.client.HttpRequest +import io.vertx.reactivex.ext.web.client.HttpResponse +import io.vertx.reactivex.ext.web.client.WebClient +import spock.lang.Shared + +class VertxRxWebClientTest extends HttpClientTest> implements AgentTestTrait { + + @Shared + Vertx vertx = Vertx.vertx(new VertxOptions()) + @Shared + def clientOptions = new WebClientOptions().setConnectTimeout(CONNECT_TIMEOUT_MS) + @Shared + WebClient client = WebClient.create(vertx, clientOptions) + + @Override + HttpRequest buildRequest(String method, URI uri, Map headers) { + def request = client.request(HttpMethod.valueOf(method), getPort(uri), uri.host, "$uri") + headers.each { request.putHeader(it.key, it.value) } + return request + } + + @Override + int sendRequest(HttpRequest request, String method, URI uri, Map headers) { + return request.rxSend().blockingGet().statusCode() + } + + @Override + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + request.rxSend() + .subscribe(new io.reactivex.functions.Consumer>() { + @Override + void accept(HttpResponse httpResponse) throws Exception { + requestResult.complete(httpResponse.statusCode()) + } + }, new io.reactivex.functions.Consumer() { + @Override + void accept(Throwable throwable) throws Exception { + requestResult.complete(throwable) + } + }) + } + + @Override + void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + if (exception.class == RuntimeException) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "https://192.0.2.1/": // non routable address + exception = exception.getCause() + } + } + super.assertClientSpanErrorEvent(spanAssert, uri, exception) + } + + @Override + String userAgent() { + return "Vert.x-WebClient" + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testHttps() { + false + } + + @Override + boolean testCausality() { + true + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + return new VertxRxSingleConnection(host, port) + } + + @Override + boolean testCallbackWithParent() { + //Make rxjava2 instrumentation work with vert.x reactive in order to fix this test + return false + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/server/VertxRxCircuitBreakerHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/server/VertxRxCircuitBreakerHttpServerTest.groovy new file mode 100644 index 000000000..102724b77 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/server/VertxRxCircuitBreakerHttpServerTest.groovy @@ -0,0 +1,149 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.vertx.circuitbreaker.CircuitBreakerOptions +import io.vertx.core.Future +import io.vertx.reactivex.circuitbreaker.CircuitBreaker +import io.vertx.reactivex.core.AbstractVerticle +import io.vertx.reactivex.ext.web.Router + +class VertxRxCircuitBreakerHttpServerTest extends VertxRxHttpServerTest { + + @Override + protected Class verticle() { + return VertxRxCircuitBreakerWebTestServer + } + + static class VertxRxCircuitBreakerWebTestServer extends AbstractVerticle { + + @Override + void start(final Future startFuture) { + int port = config().getInteger(CONFIG_HTTP_SERVER_PORT) + Router router = Router.router(super.@vertx) + CircuitBreaker breaker = + CircuitBreaker.create( + "my-circuit-breaker", + super.@vertx, + new CircuitBreakerOptions() + .setTimeout(-1) // Disable the timeout otherwise it makes each test take this long. + ) + + router.route(SUCCESS.path).handler { ctx -> + breaker.executeCommand({ future -> + future.complete(SUCCESS) + }, { it -> + if (it.failed()) { + throw it.cause() + } + HttpServerTest.ServerEndpoint endpoint = it.result() + controller(endpoint) { + ctx.response().setStatusCode(endpoint.status).end(endpoint.body) + } + }) + } + router.route(INDEXED_CHILD.path).handler { ctx -> + breaker.executeCommand({ future -> + future.complete(INDEXED_CHILD) + }, { it -> + if (it.failed()) { + throw it.cause() + } + HttpServerTest.ServerEndpoint endpoint = it.result() + controller(endpoint) { + endpoint.collectSpanAttributes { ctx.request().params().get(it) } + ctx.response().setStatusCode(endpoint.status).end() + } + }) + } + router.route(QUERY_PARAM.path).handler { ctx -> + breaker.executeCommand({ future -> + future.complete(QUERY_PARAM) + }, { it -> + if (it.failed()) { + throw it.cause() + } + HttpServerTest.ServerEndpoint endpoint = it.result() + controller(endpoint) { + ctx.response().setStatusCode(endpoint.status).end(ctx.request().query()) + } + }) + } + router.route(REDIRECT.path).handler { ctx -> + breaker.executeCommand({ future -> + future.complete(REDIRECT) + }, { + if (it.failed()) { + throw it.cause() + } + HttpServerTest.ServerEndpoint endpoint = it.result() + controller(endpoint) { + ctx.response().setStatusCode(endpoint.status).putHeader("location", endpoint.body).end() + } + }) + } + router.route(ERROR.path).handler { ctx -> + breaker.executeCommand({ future -> + future.complete(ERROR) + }, { + if (it.failed()) { + throw it.cause() + } + HttpServerTest.ServerEndpoint endpoint = it.result() + controller(endpoint) { + ctx.response().setStatusCode(endpoint.status).end(endpoint.body) + } + }) + } + router.route(EXCEPTION.path).handler { ctx -> + breaker.executeCommand({ future -> + future.fail(new Exception(EXCEPTION.body)) + }, { + try { + def cause = it.cause() + controller(EXCEPTION) { + throw cause + } + } catch (Exception ex) { + ctx.response().setStatusCode(EXCEPTION.status).end(ex.message) + } + }) + } + router.route("/path/:id/param").handler { ctx -> + breaker.executeCommand({ future -> + future.complete(PATH_PARAM) + }, { + if (it.failed()) { + throw it.cause() + } + HttpServerTest.ServerEndpoint endpoint = it.result() + controller(endpoint) { + ctx.response().setStatusCode(endpoint.status).end(ctx.request().getParam("id")) + } + }) + } + + + super.@vertx.createHttpServer() + .requestHandler { router.accept(it) } + .listen(port) { startFuture.complete() } + } + } + + @Override + boolean hasExceptionOnServerSpan(HttpServerTest.ServerEndpoint endpoint) { + return endpoint != EXCEPTION && super.hasExceptionOnServerSpan(endpoint) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/server/VertxRxHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/server/VertxRxHttpServerTest.groovy new file mode 100644 index 000000000..4cf15dbee --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/server/VertxRxHttpServerTest.groovy @@ -0,0 +1,134 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.vertx.core.DeploymentOptions +import io.vertx.core.Future +import io.vertx.core.Vertx +import io.vertx.core.VertxOptions +import io.vertx.core.json.JsonObject +import io.vertx.reactivex.core.AbstractVerticle +import io.vertx.reactivex.ext.web.Router +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +class VertxRxHttpServerTest extends HttpServerTest implements AgentTestTrait { + public static final String CONFIG_HTTP_SERVER_PORT = "http.server.port" + + @Override + Vertx startServer(int port) { + Vertx server = Vertx.vertx(new VertxOptions() + // Useful for debugging: + // .setBlockedThreadCheckInterval(Integer.MAX_VALUE) + .setClusterPort(port)) + CompletableFuture future = new CompletableFuture<>() + server.deployVerticle(verticle().getName(), + new DeploymentOptions() + .setConfig(new JsonObject().put(CONFIG_HTTP_SERVER_PORT, port)) + .setInstances(3)) { res -> + if (!res.succeeded()) { + throw new IllegalStateException("Cannot deploy server Verticle", res.cause()) + } + future.complete(null) + } + + future.get(30, TimeUnit.SECONDS) + return server + } + + @Override + void stopServer(Vertx server) { + server.close() + } + + @Override + boolean testPathParam() { + return true + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return "/path/:id/param" + case NOT_FOUND: + return "HTTP GET" + default: + return endpoint.getPath() + } + } + + @Override + boolean testConcurrency() { + return true + } + + protected Class verticle() { + return VertxReactiveWebServer + } + + static class VertxReactiveWebServer extends AbstractVerticle { + + @Override + void start(final Future startFuture) { + int port = config().getInteger(CONFIG_HTTP_SERVER_PORT) + Router router = Router.router(super.@vertx) + + router.route(SUCCESS.path).handler { ctx -> + controller(SUCCESS) { + ctx.response().setStatusCode(SUCCESS.status).end(SUCCESS.body) + } + } + router.route(INDEXED_CHILD.path).handler { ctx -> + controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { ctx.request().params().get(it) } + ctx.response().setStatusCode(INDEXED_CHILD.status).end() + } + } + router.route(QUERY_PARAM.path).handler { ctx -> + controller(QUERY_PARAM) { + ctx.response().setStatusCode(QUERY_PARAM.status).end(ctx.request().query()) + } + } + router.route(REDIRECT.path).handler { ctx -> + controller(REDIRECT) { + ctx.response().setStatusCode(REDIRECT.status).putHeader("location", REDIRECT.body).end() + } + } + router.route(ERROR.path).handler { ctx -> + controller(ERROR) { + ctx.response().setStatusCode(ERROR.status).end(ERROR.body) + } + } + router.route(EXCEPTION.path).handler { ctx -> + controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + router.route("/path/:id/param").handler { ctx -> + controller(PATH_PARAM) { + ctx.response().setStatusCode(PATH_PARAM.status).end(ctx.request().getParam("id")) + } + } + + + super.@vertx.createHttpServer() + .requestHandler { router.accept(it) } + .listen(port) { startFuture.complete() } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/VertxReactiveWebServer.java b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/VertxReactiveWebServer.java new file mode 100644 index 000000000..8ab08586b --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/VertxReactiveWebServer.java @@ -0,0 +1,177 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.reactivex.Single; +import io.vertx.core.DeploymentOptions; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.VertxOptions; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.reactivex.core.AbstractVerticle; +import io.vertx.reactivex.core.Vertx; +import io.vertx.reactivex.core.http.HttpServerResponse; +import io.vertx.reactivex.ext.jdbc.JDBCClient; +import io.vertx.reactivex.ext.sql.SQLConnection; +import io.vertx.reactivex.ext.web.Router; +import io.vertx.reactivex.ext.web.RoutingContext; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VertxReactiveWebServer extends AbstractVerticle { + + private static final Logger log = LoggerFactory.getLogger(VertxReactiveWebServer.class); + + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + public static final String TEST_REQUEST_ID_PARAMETER = "test-request-id"; + public static final String TEST_REQUEST_ID_ATTRIBUTE = "test.request.id"; + + private static final String CONFIG_HTTP_SERVER_PORT = "http.server.port"; + private static JDBCClient client; + + public static Vertx start(int port) + throws ExecutionException, InterruptedException, TimeoutException { + /* This is highly against Vertx ideas, but our tests are synchronous + so we have to make sure server is up and running */ + CompletableFuture future = new CompletableFuture<>(); + + Vertx server = Vertx.vertx(new VertxOptions()); + + client = + JDBCClient.createShared( + server, + new JsonObject() + .put("url", "jdbc:hsqldb:mem:test?shutdown=true") + .put("driver_class", "org.hsqldb.jdbcDriver")); + + log.info("Starting on port {}", port); + server.deployVerticle( + VertxReactiveWebServer.class.getName(), + new DeploymentOptions().setConfig(new JsonObject().put(CONFIG_HTTP_SERVER_PORT, port)), + res -> { + if (!res.succeeded()) { + RuntimeException exception = + new RuntimeException("Cannot deploy server Verticle", res.cause()); + future.completeExceptionally(exception); + } + future.complete(null); + }); + // block until vertx server is up + future.get(30, TimeUnit.SECONDS); + + return server; + } + + @Override + public void start(Future startFuture) { + setUpInitialData( + ready -> { + Router router = Router.router(vertx); + int port = config().getInteger(CONFIG_HTTP_SERVER_PORT); + log.info("Listening on port {}", port); + router + .route(SUCCESS.getPath()) + .handler( + ctx -> ctx.response().setStatusCode(SUCCESS.getStatus()).end(SUCCESS.getBody())); + + router.route("/listProducts").handler(VertxReactiveWebServer::handleListProducts); + + vertx + .createHttpServer() + .requestHandler(router::accept) + .listen(port, h -> startFuture.complete()); + }); + } + + @SuppressWarnings("CheckReturnValue") + private static void handleListProducts(RoutingContext routingContext) { + Long requestId = extractRequestId(routingContext); + attachRequestIdToCurrentSpan(requestId); + + Span span = tracer.spanBuilder("handleListProducts").startSpan(); + try (Scope ignored = Context.current().with(span).makeCurrent()) { + attachRequestIdToCurrentSpan(requestId); + + HttpServerResponse response = routingContext.response(); + Single jsonArraySingle = listProducts(requestId); + + jsonArraySingle.subscribe( + arr -> response.putHeader("content-type", "application/json").end(arr.encode())); + } finally { + span.end(); + } + } + + private static Single listProducts(Long requestId) { + Span span = tracer.spanBuilder("listProducts").startSpan(); + try (Scope ignored = Context.current().with(span).makeCurrent()) { + attachRequestIdToCurrentSpan(requestId); + String queryInfix = requestId != null ? " AS request" + requestId : ""; + + return client + .rxQuery("SELECT id" + queryInfix + ", name, price, weight FROM products") + .flatMap( + result -> { + JsonArray arr = new JsonArray(); + result.getRows().forEach(arr::add); + return Single.just(arr); + }); + } finally { + span.end(); + } + } + + private static Long extractRequestId(RoutingContext routingContext) { + String requestIdString = routingContext.request().params().get(TEST_REQUEST_ID_PARAMETER); + return requestIdString != null ? Long.valueOf(requestIdString) : null; + } + + private static void attachRequestIdToCurrentSpan(Long requestId) { + if (requestId != null) { + Span.current().setAttribute(TEST_REQUEST_ID_ATTRIBUTE, requestId); + } + } + + private static void setUpInitialData(Handler done) { + client.getConnection( + res -> { + if (res.failed()) { + throw new IllegalStateException(res.cause()); + } + + SQLConnection conn = res.result(); + + conn.execute( + "CREATE TABLE IF NOT EXISTS products(id INT IDENTITY, name VARCHAR(255), price FLOAT, weight INT)", + ddl -> { + if (ddl.failed()) { + throw new IllegalStateException(ddl.cause()); + } + + conn.execute( + "INSERT INTO products (name, price, weight) VALUES ('Egg Whisk', 3.99, 150), ('Tea Cosy', 5.99, 100), ('Spatula', 1.00, 80)", + fixtures -> { + if (fixtures.failed()) { + throw new IllegalStateException(fixtures.cause()); + } + + done.handle(null); + }); + }); + }); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxCircuitBreakerSingleConnection.java b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxCircuitBreakerSingleConnection.java new file mode 100644 index 000000000..005ef2cf4 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxCircuitBreakerSingleConnection.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client; + +import io.vertx.core.AsyncResult; +import io.vertx.reactivex.circuitbreaker.CircuitBreaker; +import io.vertx.reactivex.ext.web.client.HttpRequest; +import io.vertx.reactivex.ext.web.client.HttpResponse; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public class VertxRxCircuitBreakerSingleConnection extends VertxRxSingleConnection { + private final CircuitBreaker breaker; + + public VertxRxCircuitBreakerSingleConnection(String host, int port, CircuitBreaker breaker) { + super(host, port); + this.breaker = breaker; + } + + @Override + protected HttpResponse fetchResponse(HttpRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + sendRequestWithCallback( + request, + it -> { + if (it.succeeded()) { + future.complete(it.result()); + } else { + future.completeExceptionally(it.cause()); + } + }); + + return (HttpResponse) future.join(); + } + + private void sendRequestWithCallback(HttpRequest request, Consumer> consumer) { + breaker.executeCommand( + command -> + request.rxSend().doOnSuccess(command::complete).doOnError(command::fail).subscribe(), + consumer::accept); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxSingleConnection.java b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxSingleConnection.java new file mode 100644 index 000000000..c5a1d9edc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxSingleConnection.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client; + +import io.opentelemetry.instrumentation.test.base.SingleConnection; +import io.vertx.core.VertxOptions; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.reactivex.core.Vertx; +import io.vertx.reactivex.core.buffer.Buffer; +import io.vertx.reactivex.ext.web.client.HttpRequest; +import io.vertx.reactivex.ext.web.client.HttpResponse; +import io.vertx.reactivex.ext.web.client.WebClient; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +public class VertxRxSingleConnection implements SingleConnection { + private final WebClient webClient; + private final String host; + private final int port; + + public VertxRxSingleConnection(String host, int port) { + this.host = host; + this.port = port; + + WebClientOptions clientOptions = + new WebClientOptions() + .setConnectTimeout(5000) + .setMaxPoolSize(1) + .setKeepAlive(true) + .setPipelining(true); + + Vertx vertx = Vertx.vertx(new VertxOptions()); + this.webClient = WebClient.create(vertx, clientOptions); + } + + @Override + public int doRequest(String path, Map headers) throws ExecutionException { + String requestId = Objects.requireNonNull(headers.get(REQUEST_ID_HEADER)); + + String url; + try { + url = new URL("http", host, port, path).toString(); + } catch (MalformedURLException e) { + throw new ExecutionException(e); + } + + HttpRequest request = webClient.request(HttpMethod.GET, port, host, url); + headers.forEach(request::putHeader); + + HttpResponse response = fetchResponse(request); + + String responseId = response.getHeader(REQUEST_ID_HEADER); + if (!requestId.equals(responseId)) { + throw new IllegalStateException( + String.format("Received response with id %s, expected %s", responseId, requestId)); + } + + return response.statusCode(); + } + + protected HttpResponse fetchResponse(HttpRequest request) { + return request.rxSend().blockingGet(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/vertx-reactive-3.5-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/vertx-reactive-3.5-javaagent.gradle new file mode 100644 index 000000000..475d39dc2 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-reactive-3.5/javaagent/vertx-reactive-3.5-javaagent.gradle @@ -0,0 +1,40 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'io.vertx' + module = 'vertx-rx-java2' + versions = "[3.5.0,)" + } +} + +//The first Vert.x version that uses rx-java 2 +ext.vertxVersion = '3.5.0' + +dependencies { + library "io.vertx:vertx-web:${vertxVersion}" + library "io.vertx:vertx-rx-java2:${vertxVersion}" + + testInstrumentation project(':instrumentation:jdbc:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:vertx-web-3.0:javaagent') + //TODO we should include rjxava2 instrumentation here as well + + testLibrary "io.vertx:vertx-web-client:${vertxVersion}" + testLibrary "io.vertx:vertx-jdbc-client:${vertxVersion}" + testLibrary "io.vertx:vertx-circuit-breaker:${vertxVersion}" + testImplementation 'org.hsqldb:hsqldb:2.3.4' + + // Vert.x 4.0 is incompatible with our tests. + // 3.9.7 Requires Netty 4.1.60, no other version works with it. + latestDepTestLibrary enforcedPlatform("io.netty:netty-bom:4.1.60.Final") + latestDepTestLibrary "io.vertx:vertx-web:3.+" + latestDepTestLibrary "io.vertx:vertx-web-client:3.+" + latestDepTestLibrary "io.vertx:vertx-jdbc-client:3.+" + latestDepTestLibrary "io.vertx:vertx-circuit-breaker:3.+" + latestDepTestLibrary "io.vertx:vertx-rx-java2:3.+" +} + +test { + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RouteInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RouteInstrumentation.java new file mode 100644 index 000000000..41f8b1fb1 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RouteInstrumentation.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RouteInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.vertx.ext.web.Route"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.vertx.ext.web.Route")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("handler")).and(takesArgument(0, named("io.vertx.core.Handler"))), + this.getClass().getName() + "$HandlerAdvice"); + } + + @SuppressWarnings("unused") + public static class HandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.Argument(value = 0, readOnly = false) Handler handler) { + handler = new RoutingContextHandlerWrapper(handler); + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RoutingContextHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RoutingContextHandlerWrapper.java new file mode 100644 index 000000000..aacbf9ec3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RoutingContextHandlerWrapper.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** This is used to wrap Vert.x Handlers to provide nice user-friendly SERVER span names */ +public final class RoutingContextHandlerWrapper implements Handler { + + private static final Logger log = LoggerFactory.getLogger(RoutingContextHandlerWrapper.class); + + private final Handler handler; + + public RoutingContextHandlerWrapper(Handler handler) { + this.handler = handler; + } + + @Override + public void handle(RoutingContext context) { + Span serverSpan = ServerSpan.fromContextOrNull(Context.current()); + try { + if (serverSpan != null) { + // TODO should update only SERVER span using + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/465 + serverSpan.updateName(context.currentRoute().getPath()); + } + } catch (RuntimeException ex) { + log.error("Failed to update server span name with vert.x route", ex); + } + try { + handler.handle(context); + } catch (Throwable throwable) { + if (serverSpan != null) { + serverSpan.recordException(unwrapThrowable(throwable)); + } + throw throwable; + } + } + + private static Throwable unwrapThrowable(Throwable throwable) { + if (throwable.getCause() != null + && (throwable instanceof ExecutionException + || throwable instanceof CompletionException + || throwable instanceof InvocationTargetException + || throwable instanceof UndeclaredThrowableException)) { + return unwrapThrowable(throwable.getCause()); + } + return throwable; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxWebInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxWebInstrumentationModule.java new file mode 100644 index 000000000..e0daaf344 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxWebInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class VertxWebInstrumentationModule extends InstrumentationModule { + + public VertxWebInstrumentationModule() { + super("vertx-web", "vertx-web-3.0", "vertx"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RouteInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/Contexts.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/Contexts.java new file mode 100644 index 000000000..ed2bd7e9d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/Contexts.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.client; + +import io.opentelemetry.context.Context; + +public class Contexts { + public final Context parentContext; + public final Context context; + + public Contexts(Context parentContext, Context context) { + this.parentContext = parentContext; + this.context = context; + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/ExceptionHandlerWrapper.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/ExceptionHandlerWrapper.java new file mode 100644 index 000000000..e56146efc --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/ExceptionHandlerWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.client; + +import static io.opentelemetry.javaagent.instrumentation.vertx.client.VertxClientTracer.tracer; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpClientRequest; + +public class ExceptionHandlerWrapper implements Handler { + private final HttpClientRequest request; + private final ContextStore contextStore; + private final Handler handler; + + public ExceptionHandlerWrapper( + HttpClientRequest request, + ContextStore contextStore, + Handler handler) { + this.request = request; + this.contextStore = contextStore; + this.handler = handler; + } + + @Override + public void handle(Throwable throwable) { + Contexts contexts = contextStore.get(request); + if (contexts == null) { + callHandler(throwable); + return; + } + + tracer().endExceptionally(contexts.context, throwable); + + try (Scope ignored = contexts.parentContext.makeCurrent()) { + callHandler(throwable); + } + } + + private void callHandler(Throwable throwable) { + handler.handle(throwable); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/HttpRequestInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/HttpRequestInstrumentation.java new file mode 100644 index 000000000..41bdc12d8 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/HttpRequestInstrumentation.java @@ -0,0 +1,212 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.client; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.vertx.client.VertxClientTracer.tracer; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPrivate; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Two things happen in this instrumentation. + * + *

First, {@link EndRequestAdvice}, {@link HandleExceptionAdvice} and {@link + * HandleResponseAdvice} deal with the common start span/end span functionality. As Vert.x is async + * framework, calls to the instrumented methods may happen from different threads. Thus, correct + * context is stored in {@code HttpClientRequest} itself. + * + *

Second, when HttpClientRequest calls any method that actually performs write on the underlying + * Netty channel, {@link MountContextAdvice} scopes that method call into the context captured on + * the first step. This ensures proper context transfer between the client who actually initiated + * the http call and the Netty Channel that will perform that operation. The main result of this + * transfer is a suppression of Netty CLIENT span. + */ +public class HttpRequestInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.vertx.core.http.HttpClientRequest"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.vertx.core.http.HttpClientRequest")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(nameStartsWith("end").or(named("sendHead"))), + HttpRequestInstrumentation.class.getName() + "$EndRequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(named("handleException")), + HttpRequestInstrumentation.class.getName() + "$HandleExceptionAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(named("handleResponse")), + HttpRequestInstrumentation.class.getName() + "$HandleResponseAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(isPrivate()).and(nameStartsWith("write").or(nameStartsWith("connected"))), + HttpRequestInstrumentation.class.getName() + "$MountContextAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("exceptionHandler")) + .and(takesArgument(0, named("io.vertx.core.Handler"))), + HttpRequestInstrumentation.class.getName() + "$ExceptionHandlerAdvice"); + } + + @SuppressWarnings("unused") + public static class EndRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void attachContext( + @Advice.This HttpClientRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = Java8BytecodeBridge.currentContext(); + + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, request, request); + Contexts contexts = new Contexts(parentContext, context); + InstrumentationContext.get(HttpClientRequest.class, Contexts.class).put(request, contexts); + + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endScope( + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope != null) { + scope.close(); + } + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } + } + } + + @SuppressWarnings("unused") + public static class HandleExceptionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void handleException( + @Advice.This HttpClientRequest request, + @Advice.Argument(0) Throwable t, + @Advice.Local("otelScope") Scope scope) { + Contexts contexts = + InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request); + + if (contexts == null) { + return; + } + + tracer().endExceptionally(contexts.context, t); + + // Scoping all potential callbacks etc to the parent context + scope = contexts.parentContext.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void handleResponseExit(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } + + @SuppressWarnings("unused") + public static class HandleResponseAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void handleResponseEnter( + @Advice.This HttpClientRequest request, + @Advice.Argument(0) HttpClientResponse response, + @Advice.Local("otelScope") Scope scope) { + Contexts contexts = + InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request); + + if (contexts == null) { + return; + } + + tracer().end(contexts.context, response); + + // Scoping all potential callbacks etc to the parent context + scope = contexts.parentContext.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void handleResponseExit(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } + + @SuppressWarnings("unused") + public static class MountContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void mountContext( + @Advice.This HttpClientRequest request, @Advice.Local("otelScope") Scope scope) { + Contexts contexts = + InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request); + if (contexts == null) { + return; + } + + scope = contexts.context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void unmountContext(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } + + @SuppressWarnings("unused") + public static class ExceptionHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapExceptionHandler( + @Advice.This HttpClientRequest request, + @Advice.Argument(value = 0, readOnly = false) Handler handler) { + if (handler != null) { + ContextStore contextStore = + InstrumentationContext.get(HttpClientRequest.class, Contexts.class); + handler = new ExceptionHandlerWrapper(request, contextStore, handler); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumentationModule.java new file mode 100644 index 000000000..f3c73f82a --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.client; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class VertxClientInstrumentationModule extends InstrumentationModule { + + public VertxClientInstrumentationModule() { + super("vertx-client", "vertx"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpRequestInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientTracer.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientTracer.java new file mode 100644 index 000000000..1132d6aad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientTracer.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.client; + +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import java.net.URI; +import java.net.URISyntaxException; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class VertxClientTracer + extends HttpClientTracer { + private static final VertxClientTracer TRACER = new VertxClientTracer(); + + public static VertxClientTracer tracer() { + return TRACER; + } + + public VertxClientTracer() { + super(NetPeerAttributes.INSTANCE); + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.vertx-core-3.0"; + } + + @Override + protected String method(HttpClientRequest request) { + return request.method().name(); + } + + @Override + @Nullable + protected URI url(HttpClientRequest request) throws URISyntaxException { + return new URI(request.uri()); + } + + @Override + @Nullable + protected Integer status(HttpClientResponse response) { + return response.statusCode(); + } + + @Override + @Nullable + protected String requestHeader(HttpClientRequest request, String name) { + return request.headers().get(name); + } + + @Override + @Nullable + protected String responseHeader(HttpClientResponse response, String name) { + return response.getHeader(name); + } + + @Override + protected TextMapSetter getSetter() { + return Propagator.INSTANCE; + } + + private static class Propagator implements TextMapSetter { + private static final Propagator INSTANCE = new Propagator(); + + @Override + public void set(HttpClientRequest carrier, String key, String value) { + if (carrier != null) { + carrier.putHeader(key, value); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/package-info.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/package-info.java new file mode 100644 index 000000000..be2754603 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/package-info.java @@ -0,0 +1,10 @@ +/** + * The majority of monitoring needs of Vert.x application is covered by generic instrumentations. + * Such as those of netty or JDBC. + * + *

{@link io.opentelemetry.javaagent.instrumentation.vertx.VertxWebInstrumentationModule} wraps + * all Vert.x route handlers in order to update the name of the currently active SERVER span with + * the name of route. This is, arguably, a much more user-friendly name that defaults provided by + * HTTP server instrumentations. + */ +package io.opentelemetry.javaagent.instrumentation.vertx; diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/client/VertxHttpClientTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/client/VertxHttpClientTest.groovy new file mode 100644 index 000000000..740205dfd --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/client/VertxHttpClientTest.groovy @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.vertx.core.Vertx +import io.vertx.core.VertxOptions +import io.vertx.core.http.HttpClientOptions +import io.vertx.core.http.HttpClientRequest +import io.vertx.core.http.HttpMethod +import java.util.concurrent.CompletableFuture +import spock.lang.Shared + +class VertxHttpClientTest extends HttpClientTest implements AgentTestTrait { + + @Shared + def vertx = Vertx.vertx(new VertxOptions()) + @Shared + def clientOptions = new HttpClientOptions().setConnectTimeout(CONNECT_TIMEOUT_MS) + @Shared + def httpClient = vertx.createHttpClient(clientOptions) + + @Override + HttpClientRequest buildRequest(String method, URI uri, Map headers) { + def request = httpClient.request(HttpMethod.valueOf(method), getPort(uri), uri.host, "$uri") + headers.each { request.putHeader(it.key, it.value) } + return request + } + + CompletableFuture sendRequest(HttpClientRequest request) { + CompletableFuture future = new CompletableFuture<>() + + request.handler { response -> + future.complete(response.statusCode()) + }.exceptionHandler {throwable -> + future.completeExceptionally(throwable) + } + request.end() + + return future + } + + @Override + int sendRequest(HttpClientRequest request, String method, URI uri, Map headers) { + // Vertx doesn't seem to provide any synchronous API so bridge through a callback + return sendRequest(request).get() + } + + @Override + void sendRequestWithCallback(HttpClientRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + sendRequest(request).whenComplete { status, throwable -> + requestResult.complete({ status }, throwable) + } + } + + @Override + boolean testRedirects() { + false + } + + @Override + boolean testReusedRequest() { + // vertx requests can't be reused + false + } + + @Override + boolean testHttps() { + false + } + + @Override + SingleConnection createSingleConnection(String host, int port) { + //This test fails on Vert.x 3.0 and only works starting from 3.1 + //Most probably due to https://github.com/eclipse-vertx/vert.x/pull/1126 + boolean shouldRun = Boolean.getBoolean("testLatestDeps") + return shouldRun ? new VertxSingleConnection(host, port) : null + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/client/VertxSingleConnection.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/client/VertxSingleConnection.java new file mode 100644 index 000000000..69f560976 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/client/VertxSingleConnection.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package client; + +import static io.opentelemetry.instrumentation.test.base.SingleConnection.REQUEST_ID_HEADER; + +import io.opentelemetry.instrumentation.test.base.SingleConnection; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class VertxSingleConnection implements SingleConnection { + + private final HttpClient httpClient; + private final String host; + private final int port; + + public VertxSingleConnection(String host, int port) { + this.host = host; + this.port = port; + HttpClientOptions clientOptions = + new HttpClientOptions().setMaxPoolSize(1).setKeepAlive(true).setPipelining(true); + httpClient = Vertx.vertx(new VertxOptions()).createHttpClient(clientOptions); + } + + @Override + public int doRequest(String path, Map headers) + throws ExecutionException, InterruptedException { + String requestId = Objects.requireNonNull(headers.get(REQUEST_ID_HEADER)); + + String url; + try { + url = new URL("http", host, port, path).toString(); + } catch (MalformedURLException e) { + throw new ExecutionException(e); + } + HttpClientRequest request = httpClient.request(HttpMethod.GET, port, host, url); + headers.forEach(request::putHeader); + + CompletableFuture future = new CompletableFuture<>(); + request.handler(future::complete); + + request.end(); + HttpClientResponse response = future.get(); + String responseId = response.getHeader(REQUEST_ID_HEADER); + if (!requestId.equals(responseId)) { + throw new IllegalStateException( + String.format("Received response with id %s, expected %s", responseId, requestId)); + } + return response.statusCode(); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/server/VertxHttpServerTest.groovy b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/server/VertxHttpServerTest.groovy new file mode 100644 index 000000000..5add581a3 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/groovy/server/VertxHttpServerTest.groovy @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.vertx.core.AbstractVerticle +import io.vertx.core.DeploymentOptions +import io.vertx.core.Vertx +import io.vertx.core.VertxOptions +import io.vertx.core.json.JsonObject +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +class VertxHttpServerTest extends HttpServerTest implements AgentTestTrait { + @Override + Vertx startServer(int port) { + Vertx server = Vertx.vertx(new VertxOptions() + // Useful for debugging: + // .setBlockedThreadCheckInterval(Integer.MAX_VALUE) + .setClusterPort(port)) + CompletableFuture future = new CompletableFuture<>() + server.deployVerticle(verticle().getName(), + new DeploymentOptions() + .setConfig(new JsonObject().put(VertxWebServer.CONFIG_HTTP_SERVER_PORT, port)) + .setInstances(3)) { res -> + if (!res.succeeded()) { + throw new IllegalStateException("Cannot deploy server Verticle", res.cause()) + } + future.complete(null) + } + + future.get(30, TimeUnit.SECONDS) + return server + } + + protected Class verticle() { + return VertxWebServer + } + + @Override + void stopServer(Vertx server) { + server.close() + } + + @Override + boolean testPathParam() { + return true + } + + @Override + boolean testConcurrency() { + return true + } + + @Override + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return "/path/:id/param" + case NOT_FOUND: + return "HTTP GET" + default: + return endpoint.getPath() + } + } + +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/java/server/VertxWebServer.java b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/java/server/VertxWebServer.java new file mode 100644 index 000000000..69295cf02 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/src/test/java/server/VertxWebServer.java @@ -0,0 +1,121 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package server; + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM; +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.test.base.HttpServerTest; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class VertxWebServer extends AbstractVerticle { + public static final String CONFIG_HTTP_SERVER_PORT = "http.server.port"; + + @Override + public void start(Future startFuture) { + int port = config().getInteger(CONFIG_HTTP_SERVER_PORT); + Router router = Router.router(vertx); + + //noinspection Convert2Lambda + router + .route(SUCCESS.getPath()) + .handler( + // This is not a closure/lambda on purpose to verify how do we instrument actual Handler + // classes + new Handler() { + @Override + public void handle(RoutingContext ctx) { + HttpServerTest.controller( + SUCCESS, + () -> { + ctx.response().setStatusCode(SUCCESS.getStatus()).end(SUCCESS.getBody()); + return null; + }); + } + }); + router + .route(INDEXED_CHILD.getPath()) + .handler( + ctx -> + HttpServerTest.controller( + INDEXED_CHILD, + () -> { + INDEXED_CHILD.collectSpanAttributes(it -> ctx.request().getParam(it)); + ctx.response().setStatusCode(INDEXED_CHILD.getStatus()).end(); + return null; + })); + router + .route(QUERY_PARAM.getPath()) + .handler( + ctx -> + HttpServerTest.controller( + QUERY_PARAM, + () -> { + ctx.response() + .setStatusCode(QUERY_PARAM.getStatus()) + .end(ctx.request().query()); + return null; + })); + router + .route(REDIRECT.getPath()) + .handler( + ctx -> + HttpServerTest.controller( + REDIRECT, + () -> { + ctx.response() + .setStatusCode(REDIRECT.getStatus()) + .putHeader("location", REDIRECT.getBody()) + .end(); + return null; + })); + router + .route(ERROR.getPath()) + .handler( + ctx -> + HttpServerTest.controller( + ERROR, + () -> { + ctx.response().setStatusCode(ERROR.getStatus()).end(ERROR.getBody()); + return null; + })); + router + .route(EXCEPTION.getPath()) + .handler( + ctx -> + HttpServerTest.controller( + EXCEPTION, + () -> { + throw new Exception(EXCEPTION.getBody()); + })); + router + .route("/path/:id/param") + .handler( + ctx -> + HttpServerTest.controller( + PATH_PARAM, + () -> { + ctx.response() + .setStatusCode(PATH_PARAM.getStatus()) + .end(ctx.request().getParam("id")); + return null; + })); + + vertx + .createHttpServer() + .requestHandler(router::accept) + .listen(port, it -> startFuture.complete()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/vertx-web-3.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/vertx-web-3.0-javaagent.gradle new file mode 100644 index 000000000..0962cb60d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/vertx-web-3.0/javaagent/vertx-web-3.0-javaagent.gradle @@ -0,0 +1,35 @@ +apply from: "${rootDir}/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'io.vertx' + module = 'vertx-web' + versions = "[3.0.0,4.0.0)" + //TODO we should split this module into client and server + //They have different version applicability +// assertInverse = true + } +} + +ext.vertxVersion = '3.0.0' + +dependencies { + library "io.vertx:vertx-web:${vertxVersion}" + + //We need both version as different versions of Vert.x use different versions of Netty + testInstrumentation project(':instrumentation:netty:netty-4.0:javaagent') + testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent') + testInstrumentation project(':instrumentation:jdbc:javaagent') + + testImplementation "io.vertx:vertx-jdbc-client:${vertxVersion}" + + // Vert.x 4.0 is incompatible with our tests. + // 3.9.7 Requires Netty 4.1.60, no other version works with it. + latestDepTestLibrary enforcedPlatform("io.netty:netty-bom:4.1.60.Final") + latestDepTestLibrary "io.vertx:vertx-web:3.+" + latestDepTestLibrary "io.vertx:vertx-web-client:3.+" +} + +test { + systemProperty "testLatestDeps", testLatestDeps +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/DefaultExceptionMapperInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/DefaultExceptionMapperInstrumentation.java new file mode 100644 index 000000000..ae944b434 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/DefaultExceptionMapperInstrumentation.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import java.lang.reflect.InvocationTargetException; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.wicket.WicketRuntimeException; + +public class DefaultExceptionMapperInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.wicket.DefaultExceptionMapper"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("mapUnexpectedExceptions").and(takesArgument(0, named(Exception.class.getName()))), + DefaultExceptionMapperInstrumentation.class.getName() + "$ExceptionAdvice"); + } + + @SuppressWarnings("unused") + public static class ExceptionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onExit(@Advice.Argument(0) Exception exception) { + Span serverSpan = ServerSpan.fromContextOrNull(Java8BytecodeBridge.currentContext()); + if (serverSpan != null) { + // unwrap exception + Throwable throwable = exception; + while (throwable.getCause() != null + && (throwable instanceof WicketRuntimeException + || throwable instanceof InvocationTargetException)) { + throwable = throwable.getCause(); + } + // as we don't create a span for wicket we record exception on server span + serverSpan.recordException(throwable); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java new file mode 100644 index 000000000..1582a4f90 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTROLLER; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming; +import io.opentelemetry.instrumentation.api.tracer.ServerSpan; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.wicket.core.request.handler.IPageClassRequestHandler; +import org.apache.wicket.request.IRequestHandler; + +public class RequestHandlerExecutorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.wicket.request.RequestHandlerExecutor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(takesArgument(0, named("org.apache.wicket.request.IRequestHandler"))), + RequestHandlerExecutorInstrumentation.class.getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onExit(@Advice.Argument(0) IRequestHandler handler) { + Context context = Java8BytecodeBridge.currentContext(); + Span serverSpan = ServerSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + if (handler instanceof IPageClassRequestHandler) { + ServerSpanNaming.updateServerSpanName( + context, + CONTROLLER, + new ServerSpanNameSupplier(context, (IPageClassRequestHandler) handler)); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/ServerSpanNameSupplier.java b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/ServerSpanNameSupplier.java new file mode 100644 index 000000000..4b9a5f3ad --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/ServerSpanNameSupplier.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import java.util.function.Supplier; +import org.apache.wicket.core.request.handler.IPageClassRequestHandler; +import org.apache.wicket.request.cycle.RequestCycle; + +public class ServerSpanNameSupplier implements Supplier { + + private final Context context; + private final IPageClassRequestHandler handler; + + public ServerSpanNameSupplier(Context context, IPageClassRequestHandler handler) { + this.context = context; + this.handler = handler; + } + + @Override + public String get() { + // using class name as page name + String pageName = handler.getPageClass().getName(); + // wicket filter mapping without wildcard, if wicket filter is mapped to /* + // this will be an empty string + String filterPath = RequestCycle.get().getRequest().getFilterPath(); + return ServletContextPath.prepend(context, filterPath + "/" + pageName); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketInstrumentationModule.java b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketInstrumentationModule.java new file mode 100644 index 000000000..d611f204e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketInstrumentationModule.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.hasClassesNamed; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Arrays; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class WicketInstrumentationModule extends InstrumentationModule { + + public WicketInstrumentationModule() { + super("wicket", "wicket-8.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // missing before 8.0 + return hasClassesNamed("org.apache.wicket.request.RequestHandlerExecutor"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new RequestHandlerExecutorInstrumentation(), new DefaultExceptionMapperInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy new file mode 100644 index 000000000..7f0be996d --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicServerSpan + +import hello.HelloApplication +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import javax.servlet.DispatcherType +import org.apache.wicket.protocol.http.WicketFilter +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.DefaultServlet +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.util.resource.FileResource +import org.jsoup.Jsoup + +class WicketTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + @Override + Server startServer(int port) { + def server = new Server(port) + ServletContextHandler context = new ServletContextHandler(0) + context.setContextPath(getContextPath()) + def resource = new FileResource(getClass().getResource("/")) + context.setBaseResource(resource) + server.setHandler(context) + + context.addServlet(DefaultServlet, "/") + def registration = context.getServletContext().addFilter("WicketApplication", WicketFilter) + registration.setInitParameter("applicationClassName", HelloApplication.getName()) + registration.setInitParameter("filterMappingUrlPattern", "/wicket-test/*") + registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/wicket-test/*") + + server.start() + + return server + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/jetty-context" + } + + def "test hello"() { + setup: + AggregatedHttpResponse response = client.get(address.resolve("wicket-test/").toString()).aggregate().join() + def doc = Jsoup.parse(response.contentUtf8()) + + expect: + response.status().code() == 200 + doc.selectFirst("#message").text() == "Hello World!" + + assertTraces(1) { + trace(0, 1) { + basicServerSpan(it, 0, getContextPath() + "/wicket-test/hello.HelloPage") + } + } + } + + def "test exception"() { + setup: + AggregatedHttpResponse response = client.get(address.resolve("wicket-test/exception").toString()).aggregate().join() + + expect: + response.status().code() == 500 + + assertTraces(1) { + trace(0, 1) { + basicServerSpan(it, 0, getContextPath() + "/wicket-test/hello.ExceptionPage", null, new Exception("test exception")) + } + } + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy new file mode 100644 index 000000000..2314d9804 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import org.apache.wicket.markup.html.WebPage + +class ExceptionPage extends WebPage { + ExceptionPage() { + throw new Exception("test exception") + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy new file mode 100644 index 000000000..6f7295027 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import org.apache.wicket.Page +import org.apache.wicket.RuntimeConfigurationType +import org.apache.wicket.protocol.http.WebApplication + +class HelloApplication extends WebApplication { + @Override + Class getHomePage() { + HelloPage + } + + @Override + protected void init() { + super.init() + + mountPage("/exception", ExceptionPage) + } + + @Override + RuntimeConfigurationType getConfigurationType() { + return RuntimeConfigurationType.DEPLOYMENT + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy new file mode 100644 index 000000000..80e0df316 --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import org.apache.wicket.markup.html.WebPage +import org.apache.wicket.markup.html.basic.Label + +class HelloPage extends WebPage { + HelloPage() { + add(new Label("message", "Hello World!")) + } +} diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html new file mode 100644 index 000000000..b4cd0e1be --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html @@ -0,0 +1,6 @@ + + + + Message goes here + + diff --git a/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/wicket-8.0-javaagent.gradle b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/wicket-8.0-javaagent.gradle new file mode 100644 index 000000000..72e92f21e --- /dev/null +++ b/opentelemetry-java-instrumentation/instrumentation/wicket-8.0/javaagent/wicket-8.0-javaagent.gradle @@ -0,0 +1,22 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'org.apache.wicket' + module = 'wicket' + versions = "[8.0.0,]" + assertInverse = true + } +} + +dependencies { + library "org.apache.wicket:wicket:8.0.0" + + testImplementation(project(':testing-common')) + testImplementation "org.jsoup:jsoup:1.13.1" + testImplementation "org.eclipse.jetty:jetty-server:8.0.0.v20110901" + testImplementation "org.eclipse.jetty:jetty-servlet:8.0.0.v20110901" + + testInstrumentation project(":instrumentation:servlet:servlet-3.0:javaagent") + testInstrumentation project(":instrumentation:servlet:servlet-javax-common:javaagent") +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/javaagent-api.gradle b/opentelemetry-java-instrumentation/javaagent-api/javaagent-api.gradle new file mode 100644 index 000000000..68990491e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/javaagent-api.gradle @@ -0,0 +1,19 @@ +group = 'io.opentelemetry.javaagent' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.jacoco-conventions" +apply plugin: "otel.publish-conventions" + +dependencies { + api "run.mone:opentelemetry-api" + compileOnly "run.mone:opentelemetry-sdk" + implementation "org.slf4j:slf4j-api" + implementation project(':instrumentation-api') + compileOnly "com.google.auto.value:auto-value-annotations" + annotationProcessor "com.google.auto.value:auto-value" + + testImplementation project(':testing-common') + testImplementation "org.mockito:mockito-core" + testImplementation "org.mockito:mockito-junit-jupiter" + testImplementation "org.assertj:assertj-core" +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/CallDepth.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/CallDepth.java new file mode 100644 index 000000000..999a87f76 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/CallDepth.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +public final class CallDepth { + private int depth; + + CallDepth() { + this.depth = 0; + } + + public int getAndIncrement() { + return this.depth++; + } + + public int decrementAndGet() { + return --this.depth; + } + + /** + * Get current call depth. This method may be used by vendor distributions to extend existing + * instrumentations. + */ + public int get() { + return depth; + } + + public void reset() { + depth = 0; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/CallDepthThreadLocalMap.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/CallDepthThreadLocalMap.java new file mode 100644 index 000000000..43d83adcc --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/CallDepthThreadLocalMap.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +/** + * Utility to track nested instrumentation. + * + *

For example, this can be used to track nested calls to super() in constructors by calling + * #incrementCallDepth at the beginning of each constructor. + * + *

This works the following way. When you enter some method that you want to track, you call + * {@link #incrementCallDepth} method. If returned number is larger than 0, then you have already + * been in this method and are in recursive call now. When you then leave the method, you call + * {@link #decrementCallDepth} method. If returned number is larger than 0, then you have already + * been in this method and are in recursive call now. + * + *

In short, the semantic of both methods is the same: they will return value 0 if and only if + * current method invocation is the first one for the current call stack. + */ +public final class CallDepthThreadLocalMap { + + private static final ClassValue TLS = + new ClassValue() { + @Override + protected ThreadLocalDepth computeValue(Class type) { + return new ThreadLocalDepth(); + } + }; + + public static CallDepth getCallDepth(Class k) { + return TLS.get(k).get(); + } + + public static int incrementCallDepth(Class k) { + return TLS.get(k).get().getAndIncrement(); + } + + public static int decrementCallDepth(Class k) { + return TLS.get(k).get().decrementAndGet(); + } + + public static void reset(Class k) { + TLS.get(k).get().reset(); + } + + private static final class ThreadLocalDepth extends ThreadLocal { + @Override + protected CallDepth initialValue() { + return new CallDepth(); + } + } + + private CallDepthThreadLocalMap() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/ClassHierarchyIterable.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/ClassHierarchyIterable.java new file mode 100644 index 000000000..6a8b1a981 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/ClassHierarchyIterable.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Iterates over a class, its superclass, and its interfaces in the following breath-first-like + * manner: + * + *

1. BaseClass + * + *

2. BaseClass's Interfaces + * + *

3. BaseClass's superclass + * + *

4. BaseClass's Interfaces' Interfaces + * + *

5. Superclass's Interfaces + * + *

6. Superclass's superclass + * + *

... + */ +public class ClassHierarchyIterable implements Iterable> { + private final Class baseClass; + + public ClassHierarchyIterable(Class baseClass) { + this.baseClass = baseClass; + } + + @Override + public Iterator> iterator() { + return new ClassIterator(); + } + + public class ClassIterator implements Iterator> { + @Nullable private Class next; + private final Set> queuedInterfaces = new HashSet<>(); + private final Queue> classesToExpand = new ArrayDeque<>(); + + public ClassIterator() { + classesToExpand.add(baseClass); + } + + @Override + public boolean hasNext() { + calculateNextIfNecessary(); + + return next != null; + } + + @Override + public Class next() { + calculateNextIfNecessary(); + + if (next == null) { + throw new NoSuchElementException(); + } + + Class next = this.next; + this.next = null; + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + private void calculateNextIfNecessary() { + if (next == null && !classesToExpand.isEmpty()) { + next = classesToExpand.remove(); + queueNewInterfaces(next.getInterfaces()); + + Class superClass = next.getSuperclass(); + if (superClass != null) { + classesToExpand.add(next.getSuperclass()); + } + } + } + + private void queueNewInterfaces(Class[] interfaces) { + for (Class clazz : interfaces) { + if (queuedInterfaces.add(clazz)) { + classesToExpand.add(clazz); + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/ContextStore.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/ContextStore.java new file mode 100644 index 000000000..efa26f5ca --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/ContextStore.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +/** + * Interface to represent context storage for instrumentations. + * + *

Context instances are weakly referenced and will be garbage collected when their corresponding + * key instance is collected. + * + * @param key type to do context lookups + * @param context type + */ +public interface ContextStore { + + /** + * Factory interface to create context instances. + * + * @param context type + */ + @FunctionalInterface + interface Factory { + + /** Returns a new context instance. */ + C create(); + } + + /** + * Get context given the key. + * + * @param key the key to lookup + * @return context object + */ + C get(K key); + + /** + * Put new context instance for given key. + * + * @param key key to use + * @param context context instance to save + */ + void put(K key, C context); + + /** + * Put new context instance if key is absent. + * + * @param key key to use + * @param context new context instance to put + * @return old instance if it was present, or new instance + */ + C putIfAbsent(K key, C context); + + /** + * Put new context instance if key is absent. Uses context factory to avoid creating objects if + * not needed. + * + * @param key key to use + * @param contextFactory factory instance to produce new context object + * @return old instance if it was present, or new instance + */ + C putIfAbsent(K key, Factory contextFactory); +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/InstrumentationContext.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/InstrumentationContext.java new file mode 100644 index 000000000..9f80bc065 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/InstrumentationContext.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +/** Instrumentation Context API. */ +public class InstrumentationContext { + private InstrumentationContext() {} + + /** + * Find a {@link ContextStore} instance for given key class and context class. + * + *

Conceptually this can be thought of as a map lookup to fetch a second level map given + * keyClass. + * + *

In reality, the calls to this method are re-written to something more performant + * while injecting advice into a method. + * + *

This method must only be called within an Advice class. + * + * @param keyClass The key class context is attached to. + * @param contextClass The context class attached to the user class. + * @param key class + * @param context class + * @return The instance of context store for given arguments. + */ + public static ContextStore get( + Class keyClass, Class contextClass) { + throw new IllegalStateException( + "Calls to this method will be rewritten by Instrumentation Context Provider (e.g. FieldBackedProvider)"); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/Java8BytecodeBridge.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/Java8BytecodeBridge.java new file mode 100644 index 000000000..52ad825f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/Java8BytecodeBridge.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; + +/** + * A helper for accessing methods that rely on new Java 8 bytecode features such as calling a static + * interface methods. In instrumentation, we may need to call these methods in code that is inlined + * into an instrumented class, however many times the instrumented class has been compiled to a + * previous version of bytecode and so we cannot inline calls to static interface methods, as those + * were not supported prior to Java 8 and will lead to a class verification error. + */ +public final class Java8BytecodeBridge { + + /** Calls {@link Context#current()}. */ + public static Context currentContext() { + return Context.current(); + } + + /** Calls {@link Context#root()}. */ + public static Context rootContext() { + return Context.root(); + } + + /** Calls {@link Span#current()}. */ + public static Span currentSpan() { + return Span.current(); + } + + /** Calls {@link Span#fromContext(Context)}. */ + public static Span spanFromContext(Context context) { + return Span.fromContext(context); + } + + private Java8BytecodeBridge() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/OpenTelemetrySdkAccess.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/OpenTelemetrySdkAccess.java new file mode 100644 index 000000000..2fc10fda8 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/OpenTelemetrySdkAccess.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +import java.util.concurrent.TimeUnit; + +/** + * A helper to facilitate accessing OpenTelemetry SDK methods from instrumentation. Because + * instrumentation runs in the app classloader, they do not have access to our SDK in the agent + * classloader. So we use this class in the bootstrap classloader to bridge between the two - the + * agent classloader will register implementations of needed SDK functions that can be called from + * instrumentation. + */ +public final class OpenTelemetrySdkAccess { + + /** + * Interface matching {@link io.opentelemetry.sdk.trace.SdkTracerProvider#forceFlush()} to allow + * holding a reference to it. + */ + public interface ForceFlusher { + /** Executes force flush. */ + void run(int timeout, TimeUnit unit); + } + + private static volatile ForceFlusher forceFlush; + + /** Forces flushing of pending spans. */ + public static void forceFlush(int timeout, TimeUnit unit) { + forceFlush.run(timeout, unit); + } + + /** + * Sets the {@link Runnable} to execute when instrumentation needs to force flush. This is called + * from the agent classloader to execute the SDK's force flush mechanism. Instrumentation must not + * call this. + */ + public static void internalSetForceFlush(ForceFlusher forceFlush) { + if (OpenTelemetrySdkAccess.forceFlush != null) { + // Only possible by misuse of this API, just ignore. + return; + } + OpenTelemetrySdkAccess.forceFlush = forceFlush; + } + + private OpenTelemetrySdkAccess() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/Pair.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/Pair.java new file mode 100644 index 000000000..3b8cfd4a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/Pair.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +public final class Pair { + + public static Pair of(T left, U right) { + return new Pair<>(left, right); + } + + private final T left; + private final U right; + + Pair(T left, U right) { + this.left = left; + this.right = right; + } + + public T getLeft() { + return left; + } + + public U getRight() { + return right; + } + + public boolean hasLeft() { + return null != left; + } + + public boolean hasRight() { + return null != right; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/AdviceUtils.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/AdviceUtils.java new file mode 100644 index 000000000..62f2d98fc --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/AdviceUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.concurrent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; + +/** Helper utils for Runnable/Callable instrumentation. */ +public final class AdviceUtils { + + /** + * Start scope for a given task. + * + * @param contextStore context storage for task's state + * @param task task to start scope for + * @param task's type + * @return scope if scope was started, or null + */ + public static Scope startTaskScope(ContextStore contextStore, T task) { + State state = contextStore.get(task); + if (state != null) { + Context parentContext = state.getAndResetParentContext(); + if (parentContext != null) { + return parentContext.makeCurrent(); + } + } + return null; + } + + private AdviceUtils() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/CallableWrapper.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/CallableWrapper.java new file mode 100644 index 000000000..ac933e37b --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/CallableWrapper.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.concurrent; + +import java.util.concurrent.Callable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is used to wrap lambda callables since currently we cannot instrument them + * + *

FIXME: We should remove this once https://github.com/raphw/byte-buddy/issues/558 is fixed + */ +public final class CallableWrapper implements Callable { + + private static final Logger log = LoggerFactory.getLogger(CallableWrapper.class); + + private final Callable callable; + + public CallableWrapper(Callable callable) { + this.callable = callable; + } + + @Override + public Object call() throws Exception { + return callable.call(); + } + + public static Callable wrapIfNeeded(Callable task) { + // We wrap only lambdas' anonymous classes and if given object has not already been wrapped. + // Anonymous classes have '/' in class name which is not allowed in 'normal' classes. + if (task.getClass().getName().contains("/") && !(task instanceof CallableWrapper)) { + log.debug("Wrapping callable task {}", task); + return new CallableWrapper(task); + } + return task; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/ExecutorInstrumentationUtils.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/ExecutorInstrumentationUtils.java new file mode 100644 index 000000000..5e1027f3c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/ExecutorInstrumentationUtils.java @@ -0,0 +1,190 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.concurrent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; + +/** Utils for concurrent instrumentations. */ +public final class ExecutorInstrumentationUtils { + private static final String AGENT_CLASSLOADER_NAME = + "io.opentelemetry.javaagent.bootstrap.AgentClassLoader"; + + private static final ClassValue INSTRUMENTED_RUNNABLE_CLASS = + new ClassValue() { + @Override + protected Boolean computeValue(Class taskClass) { + // ForkJoinPool threads are initialized lazily and continue to handle tasks similar to an + // event loop. They should not have context propagated to the base of the thread, tasks + // themselves will have it through other means. + if (taskClass.getName().equals("java.util.concurrent.ForkJoinWorkerThread")) { + return false; + } + + // ThreadPoolExecutor worker threads may be initialized lazily and manage interruption of + // other threads. The actual tasks being run on those threads will propagate context but + // we should not propagate onto this management thread. + if (taskClass.getName().equals("java.util.concurrent.ThreadPoolExecutor$Worker")) { + return false; + } + + // TODO Workaround for + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/787 + if (taskClass + .getName() + .equals("org.apache.tomcat.util.net.NioEndpoint$SocketProcessor")) { + return false; + } + + // ScheduledRunnable is a wrapper around a Runnable and doesn't itself need context. + if (taskClass.getName().equals("io.reactivex.internal.schedulers.ScheduledRunnable")) { + return false; + } + + // HttpConnection implements Runnable. When async request is completed HttpConnection + // may be sent to process next request while context from previous request hasn't been + // cleared yet. + if (taskClass.getName().equals("org.eclipse.jetty.server.HttpConnection")) { + return false; + } + + // This is a Mailbox created by akka.dispatch.Dispatcher#createMailbox. We must not add + // a context to it as context should only be carried by individual envelopes in the queue + // of this mailbox. + if (taskClass.getName().equals("akka.dispatch.Dispatcher$$anon$1")) { + return false; + } + + Class enclosingClass = taskClass.getEnclosingClass(); + if (enclosingClass != null) { + // Avoid context leak on jetty. Runnable submitted from SelectChannelEndPoint is used to + // process a new request which should not have context from them current request. + if (enclosingClass.getName().equals("org.eclipse.jetty.io.nio.SelectChannelEndPoint")) { + return false; + } + + // Don't instrument the executor's own runnables. These runnables may never return until + // netty shuts down. + if (enclosingClass + .getName() + .equals("io.netty.util.concurrent.SingleThreadEventExecutor")) { + return false; + } + + // OkHttp task runner is a lazily-initialized shared pool of continuosly running threads + // similar to an event loop. The submitted tasks themselves should already be + // instrumented to allow async propagation. + if (enclosingClass.getName().equals("okhttp3.internal.concurrent.TaskRunner")) { + return false; + } + + // OkHttp connection pool lazily initializes a long running task to detect expired + // connections and should not itself be instrumented. + if (enclosingClass.getName().equals("com.squareup.okhttp.ConnectionPool")) { + return false; + } + + // Avoid instrumenting internal OrderedExecutor worker class + if (enclosingClass + .getName() + .equals("org.hornetq.utils.OrderedExecutorFactory$OrderedExecutor")) { + return false; + } + + // Avoid instrumenting internal rabbit consumer task + if (enclosingClass + .getName() + .equals( + "org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer")) { + return false; + } + } + + // Don't trace runnables from libraries that are packaged inside the agent. + // Although GlobalClassloaderIgnoresMatcher excludes these classes from instrumentation + // their instances can still be passed to executors which we have instrumented so we need + // to exclude them here too. + ClassLoader taskClassLoader = taskClass.getClassLoader(); + if (taskClassLoader != null + && AGENT_CLASSLOADER_NAME.equals(taskClassLoader.getClass().getName())) { + return false; + } + + if (taskClass.getName().startsWith("ratpack.exec.internal.")) { + // Context is passed through Netty channels in Ratpack as executor instrumentation is + // not suitable. As the context that would be propagated via executor would be + // incorrect, skip the propagation. Not checking for concrete class names as this covers + // anonymous classes from ratpack.exec.internal.DefaultExecution and + // ratpack.exec.internal.DefaultExecController. + return false; + } + + return true; + } + }; + + /** + * Checks if given task should get state attached. + * + * @param task task object + * @return true iff given task object should be wrapped + */ + public static boolean shouldAttachStateToTask(Object task) { + if (task == null) { + return false; + } + + if (Context.current() == Context.root()) { + // not much point in propagating root context + // plus it causes failures under otel.javaagent.testing.fail-on-context-leak=true + return false; + } + + return INSTRUMENTED_RUNNABLE_CLASS.get(task.getClass()); + } + + /** + * Create task state given current scope. + * + * @param task class type + * @param contextStore context storage + * @param task task instance + * @param context current context + * @return new state + */ + public static State setupState(ContextStore contextStore, T task, Context context) { + State state = contextStore.putIfAbsent(task, State.FACTORY); + if (ContextPropagationDebug.isThreadPropagationDebuggerEnabled()) { + context = + ContextPropagationDebug.appendLocations(context, new Exception().getStackTrace(), task); + } + state.setParentContext(context); + return state; + } + + /** + * Clean up after job submission method has exited. + * + * @param state task instrumentation state + * @param throwable throwable that may have been thrown + */ + public static void cleanUpOnMethodExit(State state, Throwable throwable) { + if (null != state && null != throwable) { + /* + Note: this may potentially clear somebody else's parent span if we didn't set it + up in setupState because it was already present before us. This should be safe but + may lead to non-attributed async work in some very rare cases. + Alternative is to not clear parent span here if we did not set it up in setupState + but this may potentially lead to memory leaks if callers do not properly handle + exceptions. + */ + state.clearParentContext(); + } + } + + private ExecutorInstrumentationUtils() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/RunnableWrapper.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/RunnableWrapper.java new file mode 100644 index 000000000..633e639e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/RunnableWrapper.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.concurrent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is used to wrap lambda runnables since currently we cannot instrument them + * + *

FIXME: We should remove this once https://github.com/raphw/byte-buddy/issues/558 is fixed + */ +public final class RunnableWrapper implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(RunnableWrapper.class); + + private final Runnable runnable; + + public RunnableWrapper(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void run() { + runnable.run(); + } + + public static Runnable wrapIfNeeded(Runnable task) { + // We wrap only lambdas' anonymous classes and if given object has not already been wrapped. + // Anonymous classes have '/' in class name which is not allowed in 'normal' classes. + if (task.getClass().getName().contains("/") && !(task instanceof RunnableWrapper)) { + log.debug("Wrapping runnable task {}", task); + return new RunnableWrapper(task); + } + return task; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/State.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/State.java new file mode 100644 index 000000000..2a7a42d6d --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/concurrent/State.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.concurrent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class State { + + private static final Logger log = LoggerFactory.getLogger(State.class); + + private static final AtomicReferenceFieldUpdater parentContextUpdater = + AtomicReferenceFieldUpdater.newUpdater(State.class, Context.class, "parentContext"); + + public static final ContextStore.Factory FACTORY = State::new; + + // Used by AtomicReferenceFieldUpdater + @SuppressWarnings("UnusedVariable") + private volatile Context parentContext; + + private State() {} + + public void setParentContext(Context parentContext) { + boolean result = parentContextUpdater.compareAndSet(this, null, parentContext); + if (!result) { + Context currentParent = parentContextUpdater.get(this); + if (currentParent != parentContext) { + if (log.isDebugEnabled()) { + log.debug( + "Failed to set parent context because another parent context is " + + "already set {}: new: {}, old: {}", + this, + parentContext, + currentParent); + } + } + } + } + + public void clearParentContext() { + parentContextUpdater.set(this, null); + } + + public Context getAndResetParentContext() { + return parentContextUpdater.getAndSet(this, null); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/PeerServiceAttributesExtractor.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/PeerServiceAttributesExtractor.java new file mode 100644 index 000000000..03ff6a6ea --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/PeerServiceAttributesExtractor.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.instrumenter; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Extractor of the {@code peer.service} span attribute, described in the + * specification. + * + *

Peer service name mappings can be configured using the {@code + * otel.instrumentation.common.peer-service-mapping} configuration property. The format used is a + * comma-separated list of {@code host=name} pairs. + */ +public final class PeerServiceAttributesExtractor + extends AttributesExtractor { + private static final Map JAVAAGENT_PEER_SERVICE_MAPPING = + Config.get().getMap("otel.instrumentation.common.peer-service-mapping"); + + private final Map peerServiceMapping; + private final NetAttributesExtractor netAttributesExtractor; + + // visible for tests + PeerServiceAttributesExtractor( + Map peerServiceMapping, + NetAttributesExtractor netAttributesExtractor) { + this.peerServiceMapping = peerServiceMapping; + this.netAttributesExtractor = netAttributesExtractor; + } + + /** + * Returns a new {@link PeerServiceAttributesExtractor} that will use the passed {@code + * netAttributesExtractor} instance to determine the value of the {@code peer.service} attribute. + */ + public static PeerServiceAttributesExtractor create( + NetAttributesExtractor netAttributesExtractor) { + return new PeerServiceAttributesExtractor<>( + JAVAAGENT_PEER_SERVICE_MAPPING, netAttributesExtractor); + } + + /** + * Returns a new {@link PeerServiceAttributesExtractor} that will create a new instance of type + * {@code netAttributesExtractorImplClassName} (which must extend {@link NetAttributesExtractor}) + * and use it to determine the value of the {@code peer.service} attribute. + */ + @Nullable + public static + PeerServiceAttributesExtractor createUsingReflection( + String netAttributesExtractorImplClassName) { + return ReflectionPeerServiceAttributesExtractorFactory.create( + netAttributesExtractorImplClassName); + } + + @Override + protected void onStart(AttributesBuilder attributes, REQUEST request) { + onEnd(attributes, request, null); + } + + @Override + protected void onEnd(AttributesBuilder attributes, REQUEST request, RESPONSE response) { + String peerName = netAttributesExtractor.peerName(request, response); + String peerService = mapToPeerService(peerName); + if (peerService == null) { + String peerIp = netAttributesExtractor.peerIp(request, response); + peerService = mapToPeerService(peerIp); + } + if (peerService != null) { + attributes.put(SemanticAttributes.PEER_SERVICE, peerService); + } + } + + private String mapToPeerService(String endpoint) { + if (endpoint == null) { + return null; + } + return peerServiceMapping.get(endpoint); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/ReflectionPeerServiceAttributesExtractorFactory.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/ReflectionPeerServiceAttributesExtractorFactory.java new file mode 100644 index 000000000..5bdcf2e19 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/ReflectionPeerServiceAttributesExtractorFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.instrumenter; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ReflectionPeerServiceAttributesExtractorFactory { + private static final Logger log = + LoggerFactory.getLogger(ReflectionPeerServiceAttributesExtractorFactory.class); + + @Nullable + static PeerServiceAttributesExtractor create( + String netAttributesImplClassName) { + Constructor> constructor = null; + try { + Class> netAttributesExtractorClass = + (Class>) + Class.forName(netAttributesImplClassName); + constructor = netAttributesExtractorClass.getDeclaredConstructor(); + constructor.setAccessible(true); + NetAttributesExtractor netAttributesExtractor = constructor.newInstance(); + return PeerServiceAttributesExtractor.create(netAttributesExtractor); + } catch (ClassNotFoundException + | NoSuchMethodException + | IllegalAccessException + | InstantiationException + | InvocationTargetException e) { + log.warn( + "Could not add PeerServiceAttributesExtractor wrapping {}", + netAttributesImplClassName, + e); + return null; + } finally { + if (constructor != null) { + constructor.setAccessible(false); + } + } + } + + private ReflectionPeerServiceAttributesExtractorFactory() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/internal/BootstrapPackagePrefixesHolder.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/internal/BootstrapPackagePrefixesHolder.java new file mode 100644 index 000000000..f5e3d62af --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/internal/BootstrapPackagePrefixesHolder.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.internal; + +import java.util.Collections; +import java.util.List; + +/** + * {@link BootstrapPackagePrefixesHolder} is an utility class that holds package prefixes. The + * classes from these packages are pushed to the bootstrap classloader. + * + *

The prefixes are loaded by {@code AgentInstaller} and consumed by classloader instrumentation. + * The instrumentation does not have access to the installer, therefore this utility class is used + * to share package prefixes. + */ +public final class BootstrapPackagePrefixesHolder { + + private static volatile List bootstrapPackagePrefixes; + + public static List getBoostrapPackagePrefixes() { + return bootstrapPackagePrefixes; + } + + public static void setBoostrapPackagePrefixes(List prefixes) { + if (bootstrapPackagePrefixes != null) { + // Only possible by misuse of this API, just ignore. + return; + } + bootstrapPackagePrefixes = Collections.unmodifiableList(prefixes); + } + + private BootstrapPackagePrefixesHolder() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/internal/InClassLoaderMatcher.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/internal/InClassLoaderMatcher.java new file mode 100644 index 000000000..2673b996e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/internal/InClassLoaderMatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.internal; + +public final class InClassLoaderMatcher { + + private static final ThreadLocal inClassLoaderMatcher = + ThreadLocal.withInitial(MutableBoolean::new); + + private InClassLoaderMatcher() {} + + /** + * Returns whether the ClassLoaderMatcher is currently executing. + * + *

This is used (at least) by the {@code internal-eclipse-osgi} instrumentation in order to + * suppress a side effect in the Eclipse OSGi class loader that occurs when ClassLoaderMatcher + * calls ClassLoader.getResource(). See {@code EclipseOsgiInstrumentationModule} for more details. + */ + public static boolean get() { + return inClassLoaderMatcher.get().value; + } + + /** + * WARNING This should not be used by instrumentation. It should only be used by + * io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher. + * + *

The reason it can't be (easily) hidden is that this class needs to live in the bootstrap + * class loader to be accessible to instrumentation, while the ClassLoaderMatcher lives in the + * agent class loader. + */ + public static boolean getAndSet(boolean value) { + return inClassLoaderMatcher.get().getAndSet(value); + } + + /** + * WARNING This should not be used by instrumentation. It should only be used by + * io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher. + * + *

The reason it can't be (easily) hidden is that this class needs to live in the bootstrap + * class loader to be accessible to instrumentation, while the ClassLoaderMatcher lives in the + * agent class loader. + */ + public static void set(boolean value) { + inClassLoaderMatcher.get().value = value; + } + + // using MutableBoolean to avoid an extra thread local lookup for getAndSet() + private static class MutableBoolean { + + private boolean value; + + private boolean getAndSet(boolean value) { + boolean oldValue = this.value; + this.value = value; + return oldValue; + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/jaxrs/JaxrsContextPath.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/jaxrs/JaxrsContextPath.java new file mode 100644 index 000000000..be84fbfd4 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/jaxrs/JaxrsContextPath.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.jaxrs; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Helper container for storing context path for jax-rs requests. Jax-rs context path is the path + * where jax-rs servlet is mapped or the value of ApplicationPath annotation. Span name is built by + * combining servlet context path from {@link + * io.opentelemetry.instrumentation.api.servlet.ServletContextPath} jax-rs context path and the Path + * annotation from called method or class. + */ +public final class JaxrsContextPath { + private static final ContextKey CONTEXT_KEY = + ContextKey.named("opentelemetry-jaxrs-context-path-key"); + + private JaxrsContextPath() {} + + public static @Nullable Context init(Context context, String path) { + if (path == null || path.isEmpty() || "/".equals(path)) { + return null; + } + // normalize path to have a leading slash and no trailing slash + if (!path.startsWith("/")) { + path = "/" + path; + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return context.with(CONTEXT_KEY, path); + } + + public static String prepend(Context context, String spanName) { + String value = context.get(CONTEXT_KEY); + // checking isEmpty just to avoid unnecessary string concat / allocation + if (value != null && !value.isEmpty()) { + return value + spanName; + } else { + return spanName; + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/rmi/ThreadLocalContext.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/rmi/ThreadLocalContext.java new file mode 100644 index 000000000..2d6e0babd --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/rmi/ThreadLocalContext.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.rmi; + +import io.opentelemetry.context.Context; + +public class ThreadLocalContext { + public static final ThreadLocalContext THREAD_LOCAL_CONTEXT = new ThreadLocalContext(); + private final ThreadLocal local; + + public ThreadLocalContext() { + local = new ThreadLocal<>(); + } + + public void set(Context context) { + local.set(context); + } + + public Context getAndResetContext() { + Context context = local.get(); + if (context != null) { + local.remove(); + } + return context; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/undertow/KeyHolder.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/undertow/KeyHolder.java new file mode 100644 index 000000000..e6f28d6ee --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/undertow/KeyHolder.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.undertow; + +import java.util.IdentityHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Undertow's {@code io.undertow.server.HttpServerExchange} uses {@code + * io.undertow.util.AttachmentKey} as a key for storing arbitrary data. It uses {@link + * IdentityHashMap} and thus all keys are compared for equality by object reference. This means that + * we cannot hold an instance of {@code io.undertow.util.AttachmentKey} in a static field of the + * corresponding Tracer, as we usually do. Tracers are loaded into user's classloaders and thus it + * is totally possible to have several instances of tracers. Each of those instances will have a + * separate value in a static field and {@code io.undertow.server.HttpServerExchange} will treat + * them as different keys then. + * + *

That is why this class exists and resides in a separate package. This package is treated in a + * special way and is always loaded by bootstrap classloader. This makes sure that this class is + * available to all tracers from every classloader. + * + *

But at the same time, being loaded by bootstrap classloader, this class itself cannot initiate + * the loading of {@code io.undertow.util.AttachmentKey} class. Class has to be loaded by any + * classloader that has it, e.g. by the classloader of a Tracer that uses this key holder. After + * that, all Tracers, loaded by all classloaders, will be able to use exactly the same sole + * instance of the key. + */ +// TODO allow instrumentation to have their own classes that should go to bootstrap classloader +public final class KeyHolder { + public static final ConcurrentMap, Object> contextKeys = new ConcurrentHashMap<>(); + + private KeyHolder() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/undertow/UndertowActiveHandlers.java b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/undertow/UndertowActiveHandlers.java new file mode 100644 index 000000000..6e6fc85cf --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/main/java/io/opentelemetry/javaagent/instrumentation/api/undertow/UndertowActiveHandlers.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.undertow; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import java.util.concurrent.atomic.AtomicInteger; + +/** Helper container for keeping track of request processing state in undertow. */ +public final class UndertowActiveHandlers { + private static final ContextKey CONTEXT_KEY = + ContextKey.named("opentelemetry-undertow-active-handlers"); + + private UndertowActiveHandlers() {} + + /** + * Attach to context. + * + * @param context server context + * @param initialValue initial value for counter + * @return new context + */ + public static Context init(Context context, int initialValue) { + return context.with(CONTEXT_KEY, new AtomicInteger(initialValue)); + } + + /** + * Increment counter. + * + * @param context server context + */ + public static void increment(Context context) { + context.get(CONTEXT_KEY).incrementAndGet(); + } + + /** + * Decrement counter. + * + * @param context server context + * @return value of counter after decrementing it + */ + public static int decrementAndGet(Context context) { + return context.get(CONTEXT_KEY).decrementAndGet(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/test/java/io/opentelemetry/javaagent/instrumentation/api/CallDepthThreadLocalMapTest.java b/opentelemetry-java-instrumentation/javaagent-api/src/test/java/io/opentelemetry/javaagent/instrumentation/api/CallDepthThreadLocalMapTest.java new file mode 100644 index 000000000..9b255d714 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/test/java/io/opentelemetry/javaagent/instrumentation/api/CallDepthThreadLocalMapTest.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class CallDepthThreadLocalMapTest { + + @Test + void incrementDecrement() { + assertThat(CallDepthThreadLocalMap.incrementCallDepth(String.class)).isZero(); + assertThat(CallDepthThreadLocalMap.incrementCallDepth(Integer.class)).isZero(); + + assertThat(CallDepthThreadLocalMap.incrementCallDepth(String.class)).isOne(); + assertThat(CallDepthThreadLocalMap.incrementCallDepth(Integer.class)).isOne(); + + CallDepthThreadLocalMap.reset(String.class); + assertThat(CallDepthThreadLocalMap.incrementCallDepth(Integer.class)).isEqualTo(2); + + CallDepthThreadLocalMap.reset(Integer.class); + + assertThat(CallDepthThreadLocalMap.incrementCallDepth(String.class)).isZero(); + assertThat(CallDepthThreadLocalMap.incrementCallDepth(Integer.class)).isZero(); + + assertThat(CallDepthThreadLocalMap.incrementCallDepth(String.class)).isOne(); + assertThat(CallDepthThreadLocalMap.incrementCallDepth(Integer.class)).isOne(); + + assertThat(CallDepthThreadLocalMap.decrementCallDepth(String.class)).isOne(); + assertThat(CallDepthThreadLocalMap.decrementCallDepth(Integer.class)).isOne(); + + assertThat(CallDepthThreadLocalMap.decrementCallDepth(String.class)).isZero(); + assertThat(CallDepthThreadLocalMap.decrementCallDepth(Integer.class)).isZero(); + + assertThat(CallDepthThreadLocalMap.incrementCallDepth(Double.class)).isZero(); + assertThat(CallDepthThreadLocalMap.decrementCallDepth(Double.class)).isZero(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-api/src/test/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/PeerServiceAttributesExtractorTest.java b/opentelemetry-java-instrumentation/javaagent-api/src/test/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/PeerServiceAttributesExtractorTest.java new file mode 100644 index 000000000..300f9a7e2 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-api/src/test/java/io/opentelemetry/javaagent/instrumentation/api/instrumenter/PeerServiceAttributesExtractorTest.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.api.instrumenter; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PeerServiceAttributesExtractorTest { + @Mock NetAttributesExtractor netAttributesExtractor; + + @Test + void shouldNotSetAnyValueIfNetExtractorReturnsNulls() { + // given + Map peerServiceMapping = singletonMap("1.2.3.4", "myService"); + + PeerServiceAttributesExtractor underTest = + new PeerServiceAttributesExtractor<>(peerServiceMapping, netAttributesExtractor); + + // when + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, "request"); + underTest.onEnd(attributes, "request", "response"); + + // then + assertTrue(attributes.build().isEmpty()); + } + + @Test + void shouldNotSetAnyValueIfPeerNameDoesNotMatch() { + // given + Map peerServiceMapping = singletonMap("example.com", "myService"); + + PeerServiceAttributesExtractor underTest = + new PeerServiceAttributesExtractor<>(peerServiceMapping, netAttributesExtractor); + + given(netAttributesExtractor.peerName(any(), any())).willReturn("example2.com"); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, "request"); + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, "request", "response"); + + // then + assertTrue(startAttributes.build().isEmpty()); + assertTrue(endAttributes.build().isEmpty()); + } + + @Test + void shouldNotSetAnyValueIfPeerIpDoesNotMatch() { + // given + Map peerServiceMapping = singletonMap("1.2.3.4", "myService"); + + PeerServiceAttributesExtractor underTest = + new PeerServiceAttributesExtractor<>(peerServiceMapping, netAttributesExtractor); + + given(netAttributesExtractor.peerIp(any(), any())).willReturn("1.2.3.5"); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, "request"); + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, "request", "response"); + + // then + assertTrue(startAttributes.build().isEmpty()); + assertTrue(endAttributes.build().isEmpty()); + } + + @Test + void shouldSetPeerNameIfItMatches() { + // given + Map peerServiceMapping = new HashMap<>(); + peerServiceMapping.put("example.com", "myService"); + peerServiceMapping.put("1.2.3.4", "someOtherService"); + + PeerServiceAttributesExtractor underTest = + new PeerServiceAttributesExtractor<>(peerServiceMapping, netAttributesExtractor); + + given(netAttributesExtractor.peerName(any(), any())).willReturn("example.com"); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, "request"); + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, "request", "response"); + + // then + assertThat(startAttributes.build()) + .containsOnly(entry(SemanticAttributes.PEER_SERVICE, "myService")); + assertThat(endAttributes.build()) + .containsOnly(entry(SemanticAttributes.PEER_SERVICE, "myService")); + } + + @Test + void shouldSetPeerIpIfItMatchesAndNameDoesNot() { + // given + Map peerServiceMapping = new HashMap<>(); + peerServiceMapping.put("example.com", "myService"); + peerServiceMapping.put("1.2.3.4", "someOtherService"); + + PeerServiceAttributesExtractor underTest = + new PeerServiceAttributesExtractor<>(peerServiceMapping, netAttributesExtractor); + + given(netAttributesExtractor.peerName(any(), any())).willReturn("test.com"); + given(netAttributesExtractor.peerIp(any(), any())).willReturn("1.2.3.4"); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, "request"); + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, "request", "response"); + + // then + assertThat(startAttributes.build()) + .containsOnly(entry(SemanticAttributes.PEER_SERVICE, "someOtherService")); + assertThat(endAttributes.build()) + .containsOnly(entry(SemanticAttributes.PEER_SERVICE, "someOtherService")); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap-tests/javaagent-bootstrap-tests.gradle b/opentelemetry-java-instrumentation/javaagent-bootstrap-tests/javaagent-bootstrap-tests.gradle new file mode 100644 index 000000000..843da4c6c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap-tests/javaagent-bootstrap-tests.gradle @@ -0,0 +1,10 @@ +apply plugin: "otel.java-conventions" + +dependencies { + // For testing javaagent-bootstrap's Caffeine patch, we need to compile against our cache API + // but make sure to run against javaagent-bootstrap + testCompileOnly project(':instrumentation-api-caching') + testRuntimeOnly project(":javaagent-bootstrap") + + testImplementation "org.assertj:assertj-core" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap-tests/src/test/java/io/opentelemetry/instrumentation/api/caching/PatchCaffeineTest.java b/opentelemetry-java-instrumentation/javaagent-bootstrap-tests/src/test/java/io/opentelemetry/instrumentation/api/caching/PatchCaffeineTest.java new file mode 100644 index 000000000..ebadddb0f --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap-tests/src/test/java/io/opentelemetry/instrumentation/api/caching/PatchCaffeineTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.caching; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +class PatchCaffeineTest { + + @Test + void cleanupNotForkJoinTask() { + AtomicReference errorRef = new AtomicReference<>(); + Cache cache = + Cache.newBuilder() + .setExecutor( + task -> { + try { + assertThat(task).isNotInstanceOf(ForkJoinTask.class); + } catch (AssertionError e) { + errorRef.set(e); + } + }) + .setMaximumSize(1) + .build(); + assertThat(cache.computeIfAbsent("cat", unused -> "meow")).isEqualTo("meow"); + assertThat(cache.computeIfAbsent("dog", unused -> "bark")).isEqualTo("bark"); + AssertionError error = errorRef.get(); + if (error != null) { + throw error; + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/javaagent-bootstrap.gradle b/opentelemetry-java-instrumentation/javaagent-bootstrap/javaagent-bootstrap.gradle new file mode 100644 index 000000000..dec552c15 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/javaagent-bootstrap.gradle @@ -0,0 +1,41 @@ +group = 'io.opentelemetry.javaagent' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +// patch inner class from Caffeine to avoid ForkJoinTask from being loaded too early +sourceSets { + patch { + java {} + } +} +jar { + from(sourceSets.patch.output) { + include 'io/opentelemetry/instrumentation/api/internal/shaded/caffeine/cache/BoundedLocalCache$PerformCleanupTask.class' + } +} + +configurations { + // classpath used by the instrumentation muzzle plugin + instrumentationMuzzle { + canBeConsumed = true + canBeResolved = false + extendsFrom implementation + } +} + +dependencies { + api "run.mone:opentelemetry-api" + api "run.mone:opentelemetry-api-metrics" + compileOnly "run.mone:opentelemetry-api" + implementation "org.slf4j:slf4j-api" + implementation "org.slf4j:slf4j-simple" + // ^ Generally a bad idea for libraries, but we're shadowing. + + implementation project(':javaagent-api') + implementation project(':instrumentation-api') + + testImplementation project(':testing-common') + testImplementation "org.mockito:mockito-core" + testImplementation "org.assertj:assertj-core" +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/OpenTelemetryAgent.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/OpenTelemetryAgent.java new file mode 100644 index 000000000..c986ad305 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/OpenTelemetryAgent.java @@ -0,0 +1,207 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent; + +import io.opentelemetry.javaagent.bootstrap.AgentInitializer; +import java.io.File; +import java.io.IOException; +import java.lang.instrument.Instrumentation; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Field; +import java.net.URISyntaxException; +import java.security.CodeSource; +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Premain-Class for the OpenTelemetry Java agent. + * + *

The bootstrap process of the agent is somewhat complicated and care has to be taken to make + * sure things do not get broken by accident. + * + *

JVM loads this class onto app's classloader, afterwards agent needs to inject its classes onto + * bootstrap classpath. This leads to this class being visible on bootstrap. This in turn means that + * this class may be loaded again on bootstrap by accident if we ever reference it after bootstrap + * has been setup. + * + *

In order to avoid this we need to make sure we do a few things: + * + *

    + *
  • Do as little as possible here + *
  • Never reference this class after we have setup bootstrap and jumped over to 'real' agent + * code + *
  • Do not store any static data in this class + *
  • Do dot touch any logging facilities here so we can configure them later + *
+ */ +// Too early for logging +@SuppressWarnings("SystemOut") +public final class OpenTelemetryAgent { + private static final Class thisClass = OpenTelemetryAgent.class; + + public static void premain(String agentArgs, Instrumentation inst) { + agentmain(agentArgs, inst); + } + + public static void agentmain(String agentArgs, Instrumentation inst) { + try { + File javaagentFile = installBootstrapJar(inst); + AgentInitializer.initialize(inst, javaagentFile); + } catch (Throwable ex) { + // Don't rethrow. We don't have a log manager here, so just print. + System.err.println("ERROR " + thisClass.getName()); + ex.printStackTrace(); + } + } + + private static synchronized File installBootstrapJar(Instrumentation inst) + throws IOException, URISyntaxException { + + // First try Code Source + CodeSource codeSource = thisClass.getProtectionDomain().getCodeSource(); + + if (codeSource != null) { + File javaagentFile = new File(codeSource.getLocation().toURI()); + + if (javaagentFile.isFile()) { + // passing verify false for vendors who sign the agent jar, because jar file signature + // verification is very slow before the JIT compiler starts up, which on Java 8 is not until + // after premain executes + JarFile agentJar = new JarFile(javaagentFile, false); + verifyJarManifestMainClassIsThis(javaagentFile, agentJar); + inst.appendToBootstrapClassLoaderSearch(agentJar); + return javaagentFile; + } + } + + System.out.println("Could not get bootstrap jar from code source, using -javaagent arg"); + + // ManagementFactory indirectly references java.util.logging.LogManager + // - On Oracle-based JDKs after 1.8 + // - On IBM-based JDKs since at least 1.7 + // This prevents custom log managers from working correctly + // Use reflection to bypass the loading of the class + List arguments = getVmArgumentsThroughReflection(); + + String agentArgument = null; + for (String arg : arguments) { + if (arg.startsWith("-javaagent")) { + if (agentArgument == null) { + agentArgument = arg; + } else { + throw new IllegalStateException( + "Multiple javaagents specified and code source unavailable, " + + "not installing tracing agent"); + } + } + } + + if (agentArgument == null) { + throw new IllegalStateException( + "Could not find javaagent parameter and code source unavailable, " + + "not installing tracing agent"); + } + + // argument is of the form -javaagent:/path/to/java-agent.jar=optionalargumentstring + Matcher matcher = Pattern.compile("-javaagent:([^=]+).*").matcher(agentArgument); + + if (!matcher.matches()) { + throw new IllegalStateException("Unable to parse javaagent parameter: " + agentArgument); + } + + File javaagentFile = new File(matcher.group(1)); + if (!javaagentFile.isFile()) { + throw new IllegalStateException("Unable to find javaagent file: " + javaagentFile); + } + + JarFile agentJar = new JarFile(javaagentFile, false); + verifyJarManifestMainClassIsThis(javaagentFile, agentJar); + inst.appendToBootstrapClassLoaderSearch(agentJar); + return javaagentFile; + } + + private static List getVmArgumentsThroughReflection() { + ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + try { + // Try Oracle-based + Class managementFactoryHelperClass = + classLoader.loadClass("sun.management.ManagementFactoryHelper"); + + Class vmManagementClass = classLoader.loadClass("sun.management.VMManagement"); + + Object vmManagement; + + try { + vmManagement = + managementFactoryHelperClass.getDeclaredMethod("getVMManagement").invoke(null); + } catch (NoSuchMethodException e) { + // Older vm before getVMManagement() existed + Field field = managementFactoryHelperClass.getDeclaredField("jvm"); + field.setAccessible(true); + vmManagement = field.get(null); + field.setAccessible(false); + } + + return (List) vmManagementClass.getMethod("getVmArguments").invoke(vmManagement); + + } catch (ReflectiveOperationException e) { + try { // Try IBM-based. + Class vmClass = classLoader.loadClass("com.ibm.oti.vm.VM"); + String[] argArray = (String[]) vmClass.getMethod("getVMArgs").invoke(null); + return Arrays.asList(argArray); + } catch (ReflectiveOperationException e1) { + // Fallback to default + System.out.println( + "WARNING: Unable to get VM args through reflection. " + + "A custom java.util.logging.LogManager may not work correctly"); + + return ManagementFactory.getRuntimeMXBean().getInputArguments(); + } + } + } + + // this protects against the case where someone adds the contents of opentelemetry-javaagent.jar + // by mistake to their application's "uber.jar" + // + // the reason this can cause issues is because we locate the agent jar based on the CodeSource of + // the OpenTelemetryAgent class, and then we add that jar file to the bootstrap class path + // + // but if we find the OpenTelemetryAgent class in an uber jar file, and we add that (whole) uber + // jar file to the bootstrap class loader, that can cause some applications to break, as there's a + // lot of application and library code that doesn't handle getClassLoader() returning null + // (e.g. https://github.com/qos-ch/logback/pull/291) + private static void verifyJarManifestMainClassIsThis(File jarFile, JarFile agentJar) + throws IOException { + Manifest manifest = agentJar.getManifest(); + if (manifest.getMainAttributes().getValue("Premain-Class") == null) { + throw new IllegalStateException( + "The agent was not installed, because the agent was found in '" + + jarFile + + "', which doesn't contain a Premain-Class manifest attribute. Make sure that you" + + " haven't included the agent jar file inside of an application uber jar."); + } + } + + /** + * Main entry point. + * + * @param args command line arguments + */ + public static void main(String... args) { + try { + System.out.println(OpenTelemetryAgent.class.getPackage().getImplementationVersion()); + } catch (RuntimeException e) { + System.out.println("Failed to parse agent version"); + e.printStackTrace(); + } + } + + private OpenTelemetryAgent() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentClassLoader.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentClassLoader.java new file mode 100644 index 000000000..9de2c697b --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentClassLoader.java @@ -0,0 +1,432 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.security.CodeSource; +import java.security.Permission; +import java.security.cert.Certificate; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Classloader used to run the core agent. + * + *

It is built around the concept of a jar inside another jar. This classloader loads the files + * of the internal jar to load classes and resources. + */ +public class AgentClassLoader extends URLClassLoader { + + // NOTE it's important not to use slf4j in this class, because this class is used before slf4j is + // configured, and so using slf4j here would initialize slf4j-simple before we have a chance to + // configure the logging levels + + static { + ClassLoader.registerAsParallelCapable(); + } + + private static final String AGENT_INITIALIZER_JAR = + System.getProperty("otel.javaagent.experimental.initializer.jar", ""); + + private static final String META_INF = "META-INF/"; + private static final String META_INF_MANIFEST_MF = META_INF + "MANIFEST.MF"; + private static final String META_INF_VERSIONS = META_INF + "versions/"; + + // multi release jars were added in java 9 + private static final int MIN_MULTI_RELEASE_JAR_JAVA_VERSION = 9; + // current java version + private static final int JAVA_VERSION = getJavaVersion(); + private static final boolean MULTI_RELEASE_JAR_ENABLE = + JAVA_VERSION >= MIN_MULTI_RELEASE_JAR_JAVA_VERSION; + + // Calling java.lang.instrument.Instrumentation#appendToBootstrapClassLoaderSearch + // adds a jar to the bootstrap class lookup, but not to the resource lookup. + // As a workaround, we keep a reference to the bootstrap jar + // to use only for resource lookups. + private final BootstrapClassLoaderProxy bootstrapProxy; + + private final JarFile jarFile; + private final URL jarBase; + private final String jarEntryPrefix; + private final CodeSource codeSource; + private final Manifest manifest; + + /** + * Construct a new AgentClassLoader. + * + * @param javaagentFile Used for resource lookups. + * @param internalJarFileName File name of the internal jar + * @param parent Classloader parent. Should null (bootstrap), or the platform classloader for java + */ + public AgentClassLoader(File javaagentFile, String internalJarFileName, ClassLoader parent) { + super(new URL[] {}, parent); + if (javaagentFile == null) { + throw new IllegalArgumentException("Agent jar location should be set"); + } + if (internalJarFileName == null) { + throw new IllegalArgumentException("Internal jar file name should be set"); + } + + bootstrapProxy = new BootstrapClassLoaderProxy(this); + + jarEntryPrefix = + internalJarFileName + + (internalJarFileName.isEmpty() || internalJarFileName.endsWith("/") ? "" : "/"); + try { + jarFile = new JarFile(javaagentFile, false); + // base url for constructing jar entry urls + // we use a custom protocol instead of typical jar:file: because we don't want to be affected + // by user code disabling URLConnection caching for jar protocol e.g. tomcat does this + jarBase = + new URL("x-internal-jar", null, 0, "/", new AgentClassLoaderUrlStreamHandler(jarFile)); + codeSource = new CodeSource(javaagentFile.toURI().toURL(), (Certificate[]) null); + manifest = getManifest(jarFile, jarEntryPrefix + META_INF_MANIFEST_MF); + } catch (IOException e) { + throw new IllegalStateException("Unable to open agent jar", e); + } + + if (!AGENT_INITIALIZER_JAR.isEmpty()) { + URL url; + try { + url = new File(AGENT_INITIALIZER_JAR).toURI().toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException( + "Filename could not be parsed: " + + AGENT_INITIALIZER_JAR + + ". Initializer is not installed", + e); + } + + addURL(url); + } + } + + private static int getJavaVersion() { + String javaSpecVersion = System.getProperty("java.specification.version"); + if ("1.8".equals(javaSpecVersion)) { + return 8; + } + return Integer.parseInt(javaSpecVersion); + } + + private static Manifest getManifest(JarFile jarFile, String manifestPath) { + JarEntry manifestEntry = jarFile.getJarEntry(manifestPath); + if (manifestEntry == null) { + throw new IllegalStateException("Manifest entry not found"); + } + try (InputStream is = jarFile.getInputStream(manifestEntry)) { + return new Manifest(is); + } catch (IOException exception) { + throw new IllegalStateException("Failed to read manifest", exception); + } + } + + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + // ContextStorageOverride is meant for library instrumentation we don't want it to apply to our + // bundled grpc + if ("io.grpc.override.ContextStorageOverride".equals(name)) { + throw new ClassNotFoundException(name); + } + + return super.loadClass(name, resolve); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + JarEntry jarEntry = findJarEntry(name.replace('.', '/') + ".class"); + if (jarEntry != null) { + byte[] bytes; + try { + bytes = getJarEntryBytes(jarEntry); + } catch (IOException exception) { + throw new ClassNotFoundException(name, exception); + } + + definePackageIfNeeded(name); + return defineClass(name, bytes, 0, bytes.length, codeSource); + } + + // find class from agent initializer jar + return super.findClass(name); + } + + private byte[] getJarEntryBytes(JarEntry jarEntry) throws IOException { + int size = (int) jarEntry.getSize(); + byte[] buffer = new byte[size]; + try (InputStream is = jarFile.getInputStream(jarEntry)) { + int offset = 0; + int read; + + while (offset < size && (read = is.read(buffer, offset, size - offset)) != -1) { + offset += read; + } + } + + return buffer; + } + + private void definePackageIfNeeded(String className) { + String packageName = getPackageName(className); + if (packageName == null) { + return; + } + if (getPackage(packageName) == null) { + try { + definePackage(packageName, manifest, codeSource.getLocation()); + } catch (IllegalArgumentException exception) { + if (getPackage(packageName) == null) { + throw new IllegalStateException("Failed to define package", exception); + } + } + } + } + + private static String getPackageName(String className) { + int index = className.lastIndexOf('.'); + return index == -1 ? null : className.substring(0, index); + } + + private JarEntry findJarEntry(String name) { + // shading renames .class to .classdata + boolean isClass = name.endsWith(".class"); + if (isClass) { + name += getClassSuffix(); + } + + JarEntry jarEntry = jarFile.getJarEntry(jarEntryPrefix + name); + if (MULTI_RELEASE_JAR_ENABLE) { + jarEntry = findVersionedJarEntry(jarEntry, name); + } + return jarEntry; + } + + // suffix appended to class resource names + // this is in a protected method so that unit tests could override it + protected String getClassSuffix() { + return ""; + } + + private JarEntry findVersionedJarEntry(JarEntry jarEntry, String name) { + // same logic as in JarFile.getVersionedEntry + if (!name.startsWith(META_INF)) { + // search for versioned entry by looping over possible versions form high to low + int version = JAVA_VERSION; + while (version >= MIN_MULTI_RELEASE_JAR_JAVA_VERSION) { + JarEntry versionedJarEntry = + jarFile.getJarEntry(jarEntryPrefix + META_INF_VERSIONS + version + "/" + name); + if (versionedJarEntry != null) { + return versionedJarEntry; + } + version--; + } + } + + return jarEntry; + } + + @Override + public URL getResource(String resourceName) { + URL bootstrapResource = bootstrapProxy.getResource(resourceName); + if (null == bootstrapResource) { + return super.getResource(resourceName); + } else { + return bootstrapResource; + } + } + + @Override + public URL findResource(String name) { + URL url = findJarResource(name); + if (url != null) { + return url; + } + + // find resource from agent initializer jar + return super.findResource(name); + } + + private URL findJarResource(String name) { + JarEntry jarEntry = findJarEntry(name); + return getJarEntryUrl(jarEntry); + } + + private URL getJarEntryUrl(JarEntry jarEntry) { + if (jarEntry != null) { + try { + return new URL(jarBase, jarEntry.getName()); + } catch (MalformedURLException e) { + throw new IllegalStateException( + "Failed to construct url for jar entry " + jarEntry.getName(), e); + } + } + + return null; + } + + @Override + public Enumeration findResources(String name) throws IOException { + // find resources from agent initializer jar + Enumeration delegate = super.findResources(name); + // agent jar can have only once resource for given name + URL url = findJarResource(name); + if (url != null) { + return new Enumeration() { + boolean first = true; + + @Override + public boolean hasMoreElements() { + return first || delegate.hasMoreElements(); + } + + @Override + public URL nextElement() { + if (first) { + first = false; + return url; + } + return delegate.nextElement(); + } + }; + } + + return delegate; + } + + public BootstrapClassLoaderProxy getBootstrapProxy() { + return bootstrapProxy; + } + + /** + * A stand-in for the bootstrap classloader. Used to look up bootstrap resources and resources + * appended by instrumentation. + * + *

This class is thread safe. + */ + public static final class BootstrapClassLoaderProxy extends ClassLoader { + private final AgentClassLoader agentClassLoader; + + static { + ClassLoader.registerAsParallelCapable(); + } + + public BootstrapClassLoaderProxy(AgentClassLoader agentClassLoader) { + super(null); + this.agentClassLoader = agentClassLoader; + } + + @Override + public URL getResource(String resourceName) { + // find resource from boot loader + URL url = super.getResource(resourceName); + if (url != null) { + return url; + } + // find from agent jar + if (agentClassLoader != null) { + JarEntry jarEntry = agentClassLoader.jarFile.getJarEntry(resourceName); + return agentClassLoader.getJarEntryUrl(jarEntry); + } + return null; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + throw new ClassNotFoundException(name); + } + } + + private static class AgentClassLoaderUrlStreamHandler extends URLStreamHandler { + private final JarFile jarFile; + + AgentClassLoaderUrlStreamHandler(JarFile jarFile) { + this.jarFile = jarFile; + } + + @Override + protected URLConnection openConnection(URL url) { + return new AgentClassLoaderUrlConnection(url, jarFile); + } + } + + private static class AgentClassLoaderUrlConnection extends URLConnection { + private final JarFile jarFile; + @Nullable private final String entryName; + @Nullable private JarEntry jarEntry; + + AgentClassLoaderUrlConnection(URL url, JarFile jarFile) { + super(url); + this.jarFile = jarFile; + String path = url.getFile(); + if (path.startsWith("/")) { + path = path.substring(1); + } + if (path.isEmpty()) { + path = null; + } + this.entryName = path; + } + + @Override + public void connect() throws IOException { + if (!connected) { + if (entryName != null) { + jarEntry = jarFile.getJarEntry(entryName); + if (jarEntry == null) { + throw new FileNotFoundException( + "JAR entry " + entryName + " not found in " + jarFile.getName()); + } + } + connected = true; + } + } + + @Override + public InputStream getInputStream() throws IOException { + connect(); + + if (entryName == null) { + throw new IOException("no entry name specified"); + } else { + if (jarEntry == null) { + throw new FileNotFoundException( + "JAR entry " + entryName + " not found in " + jarFile.getName()); + } + return jarFile.getInputStream(jarEntry); + } + } + + @Override + public Permission getPermission() { + return null; + } + + @Override + public long getContentLengthLong() { + try { + connect(); + + if (jarEntry != null) { + return jarEntry.getSize(); + } + } catch (IOException ignored) { + // Ignore + } + return -1; + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java new file mode 100644 index 000000000..76177fd2e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import java.io.File; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Agent start up logic. + * + *

This class is loaded and called by {@code io.opentelemetry.javaagent.OpenTelemetryAgent} + * + *

The intention is for this class to be loaded by bootstrap classloader to make sure we have + * unimpeded access to the rest of agent parts. + */ +public final class AgentInitializer { + + // Accessed via reflection from tests. + // fields must be managed under class lock + @Nullable private static ClassLoader agentClassLoader = null; + + // called via reflection in the OpenTelemetryAgent class + public static void initialize(Instrumentation inst, File javaagentFile) throws Exception { + if (agentClassLoader == null) { + agentClassLoader = createAgentClassLoader("inst", javaagentFile); + + Class agentInstallerClass = + agentClassLoader.loadClass("io.opentelemetry.javaagent.tooling.AgentInstaller"); + Method agentInstallerMethod = + agentInstallerClass.getMethod("installBytebuddyAgent", Instrumentation.class); + ClassLoader savedContextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(agentClassLoader); + agentInstallerMethod.invoke(null, inst); + } finally { + Thread.currentThread().setContextClassLoader(savedContextClassLoader); + } + } + } + + // TODO misleading name + public static synchronized ClassLoader getAgentClassLoader() { + return agentClassLoader; + } + + /** + * Create the agent classloader. This must be called after the bootstrap jar has been appended to + * the bootstrap classpath. + * + * @param innerJarFilename Filename of internal jar to use for the classpath of the agent + * classloader + * @return Agent Classloader + */ + private static ClassLoader createAgentClassLoader(String innerJarFilename, File javaagentFile) + throws Exception { + ClassLoader agentParent; + if (isJavaBefore9()) { + agentParent = null; // bootstrap + } else { + // platform classloader is parent of system in java 9+ + agentParent = getPlatformClassLoader(); + } + + ClassLoader agentClassLoader = + new AgentClassLoader(javaagentFile, innerJarFilename, agentParent); + + Class extensionClassLoaderClass = + agentClassLoader.loadClass("io.opentelemetry.javaagent.tooling.ExtensionClassLoader"); + return (ClassLoader) + extensionClassLoaderClass + .getDeclaredMethod("getInstance", ClassLoader.class, File.class) + .invoke(null, agentClassLoader, javaagentFile); + } + + private static ClassLoader getPlatformClassLoader() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + /* + Must invoke ClassLoader.getPlatformClassLoader by reflection to remain + compatible with java 8. + */ + Method method = ClassLoader.class.getDeclaredMethod("getPlatformClassLoader"); + return (ClassLoader) method.invoke(null); + } + + public static boolean isJavaBefore9() { + return System.getProperty("java.version").startsWith("1."); + } + + private AgentInitializer() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ClassLoaderMatcherCacheHolder.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ClassLoaderMatcherCacheHolder.java new file mode 100644 index 000000000..9c966be31 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ClassLoaderMatcherCacheHolder.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.lock.qual.GuardedBy; + +/** + * A holder of all ClassLoaderMatcher caches. We store them in the bootstrap classloader so that + * instrumentation can invalidate the ClassLoaderMatcher for a particular ClassLoader, e.g. when + * {@link java.net.URLClassLoader#addURL(URL)} is called. + */ +public class ClassLoaderMatcherCacheHolder { + + @GuardedBy("allCaches") + private static final List> allCaches = new ArrayList<>(); + + private ClassLoaderMatcherCacheHolder() {} + + public static void addCache(Cache cache) { + synchronized (allCaches) { + allCaches.add(cache); + } + } + + public static void invalidateAllCachesForClassLoader(ClassLoader loader) { + synchronized (allCaches) { + for (Cache cache : allCaches) { + cache.remove(loader); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ExceptionLogger.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ExceptionLogger.java new file mode 100644 index 000000000..f1d0289d8 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ExceptionLogger.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class used for exception handler logging. + * + *

See io.opentelemetry.javaagent.tooling.ExceptionHandlers + */ +public final class ExceptionLogger { + public static final Logger LOGGER = LoggerFactory.getLogger(ExceptionLogger.class); + + private ExceptionLogger() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/FieldBackedContextStoreAppliedMarker.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/FieldBackedContextStoreAppliedMarker.java new file mode 100644 index 000000000..ae429492d --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/FieldBackedContextStoreAppliedMarker.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +/** + * Marker interface to show that fields for FieldBackedContextStore have been applied to a class. + */ +public interface FieldBackedContextStoreAppliedMarker {} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/HelperResources.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/HelperResources.java new file mode 100644 index 000000000..13f673c8a --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/HelperResources.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A holder of resources needed by instrumentation. We store them in the bootstrap classloader so + * instrumentation can store from the agent classloader and apps can retrieve from the app + * classloader. + */ +public final class HelperResources { + + private static final Cache> RESOURCES = + Cache.newBuilder().setWeakKeys().build(); + + /** Registers the {@code payload} to be available to instrumentation at {@code path}. */ + public static void register(ClassLoader classLoader, String path, URL url) { + RESOURCES.computeIfAbsent(classLoader, unused -> new ConcurrentHashMap<>()).put(path, url); + } + + /** + * Returns a {@link URL} that can be used to retrieve the content of the resource at {@code path}, + * or {@code null} if no resource could be found at {@code path}. + */ + public static URL load(ClassLoader classLoader, String path) { + Map map = RESOURCES.get(classLoader); + if (map == null) { + return null; + } + + return map.get(path); + } + + private HelperResources() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/PatchLogger.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/PatchLogger.java new file mode 100644 index 000000000..ece78da3e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/PatchLogger.java @@ -0,0 +1,360 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import java.text.MessageFormat; +import java.util.ResourceBundle; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dependencies of the agent sometimes call java.util.logging.Logger.getLogger(). This can have the + * effect of initializing the global LogManager incompatibly with the user's app. + * + *

Shadow rewrites will redirect those calls to this class, which will return a safe PatchLogger. + * + *

This also has the desired outcome of redirecting all logging to a single destination (SLF4J). + */ +public class PatchLogger { + + public static final String GLOBAL_LOGGER_NAME = "global"; + + public static final PatchLogger global = new PatchLogger(GLOBAL_LOGGER_NAME); + + private final Logger slf4jLogger; + + private ResourceBundle resourceBundle; + + public static PatchLogger getLogger(String name) { + return new PatchLogger(name); + } + + public static PatchLogger getLogger(String name, String resourceBundleName) { + return new PatchLogger(name); + } + + private PatchLogger(String name) { + this(LoggerFactory.getLogger(name)); + } + + // visible for testing + PatchLogger(Logger slf4jLogger) { + this.slf4jLogger = slf4jLogger; + } + + // visible for testing + Logger getSlf4jLogger() { + return slf4jLogger; + } + + public String getName() { + return slf4jLogger.getName(); + } + + public void severe(String msg) { + slf4jLogger.error(msg); + } + + public void warning(String msg) { + slf4jLogger.warn(msg); + } + + public void info(String msg) { + slf4jLogger.info(msg); + } + + public void config(String msg) { + slf4jLogger.info(msg); + } + + public void fine(String msg) { + slf4jLogger.debug(msg); + } + + public void finer(String msg) { + slf4jLogger.trace(msg); + } + + public void finest(String msg) { + slf4jLogger.trace(msg); + } + + public void log(LogRecord record) { + Level level = record.getLevel(); + if (level.intValue() >= Level.SEVERE.intValue()) { + if (slf4jLogger.isErrorEnabled()) { + slf4jLogger.error(getMessage(record), record.getThrown()); + } + } else if (level.intValue() >= Level.WARNING.intValue()) { + if (slf4jLogger.isWarnEnabled()) { + slf4jLogger.warn(getMessage(record), record.getThrown()); + } + } else if (level.intValue() >= Level.CONFIG.intValue()) { + if (slf4jLogger.isInfoEnabled()) { + slf4jLogger.info(getMessage(record), record.getThrown()); + } + } else if (level.intValue() >= Level.FINE.intValue()) { + if (slf4jLogger.isDebugEnabled()) { + slf4jLogger.debug(getMessage(record), record.getThrown()); + } + } else { + if (slf4jLogger.isTraceEnabled()) { + slf4jLogger.trace(getMessage(record), record.getThrown()); + } + } + } + + public void log(Level level, String msg) { + if (level.intValue() >= Level.SEVERE.intValue()) { + slf4jLogger.error(msg); + } else if (level.intValue() >= Level.WARNING.intValue()) { + slf4jLogger.warn(msg); + } else if (level.intValue() >= Level.CONFIG.intValue()) { + slf4jLogger.info(msg); + } else if (level.intValue() >= Level.FINE.intValue()) { + slf4jLogger.debug(msg); + } else { + slf4jLogger.trace(msg); + } + } + + public void log(Level level, String msg, Object param1) { + if (level.intValue() >= Level.SEVERE.intValue()) { + if (slf4jLogger.isErrorEnabled()) { + slf4jLogger.error(MessageFormat.format(msg, param1)); + } + } else if (level.intValue() >= Level.WARNING.intValue()) { + if (slf4jLogger.isWarnEnabled()) { + slf4jLogger.warn(MessageFormat.format(msg, param1)); + } + } else if (level.intValue() >= Level.CONFIG.intValue()) { + if (slf4jLogger.isInfoEnabled()) { + slf4jLogger.info(MessageFormat.format(msg, param1)); + } + } else if (level.intValue() >= Level.FINE.intValue()) { + if (slf4jLogger.isDebugEnabled()) { + slf4jLogger.debug(MessageFormat.format(msg, param1)); + } + } else { + if (slf4jLogger.isTraceEnabled()) { + slf4jLogger.trace(MessageFormat.format(msg, param1)); + } + } + } + + public void log(Level level, String msg, Object[] params) { + if (level.intValue() >= Level.SEVERE.intValue()) { + if (slf4jLogger.isErrorEnabled()) { + slf4jLogger.error(MessageFormat.format(msg, params)); + } + } else if (level.intValue() >= Level.WARNING.intValue()) { + if (slf4jLogger.isWarnEnabled()) { + slf4jLogger.warn(MessageFormat.format(msg, params)); + } + } else if (level.intValue() >= Level.CONFIG.intValue()) { + if (slf4jLogger.isInfoEnabled()) { + slf4jLogger.info(MessageFormat.format(msg, params)); + } + } else if (level.intValue() >= Level.FINE.intValue()) { + if (slf4jLogger.isDebugEnabled()) { + slf4jLogger.debug(MessageFormat.format(msg, params)); + } + } else { + if (slf4jLogger.isTraceEnabled()) { + slf4jLogger.trace(MessageFormat.format(msg, params)); + } + } + } + + public void log(Level level, String msg, Throwable thrown) { + if (level.intValue() >= Level.SEVERE.intValue()) { + slf4jLogger.error(msg, thrown); + } else if (level.intValue() >= Level.WARNING.intValue()) { + slf4jLogger.warn(msg, thrown); + } else if (level.intValue() >= Level.CONFIG.intValue()) { + slf4jLogger.info(msg, thrown); + } else if (level.intValue() >= Level.FINE.intValue()) { + slf4jLogger.debug(msg, thrown); + } else { + slf4jLogger.trace(msg, thrown); + } + } + + public boolean isLoggable(Level level) { + if (level.intValue() >= Level.SEVERE.intValue()) { + return slf4jLogger.isErrorEnabled(); + } else if (level.intValue() >= Level.WARNING.intValue()) { + return slf4jLogger.isWarnEnabled(); + } else if (level.intValue() >= Level.CONFIG.intValue()) { + return slf4jLogger.isInfoEnabled(); + } else if (level.intValue() >= Level.FINE.intValue()) { + return slf4jLogger.isDebugEnabled(); + } else { + return slf4jLogger.isTraceEnabled(); + } + } + + public Level getLevel() { + if (slf4jLogger.isErrorEnabled()) { + return Level.SEVERE; + } else if (slf4jLogger.isWarnEnabled()) { + return Level.WARNING; + } else if (slf4jLogger.isInfoEnabled()) { + return Level.CONFIG; + } else if (slf4jLogger.isDebugEnabled()) { + return Level.FINE; + } else if (slf4jLogger.isTraceEnabled()) { + return Level.FINEST; + } else { + return Level.OFF; + } + } + + public void logp(Level level, String sourceClass, String sourceMethod, String msg) { + log(level, msg); + } + + public void logp( + Level level, String sourceClass, String sourceMethod, String msg, Object param1) { + log(level, msg, param1); + } + + public void logp( + Level level, String sourceClass, String sourceMethod, String msg, Object[] params) { + log(level, msg, params); + } + + public void logp( + Level level, String sourceClass, String sourceMethod, String msg, Throwable thrown) { + log(level, msg, thrown); + } + + public void logrb( + Level level, String sourceClass, String sourceMethod, String bundleName, String msg) { + log(level, msg); + } + + public void logrb( + Level level, + String sourceClass, + String sourceMethod, + String bundleName, + String msg, + Object param1) { + log(level, msg, param1); + } + + public void logrb( + Level level, + String sourceClass, + String sourceMethod, + String bundleName, + String msg, + Object[] params) { + log(level, msg, params); + } + + public void logrb( + Level level, + String sourceClass, + String sourceMethod, + ResourceBundle bundle, + String msg, + Object... params) { + log(level, msg, params); + } + + public void logrb(Level level, ResourceBundle bundle, String msg, Object... params) { + log(level, msg, params); + } + + public void logrb( + Level level, + String sourceClass, + String sourceMethod, + String bundleName, + String msg, + Throwable thrown) { + log(level, msg, thrown); + } + + public void logrb( + Level level, + String sourceClass, + String sourceMethod, + ResourceBundle bundle, + String msg, + Throwable thrown) { + log(level, msg, thrown); + } + + public void logrb(Level level, ResourceBundle bundle, String msg, Throwable thrown) { + log(level, msg, thrown); + } + + public void entering(String sourceClass, String sourceMethod) {} + + public void entering(String sourceClass, String sourceMethod, Object param1) {} + + public void entering(String sourceClass, String sourceMethod, Object[] params) {} + + public void exiting(String sourceClass, String sourceMethod) {} + + public void exiting(String sourceClass, String sourceMethod, Object result) {} + + public void throwing(String sourceClass, String sourceMethod, Throwable thrown) {} + + public ResourceBundle getResourceBundle() { + return resourceBundle; + } + + public void setResourceBundle(ResourceBundle resourceBundle) { + this.resourceBundle = resourceBundle; + } + + public String getResourceBundleName() { + return null; + } + + public PatchLogger getParent() { + return getLogger(""); + } + + public void setParent(PatchLogger parent) {} + + public void setLevel(Level newLevel) {} + + public Handler[] getHandlers() { + return new Handler[0]; + } + + public void addHandler(Handler handler) {} + + public static PatchLogger getAnonymousLogger() { + return getLogger(""); + } + + public static PatchLogger getAnonymousLogger(String resourceBundleName) { + return getLogger(""); + } + + public static PatchLogger getGlobal() { + return global; + } + + private static String getMessage(LogRecord record) { + String msg = record.getMessage(); + Object[] params = record.getParameters(); + if (params == null) { + return msg; + } else { + return MessageFormat.format(msg, params); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/patch/java/io/opentelemetry/instrumentation/api/internal/shaded/caffeine/cache/BoundedLocalCache.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/patch/java/io/opentelemetry/instrumentation/api/internal/shaded/caffeine/cache/BoundedLocalCache.java new file mode 100644 index 000000000..daf7f21a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/patch/java/io/opentelemetry/instrumentation/api/internal/shaded/caffeine/cache/BoundedLocalCache.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2017 Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Copyright 2014 Ben Manes. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.instrumentation.api.internal.shaded.caffeine.cache; + +import java.lang.ref.WeakReference; + +/** skeleton outer class just for compilation purposes, not included in the final patch. */ +abstract class BoundedLocalCache { + abstract void performCleanUp(Runnable task); + + /** patched to not extend ForkJoinTask as we don't want that class loaded too early. */ + static final class PerformCleanupTask implements Runnable { + private static final long serialVersionUID = 1L; + + final WeakReference> reference; + + PerformCleanupTask(BoundedLocalCache cache) { + reference = new WeakReference<>(cache); + } + + @Override + public void run() { + BoundedLocalCache cache = reference.get(); + if (cache != null) { + cache.performCleanUp(/* ignored */ null); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/test/groovy/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.groovy b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/test/groovy/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.groovy new file mode 100644 index 000000000..cfed25bf4 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/test/groovy/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.groovy @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap + +import io.opentelemetry.sdk.internal.JavaVersionSpecific +import java.lang.reflect.Field +import java.util.concurrent.Phaser +import spock.lang.Specification + +class AgentClassLoaderTest extends Specification { + + def "agent classloader does not lock classloading around instance"() { + setup: + def className1 = 'some/class/Name1' + def className2 = 'some/class/Name2' + // any jar would do, use opentelemety sdk + URL testJarLocation = JavaVersionSpecific.getProtectionDomain().getCodeSource().getLocation() + AgentClassLoader loader = new AgentClassLoader(new File(testJarLocation.toURI()), "", null) + Phaser threadHoldLockPhase = new Phaser(2) + Phaser acquireLockFromMainThreadPhase = new Phaser(2) + + when: + Thread thread1 = new Thread() { + @Override + void run() { + synchronized (loader.getClassLoadingLock(className1)) { + threadHoldLockPhase.arrive() + acquireLockFromMainThreadPhase.arriveAndAwaitAdvance() + } + } + } + thread1.start() + + Thread thread2 = new Thread() { + @Override + void run() { + threadHoldLockPhase.arriveAndAwaitAdvance() + synchronized (loader.getClassLoadingLock(className2)) { + acquireLockFromMainThreadPhase.arrive() + } + } + } + thread2.start() + thread1.join() + thread2.join() + boolean applicationDidNotDeadlock = true + + then: + applicationDidNotDeadlock + } + + def "multi release jar"() { + setup: + boolean jdk8 = "1.8" == System.getProperty("java.specification.version") + // sdk is a multi release jar + URL multiReleaseJar = JavaVersionSpecific.getProtectionDomain().getCodeSource().getLocation() + AgentClassLoader loader = new AgentClassLoader(new File(multiReleaseJar.toURI()), "", null) { + @Override + protected String getClassSuffix() { + return "" + } + } + + when: + URL url = loader.findResource("io/opentelemetry/sdk/internal/CurrentJavaVersionSpecific.class") + + then: + url != null + // versioned resource is found when not running on jdk 8 + jdk8 != url.toString().contains("META-INF/versions/9/") + + and: + Class clazz = loader.loadClass(JavaVersionSpecific.getName()) + // class was loaded by agent loader used in this test + clazz.getClassLoader() == loader + // extract value of private static field that gets a different class depending on java version + Field field = clazz.getDeclaredField("CURRENT") + field.setAccessible(true) + Object javaVersionSpecific = field.get(null) + // expect a versioned class on java 9+ + jdk8 != javaVersionSpecific.getClass().getName().endsWith("Java9VersionSpecific") + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/PatchLoggerTest.java b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/PatchLoggerTest.java new file mode 100644 index 000000000..209a48ab4 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/PatchLoggerTest.java @@ -0,0 +1,870 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.logging.Level; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +class PatchLoggerTest { + @Test + void testImplementsAllMethods() { + Set patchLoggerMethods = new HashSet<>(); + for (Method method : PatchLogger.class.getMethods()) { + MethodSignature methodSignature = new MethodSignature(); + methodSignature.name = method.getName(); + for (Class clazz : method.getParameterTypes()) { + String parameterType = clazz.getName(); + methodSignature.parameterTypes.add( + parameterType.replaceFirst( + "io.opentelemetry.javaagent.bootstrap.PatchLogger", "java.util.logging.Logger")); + } + methodSignature.returnType = + method + .getReturnType() + .getName() + .replace( + "io.opentelemetry.javaagent.bootstrap.PatchLogger", "java.util.logging.Logger"); + patchLoggerMethods.add(methodSignature); + } + Set julLoggerMethods = new HashSet<>(); + for (Method method : java.util.logging.Logger.class.getMethods()) { + String methodName = method.getName(); + if (methodName.contains("Handler") || methodName.contains("Filter")) { + continue; + } + MethodSignature builder = new MethodSignature(); + builder.name = methodName; + List parameterTypes = new ArrayList<>(); + for (Class clazz : method.getParameterTypes()) { + parameterTypes.add(clazz.getName()); + } + if (parameterTypes.contains("java.util.function.Supplier")) { + // FIXME it would be good to include Java 8 methods + continue; + } + builder.parameterTypes.addAll(parameterTypes); + builder.returnType = method.getReturnType().getName(); + julLoggerMethods.add(builder); + } + assertThat(patchLoggerMethods).containsAll(julLoggerMethods); + } + + @Test + void testGetLogger() { + PatchLogger logger = PatchLogger.getLogger("abc"); + assertThat(logger.getSlf4jLogger().getName()).isEqualTo("abc"); + } + + @Test + void testGetName() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.getName()).thenReturn("xyz"); + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + // then + assertThat(logger.getName()).isEqualTo("xyz"); + } + + @Test + void testNormalMethods() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.severe("ereves"); + logger.warning("gninraw"); + logger.info("ofni"); + logger.config("gifnoc"); + logger.fine("enif"); + logger.finer("renif"); + logger.finest("tsenif"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves"); + inOrder.verify(slf4jLogger).warn("gninraw"); + inOrder.verify(slf4jLogger).info("ofni"); + inOrder.verify(slf4jLogger).info("gifnoc"); + inOrder.verify(slf4jLogger).debug("enif"); + inOrder.verify(slf4jLogger).trace("renif"); + inOrder.verify(slf4jLogger).trace("tsenif"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testParameterizedLevelMethodsWithNoParams() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.log(Level.SEVERE, "ereves"); + logger.log(Level.WARNING, "gninraw"); + logger.log(Level.INFO, "ofni"); + logger.log(Level.CONFIG, "gifnoc"); + logger.log(Level.FINE, "enif"); + logger.log(Level.FINER, "renif"); + logger.log(Level.FINEST, "tsenif"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves"); + inOrder.verify(slf4jLogger).warn("gninraw"); + inOrder.verify(slf4jLogger).info("ofni"); + inOrder.verify(slf4jLogger).info("gifnoc"); + inOrder.verify(slf4jLogger).debug("enif"); + inOrder.verify(slf4jLogger).trace("renif"); + inOrder.verify(slf4jLogger).trace("tsenif"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testParameterizedLevelMethodsWithSingleParam() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.log(Level.SEVERE, "ereves: {0}", "a"); + logger.log(Level.WARNING, "gninraw: {0}", "b"); + logger.log(Level.INFO, "ofni: {0}", "c"); + logger.log(Level.CONFIG, "gifnoc: {0}", "d"); + logger.log(Level.FINE, "enif: {0}", "e"); + logger.log(Level.FINER, "renif: {0}", "f"); + logger.log(Level.FINEST, "tsenif: {0}", "g"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).isErrorEnabled(); + inOrder.verify(slf4jLogger).error("ereves: a"); + inOrder.verify(slf4jLogger).isWarnEnabled(); + inOrder.verify(slf4jLogger).warn("gninraw: b"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("ofni: c"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("gifnoc: d"); + inOrder.verify(slf4jLogger).isDebugEnabled(); + inOrder.verify(slf4jLogger).debug("enif: e"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("renif: f"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("tsenif: g"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testParameterizedLevelMethodsWithArrayOfParams() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.log(Level.SEVERE, "ereves: {0},{1}", new Object[] {"a", "b"}); + logger.log(Level.WARNING, "gninraw: {0},{1}", new Object[] {"b", "c"}); + logger.log(Level.INFO, "ofni: {0},{1}", new Object[] {"c", "d"}); + logger.log(Level.CONFIG, "gifnoc: {0},{1}", new Object[] {"d", "e"}); + logger.log(Level.FINE, "enif: {0},{1}", new Object[] {"e", "f"}); + logger.log(Level.FINER, "renif: {0},{1}", new Object[] {"f", "g"}); + logger.log(Level.FINEST, "tsenif: {0},{1}", new Object[] {"g", "h"}); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).isErrorEnabled(); + inOrder.verify(slf4jLogger).error("ereves: a,b"); + inOrder.verify(slf4jLogger).isWarnEnabled(); + inOrder.verify(slf4jLogger).warn("gninraw: b,c"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("ofni: c,d"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("gifnoc: d,e"); + inOrder.verify(slf4jLogger).isDebugEnabled(); + inOrder.verify(slf4jLogger).debug("enif: e,f"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("renif: f,g"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("tsenif: g,h"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testParameterizedLevelMethodsWithThrowable() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + Throwable a = new Throwable(); + Throwable b = new Throwable(); + Throwable c = new Throwable(); + Throwable d = new Throwable(); + Throwable e = new Throwable(); + Throwable f = new Throwable(); + Throwable g = new Throwable(); + + // when + logger.log(Level.SEVERE, "ereves", a); + logger.log(Level.WARNING, "gninraw", b); + logger.log(Level.INFO, "ofni", c); + logger.log(Level.CONFIG, "gifnoc", d); + logger.log(Level.FINE, "enif", e); + logger.log(Level.FINER, "renif", f); + logger.log(Level.FINEST, "tsenif", g); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves", a); + inOrder.verify(slf4jLogger).warn("gninraw", b); + inOrder.verify(slf4jLogger).info("ofni", c); + inOrder.verify(slf4jLogger).info("gifnoc", d); + inOrder.verify(slf4jLogger).debug("enif", e); + inOrder.verify(slf4jLogger).trace("renif", f); + inOrder.verify(slf4jLogger).trace("tsenif", g); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testIsLoggableAll() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + + // then + assertThat(logger.isLoggable(Level.SEVERE)).isTrue(); + assertThat(logger.isLoggable(Level.WARNING)).isTrue(); + assertThat(logger.isLoggable(Level.INFO)).isTrue(); + assertThat(logger.isLoggable(Level.CONFIG)).isTrue(); + assertThat(logger.isLoggable(Level.FINE)).isTrue(); + assertThat(logger.isLoggable(Level.FINER)).isTrue(); + assertThat(logger.isLoggable(Level.FINEST)).isTrue(); + } + + @Test + void testIsLoggableSome() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(false); + when(slf4jLogger.isDebugEnabled()).thenReturn(false); + when(slf4jLogger.isInfoEnabled()).thenReturn(false); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + + // then + assertThat(logger.isLoggable(Level.SEVERE)).isTrue(); + assertThat(logger.isLoggable(Level.WARNING)).isTrue(); + assertThat(logger.isLoggable(Level.INFO)).isFalse(); + assertThat(logger.isLoggable(Level.CONFIG)).isFalse(); + assertThat(logger.isLoggable(Level.FINE)).isFalse(); + assertThat(logger.isLoggable(Level.FINER)).isFalse(); + assertThat(logger.isLoggable(Level.FINEST)).isFalse(); + } + + @Test + void testIsLoggableNone() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(false); + when(slf4jLogger.isDebugEnabled()).thenReturn(false); + when(slf4jLogger.isInfoEnabled()).thenReturn(false); + when(slf4jLogger.isWarnEnabled()).thenReturn(false); + when(slf4jLogger.isErrorEnabled()).thenReturn(false); + + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + + // then + assertThat(logger.isLoggable(Level.SEVERE)).isFalse(); + assertThat(logger.isLoggable(Level.WARNING)).isFalse(); + assertThat(logger.isLoggable(Level.INFO)).isFalse(); + assertThat(logger.isLoggable(Level.CONFIG)).isFalse(); + assertThat(logger.isLoggable(Level.FINE)).isFalse(); + assertThat(logger.isLoggable(Level.FINER)).isFalse(); + assertThat(logger.isLoggable(Level.FINEST)).isFalse(); + } + + @Test + void testGetLevelSevere() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + // then + assertThat(logger.getLevel()).isEqualTo(Level.SEVERE); + } + + @Test + void testGetLevelWarning() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + // then + assertThat(logger.getLevel()).isEqualTo(Level.WARNING); + } + + @Test + void testGetLevelConfig() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + // then + assertThat(logger.getLevel()).isEqualTo(Level.CONFIG); + } + + @Test + void testGetLevelFine() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + // then + assertThat(logger.getLevel()).isEqualTo(Level.FINE); + } + + @Test + void testGetLevelFinest() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + // then + assertThat(logger.getLevel()).isEqualTo(Level.FINEST); + } + + @Test + void testGetLevelOff() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + // then + assertThat(logger.getLevel()).isEqualTo(Level.OFF); + } + + @Test + void testLogpParameterizedLevelMethodsWithNoParams() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.logp(Level.SEVERE, null, null, "ereves"); + logger.logp(Level.WARNING, null, null, "gninraw"); + logger.logp(Level.INFO, null, null, "ofni"); + logger.logp(Level.CONFIG, null, null, "gifnoc"); + logger.logp(Level.FINE, null, null, "enif"); + logger.logp(Level.FINER, null, null, "renif"); + logger.logp(Level.FINEST, null, null, "tsenif"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves"); + inOrder.verify(slf4jLogger).warn("gninraw"); + inOrder.verify(slf4jLogger).info("ofni"); + inOrder.verify(slf4jLogger).info("gifnoc"); + inOrder.verify(slf4jLogger).debug("enif"); + inOrder.verify(slf4jLogger).trace("renif"); + inOrder.verify(slf4jLogger).trace("tsenif"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogpParameterizedLevelMethodsWithSingleParam() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.logp(Level.SEVERE, null, null, "ereves: {0}", "a"); + logger.logp(Level.WARNING, null, null, "gninraw: {0}", "b"); + logger.logp(Level.INFO, null, null, "ofni: {0}", "c"); + logger.logp(Level.CONFIG, null, null, "gifnoc: {0}", "d"); + logger.logp(Level.FINE, null, null, "enif: {0}", "e"); + logger.logp(Level.FINER, null, null, "renif: {0}", "f"); + logger.logp(Level.FINEST, null, null, "tsenif: {0}", "g"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).isErrorEnabled(); + inOrder.verify(slf4jLogger).error("ereves: a"); + inOrder.verify(slf4jLogger).isWarnEnabled(); + inOrder.verify(slf4jLogger).warn("gninraw: b"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("ofni: c"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("gifnoc: d"); + inOrder.verify(slf4jLogger).isDebugEnabled(); + inOrder.verify(slf4jLogger).debug("enif: e"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("renif: f"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("tsenif: g"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogpParameterizedLevelMethodsWithArrayOfParams() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.logp(Level.SEVERE, null, null, "ereves: {0},{1}", new Object[] {"a", "b"}); + logger.logp(Level.WARNING, null, null, "gninraw: {0},{1}", new Object[] {"b", "c"}); + logger.logp(Level.INFO, null, null, "ofni: {0},{1}", new Object[] {"c", "d"}); + logger.logp(Level.CONFIG, null, null, "gifnoc: {0},{1}", new Object[] {"d", "e"}); + logger.logp(Level.FINE, null, null, "enif: {0},{1}", new Object[] {"e", "f"}); + logger.logp(Level.FINER, null, null, "renif: {0},{1}", new Object[] {"f", "g"}); + logger.logp(Level.FINEST, null, null, "tsenif: {0},{1}", new Object[] {"g", "h"}); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).isErrorEnabled(); + inOrder.verify(slf4jLogger).error("ereves: a,b"); + inOrder.verify(slf4jLogger).isWarnEnabled(); + inOrder.verify(slf4jLogger).warn("gninraw: b,c"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("ofni: c,d"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("gifnoc: d,e"); + inOrder.verify(slf4jLogger).isDebugEnabled(); + inOrder.verify(slf4jLogger).debug("enif: e,f"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("renif: f,g"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("tsenif: g,h"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogpParameterizedLevelMethodsWithThrowable() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + Throwable a = new Throwable(); + Throwable b = new Throwable(); + Throwable c = new Throwable(); + Throwable d = new Throwable(); + Throwable e = new Throwable(); + Throwable f = new Throwable(); + Throwable g = new Throwable(); + + // when + logger.logp(Level.SEVERE, null, null, "ereves", a); + logger.logp(Level.WARNING, null, null, "gninraw", b); + logger.logp(Level.INFO, null, null, "ofni", c); + logger.logp(Level.CONFIG, null, null, "gifnoc", d); + logger.logp(Level.FINE, null, null, "enif", e); + logger.logp(Level.FINER, null, null, "renif", f); + logger.logp(Level.FINEST, null, null, "tsenif", g); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves", a); + inOrder.verify(slf4jLogger).warn("gninraw", b); + inOrder.verify(slf4jLogger).info("ofni", c); + inOrder.verify(slf4jLogger).info("gifnoc", d); + inOrder.verify(slf4jLogger).debug("enif", e); + inOrder.verify(slf4jLogger).trace("renif", f); + inOrder.verify(slf4jLogger).trace("tsenif", g); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogrbParameterizedLevelMethodsWithNoParams() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.logrb(Level.SEVERE, null, null, null, "ereves"); + logger.logrb(Level.WARNING, null, null, null, "gninraw"); + logger.logrb(Level.INFO, null, null, null, "ofni"); + logger.logrb(Level.CONFIG, null, null, null, "gifnoc"); + logger.logrb(Level.FINE, null, null, null, "enif"); + logger.logrb(Level.FINER, null, null, null, "renif"); + logger.logrb(Level.FINEST, null, null, null, "tsenif"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves"); + inOrder.verify(slf4jLogger).warn("gninraw"); + inOrder.verify(slf4jLogger).info("ofni"); + inOrder.verify(slf4jLogger).info("gifnoc"); + inOrder.verify(slf4jLogger).debug("enif"); + inOrder.verify(slf4jLogger).trace("renif"); + inOrder.verify(slf4jLogger).trace("tsenif"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogrbParameterizedLevelMethodsWithSingleParam() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.logrb(Level.SEVERE, null, null, null, "ereves: {0}", "a"); + logger.logrb(Level.WARNING, null, null, null, "gninraw: {0}", "b"); + logger.logrb(Level.INFO, null, null, null, "ofni: {0}", "c"); + logger.logrb(Level.CONFIG, null, null, null, "gifnoc: {0}", "d"); + logger.logrb(Level.FINE, null, null, null, "enif: {0}", "e"); + logger.logrb(Level.FINER, null, null, null, "renif: {0}", "f"); + logger.logrb(Level.FINEST, null, null, null, "tsenif: {0}", "g"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).isErrorEnabled(); + inOrder.verify(slf4jLogger).error("ereves: a"); + inOrder.verify(slf4jLogger).isWarnEnabled(); + inOrder.verify(slf4jLogger).warn("gninraw: b"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("ofni: c"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("gifnoc: d"); + inOrder.verify(slf4jLogger).isDebugEnabled(); + inOrder.verify(slf4jLogger).debug("enif: e"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("renif: f"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("tsenif: g"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogrbParameterizedLevelMethodsWithArrayOfParams() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.logrb( + Level.SEVERE, null, null, (String) null, "ereves: {0},{1}", new Object[] {"a", "b"}); + logger.logrb( + Level.WARNING, null, null, (String) null, "gninraw: {0},{1}", new Object[] {"b", "c"}); + logger.logrb(Level.INFO, null, null, (String) null, "ofni: {0},{1}", new Object[] {"c", "d"}); + logger.logrb( + Level.CONFIG, null, null, (String) null, "gifnoc: {0},{1}", new Object[] {"d", "e"}); + logger.logrb(Level.FINE, null, null, (String) null, "enif: {0},{1}", new Object[] {"e", "f"}); + logger.logrb(Level.FINER, null, null, (String) null, "renif: {0},{1}", new Object[] {"f", "g"}); + logger.logrb( + Level.FINEST, null, null, (String) null, "tsenif: {0},{1}", new Object[] {"g", "h"}); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).isErrorEnabled(); + inOrder.verify(slf4jLogger).error("ereves: a,b"); + inOrder.verify(slf4jLogger).isWarnEnabled(); + inOrder.verify(slf4jLogger).warn("gninraw: b,c"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("ofni: c,d"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("gifnoc: d,e"); + inOrder.verify(slf4jLogger).isDebugEnabled(); + inOrder.verify(slf4jLogger).debug("enif: e,f"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("renif: f,g"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("tsenif: g,h"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogrbParameterizedLevelMethodsWithVarArgsOfParams() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.logrb(Level.SEVERE, (String) null, null, null, "ereves: {0},{1}", "a", "b"); + logger.logrb(Level.WARNING, (String) null, null, null, "gninraw: {0},{1}", "b", "c"); + logger.logrb(Level.INFO, (String) null, null, null, "ofni: {0},{1}", "c", "d"); + logger.logrb(Level.CONFIG, (String) null, null, null, "gifnoc: {0},{1}", "d", "e"); + logger.logrb(Level.FINE, (String) null, null, null, "enif: {0},{1}", "e", "f"); + logger.logrb(Level.FINER, (String) null, null, null, "renif: {0},{1}", "f", "g"); + logger.logrb(Level.FINEST, (String) null, null, null, "tsenif: {0},{1}", "g", "h"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).isErrorEnabled(); + inOrder.verify(slf4jLogger).error("ereves: a,b"); + inOrder.verify(slf4jLogger).isWarnEnabled(); + inOrder.verify(slf4jLogger).warn("gninraw: b,c"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("ofni: c,d"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("gifnoc: d,e"); + inOrder.verify(slf4jLogger).isDebugEnabled(); + inOrder.verify(slf4jLogger).debug("enif: e,f"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("renif: f,g"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("tsenif: g,h"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogrbParameterizedLevelMethodsWithVarArgsOfParams2() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + when(slf4jLogger.isTraceEnabled()).thenReturn(true); + when(slf4jLogger.isDebugEnabled()).thenReturn(true); + when(slf4jLogger.isInfoEnabled()).thenReturn(true); + when(slf4jLogger.isWarnEnabled()).thenReturn(true); + when(slf4jLogger.isErrorEnabled()).thenReturn(true); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.logrb(Level.SEVERE, (ResourceBundle) null, "ereves: {0},{1}", "a", "b"); + logger.logrb(Level.WARNING, (ResourceBundle) null, "gninraw: {0},{1}", "b", "c"); + logger.logrb(Level.INFO, (ResourceBundle) null, "ofni: {0},{1}", "c", "d"); + logger.logrb(Level.CONFIG, (ResourceBundle) null, "gifnoc: {0},{1}", "d", "e"); + logger.logrb(Level.FINE, (ResourceBundle) null, "enif: {0},{1}", "e", "f"); + logger.logrb(Level.FINER, (ResourceBundle) null, "renif: {0},{1}", "f", "g"); + logger.logrb(Level.FINEST, (ResourceBundle) null, "tsenif: {0},{1}", "g", "h"); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).isErrorEnabled(); + inOrder.verify(slf4jLogger).error("ereves: a,b"); + inOrder.verify(slf4jLogger).isWarnEnabled(); + inOrder.verify(slf4jLogger).warn("gninraw: b,c"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("ofni: c,d"); + inOrder.verify(slf4jLogger).isInfoEnabled(); + inOrder.verify(slf4jLogger).info("gifnoc: d,e"); + inOrder.verify(slf4jLogger).isDebugEnabled(); + inOrder.verify(slf4jLogger).debug("enif: e,f"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("renif: f,g"); + inOrder.verify(slf4jLogger).isTraceEnabled(); + inOrder.verify(slf4jLogger).trace("tsenif: g,h"); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogrbParameterizedLevelMethodsWithThrowable() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + Throwable a = new Throwable(); + Throwable b = new Throwable(); + Throwable c = new Throwable(); + Throwable d = new Throwable(); + Throwable e = new Throwable(); + Throwable f = new Throwable(); + Throwable g = new Throwable(); + + // when + logger.logrb(Level.SEVERE, null, null, (String) null, "ereves", a); + logger.logrb(Level.WARNING, null, null, (String) null, "gninraw", b); + logger.logrb(Level.INFO, null, null, (String) null, "ofni", c); + logger.logrb(Level.CONFIG, null, null, (String) null, "gifnoc", d); + logger.logrb(Level.FINE, null, null, (String) null, "enif", e); + logger.logrb(Level.FINER, null, null, (String) null, "renif", f); + logger.logrb(Level.FINEST, null, null, (String) null, "tsenif", g); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves", a); + inOrder.verify(slf4jLogger).warn("gninraw", b); + inOrder.verify(slf4jLogger).info("ofni", c); + inOrder.verify(slf4jLogger).info("gifnoc", d); + inOrder.verify(slf4jLogger).debug("enif", e); + inOrder.verify(slf4jLogger).trace("renif", f); + inOrder.verify(slf4jLogger).trace("tsenif", g); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogrbParameterizedLevelMethodsWithThrowable2() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + Throwable a = new Throwable(); + Throwable b = new Throwable(); + Throwable c = new Throwable(); + Throwable d = new Throwable(); + Throwable e = new Throwable(); + Throwable f = new Throwable(); + Throwable g = new Throwable(); + + // when + logger.logrb(Level.SEVERE, null, null, (ResourceBundle) null, "ereves", a); + logger.logrb(Level.WARNING, null, null, (ResourceBundle) null, "gninraw", b); + logger.logrb(Level.INFO, null, null, (ResourceBundle) null, "ofni", c); + logger.logrb(Level.CONFIG, null, null, (ResourceBundle) null, "gifnoc", d); + logger.logrb(Level.FINE, null, null, (ResourceBundle) null, "enif", e); + logger.logrb(Level.FINER, null, null, (ResourceBundle) null, "renif", f); + logger.logrb(Level.FINEST, null, null, (ResourceBundle) null, "tsenif", g); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves", a); + inOrder.verify(slf4jLogger).warn("gninraw", b); + inOrder.verify(slf4jLogger).info("ofni", c); + inOrder.verify(slf4jLogger).info("gifnoc", d); + inOrder.verify(slf4jLogger).debug("enif", e); + inOrder.verify(slf4jLogger).trace("renif", f); + inOrder.verify(slf4jLogger).trace("tsenif", g); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testLogrbParameterizedLevelMethodsWithResourceBundleObjectAndThrowable() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + Throwable a = new Throwable(); + Throwable b = new Throwable(); + Throwable c = new Throwable(); + Throwable d = new Throwable(); + Throwable e = new Throwable(); + Throwable f = new Throwable(); + Throwable g = new Throwable(); + + // when + logger.logrb(Level.SEVERE, null, null, (ResourceBundle) null, "ereves", a); + logger.logrb(Level.WARNING, null, null, (ResourceBundle) null, "gninraw", b); + logger.logrb(Level.INFO, null, null, (ResourceBundle) null, "ofni", c); + logger.logrb(Level.CONFIG, null, null, (ResourceBundle) null, "gifnoc", d); + logger.logrb(Level.FINE, null, null, (ResourceBundle) null, "enif", e); + logger.logrb(Level.FINER, null, null, (ResourceBundle) null, "renif", f); + logger.logrb(Level.FINEST, null, null, (ResourceBundle) null, "tsenif", g); + + // then + InOrder inOrder = Mockito.inOrder(slf4jLogger); + inOrder.verify(slf4jLogger).error("ereves", a); + inOrder.verify(slf4jLogger).warn("gninraw", b); + inOrder.verify(slf4jLogger).info("ofni", c); + inOrder.verify(slf4jLogger).info("gifnoc", d); + inOrder.verify(slf4jLogger).debug("enif", e); + inOrder.verify(slf4jLogger).trace("renif", f); + inOrder.verify(slf4jLogger).trace("tsenif", g); + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testEnteringExitingThrowingMethods() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + PatchLogger logger = new PatchLogger(slf4jLogger); + + // when + logger.entering(null, null); + logger.entering(null, null, new Object()); + logger.entering(null, null, new Object[0]); + logger.exiting(null, null); + logger.exiting(null, null, new Object()); + logger.throwing(null, null, null); + + // then + verifyNoMoreInteractions(slf4jLogger); + } + + @Test + void testResourceBundle() { + // given + org.slf4j.Logger slf4jLogger = mock(org.slf4j.Logger.class); + + // when + PatchLogger logger = new PatchLogger(slf4jLogger); + + // then + assertThat(logger.getResourceBundle()).isNull(); + assertThat(logger.getResourceBundleName()).isNull(); + verifyNoMoreInteractions(slf4jLogger); + } + + static class MethodSignature { + String name; + List parameterTypes = new ArrayList<>(); + String returnType; + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof MethodSignature)) { + return false; + } + MethodSignature other = (MethodSignature) obj; + return Objects.equals(name, other.name) + && Objects.equals(parameterTypes, other.parameterTypes) + && Objects.equals(returnType, other.returnType); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameterTypes, returnType); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-exporters/javaagent-exporters.gradle b/opentelemetry-java-instrumentation/javaagent-exporters/javaagent-exporters.gradle new file mode 100644 index 000000000..8664601bc --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-exporters/javaagent-exporters.gradle @@ -0,0 +1,32 @@ +// Project to collect and shade exporter dependencies included in the agent's full distribution. + +plugins { + id "otel.java-conventions" + id "otel.shadow-conventions" +} + +dependencies { + implementation "run.mone:opentelemetry-exporter-jaeger" + implementation "run.mone:opentelemetry-exporter-otlp" + implementation "run.mone:opentelemetry-exporter-otlp-metrics" + + implementation "run.mone:opentelemetry-exporter-prometheus" + implementation "io.prometheus:simpleclient" + implementation "io.prometheus:simpleclient_httpserver" + + implementation "run.mone:opentelemetry-exporter-zipkin" + implementation "run.mone:opentelemetry-exporter-jaeger" + implementation "run.mone:opentelemetry-exporter-prometheus" + + // TODO(anuraaga): Move version to dependency management + implementation "io.grpc:grpc-netty-shaded:1.38.0" + + implementation("io.grpc:grpc-protobuf:1.38.0") + implementation("io.grpc:grpc-stub:1.38.0") + implementation("com.google.protobuf:protobuf-java:3.17.2") + implementation("com.google.protobuf:protobuf-java-util:3.17.2") + + implementation("run.mone:opentelemetry-sdk-metrics") + implementation("io.prometheus:simpleclient:0.11.0") + +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/javaagent-extension-api.gradle b/opentelemetry-java-instrumentation/javaagent-extension-api/javaagent-extension-api.gradle new file mode 100644 index 000000000..54b0356e4 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/javaagent-extension-api.gradle @@ -0,0 +1,28 @@ +group = 'io.opentelemetry.javaagent' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +configurations { + // classpath used by the instrumentation muzzle plugin + instrumentationMuzzle { + canBeConsumed = true + canBeResolved = false + extendsFrom api, implementation + } +} + +dependencies { + api "run.mone:opentelemetry-sdk" + // metrics are unstable, do not expose as api + implementation "run.mone:opentelemetry-sdk-metrics" + api "net.bytebuddy:byte-buddy" + api "org.slf4j:slf4j-api" + + implementation project(":instrumentation-api") + implementation project(":javaagent-api") + // TODO: ideally this module should not depend on bootstrap, bootstrap should be an internal component + implementation project(":javaagent-bootstrap") + + instrumentationMuzzle sourceSets.main.output +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/AgentExtension.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/AgentExtension.java new file mode 100644 index 000000000..5dfcc46f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/AgentExtension.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * An {@link AgentExtension} provides a way to modify/enrich the OpenTelemetry Javaagent behavior. + * It can be an {@link InstrumentationModule} or a completely custom implementation. Because an + * extension can heavily modify the javaagent's behavior extreme caution is advised. + * + *

This is a service provider interface that requires implementations to be registered in a + * provider-configuration file stored in the {@code META-INF/services} resource directory. + */ +public interface AgentExtension extends Ordered { + + /** + * Extend the passed {@code agentBuilder} with custom logic (e.g. instrumentation). + * + * @return The customized agent. Note that this method MUST return a non-null {@link AgentBuilder} + * instance that contains all customizations defined in this extension. + */ + AgentBuilder extend(AgentBuilder agentBuilder); + + /** + * Returns the name of the extension. It does not have to be unique, but it should be + * human-readable: javaagent uses the extension name in its logs. + */ + String extensionName(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/AgentListener.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/AgentListener.java new file mode 100644 index 000000000..007e6de52 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/AgentListener.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import java.lang.instrument.Instrumentation; +import java.util.Map; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * {@link AgentListener} can be used to execute code before/after Java agent installation, for + * example to install any implementation providers that are used by instrumentations. For instance, + * this project uses this SPI to install OpenTelemetry SDK. + * + *

This is a service provider interface that requires implementations to be registered in a + * provider-configuration file stored in the {@code META-INF/services} resource directory. + */ +public interface AgentListener extends Ordered { + + /** + * Runs before the {@link AgentBuilder} construction, before any instrumentation is added. + * + *

Execute only a minimal code because any classes loaded before the agent installation will + * have to be retransformed, which takes extra time, and more importantly means that fields can't + * be added to those classes - which causes {@link InstrumentationContext} to fall back to the + * less performant {@link Map} implementation for those classes. + */ + default void beforeAgent(Config config) {} + + /** + * Runs after instrumentations are added to {@link AgentBuilder} and after the agent is installed + * on an {@link Instrumentation}. + */ + default void afterAgent(Config config) {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/Ordered.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/Ordered.java new file mode 100644 index 000000000..76b7f0dd1 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/Ordered.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension; + +public interface Ordered { + /** + * Returns the order of applying the SPI implementing this interface. Higher values are added + * later, for example: an SPI with order=1 will run after an SPI with order=0. + */ + default int order() { + return 0; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/ignore/IgnoredTypesBuilder.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/ignore/IgnoredTypesBuilder.java new file mode 100644 index 000000000..ba33f6f39 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/ignore/IgnoredTypesBuilder.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.ignore; + +/** + * This interface defines different ways to ignore/allow instrumenting classes or packages. + * + *

This interface should not be implemented by the javaagent extension developer - the javaagent + * will provide the implementation. + */ +public interface IgnoredTypesBuilder { + + /** + * Ignore the class or package specified by {@code classNameOrPrefix} and exclude it from being + * instrumented. Calling this will overwrite any previous settings for passed prefix. + * + *

{@code classNameOrPrefix} can be the full class name (ex. {@code com.example.MyClass}), + * package name (ex. {@code com.example.mypackage.}), or outer class name (ex {@code + * com.example.OuterClass$}) + * + * @return {@code this} + */ + IgnoredTypesBuilder ignoreClass(String classNameOrPrefix); + + /** + * Allow the class or package specified by {@code classNameOrPrefix} to be instrumented. Calling + * this will overwrite any previous settings for passed prefix; in particular, calling this method + * will override any previous {@link #ignoreClass(String)} setting. + * + *

{@code classNameOrPrefix} can be the full class name (ex. {@code com.example.MyClass}), + * package name (ex. {@code com.example.mypackage.}), or outer class name (ex {@code + * com.example.OuterClass$}) + * + * @return {@code this} + */ + IgnoredTypesBuilder allowClass(String classNameOrPrefix); + + /** + * Ignore the class loader specified by {@code classNameOrPrefix} and exclude it from being + * instrumented. Calling this will overwrite any previous settings for passed prefix. + * + *

{@code classNameOrPrefix} can be the full class name (ex. {@code com.example.MyClass}), + * package name (ex. {@code com.example.mypackage.}), or outer class name (ex {@code + * com.example.OuterClass$}) + * + * @return {@code this} + */ + IgnoredTypesBuilder ignoreClassLoader(String classNameOrPrefix); + + /** + * Allow the class loader specified by {@code classNameOrPrefix} to be instrumented. Calling this + * will overwrite any previous settings for passed prefix; in particular, calling this method will + * override any previous {@link #ignoreClassLoader(String)} setting. + * + *

{@code classNameOrPrefix} can be the full class name (ex. {@code com.example.MyClass}), + * package name (ex. {@code com.example.mypackage.}), or outer class name (ex {@code + * com.example.OuterClass$}) + * + * @return {@code this} + */ + IgnoredTypesBuilder allowClassLoader(String classNameOrPrefix); + + /** + * Ignore the Java concurrent task class specified by {@code classNameOrPrefix} and exclude it + * from being instrumented. Concurrent task classes implement or extend one of the following + * classes: + * + *

    + *
  • {@link java.lang.Runnable} + *
  • {@link java.util.concurrent.Callable} + *
  • {@link java.util.concurrent.ForkJoinTask} + *
  • {@link java.util.concurrent.Future} + *
+ * + *

Calling this will overwrite any previous settings for passed prefix. + * + *

{@code classNameOrPrefix} can be the full class name (ex. {@code com.example.MyClass}), + * package name (ex. {@code com.example.mypackage.}), or outer class name (ex {@code + * com.example.OuterClass$}) + * + * @return {@code this} + */ + IgnoredTypesBuilder ignoreTaskClass(String classNameOrPrefix); + + /** + * Allow the Java concurrent task class specified by {@code classNameOrPrefix} to be instrumented. + * Concurrent task classes implement or extend one of the following classes: + * + *

    + *
  • {@link java.lang.Runnable} + *
  • {@link java.util.concurrent.Callable} + *
  • {@link java.util.concurrent.ForkJoinTask} + *
  • {@link java.util.concurrent.Future} + *
+ * + *

Calling this will will overwrite any previous settings for passed prefix; in particular, + * calling this method will override any previous {@link #ignoreTaskClass(String)} setting. + * + *

{@code classNameOrPrefix} can be the full class name (ex. {@code com.example.MyClass}), + * package name (ex. {@code com.example.mypackage.}), or outer class name (ex {@code + * com.example.OuterClass$}) + * + * @return {@code this} + */ + IgnoredTypesBuilder allowTaskClass(String classNameOrPrefix); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/ignore/IgnoredTypesConfigurer.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/ignore/IgnoredTypesConfigurer.java new file mode 100644 index 000000000..35972ea80 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/ignore/IgnoredTypesConfigurer.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.ignore; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.Ordered; + +/** + * An {@link IgnoredTypesConfigurer} can be used to augment built-in instrumentation restrictions: + * ignore some classes and exclude them from being instrumented, or explicitly allow them to be + * instrumented if the agent ignored them by default. + * + *

This is a service provider interface that requires implementations to be registered in a + * provider-configuration file stored in the {@code META-INF/services} resource directory. + */ +public interface IgnoredTypesConfigurer extends Ordered { + + /** + * Configure the passed {@code builder} and define which classes should be ignored when + * instrumenting. + */ + void configure(Config config, IgnoredTypesBuilder builder); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModule.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModule.java new file mode 100644 index 000000000..bd132b299 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModule.java @@ -0,0 +1,189 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation; + +import static java.util.Arrays.asList; +import static net.bytebuddy.matcher.ElementMatchers.any; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.Ordered; +import io.opentelemetry.javaagent.extension.muzzle.ClassRef; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Instrumentation module groups several connected {@link TypeInstrumentation}s together, sharing + * classloader matcher, helper classes, muzzle safety checks, etc. Ideally all types in a single + * instrumented library should live in a single module. + * + *

Classes extending {@link InstrumentationModule} should be public and non-final so that it's + * possible to extend and reuse them in vendor distributions. + * + *

{@link InstrumentationModule} is an SPI, you need to ensure that a proper {@code + * META-INF/services/} provider file is created for it to be picked up by the agent. See {@link + * java.util.ServiceLoader} for more details. + */ +public abstract class InstrumentationModule implements Ordered { + private static final boolean DEFAULT_ENABLED = + Config.get().getBoolean("otel.instrumentation.common.default-enabled", true); + public static final List EXCLUDE_MODULE = + Config.get().getList("otel.instrumentation.exclude.module"); + + private final Set instrumentationNames; + + /** + * Creates an instrumentation module. Note that all implementations of {@link + * InstrumentationModule} must have a default constructor (for SPI), so they have to pass the + * instrumentation names to the super class constructor. + * + *

The instrumentation names should follow several rules: + * + *

    + *
  • Instrumentation names should consist of hyphen-separated words, e.g. {@code + * instrumented-library}; + *
  • In general, instrumentation names should be the as close as possible to the gradle module + * name - which in turn should be as close as possible to the instrumented library name; + *
  • The main instrumentation name should be the same as the gradle module name, minus the + * version if it's a part of the module name. When several versions of a library are + * instrumented they should all share the same main instrumentation name so that it's easy + * to enable/disable the instrumentation regardless of the runtime library version; + *
  • If the gradle module has a version as a part of its name, an additional instrumentation + * name containing the version should be passed, e.g. {@code instrumented-library-1.0}. + *
+ */ + protected InstrumentationModule( + String mainInstrumentationName, String... additionalInstrumentationNames) { + this(toList(mainInstrumentationName, additionalInstrumentationNames)); + } + + /** + * Creates an instrumentation module. + * + * @see #InstrumentationModule(String, String...) + */ + protected InstrumentationModule(List instrumentationNames) { + if (instrumentationNames.isEmpty()) { + throw new IllegalArgumentException("InstrumentationModules must be named"); + } + this.instrumentationNames = new LinkedHashSet<>(instrumentationNames); + } + + private static List toList(String first, String[] rest) { + List instrumentationNames = new ArrayList<>(rest.length + 1); + instrumentationNames.add(first); + instrumentationNames.addAll(asList(rest)); + return instrumentationNames; + } + + /** + * Returns the main instrumentation name. See {@link #InstrumentationModule(String, String...)} + * for more details about instrumentation names. + */ + public final String instrumentationName() { + return instrumentationNames.iterator().next(); + } + + /** Returns true if this instrumentation module should be installed. */ + public final boolean isEnabled() { + return Config.get().isInstrumentationEnabled(instrumentationNames, defaultEnabled()); + } + + /** + * Allows instrumentation modules to disable themselves by default, or to additionally disable + * themselves on some other condition. + */ + protected boolean defaultEnabled() { + return DEFAULT_ENABLED; + } + + /** + * Instrumentation modules can override this method to specify additional packages (or classes) + * that should be treated as "library instrumentation" packages. Classes from those packages will + * be treated by muzzle as instrumentation helper classes: they will be scanned for references and + * automatically injected into the application classloader if they're used in any type + * instrumentation. The classes for which this predicate returns {@code true} will be treated as + * helper classes, in addition to the default ones defined in the {@code + * InstrumentationClassPredicate} class. + * + * @param className The name of the class that may or may not be a helper class. + */ + public boolean isHelperClass(String className) { + return false; + } + + /** Returns a list of resource names to inject into the user's classloader. */ + public List helperResourceNames() { + return Collections.emptyList(); + } + + /** + * An instrumentation module can implement this method to make sure that the classloader contains + * the particular library version. It is useful to implement that if the muzzle check does not + * fail for versions out of the instrumentation's scope. + * + *

E.g. supposing version 1.0 has class {@code A}, but it was removed in version 2.0; A is not + * used in the helper classes at all; this module is instrumenting 2.0: this method will return + * {@code not(hasClassesNamed("A"))}. + * + * @return A type matcher used to match the classloader under transform + */ + public ElementMatcher.Junction classLoaderMatcher() { + return any(); + } + + /** Returns a list of all individual type instrumentation in this module. */ + public abstract List typeInstrumentations(); + + /** + * Returns references to helper and library classes used in this module's type instrumentation + * advices, grouped by {@link ClassRef#getClassName()}. + * + *

The actual implementation of this method is generated automatically during compilation by + * the {@code io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin} + * ByteBuddy plugin. + * + *

This method is generated automatically: if you override it, the muzzle compile plugin + * will not generate a new implementation, it will leave the existing one. + */ + public Map getMuzzleReferences() { + return Collections.emptyMap(); + } + + /** + * Returns a list of instrumentation helper classes, automatically detected by muzzle during + * compilation. Those helpers will be injected into the application classloader. + * + *

The actual implementation of this method is generated automatically during compilation by + * the {@code io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin} + * ByteBuddy plugin. + * + *

This method is generated automatically: if you override it, the muzzle compile plugin + * will not generate a new implementation, it will leave the existing one. + */ + public List getMuzzleHelperClassNames() { + return Collections.emptyList(); + } + + /** + * Returns a map of {@code class-name to context-class-name}. Keys (and their subclasses) will be + * associated with a context class stored in the value. + * + *

The actual implementation of this method is generated automatically during compilation by + * the {@code io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin} + * ByteBuddy plugin. + * + *

This method is generated automatically: if you override it, the muzzle compile plugin + * will not generate a new implementation, it will leave the existing one. + */ + public Map getMuzzleContextStoreClasses() { + return Collections.emptyMap(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeInstrumentation.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeInstrumentation.java new file mode 100644 index 000000000..e77a4206e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeInstrumentation.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation; + +import static net.bytebuddy.matcher.ElementMatchers.any; + +import io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +/** + * Interface representing a single type instrumentation. Part of an {@link InstrumentationModule}. + * + *

Classes implementing {@link TypeInstrumentation} should be public and non-final so that it's + * possible to extend and reuse them in vendor distributions. + */ +public interface TypeInstrumentation { + /** + * An optimization to short circuit matching in the case where the instrumented library is not + * even present on the class path. + * + *

Most applications have only a small subset of libraries on their class path, so this ends up + * being a very useful optimization. + * + *

Some background on type matcher performance: + * + *

Type matchers that only match against the type name are fast, e.g. {@link + * ElementMatchers#named(String)}. + * + *

All other type matchers require some level of bytecode inspection, e.g. {@link + * ElementMatchers#isAnnotatedWith(ElementMatcher)}. + * + *

Type matchers that need to inspect the super class hierarchy are even more expensive, e.g. + * {@link AgentElementMatchers#implementsInterface(ElementMatcher)}. This is because they require + * inspecting multiple super classes/interfaces as well (which may not even be loaded yet in which + * case their bytecode has to be read and inspected). + * + * @return A type matcher that rejects classloaders that do not contain desired interfaces or base + * classes. + */ + default ElementMatcher classLoaderOptimization() { + return any(); + } + + /** + * Returns a type matcher defining which classes should undergo transformations defined in the + * {@link #transform(TypeTransformer)} method. + */ + ElementMatcher typeMatcher(); + + /** + * Define transformations that should be applied to classes matched by {@link #typeMatcher()}, for + * example: apply advice classes to chosen methods ({@link + * TypeTransformer#applyAdviceToMethod(ElementMatcher, String)}. + */ + void transform(TypeTransformer transformer); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeTransformer.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeTransformer.java new file mode 100644 index 000000000..52d51312e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeTransformer.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This interface represents type transformations that can be applied to a type instrumented using + * {@link TypeInstrumentation}. + * + *

This interface should not be implemented by the javaagent extension developer - the javaagent + * will provide the implementation of all transformations described here. + */ +public interface TypeTransformer { + /** + * Apply the advice class named {@code adviceClassName} to the instrumented type methods that + * match {@code methodMatcher}. + */ + void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName); + + /** + * Apply a custom ByteBuddy {@link AgentBuilder.Transformer} to the instrumented type. Note that + * since this is a completely custom transformer, muzzle won't be able to scan for references or + * helper classes. + */ + void applyTransformer(AgentBuilder.Transformer transformer); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/AgentElementMatchers.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/AgentElementMatchers.java new file mode 100644 index 000000000..ddfb4846a --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/AgentElementMatchers.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.matcher; + +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This class provides some custom ByteBuddy element matchers to use when applying instrumentation. + */ +public final class AgentElementMatchers { + + public static ElementMatcher.Junction extendsClass( + ElementMatcher matcher) { + return not(isInterface()).and(new SafeExtendsClassMatcher(new SafeErasureMatcher<>(matcher))); + } + + public static ElementMatcher.Junction implementsInterface( + ElementMatcher matcher) { + return new SafeHasSuperTypeMatcher( + new SafeErasureMatcher<>(matcher), /* interfacesOnly= */ true); + } + + public static ElementMatcher.Junction safeHasSuperType( + ElementMatcher matcher) { + return new SafeHasSuperTypeMatcher( + new SafeErasureMatcher<>(matcher), /* interfacesOnly= */ false); + } + + /** + * Matches method's declaring class against a given type matcher. + * + * @param matcher type matcher to match method's declaring type against. + * @param Type of the matched object + * @return a matcher that matches method's declaring class against a given type matcher. + */ + public static ElementMatcher.Junction methodIsDeclaredByType( + ElementMatcher matcher) { + return new MethodDeclaringTypeMatcher<>(matcher); + } + + /** + * Matches a method and all its declarations up the class hierarchy including interfaces using + * provided matcher. + * + * @param matcher method matcher to apply to method declarations up the hierarchy. + * @param Type of the matched object + * @return A matcher that matches a method and all its declarations up the class hierarchy + * including interfaces. + */ + public static ElementMatcher.Junction hasSuperMethod( + ElementMatcher matcher) { + return new HasSuperMethodMatcher<>(matcher); + } + + /** + * Wraps another matcher to assure that an element is not matched in case that the matching causes + * an {@link Exception}. Logs exception if it happens. + * + * @param matcher The element matcher that potentially throws an exception. + * @param The type of the matched object. + * @return A matcher that returns {@code false} in case that the given matcher throws an + * exception. + */ + public static ElementMatcher.Junction failSafe( + ElementMatcher matcher, String description) { + return new LoggingFailSafeMatcher<>(matcher, /* fallback= */ false, description); + } + + static String safeTypeDefinitionName(TypeDefinition td) { + try { + return td.getTypeName(); + } catch (IllegalStateException ex) { + String message = ex.getMessage(); + if (message.startsWith("Cannot resolve type description for ")) { + return message.replace("Cannot resolve type description for ", ""); + } else { + return "?"; + } + } + } + + private AgentElementMatchers() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderMatcher.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderMatcher.java new file mode 100644 index 000000000..84c291d97 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderMatcher.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.matcher; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import io.opentelemetry.javaagent.bootstrap.ClassLoaderMatcherCacheHolder; +import io.opentelemetry.javaagent.instrumentation.api.internal.InClassLoaderMatcher; +import net.bytebuddy.matcher.ElementMatcher; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class ClassLoaderMatcher { + + @Nullable public static final ClassLoader BOOTSTRAP_CLASSLOADER = null; + + /** A private constructor that must not be invoked. */ + private ClassLoaderMatcher() { + throw new UnsupportedOperationException(); + } + + /** + * NOTICE: Does not match the bootstrap classpath. Don't use with classes expected to be on the + * bootstrap. + * + * @param classNames list of names to match. returns true if empty. + * @return true if class is available as a resource and not the bootstrap classloader. + */ + public static ElementMatcher.Junction.AbstractBase hasClassesNamed( + String... classNames) { + return new ClassLoaderHasClassesNamedMatcher(classNames); + } + + private static class ClassLoaderHasClassesNamedMatcher + extends ElementMatcher.Junction.AbstractBase { + + private final Cache cache = + Cache.newBuilder().setWeakKeys().setMaximumSize(25).build(); + + private final String[] resources; + + private ClassLoaderHasClassesNamedMatcher(String... classNames) { + resources = classNames; + for (int i = 0; i < resources.length; i++) { + resources[i] = resources[i].replace(".", "/") + ".class"; + } + ClassLoaderMatcherCacheHolder.addCache(cache); + } + + private boolean hasResources(ClassLoader cl) { + boolean priorValue = InClassLoaderMatcher.getAndSet(true); + try { + for (String resource : resources) { + if (cl.getResource(resource) == null) { + return false; + } + } + } finally { + InClassLoaderMatcher.set(priorValue); + } + return true; + } + + @Override + public boolean matches(ClassLoader cl) { + if (cl == BOOTSTRAP_CLASSLOADER) { + // Can't match the bootstrap classloader. + return false; + } + return cache.computeIfAbsent(cl, this::hasResources); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/HasSuperMethodMatcher.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/HasSuperMethodMatcher.java new file mode 100644 index 000000000..bbbe553f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/HasSuperMethodMatcher.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.matcher; + +import static io.opentelemetry.javaagent.extension.matcher.SafeHasSuperTypeMatcher.safeGetSuperClass; +import static net.bytebuddy.matcher.ElementMatchers.hasSignature; + +import java.util.HashSet; +import java.util.Set; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeList; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Matches a method and all its declarations up the class hierarchy including interfaces using + * provided matcher. + * + * @param Type of the matched method. + */ +class HasSuperMethodMatcher + extends ElementMatcher.Junction.AbstractBase { + + private final ElementMatcher matcher; + + public HasSuperMethodMatcher(ElementMatcher matcher) { + this.matcher = matcher; + } + + @Override + public boolean matches(MethodDescription target) { + if (target.isConstructor()) { + return false; + } + Junction signatureMatcher = hasSignature(target.asSignatureToken()); + TypeDefinition declaringType = target.getDeclaringType(); + Set checkedInterfaces = new HashSet<>(8); + + while (declaringType != null) { + for (MethodDescription methodDescription : declaringType.getDeclaredMethods()) { + if (signatureMatcher.matches(methodDescription) && matcher.matches(methodDescription)) { + return true; + } + } + if (matchesInterface(declaringType.getInterfaces(), signatureMatcher, checkedInterfaces)) { + return true; + } + declaringType = safeGetSuperClass(declaringType); + } + return false; + } + + private boolean matchesInterface( + TypeList.Generic interfaces, + Junction signatureMatcher, + Set checkedInterfaces) { + for (TypeDefinition type : interfaces) { + if (checkedInterfaces.add(type)) { + for (MethodDescription methodDescription : type.getDeclaredMethods()) { + if (signatureMatcher.matches(methodDescription) && matcher.matches(methodDescription)) { + return true; + } + } + if (matchesInterface(type.getInterfaces(), signatureMatcher, checkedInterfaces)) { + return true; + } + } + } + return false; + } + + @Override + public String toString() { + return "hasSuperMethodMatcher(" + matcher + ")"; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof HasSuperMethodMatcher)) { + return false; + } + HasSuperMethodMatcher other = (HasSuperMethodMatcher) obj; + return matcher.equals(other.matcher); + } + + @Override + public int hashCode() { + return matcher.hashCode(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/LoggingFailSafeMatcher.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/LoggingFailSafeMatcher.java new file mode 100644 index 000000000..ae21a16f6 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/LoggingFailSafeMatcher.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.matcher; + +import java.util.Objects; +import net.bytebuddy.matcher.ElementMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A fail-safe matcher catches exceptions that are thrown by a delegate matcher and returns an + * alternative value. + * + *

Logs exception if it was thrown. + * + * @param The type of the matched entity. + * @see net.bytebuddy.matcher.FailSafeMatcher + */ +class LoggingFailSafeMatcher extends ElementMatcher.Junction.AbstractBase { + + private static final Logger log = LoggerFactory.getLogger(LoggingFailSafeMatcher.class); + + /** The delegate matcher that might throw an exception. */ + private final ElementMatcher matcher; + + /** The fallback value in case of an exception. */ + private final boolean fallback; + + /** The text description to log if exception happens. */ + private final String description; + + /** + * Creates a new fail-safe element matcher. + * + * @param matcher The delegate matcher that might throw an exception. + * @param fallback The fallback value in case of an exception. + * @param description Descriptive string to log along with exception. + */ + public LoggingFailSafeMatcher( + ElementMatcher matcher, boolean fallback, String description) { + this.matcher = matcher; + this.fallback = fallback; + this.description = description; + } + + @Override + public boolean matches(T target) { + try { + return matcher.matches(target); + } catch (Throwable e) { + log.debug(description, e); + return fallback; + } + } + + @Override + public String toString() { + return "failSafe(try(" + matcher + ") or " + fallback + ")"; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof LoggingFailSafeMatcher)) { + return false; + } + LoggingFailSafeMatcher other = (LoggingFailSafeMatcher) obj; + return fallback == other.fallback && matcher.equals(other.matcher); + } + + @Override + public int hashCode() { + return Objects.hash(fallback, matcher); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/MethodDeclaringTypeMatcher.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/MethodDeclaringTypeMatcher.java new file mode 100644 index 000000000..2a243e7f1 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/MethodDeclaringTypeMatcher.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.matcher; + +import java.util.Objects; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Matches method's declaring class against a given type matcher. + * + * @param Type of the matched object + */ +class MethodDeclaringTypeMatcher + extends ElementMatcher.Junction.AbstractBase { + + private final ElementMatcher matcher; + + MethodDeclaringTypeMatcher(ElementMatcher matcher) { + this.matcher = matcher; + } + + @Override + public boolean matches(T target) { + return matcher.matches(target.getDeclaringType().asErasure()); + } + + @Override + public String toString() { + return "methodDeclaringTypeMatcher(matcher=" + matcher + ')'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodDeclaringTypeMatcher)) { + return false; + } + MethodDeclaringTypeMatcher that = (MethodDeclaringTypeMatcher) o; + return Objects.equals(matcher, that.matcher); + } + + @Override + public int hashCode() { + return Objects.hash(matcher); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeErasureMatcher.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeErasureMatcher.java new file mode 100644 index 000000000..a2c6420d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeErasureMatcher.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.matcher; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeTypeDefinitionName; + +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An element matcher that matches its argument's {@link TypeDescription.Generic} raw type against + * the given matcher for a {@link TypeDescription}. As a wildcard does not define an erasure, a + * runtime exception is thrown when this matcher is applied to a wildcard. + * + *

Catches and logs exception if it was thrown when getting erasure, returning false. + * + * @param The type of the matched entity. + * @see net.bytebuddy.matcher.ErasureMatcher + */ +class SafeErasureMatcher extends ElementMatcher.Junction.AbstractBase { + + private static final Logger log = LoggerFactory.getLogger(SafeErasureMatcher.class); + + /** The matcher to apply to the raw type of the matched element. */ + private final ElementMatcher matcher; + + /** + * Creates a new erasure matcher. + * + * @param matcher The matcher to apply to the raw type. + */ + public SafeErasureMatcher(ElementMatcher matcher) { + this.matcher = matcher; + } + + @Override + public boolean matches(T target) { + TypeDescription erasure = safeAsErasure(target); + if (erasure == null) { + return false; + } else { + // We would like matcher exceptions to propagate + return matcher.matches(erasure); + } + } + + static TypeDescription safeAsErasure(TypeDefinition typeDefinition) { + try { + return typeDefinition.asErasure(); + } catch (Throwable e) { + if (log.isDebugEnabled()) { + log.debug( + "{} trying to get erasure for target {}: {}", + e.getClass().getSimpleName(), + safeTypeDefinitionName(typeDefinition), + e.getMessage()); + } + return null; + } + } + + @Override + public String toString() { + return "safeErasure(" + matcher + ")"; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SafeErasureMatcher)) { + return false; + } + SafeErasureMatcher other = (SafeErasureMatcher) obj; + return matcher.equals(other.matcher); + } + + @Override + public int hashCode() { + return matcher.hashCode(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeExtendsClassMatcher.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeExtendsClassMatcher.java new file mode 100644 index 000000000..6697afb9c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeExtendsClassMatcher.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.matcher; + +import static io.opentelemetry.javaagent.extension.matcher.SafeHasSuperTypeMatcher.safeGetSuperClass; + +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// TODO: add javadoc +class SafeExtendsClassMatcher extends ElementMatcher.Junction.AbstractBase { + + private final ElementMatcher matcher; + + public SafeExtendsClassMatcher(ElementMatcher matcher) { + this.matcher = matcher; + } + + @Override + public boolean matches(TypeDescription target) { + // We do not use foreach loop and iterator interface here because we need to catch exceptions + // in {@code getSuperClass} calls + TypeDefinition typeDefinition = target; + while (typeDefinition != null) { + if (matcher.matches(typeDefinition.asGenericType())) { + return true; + } + typeDefinition = safeGetSuperClass(typeDefinition); + } + return false; + } + + @Override + public String toString() { + return "safeExtendsClass(" + matcher + ")"; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SafeExtendsClassMatcher)) { + return false; + } + SafeExtendsClassMatcher other = (SafeExtendsClassMatcher) obj; + return matcher.equals(other.matcher); + } + + @Override + public int hashCode() { + return matcher.hashCode(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeHasSuperTypeMatcher.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeHasSuperTypeMatcher.java new file mode 100644 index 000000000..77dbf7b56 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/SafeHasSuperTypeMatcher.java @@ -0,0 +1,204 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.matcher; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeTypeDefinitionName; +import static io.opentelemetry.javaagent.extension.matcher.SafeErasureMatcher.safeAsErasure; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An element matcher that matches a super type. This is different from {@link + * net.bytebuddy.matcher.HasSuperTypeMatcher} in the following way: + * + *

    + *
  • Exceptions are logged + *
  • When exception happens the rest of the inheritance subtree is discarded (since ByteBuddy + * cannot load/parse type information for it) but search in other subtrees continues + *
+ * + *

This is useful because this allows us to see when matcher's check is not complete (i.e. part + * of it fails), at the same time it makes best effort instead of failing quickly (like {@code + * failSafe(hasSuperType(...))} does) which means the code is more resilient to classpath + * inconsistencies + * + * @see net.bytebuddy.matcher.HasSuperTypeMatcher + */ +class SafeHasSuperTypeMatcher extends ElementMatcher.Junction.AbstractBase { + + private static final Logger log = LoggerFactory.getLogger(SafeHasSuperTypeMatcher.class); + + /** The matcher to apply to any super type of the matched type. */ + private final ElementMatcher matcher; + + private final boolean interfacesOnly; + + /** + * Creates a new matcher for a super type. + * + * @param matcher The matcher to apply to any super type of the matched type. + */ + public SafeHasSuperTypeMatcher( + ElementMatcher matcher, boolean interfacesOnly) { + this.matcher = matcher; + this.interfacesOnly = interfacesOnly; + } + + @Override + public boolean matches(TypeDescription target) { + Set checkedInterfaces = new HashSet<>(8); + // We do not use foreach loop and iterator interface here because we need to catch exceptions + // in {@code getSuperClass} calls + TypeDefinition typeDefinition = target; + while (typeDefinition != null) { + if (((!interfacesOnly || typeDefinition.isInterface()) + && matcher.matches(typeDefinition.asGenericType())) + || hasInterface(typeDefinition, checkedInterfaces)) { + return true; + } + typeDefinition = safeGetSuperClass(typeDefinition); + } + return false; + } + + /** + * Matches a type's interfaces against the provided matcher. + * + * @param typeDefinition The type for which to check all implemented interfaces. + * @param checkedInterfaces The interfaces that have already been checked. + * @return {@code true} if any interface matches the supplied matcher. + */ + private boolean hasInterface( + TypeDefinition typeDefinition, Set checkedInterfaces) { + for (TypeDefinition interfaceType : safeGetInterfaces(typeDefinition)) { + TypeDescription erasure = safeAsErasure(interfaceType); + if (erasure != null) { + if (checkedInterfaces.add(interfaceType.asErasure()) + && (matcher.matches(interfaceType.asGenericType()) + || hasInterface(interfaceType, checkedInterfaces))) { + return true; + } + } + } + return false; + } + + private static Iterable safeGetInterfaces(TypeDefinition typeDefinition) { + return new SafeInterfaceIterator(typeDefinition); + } + + static TypeDefinition safeGetSuperClass(TypeDefinition typeDefinition) { + try { + return typeDefinition.getSuperClass(); + } catch (Throwable e) { + if (log.isDebugEnabled()) { + log.debug( + "{} trying to get super class for target {}: {}", + e.getClass().getSimpleName(), + safeTypeDefinitionName(typeDefinition), + e.getMessage()); + } + return null; + } + } + + @Override + public String toString() { + return "safeHasSuperType(" + matcher + ")"; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SafeHasSuperTypeMatcher)) { + return false; + } + SafeHasSuperTypeMatcher other = (SafeHasSuperTypeMatcher) obj; + return matcher.equals(other.matcher); + } + + @Override + public int hashCode() { + return matcher.hashCode(); + } + + /** + * TypeDefinition#getInterfaces() produces an iterator which may throw an exception during + * iteration if an interface is absent from the classpath. + * + *

The caller MUST call hasNext() before calling next(). + * + *

This wrapper exists to allow getting interfaces even if the lookup on one fails. + */ + // Private class, let's save the allocation + @SuppressWarnings("IterableAndIterator") + private static class SafeInterfaceIterator + implements Iterator, Iterable { + private final TypeDefinition typeDefinition; + @Nullable private final Iterator it; + private TypeDefinition next; + + private SafeInterfaceIterator(TypeDefinition typeDefinition) { + this.typeDefinition = typeDefinition; + Iterator it = null; + try { + it = typeDefinition.getInterfaces().iterator(); + } catch (Throwable e) { + logException(typeDefinition, e); + } + this.it = it; + } + + @Override + public boolean hasNext() { + if (null != it && it.hasNext()) { + try { + next = it.next(); + return true; + } catch (Throwable e) { + logException(typeDefinition, e); + return false; + } + } + return false; + } + + @Override + public TypeDefinition next() { + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator iterator() { + return this; + } + + private static void logException(TypeDefinition typeDefinition, Throwable e) { + if (log.isDebugEnabled()) { + log.debug( + "{} trying to get interfaces for target {}: {}", + e.getClass().getSimpleName(), + safeTypeDefinitionName(typeDefinition), + e.getMessage()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ClassRef.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ClassRef.java new file mode 100644 index 000000000..65bd6fe1a --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ClassRef.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.muzzle; + +import static io.opentelemetry.javaagent.extension.muzzle.ReferenceMergeUtil.mergeFields; +import static io.opentelemetry.javaagent.extension.muzzle.ReferenceMergeUtil.mergeFlags; +import static io.opentelemetry.javaagent.extension.muzzle.ReferenceMergeUtil.mergeMethods; +import static io.opentelemetry.javaagent.extension.muzzle.ReferenceMergeUtil.mergeSet; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Represents a reference to a class used in the instrumentation advice or helper class code (or the + * helper class itself). + * + *

This class is used in the auto-generated {@link InstrumentationModule#getMuzzleReferences()} + * method, it is not meant to be used directly by agent extension developers. + */ +public final class ClassRef { + + private final Set sources; + private final Set flags; + private final String className; + private final String superClassName; + private final Set interfaceNames; + private final Set fields; + private final Set methods; + + ClassRef( + Set sources, + Set flags, + String className, + String superClassName, + Set interfaceNames, + Set fields, + Set methods) { + this.sources = sources; + this.flags = flags; + this.className = className; + this.superClassName = superClassName; + this.interfaceNames = interfaceNames; + this.fields = fields; + this.methods = methods; + } + + /** Start building a new {@linkplain ClassRef reference}. */ + public static ClassRefBuilder newBuilder(String className) { + return new ClassRefBuilder(className); + } + + /** Returns information about code locations where this class was referenced. */ + public Set getSources() { + return sources; + } + + /** Returns modifier flags of this class. */ + public Set getFlags() { + return flags; + } + + /** Returns the name of this class. */ + public String getClassName() { + return className; + } + + /** Returns the name of the super class, if this class extends one; null otherwise. */ + @Nullable + public String getSuperClassName() { + return superClassName; + } + + /** Returns the set of interfaces implemented by this class. */ + public Set getInterfaceNames() { + return interfaceNames; + } + + /** Returns the set of references to fields of this class. */ + public Set getFields() { + return fields; + } + + /** Returns the set of references to methods of this class. */ + public Set getMethods() { + return methods; + } + + /** + * Create a new reference which combines this reference with another reference of the same type. + * + * @param anotherReference A reference to the same class. + * @return a new {@linkplain ClassRef reference} which merges the two references. + */ + public ClassRef merge(ClassRef anotherReference) { + if (!anotherReference.getClassName().equals(className)) { + throw new IllegalStateException("illegal merge " + this + " != " + anotherReference); + } + String superName = + null == this.superClassName ? anotherReference.superClassName : this.superClassName; + + return new ClassRef( + mergeSet(sources, anotherReference.sources), + mergeFlags(flags, anotherReference.flags), + className, + superName, + mergeSet(interfaceNames, anotherReference.interfaceNames), + mergeFields(fields, anotherReference.fields), + mergeMethods(methods, anotherReference.methods)); + } + + @Override + public String toString() { + String extendsPart = superClassName == null ? "" : " extends " + superClassName; + String implementsPart = + interfaceNames.isEmpty() ? "" : " implements " + String.join(", ", interfaceNames); + return getClass().getSimpleName() + ": " + className + extendsPart + implementsPart; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ClassRefBuilder.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ClassRefBuilder.java new file mode 100644 index 000000000..d21ba1180 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ClassRefBuilder.java @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.muzzle; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import net.bytebuddy.jar.asm.Type; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * The builder of {@link ClassRef}. + * + *

This class is used in the auto-generated {@link InstrumentationModule#getMuzzleReferences()} + * method, it is not meant to be used directly by agent extension developers. + */ +public final class ClassRefBuilder { + + // this could be exposed as a system property if needed, but for now it's just helpful to be able + // to change manually here when reviewing/optimizing the generated getMuzzleReferences() method + static final boolean COLLECT_SOURCES = true; + + private final Set sources = new LinkedHashSet<>(); + private final Set flags = new LinkedHashSet<>(); + private final String className; + private final Set interfaceNames = new LinkedHashSet<>(); + private final List fields = new ArrayList<>(); + private final List methods = new ArrayList<>(); + + @Nullable private String superClassName = null; + + ClassRefBuilder(String className) { + this.className = className; + } + + public ClassRefBuilder setSuperClassName(String superName) { + this.superClassName = superName; + return this; + } + + public ClassRefBuilder addInterfaceNames(Collection interfaceNames) { + this.interfaceNames.addAll(interfaceNames); + return this; + } + + public ClassRefBuilder addInterfaceName(String interfaceName) { + interfaceNames.add(interfaceName); + return this; + } + + public ClassRefBuilder addSource(String sourceName) { + return addSource(sourceName, 0); + } + + public ClassRefBuilder addSource(String sourceName, int line) { + if (COLLECT_SOURCES) { + sources.add(new Source(sourceName, line)); + } + return this; + } + + public ClassRefBuilder addFlag(Flag flag) { + flags.add(flag); + return this; + } + + public ClassRefBuilder addField( + Source[] fieldSources, + Flag[] fieldFlags, + String fieldName, + Type fieldType, + boolean isFieldDeclared) { + FieldRef field = + new FieldRef( + COLLECT_SOURCES ? new LinkedHashSet<>(asList(fieldSources)) : emptySet(), + new LinkedHashSet<>(asList(fieldFlags)), + fieldName, + fieldType.getDescriptor(), + isFieldDeclared); + + int existingIndex = fields.indexOf(field); + if (existingIndex == -1) { + fields.add(field); + } else { + fields.set(existingIndex, field.merge(fields.get(existingIndex))); + } + return this; + } + + public ClassRefBuilder addMethod( + Source[] methodSources, + Flag[] methodFlags, + String methodName, + Type methodReturnType, + Type... methodArgumentTypes) { + MethodRef method = + new MethodRef( + COLLECT_SOURCES ? new LinkedHashSet<>(asList(methodSources)) : emptySet(), + new LinkedHashSet<>(asList(methodFlags)), + methodName, + Type.getMethodDescriptor(methodReturnType, methodArgumentTypes)); + + int existingIndex = methods.indexOf(method); + if (existingIndex == -1) { + methods.add(method); + } else { + methods.set(existingIndex, method.merge(methods.get(existingIndex))); + } + return this; + } + + public ClassRef build() { + return new ClassRef( + sources, + flags, + className, + superClassName, + interfaceNames, + new LinkedHashSet<>(fields), + new LinkedHashSet<>(methods)); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/FieldRef.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/FieldRef.java new file mode 100644 index 000000000..5cab26142 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/FieldRef.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.muzzle; + +import static io.opentelemetry.javaagent.extension.muzzle.ReferenceMergeUtil.mergeFlags; +import static io.opentelemetry.javaagent.extension.muzzle.ReferenceMergeUtil.mergeSet; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import java.util.Set; +import java.util.stream.Collectors; +import net.bytebuddy.jar.asm.Type; + +/** + * Represents a reference to a field used in the instrumentation advice or helper class code. Part + * of a {@link ClassRef}. + * + *

This class is used in the auto-generated {@link InstrumentationModule#getMuzzleReferences()} + * method, it is not meant to be used directly by agent extension developers. + */ +public final class FieldRef { + private final Set sources; + private final Set flags; + private final String name; + private final String descriptor; + private final boolean declared; + + FieldRef(Set sources, Set flags, String name, String descriptor, boolean declared) { + this.sources = sources; + this.flags = flags; + this.name = name; + this.descriptor = descriptor; + this.declared = declared; + } + + /** Returns information about code locations where this field was referenced. */ + public Set getSources() { + return sources; + } + + /** Returns modifier flags of this field. */ + public Set getFlags() { + return flags; + } + + /** Returns the field name. */ + public String getName() { + return name; + } + + /** Returns this field's type descriptor. */ + public String getDescriptor() { + return descriptor; + } + + /** + * Denotes whether this field is declared in the {@linkplain ClassRef class reference} it is a + * part of. If {@code false} then this field is just used and most likely is declared in the super + * class. + */ + public boolean isDeclared() { + return declared; + } + + FieldRef merge(FieldRef anotherField) { + if (!equals(anotherField) || !descriptor.equals(anotherField.descriptor)) { + throw new IllegalStateException("illegal merge " + this + " != " + anotherField); + } + return new FieldRef( + mergeSet(sources, anotherField.sources), + mergeFlags(flags, anotherField.flags), + name, + descriptor, + declared || anotherField.declared); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof FieldRef)) { + return false; + } + FieldRef other = (FieldRef) obj; + return name.equals(other.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + String modifiers = flags.stream().map(Flag::toString).collect(Collectors.joining(" ")); + String fieldType = Type.getType(getDescriptor()).getClassName(); + return getClass().getSimpleName() + ": " + modifiers + " " + fieldType + " " + name; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/Flag.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/Flag.java new file mode 100644 index 000000000..170f2be05 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/Flag.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.muzzle; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import net.bytebuddy.jar.asm.Opcodes; + +/** + * Expected flag (or lack of flag) on a class, method or field reference. + * + *

This class is used in the auto-generated {@link InstrumentationModule#getMuzzleReferences()} + * method, it is not meant to be used directly by agent extension developers. + */ +public interface Flag { + /** + * Predicate method that determines whether this flag is present in the passed bitmask. + * + * @see Opcodes + */ + boolean matches(int asmFlags); + + // This method is internally used to generate the InstrumentationModule#getMuzzleReferences() + // implementation + + /** Same as {@link Enum#name()}. */ + String name(); + + /** + * The constants of this enum represent the exact visibility of a referenced class, method or + * field. + * + * @see net.bytebuddy.description.modifier.Visibility + */ + enum VisibilityFlag implements Flag { + PUBLIC { + @Override + public boolean matches(int asmFlags) { + return (Opcodes.ACC_PUBLIC & asmFlags) != 0; + } + }, + PROTECTED { + @Override + public boolean matches(int asmFlags) { + return (Opcodes.ACC_PROTECTED & asmFlags) != 0; + } + }, + PACKAGE { + @Override + public boolean matches(int asmFlags) { + return !(PUBLIC.matches(asmFlags) + || PROTECTED.matches(asmFlags) + || PRIVATE.matches(asmFlags)); + } + }, + PRIVATE { + @Override + public boolean matches(int asmFlags) { + return (Opcodes.ACC_PRIVATE & asmFlags) != 0; + } + } + } + + /** + * The constants of this enum represent the minimum visibility flag required by a type access, + * method call or field access. + * + * @see net.bytebuddy.description.modifier.Visibility + */ + enum MinimumVisibilityFlag implements Flag { + PUBLIC { + @Override + public boolean matches(int asmFlags) { + return VisibilityFlag.PUBLIC.matches(asmFlags); + } + }, + PROTECTED_OR_HIGHER { + @Override + public boolean matches(int asmFlags) { + return VisibilityFlag.PUBLIC.matches(asmFlags) + || VisibilityFlag.PROTECTED.matches(asmFlags); + } + }, + PACKAGE_OR_HIGHER { + @Override + public boolean matches(int asmFlags) { + return !VisibilityFlag.PRIVATE.matches(asmFlags); + } + }, + PRIVATE_OR_HIGHER { + @Override + public boolean matches(int asmFlags) { + // you can't out-private a private + return true; + } + } + } + + /** + * The constants of this enum describe whether a method or class is abstract, final or non-final. + * + * @see net.bytebuddy.description.modifier.TypeManifestation + * @see net.bytebuddy.description.modifier.MethodManifestation + */ + enum ManifestationFlag implements Flag { + FINAL { + @Override + public boolean matches(int asmFlags) { + return (Opcodes.ACC_FINAL & asmFlags) != 0; + } + }, + NON_FINAL { + @Override + public boolean matches(int asmFlags) { + return !(ABSTRACT.matches(asmFlags) || FINAL.matches(asmFlags)); + } + }, + ABSTRACT { + @Override + public boolean matches(int asmFlags) { + return (Opcodes.ACC_ABSTRACT & asmFlags) != 0; + } + }, + INTERFACE { + @Override + public boolean matches(int asmFlags) { + return (Opcodes.ACC_INTERFACE & asmFlags) != 0; + } + }, + NON_INTERFACE { + @Override + public boolean matches(int asmFlags) { + return !INTERFACE.matches(asmFlags); + } + } + } + + /** + * The constants of this enum describe whether a method/field is static or not. + * + * @see net.bytebuddy.description.modifier.Ownership + */ + enum OwnershipFlag implements Flag { + STATIC { + @Override + public boolean matches(int asmFlags) { + return (Opcodes.ACC_STATIC & asmFlags) != 0; + } + }, + NON_STATIC { + @Override + public boolean matches(int asmFlags) { + return !STATIC.matches(asmFlags); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/MethodRef.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/MethodRef.java new file mode 100644 index 000000000..afcf33cc3 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/MethodRef.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.muzzle; + +import static io.opentelemetry.javaagent.extension.muzzle.ReferenceMergeUtil.mergeFlags; +import static io.opentelemetry.javaagent.extension.muzzle.ReferenceMergeUtil.mergeSet; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.bytebuddy.jar.asm.Type; + +/** + * Represents a reference to a method used in the instrumentation advice or helper class code. Part + * of a {@link ClassRef}. + * + *

This class is used in the auto-generated {@link InstrumentationModule#getMuzzleReferences()} + * method, it is not meant to be used directly by agent extension developers. + */ +public final class MethodRef { + private final Set sources; + private final Set flags; + private final String name; + private final String descriptor; + + MethodRef(Set sources, Set flags, String name, String descriptor) { + this.sources = sources; + this.flags = flags; + this.name = name; + this.descriptor = descriptor; + } + + /** Returns information about code locations where this method was referenced. */ + public Set getSources() { + return sources; + } + + /** Returns modifier flags of this method. */ + public Set getFlags() { + return flags; + } + + /** Returns the method name. */ + public String getName() { + return name; + } + + /** Returns this method's type descriptor. */ + public String getDescriptor() { + return descriptor; + } + + MethodRef merge(MethodRef anotherMethod) { + if (!equals(anotherMethod)) { + throw new IllegalStateException("illegal merge " + this + " != " + anotherMethod); + } + return new MethodRef( + mergeSet(sources, anotherMethod.sources), + mergeFlags(flags, anotherMethod.flags), + name, + descriptor); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof MethodRef)) { + return false; + } + MethodRef other = (MethodRef) obj; + return name.equals(other.name) && descriptor.equals(other.descriptor); + } + + @Override + public int hashCode() { + return Objects.hash(name, descriptor); + } + + @Override + public String toString() { + Type methodType = Type.getMethodType(getDescriptor()); + String returnType = methodType.getReturnType().getClassName(); + String modifiers = flags.stream().map(Flag::toString).collect(Collectors.joining(" ")); + String parameters = + Stream.of(methodType.getArgumentTypes()) + .map(Type::getClassName) + .collect(Collectors.joining(", ", "(", ")")); + return getClass().getSimpleName() + + ": " + + modifiers + + " " + + returnType + + " " + + name + + parameters; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ReferenceMergeUtil.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ReferenceMergeUtil.java new file mode 100644 index 000000000..8242741ed --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/ReferenceMergeUtil.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.muzzle; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +final class ReferenceMergeUtil { + + static Set mergeSet(Set set1, Set set2) { + Set set = new LinkedHashSet<>(); + set.addAll(set1); + set.addAll(set2); + return set; + } + + static Set mergeMethods(Set methods1, Set methods2) { + List merged = new ArrayList<>(methods1); + for (MethodRef method : methods2) { + int i = merged.indexOf(method); + if (i == -1) { + merged.add(method); + } else { + merged.set(i, merged.get(i).merge(method)); + } + } + return new LinkedHashSet<>(merged); + } + + static Set mergeFields(Set fields1, Set fields2) { + List merged = new ArrayList<>(fields1); + for (FieldRef field : fields2) { + int i = merged.indexOf(field); + if (i == -1) { + merged.add(field); + } else { + merged.set(i, merged.get(i).merge(field)); + } + } + return new LinkedHashSet<>(merged); + } + + static Set mergeFlags(Set flags1, Set flags2) { + Set merged = mergeSet(flags1, flags2); + // TODO: Assert flags are non-contradictory and resolve + // public > protected > package-private > private + return merged; + } + + private ReferenceMergeUtil() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/Source.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/Source.java new file mode 100644 index 000000000..fcebd3641 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/muzzle/Source.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.muzzle; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import java.util.Objects; + +/** + * Represents the source (file name, line number) of a reference. + * + *

This class is used in the auto-generated {@link InstrumentationModule#getMuzzleReferences()} + * method, it is not meant to be used directly by agent extension developers. + */ +public final class Source { + private final String name; + private final int line; + + public Source(String name, int line) { + this.name = name; + this.line = line; + } + + public String getName() { + return name; + } + + public int getLine() { + return line; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Source)) { + return false; + } + Source other = (Source) obj; + return name.equals(other.name) && line == other.line; + } + + @Override + public int hashCode() { + return Objects.hash(name, line); + } + + @Override + public String toString() { + return getName() + ":" + getLine(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/BootstrapPackagesProvider.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/BootstrapPackagesProvider.java new file mode 100644 index 000000000..e2f067ca6 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/BootstrapPackagesProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.spi; + +import java.util.List; + +/** + * A service provider to allow adding classes from specified package prefixes to the bootstrap + * classloader. The classes in the bootstrap classloader are available to all instrumentations. This + * is useful if large number of custom instrumentations are using functionality from common + * packages. + */ +public interface BootstrapPackagesProvider { + + /** + * Classes from returned package prefixes will be available in the bootstrap classloader. + * + * @return package prefixes. + */ + List getPackagePrefixes(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/config/PropertySource.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/config/PropertySource.java new file mode 100644 index 000000000..10881ad9f --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/config/PropertySource.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.spi.config; + +import java.util.Map; + +/** + * A service provider that allows to override default OTel agent configuration. Properties returned + * by implementations of this interface will be used after the following methods fail to find a + * non-empty property value: system properties, environment variables, properties configuration + * file. + */ +public interface PropertySource { + /** + * Returns all properties whose default values are overridden by this property source. Key of the + * map is the propertyName (same as system property name, e.g. {@code otel.traces.exporter}), + * value is the property value. + */ + Map getProperties(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/MetricExporterFactory.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/MetricExporterFactory.java new file mode 100644 index 000000000..99ee30533 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/MetricExporterFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.spi.exporter; + +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.util.Properties; +import java.util.Set; + +/** + * A {@link MetricExporterFactory} acts as the bootstrap for a {@link MetricExporter} + * implementation. An exporter must register its implementation of a {@link MetricExporterFactory} + * through the Java SPI framework. + */ +public interface MetricExporterFactory { + /** + * Creates an instance of a {@link MetricExporter} based on the provided configuration. + * + * @param config The configuration + * @return An implementation of a {@link MetricExporter} + */ + MetricExporter fromConfig(Properties config); + + /** + * Returns names of metric exporters supported by this factory. + * + *

Multiple names are useful for enabling a pair of span and metric exporters using the same + * name, while still having separate names for enabling them individually. + * + * @return The exporter names supported by this factory + */ + Set getNames(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/MetricServer.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/MetricServer.java new file mode 100644 index 000000000..f66a0c252 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/MetricServer.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.spi.exporter; + +import io.opentelemetry.sdk.metrics.export.MetricProducer; +import java.util.Properties; +import java.util.Set; + +/** + * A {@link MetricServer} acts as the bootstrap for metric exporters that use {@link MetricProducer} + * to consume the metrics. + * + *

Multiple names are useful for enabling a pair of span and metric exporters using the same + * name, while still having separate names for enabling them individually. + * + *

Implementation of {@link MetricServer} must be registered through the Java SPI framework. + */ +public interface MetricServer { + + /** + * Start the metric server that pulls metric from the {@link MetricProducer}. + * + * @param producer The metric producer + * @param config The configuration + */ + void start(MetricProducer producer, Properties config); + + /** + * Returns names of metric servers supported by this factory. + * + * @return The metric server names supported by this factory + */ + Set getNames(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/SpanExporterFactory.java b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/SpanExporterFactory.java new file mode 100644 index 000000000..e888ef1eb --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/spi/exporter/SpanExporterFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.spi.exporter; + +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Properties; +import java.util.Set; + +/** + * A {@link SpanExporterFactory} acts as the bootstrap for a {@link SpanExporter} implementation. An + * exporter must register its implementation of a {@link SpanExporterFactory} through the Java SPI + * framework. + * + * @deprecated Use {@code io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSpanExporterProvider} + * from the {@code opentelemetry-sdk-extension-autoconfigure} instead. + */ +@Deprecated +public interface SpanExporterFactory { + /** + * Creates an instance of a {@link SpanExporter} based on the provided configuration. + * + * @param config The configuration + * @return An implementation of a {@link SpanExporter} + */ + SpanExporter fromConfig(Properties config); + + /** + * Returns names of span exporters supported by this factory. + * + *

Multiple names are useful for enabling a pair of span and metric exporters using the same + * name, while still having separate names for enabling them individually. + * + * @return The exporter names supported by this factory + */ + Set getNames(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-extension-api/src/test/groovy/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModuleTest.groovy b/opentelemetry-java-instrumentation/javaagent-extension-api/src/test/groovy/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModuleTest.groovy new file mode 100644 index 000000000..49693767d --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-extension-api/src/test/groovy/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModuleTest.groovy @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation + +import io.opentelemetry.instrumentation.api.config.Config +import io.opentelemetry.instrumentation.api.config.ConfigBuilder +import spock.lang.Specification + +class InstrumentationModuleTest extends Specification { + + def "default enabled"() { + setup: + def target = new TestInstrumentationModule(["test"]) + + expect: + target.enabled + } + + def "default enabled override"() { + expect: + target.enabled == enabled + + where: + enabled | target + true | new TestInstrumentationModule(["test"]) { + @Override + protected boolean defaultEnabled() { + return true + } + } + false | new TestInstrumentationModule(["test"]) { + @Override + protected boolean defaultEnabled() { + return false + } + } + } + + def "default disabled can override to enabled #enabled"() { + setup: + Config.instance = new ConfigBuilder().readProperties([ + "otel.instrumentation.test.enabled": Boolean.toString(enabled) + ]).build() + def target = new TestInstrumentationModule(["test"]) { + @Override + protected boolean defaultEnabled() { + return false + } + } + + expect: + target.enabled == enabled + + cleanup: + Config.instance = null + + where: + enabled << [true, false] + } + + static class TestInstrumentationModule extends InstrumentationModule { + TestInstrumentationModule(List instrumentationNames) { + super(instrumentationNames) + } + + @Override + List typeInstrumentations() { + return [] + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/javaagent-tooling.gradle b/opentelemetry-java-instrumentation/javaagent-tooling/javaagent-tooling.gradle new file mode 100644 index 000000000..dc4ff30e0 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/javaagent-tooling.gradle @@ -0,0 +1,69 @@ +group = 'io.opentelemetry.javaagent' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +configurations { + // classpath used by the instrumentation muzzle plugin + instrumentationMuzzle { + canBeConsumed = true + canBeResolved = false + extendsFrom implementation + } +} + +dependencies { + // Only used during compilation by bytebuddy plugin + compileOnly "com.google.guava:guava" + + implementation project(':javaagent-bootstrap') + implementation project(':javaagent-extension-api') + implementation project(':javaagent-api') + implementation project(':instrumentation-api') + + implementation "run.mone:opentelemetry-api" + implementation "run.mone:opentelemetry-api-metrics" + implementation "run.mone:opentelemetry-sdk-metrics" + implementation "run.mone:opentelemetry-sdk" + implementation "run.mone:opentelemetry-semconv" + implementation("io.micrometer:micrometer-registry-prometheus:1.1.7") + implementation("ch.qos.logback:logback-classic:1.2.3") + implementation("ch.qos.logback:logback-core:1.2.3") + implementation "run.mone:nacos-client:1.2.1-mone-v3-SNAPSHOT" + implementation "run.mone:opentelemetry-sdk-extension-autoconfigure" + implementation("run.mone:opentelemetry-extension-kotlin") + implementation "run.mone:opentelemetry-extension-aws" + implementation "run.mone:opentelemetry-extension-trace-propagators" + implementation "run.mone:opentelemetry-sdk-extension-resources" + + annotationProcessor "com.google.auto.value:auto-value" + + + // Only the logging exporter is included in our slim distribution so we include it here. + // Other exporters are in javaagent-exporters + implementation "run.mone:opentelemetry-exporter-logging" + implementation("com.lmax:disruptor:3.4.2") + implementation("org.apache.logging.log4j:log4j-core:2.17.0") + implementation("org.apache.logging.log4j:log4j-api:2.17.0") + + api "net.bytebuddy:byte-buddy" + implementation "net.bytebuddy:byte-buddy-agent" + annotationProcessor "com.google.auto.service:auto-service" + compileOnly "com.google.auto.service:auto-service" + implementation "org.slf4j:slf4j-api" + + testImplementation project(':testing-common') + testImplementation "com.google.guava:guava" + testImplementation "org.assertj:assertj-core" + testImplementation "org.mockito:mockito-core" + testImplementation "org.mockito:mockito-junit-jupiter" + + instrumentationMuzzle sourceSets.main.output +} + +// Here we only include autoconfigure but don't include OTLP exporters to ensure they are only in +// the full distribution. We need to override the default exporter setting of OTLP as a result. +tasks.withType(Test).configureEach { + environment "OTEL_TRACES_EXPORTER", "none" + environment "OTEL_METRICS_EXPORTER", "none" +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessor.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessor.java new file mode 100644 index 000000000..426098256 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessor.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +public class AddThreadDetailsSpanProcessor implements SpanProcessor { + + @Override + public void onStart(Context context, ReadWriteSpan span) { + Thread currentThread = Thread.currentThread(); + span.setAttribute(SemanticAttributes.THREAD_ID, currentThread.getId()); + span.setAttribute(SemanticAttributes.THREAD_NAME, currentThread.getName()); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) {} + + @Override + public boolean isEndRequired() { + return false; + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java new file mode 100644 index 000000000..6fc3ab241 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java @@ -0,0 +1,594 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import static io.opentelemetry.javaagent.bootstrap.AgentInitializer.isJavaBefore9; +import static io.opentelemetry.javaagent.tooling.SafeServiceLoader.loadOrdered; +import static io.opentelemetry.javaagent.tooling.Utils.getResourceName; +import static net.bytebuddy.matcher.ElementMatchers.any; + +import com.alibaba.nacos.api.config.ConfigFactory; +import com.alibaba.nacos.api.config.ConfigService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.bootstrap.AgentClassLoader; +import io.opentelemetry.javaagent.extension.AgentExtension; +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.instrumentation.api.internal.BootstrapPackagePrefixesHolder; +import io.opentelemetry.javaagent.spi.BootstrapPackagesProvider; +import io.opentelemetry.javaagent.tooling.config.ConfigInitializer; +import io.opentelemetry.javaagent.tooling.context.FieldBackedProvider; +import io.opentelemetry.javaagent.tooling.ignore.IgnoredClassLoadersMatcher; +import io.opentelemetry.javaagent.tooling.ignore.IgnoredTypesBuilderImpl; +import io.opentelemetry.javaagent.tooling.ignore.IgnoredTypesMatcher; + +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import io.opentelemetry.sdk.common.HeraJavaagentConfig; +import io.opentelemetry.sdk.common.SystemCommon; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.agent.builder.ResettableClassFileTransformer; +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +@SuppressWarnings({"CatchAndPrintStackTrace", "SystemOut"}) +public class AgentInstaller { + + private static final Logger log; + + private static final String JAVAAGENT_ENABLED_CONFIG = "otel.javaagent.enabled"; + + + private static final String HERA_JAVAAGENT_CONFIG_DATA_ID = "hera_javaagent_config"; + private static final String HERA_JAVAAGENT_CONFIG_GROUP = "DEFAULT_GROUP"; + private static final int HERA_JAVAAGENT_CONFIG_TIME_OUT = 2000; + + // This property may be set to force synchronous AgentListener#afterAgent() execution: the + // condition for delaying the AgentListener initialization is pretty broad and in case it covers + // too much javaagent users can file a bug, force sync execution by setting this property to true + // and continue using the javaagent + private static final String FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG = + "otel.javaagent.experimental.force-synchronous-agent-listeners"; + + private static final Map> CLASS_LOAD_CALLBACKS = new HashMap<>(); + private static volatile Instrumentation instrumentation; + + public static Instrumentation getInstrumentation() { + return instrumentation; + } + + static { + LoggingConfigurer.configureLogger(); + log = LoggerFactory.getLogger(AgentInstaller.class); + + addByteBuddyRawSetting(); + BootstrapPackagePrefixesHolder.setBoostrapPackagePrefixes(loadBootstrapPackagePrefixes()); + // this needs to be done as early as possible - before the first Config.get() call + ConfigInitializer.initialize(); + // ensure java.lang.reflect.Proxy is loaded, as transformation code uses it internally + // loading java.lang.reflect.Proxy after the bytebuddy transformer is set up causes + // the internal-proxy instrumentation module to transform it, and then the bytebuddy + // transformation code also tries to load it, which leads to a ClassCircularityError + // loading java.lang.reflect.Proxy early here still allows it to be retransformed by the + // internal-proxy instrumentation module after the bytebuddy transformer is set up + Proxy.class.getName(); + + // caffeine can trigger first access of ForkJoinPool under transform(), which leads ForkJoinPool + // not to get transformed itself. + // loading it early here still allows it to be retransformed as part of agent installation below + ForkJoinPool.class.getName(); + + // caffeine uses AtomicReferenceArray, ensure it is loaded to avoid ClassCircularityError during + // transform. + AtomicReferenceArray.class.getName(); + } + + public static void installBytebuddyAgent(Instrumentation inst) { + logVersionInfo(); + Config config = Config.get(); + if (config.getBoolean(JAVAAGENT_ENABLED_CONFIG, true)) { + // set env or jvm properties + setEnvAndJvmProperties(config.getString(EnvOrJvmProperties.JVM_OTEL_NACOS_ADDRESS.getKey(), "nacos.hera-namespace:80")); + + List agentListeners = loadOrdered(AgentListener.class); + installBytebuddyAgent(inst, agentListeners); + } else { + log.debug("Tracing is disabled, not installing instrumentations."); + } + } + + /** + * Install the core bytebuddy agent along with all implementations of {@link + * InstrumentationModule}. + * + * @param inst Java Instrumentation used to install bytebuddy + * @return the agent's class transformer + */ + public static ResettableClassFileTransformer installBytebuddyAgent( + Instrumentation inst, Iterable agentListeners) { + + Config config = Config.get(); + runBeforeAgentListeners(agentListeners, config); + + instrumentation = inst; + + FieldBackedProvider.resetContextMatchers(); + + AgentBuilder agentBuilder = + new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .with(new RedefinitionDiscoveryStrategy()) + .with(AgentBuilder.DescriptionStrategy.Default.POOL_ONLY) + .with(AgentTooling.poolStrategy()) + .with(new ClassLoadListener()) + .with(AgentTooling.locationStrategy()); + // FIXME: we cannot enable it yet due to BB/JVM bug, see + // https://github.com/raphw/byte-buddy/issues/558 + // .with(AgentBuilder.LambdaInstrumentationStrategy.ENABLED) + + agentBuilder = configureIgnoredTypes(config, agentBuilder); + + if (log.isDebugEnabled()) { + agentBuilder = + agentBuilder + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .with(new RedefinitionDiscoveryStrategy()) + .with(new RedefinitionLoggingListener()) + .with(new TransformLoggingListener()); + } + + int numberOfLoadedExtensions = 0; + for (AgentExtension agentExtension : loadOrdered(AgentExtension.class)) { + log.debug( + "Loading extension {} [class {}]", + agentExtension.extensionName(), + agentExtension.getClass().getName()); + try { + agentBuilder = agentExtension.extend(agentBuilder); + numberOfLoadedExtensions++; + } catch (Exception | LinkageError e) { + log.error( + "Unable to load extension {} [class {}]", + agentExtension.extensionName(), + agentExtension.getClass().getName(), + e); + } + } + log.debug("Installed {} extension(s)", numberOfLoadedExtensions); + + ResettableClassFileTransformer resettableClassFileTransformer = agentBuilder.installOn(inst); + runAfterAgentListeners(agentListeners, config); + return resettableClassFileTransformer; + } + + private static void runBeforeAgentListeners( + Iterable agentListeners, Config config) { + for (AgentListener agentListener : agentListeners) { + agentListener.beforeAgent(config); + } + } + + private static AgentBuilder configureIgnoredTypes(Config config, AgentBuilder agentBuilder) { + IgnoredTypesBuilderImpl builder = new IgnoredTypesBuilderImpl(); + for (IgnoredTypesConfigurer configurer : loadOrdered(IgnoredTypesConfigurer.class)) { + configurer.configure(config, builder); + } + + return agentBuilder + .ignore(any(), new IgnoredClassLoadersMatcher(builder.buildIgnoredClassLoadersTrie())) + .or(new IgnoredTypesMatcher(builder.buildIgnoredTypesTrie())); + } + + private static void runAfterAgentListeners( + Iterable agentListeners, Config config) { + // java.util.logging.LogManager maintains a final static LogManager, which is created during + // class initialization. Some AgentListener implementations may use JRE bootstrap classes + // which touch this class (e.g. JFR classes or some MBeans). + // It is worth noting that starting from Java 9 (JEP 264) Java platform classes no longer use + // JUL directly, but instead they use a new System.Logger interface, so the LogManager issue + // applies mainly to Java 8. + // This means applications which require a custom LogManager may not have a chance to set the + // global LogManager if one of those AgentListeners runs first: it will incorrectly + // set the global LogManager to the default JVM one in cases where the instrumented application + // sets the LogManager system property or when the custom LogManager class is not on the system + // classpath. + // Our solution is to delay the initialization of AgentListeners when we detect a custom + // log manager being used. + // Once we see the LogManager class loading, it's safe to run AgentListener#afterAgent() because + // the application is already setting the global LogManager and AgentListener won't be able + // to touch it due to classloader locking. + boolean shouldForceSynchronousAgentListenersCalls = + Config.get().getBoolean(FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG, false); + if (!shouldForceSynchronousAgentListenersCalls + && isJavaBefore9() + && isAppUsingCustomLogManager()) { + log.debug("Custom JUL LogManager detected: delaying AgentListener#afterAgent() calls"); + registerClassLoadCallback( + "java.util.logging.LogManager", new DelayedAfterAgentCallback(config, agentListeners)); + } else { + for (AgentListener agentListener : agentListeners) { + agentListener.afterAgent(config); + } + } + } + + private static void addByteBuddyRawSetting() { + String savedPropertyValue = System.getProperty(TypeDefinition.RAW_TYPES_PROPERTY); + try { + System.setProperty(TypeDefinition.RAW_TYPES_PROPERTY, "true"); + boolean rawTypes = TypeDescription.AbstractBase.RAW_TYPES; + if (!rawTypes) { + log.debug("Too late to enable {}", TypeDefinition.RAW_TYPES_PROPERTY); + } + } finally { + if (savedPropertyValue == null) { + System.clearProperty(TypeDefinition.RAW_TYPES_PROPERTY); + } else { + System.setProperty(TypeDefinition.RAW_TYPES_PROPERTY, savedPropertyValue); + } + } + } + + private static List loadBootstrapPackagePrefixes() { + List bootstrapPackages = new ArrayList<>(Constants.BOOTSTRAP_PACKAGE_PREFIXES); + Iterable bootstrapPackagesProviders = + SafeServiceLoader.load(BootstrapPackagesProvider.class); + for (BootstrapPackagesProvider provider : bootstrapPackagesProviders) { + List packagePrefixes = provider.getPackagePrefixes(); + log.debug( + "Loaded bootstrap package prefixes from {}: {}", + provider.getClass().getName(), + packagePrefixes); + bootstrapPackages.addAll(packagePrefixes); + } + return bootstrapPackages; + } + + static class RedefinitionLoggingListener implements AgentBuilder.RedefinitionStrategy.Listener { + + private static final Logger log = LoggerFactory.getLogger(RedefinitionLoggingListener.class); + + @Override + public void onBatch(int index, List> batch, List> types) { + } + + @Override + public Iterable>> onError( + int index, List> batch, Throwable throwable, List> types) { + if (log.isDebugEnabled()) { + log.debug("Exception while retransforming {} classes: {}", batch.size(), batch, throwable); + } + return Collections.emptyList(); + } + + @Override + public void onComplete( + int amount, List> types, Map>, Throwable> failures) { + } + } + + static class TransformLoggingListener implements AgentBuilder.Listener { + + private static final TransformSafeLogger log = + TransformSafeLogger.getLogger(TransformLoggingListener.class); + + @Override + public void onError( + String typeName, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + Throwable throwable) { + if (log.isDebugEnabled()) { + log.debug( + "Failed to handle {} for transformation on classloader {}", + typeName, + classLoader, + throwable); + } + } + + @Override + public void onTransformation( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + DynamicType dynamicType) { + log.debug("Transformed {} -- {}", typeDescription.getName(), classLoader); + } + + @Override + public void onIgnored( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + boolean loaded) { + } + + @Override + public void onComplete( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { + } + + @Override + public void onDiscovery( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { + } + } + + /** + * Register a callback to run when a class is loading. + * + *

Caveats: + * + *

    + *
  • This callback will be invoked by a jvm class transformer. + *
  • Classes filtered out by {@link AgentInstaller}'s skip list will not be matched. + *
+ * + * @param className name of the class to match against + * @param callback runnable to invoke when class name matches + */ + public static void registerClassLoadCallback(String className, Runnable callback) { + synchronized (CLASS_LOAD_CALLBACKS) { + List callbacks = + CLASS_LOAD_CALLBACKS.computeIfAbsent(className, k -> new ArrayList<>()); + callbacks.add(callback); + } + } + + private static class DelayedAfterAgentCallback implements Runnable { + private final Iterable agentListeners; + private final Config config; + + private DelayedAfterAgentCallback(Config config, Iterable agentListeners) { + this.agentListeners = agentListeners; + this.config = config; + } + + @Override + public void run() { + /* + * This callback is called from within bytecode transformer. This can be a problem if callback tries + * to load classes being transformed. To avoid this we start a thread here that calls the callback. + * This seems to resolve this problem. + */ + Thread thread = new Thread(this::runAgentListeners); + thread.setName("delayed-agent-listeners"); + thread.setDaemon(true); + thread.start(); + } + + private void runAgentListeners() { + for (AgentListener agentListener : agentListeners) { + try { + agentListener.afterAgent(config); + } catch (RuntimeException e) { + log.error("Failed to execute {}", agentListener.getClass().getName(), e); + } + } + } + } + + private static class ClassLoadListener implements AgentBuilder.Listener { + @Override + public void onDiscovery( + String typeName, ClassLoader classLoader, JavaModule javaModule, boolean b) { + } + + @Override + public void onTransformation( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule javaModule, + boolean b, + DynamicType dynamicType) { + } + + @Override + public void onIgnored( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule javaModule, + boolean b) { + } + + @Override + public void onError( + String s, ClassLoader classLoader, JavaModule javaModule, boolean b, Throwable throwable) { + } + + @Override + public void onComplete( + String typeName, ClassLoader classLoader, JavaModule javaModule, boolean b) { + synchronized (CLASS_LOAD_CALLBACKS) { + List callbacks = CLASS_LOAD_CALLBACKS.get(typeName); + if (callbacks != null) { + for (Runnable callback : callbacks) { + callback.run(); + } + } + } + } + } + + private static class RedefinitionDiscoveryStrategy + implements AgentBuilder.RedefinitionStrategy.DiscoveryStrategy { + private static final AgentBuilder.RedefinitionStrategy.DiscoveryStrategy delegate = + AgentBuilder.RedefinitionStrategy.DiscoveryStrategy.Reiterating.INSTANCE; + + @Override + public Iterable>> resolve(Instrumentation instrumentation) { + // filter out our agent classes and injected helper classes + return () -> + streamOf(delegate.resolve(instrumentation)) + .map(RedefinitionDiscoveryStrategy::filterClasses) + .iterator(); + } + + private static Iterable> filterClasses(Iterable> classes) { + return () -> streamOf(classes).filter(c -> !isIgnored(c)).iterator(); + } + + private static Stream streamOf(Iterable iterable) { + return StreamSupport.stream(iterable.spliterator(), false); + } + + private static boolean isIgnored(Class c) { + ClassLoader cl = c.getClassLoader(); + if (cl instanceof AgentClassLoader || cl instanceof ExtensionClassLoader) { + return true; + } + + return HelperInjector.isInjectedClass(c); + } + } + + /** + * Detect if the instrumented application is using a custom JUL LogManager. + */ + private static boolean isAppUsingCustomLogManager() { + String jbossHome = System.getenv("JBOSS_HOME"); + if (jbossHome != null) { + log.debug("Found JBoss: {}; assuming app is using custom LogManager", jbossHome); + // JBoss/Wildfly is known to set a custom log manager after startup. + // Originally we were checking for the presence of a jboss class, + // but it seems some non-jboss applications have jboss classes on the classpath. + // This would cause AgentListener#afterAgent() calls to be delayed indefinitely. + // Checking for an environment variable required by jboss instead. + return true; + } + + String customLogManager = System.getProperty("java.util.logging.manager"); + if (customLogManager != null) { + log.debug( + "Detected custom LogManager configuration: java.util.logging.manager={}", + customLogManager); + boolean onSysClasspath = + ClassLoader.getSystemResource(getResourceName(customLogManager)) != null; + log.debug( + "Class {} is on system classpath: {}delaying AgentInstaller#afterAgent()", + customLogManager, + onSysClasspath ? "not " : ""); + // Some applications set java.util.logging.manager but never actually initialize the logger. + // Check to see if the configured manager is on the system classpath. + // If so, it should be safe to initialize AgentInstaller which will setup the log manager: + // LogManager tries to load the implementation first using system CL, then falls back to + // current context CL + return !onSysClasspath; + } + + return false; + } + + + /** + * Check if environment variables and JVM parameters are set, if not, use the corresponding configuration in Nacos and set them back. + */ + private static void setEnvAndJvmProperties(String nacosAddr) { + Map byNacos = getByNacos(nacosAddr); + if (byNacos != null && byNacos.size() > 0) { + for (HeraJavaagentConfig config : EnvOrJvmProperties.INIT_ENV_JVM_LIST) { + String propertiesKey = config.getKey(); + String envOrProperties = SystemCommon.getEnvOrProperties(propertiesKey); + if (envOrProperties == null) { + envOrProperties = byNacos.get(propertiesKey); + if(envOrProperties != null) { + System.setProperty(propertiesKey, envOrProperties); + } + } + } + } + // set env to JVM operation + setJvmToEnv(); + // set default values for env or properties + setDefaultEnv(); + } + + private static void setJvmToEnv() { + // set project env id + if(SystemCommon.getEnvOrProperties(EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_ID.getKey()) == null){ + System.setProperty(EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_ID.getKey(), SystemCommon.getEnvOrProperties(EnvOrJvmProperties.JVM_OTEL_MIONE_PROJECT_ENV_ID.getKey())); + } + // set project env name + if(SystemCommon.getEnvOrProperties(EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_NAME.getKey()) == null){ + System.setProperty(EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_NAME.getKey(), SystemCommon.getEnvOrProperties(EnvOrJvmProperties.JVM_OTEL_MIONE_PROJECT_ENV_NAME.getKey())); + } + // set project name + if(SystemCommon.getEnvOrProperties(EnvOrJvmProperties.MIONE_PROJECT_NAME.getKey()) == null){ + String service = SystemCommon.getEnvOrProperties(EnvOrJvmProperties.JVM_OTEL_RESOURCE_ATTRIBUTES.getKey()); + if (service != null && service.contains("=")) { + System.setProperty(EnvOrJvmProperties.MIONE_PROJECT_NAME.getKey(), service.split("=")[1]); + } + } + } + + private static void setDefaultEnv(){ + for(HeraJavaagentConfig config : EnvOrJvmProperties.INIT_ENV_JVM_LIST){ + if(SystemCommon.getEnvOrProperties(config.getKey()) == null && config.getDefaultValue() != null){ + System.setProperty(config.getKey(), config.getDefaultValue()); + } + } + } + + @Nullable + private static Map getByNacos(String nacosAddr) { + try { + ConfigService configService = ConfigFactory.createConfigService(nacosAddr); + String config = configService.getConfig(HERA_JAVAAGENT_CONFIG_DATA_ID, HERA_JAVAAGENT_CONFIG_GROUP, HERA_JAVAAGENT_CONFIG_TIME_OUT); + return formatConfig(config); + } catch (Throwable t) { + t.printStackTrace(); + } + return null; + } + + @Nullable + private static Map formatConfig(String nacosConfig) { + if (nacosConfig == null || nacosConfig.equals("")) { + return null; + } + Map result = new HashMap<>(); + String[] entries = nacosConfig.split(System.lineSeparator()); + for (String entry : entries) { + if (entry != null && entry.contains("=")) { + String[] kv = entry.split("=", 2); + result.put(kv[0], kv[1]); + } + } + return result; + } + + + private static void logVersionInfo() { + VersionLogger.logAllVersions(); + log.debug( + "{} loaded on {}", AgentInstaller.class.getName(), AgentInstaller.class.getClassLoader()); + } + + private AgentInstaller() { + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentTooling.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentTooling.java new file mode 100644 index 000000000..13327969c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentTooling.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import io.opentelemetry.javaagent.tooling.bytebuddy.AgentCachingPoolStrategy; +import io.opentelemetry.javaagent.tooling.bytebuddy.AgentLocationStrategy; + +/** + * This class contains class references for objects shared by the agent installer as well as muzzle + * (both compile and runtime). Extracted out from AgentInstaller to begin separating some of the + * logic out. + */ +public final class AgentTooling { + + private static final AgentLocationStrategy LOCATION_STRATEGY = new AgentLocationStrategy(); + private static final AgentCachingPoolStrategy POOL_STRATEGY = new AgentCachingPoolStrategy(); + + public static AgentLocationStrategy locationStrategy() { + return LOCATION_STRATEGY; + } + + public static AgentCachingPoolStrategy poolStrategy() { + return POOL_STRATEGY; + } + + private AgentTooling() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentTracerProviderConfigurer.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentTracerProviderConfigurer.java new file mode 100644 index 000000000..568b76bdd --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentTracerProviderConfigurer.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import com.google.auto.service.AutoService; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.spi.exporter.MetricExporterFactory; +import io.opentelemetry.javaagent.spi.exporter.SpanExporterFactory; +import io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Iterator; +import java.util.ServiceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@AutoService(SdkTracerProviderConfigurer.class) +public class AgentTracerProviderConfigurer implements SdkTracerProviderConfigurer { + private static final Logger log = LoggerFactory.getLogger(AgentTracerProviderConfigurer.class); + + static final String EXPORTER_JAR_CONFIG = "otel.javaagent.experimental.exporter.jar"; + + @Override + public void configure(SdkTracerProviderBuilder sdkTracerProviderBuilder) { + if (!Config.get().getBoolean(OpenTelemetryInstaller.JAVAAGENT_ENABLED_CONFIG, true)) { + return; + } + + // Register additional thread details logging span processor + sdkTracerProviderBuilder.addSpanProcessor(new AddThreadDetailsSpanProcessor()); + + maybeConfigureExporterJar(sdkTracerProviderBuilder); + maybeEnableLoggingExporter(sdkTracerProviderBuilder); + } + + private static void maybeEnableLoggingExporter(SdkTracerProviderBuilder builder) { + if (Config.get().isAgentDebugEnabled()) { + // don't install another instance if the user has already explicitly requested it. + if (loggingExporterIsNotAlreadyConfigured()) { + builder.addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())); + } + } + } + + private static boolean loggingExporterIsNotAlreadyConfigured() { + return !Config.get().getString("otel.traces.exporter", "").equalsIgnoreCase("logging"); + } + + private static void maybeConfigureExporterJar(SdkTracerProviderBuilder sdkTracerProviderBuilder) { + Config config = Config.get(); + String exporterJar = config.getString(EXPORTER_JAR_CONFIG); + if (exporterJar == null) { + return; + } + installExportersFromJar(exporterJar, config, sdkTracerProviderBuilder); + } + + private static synchronized void installExportersFromJar( + String exporterJar, Config config, SdkTracerProviderBuilder builder) { + URL url; + try { + url = new File(exporterJar).toURI().toURL(); + } catch (MalformedURLException e) { + log.warn("Filename could not be parsed: {}. Exporter is not installed", exporterJar); + log.warn("No valid exporter found. Tracing will run but spans are dropped"); + return; + } + ExporterClassLoader exporterLoader = + new ExporterClassLoader(url, OpenTelemetryInstaller.class.getClassLoader()); + + SpanExporterFactory spanExporterFactory = + getExporterFactory(SpanExporterFactory.class, exporterLoader); + + if (spanExporterFactory != null) { + installSpanExporter(spanExporterFactory, config, builder); + } else { + log.warn("No span exporter found in {}", exporterJar); + log.warn("No valid exporter found. Tracing will run but spans are dropped"); + } + + MetricExporterFactory metricExporterFactory = + getExporterFactory(MetricExporterFactory.class, exporterLoader); + if (metricExporterFactory != null) { + installMetricExporter(metricExporterFactory, config); + } + } + + private static F getExporterFactory(Class service, ExporterClassLoader exporterLoader) { + ServiceLoader serviceLoader = ServiceLoader.load(service, exporterLoader); + Iterator i = serviceLoader.iterator(); + if (i.hasNext()) { + F factory = i.next(); + if (i.hasNext()) { + log.warn( + "Exporter JAR defines more than one {}. Only the first one found will be used", + service.getName()); + } + return factory; + } + return null; + } + + private static void installSpanExporter( + SpanExporterFactory spanExporterFactory, Config config, SdkTracerProviderBuilder builder) { + SpanExporter spanExporter = spanExporterFactory.fromConfig(config.asJavaProperties()); + SpanProcessor spanProcessor = BatchSpanProcessor.builder(spanExporter).build(); + builder.addSpanProcessor(spanProcessor); + log.info("Installed span exporter: {}", spanExporter.getClass().getName()); + } + + private static void installMetricExporter( + MetricExporterFactory metricExporterFactory, Config config) { + MetricExporter metricExporter = metricExporterFactory.fromConfig(config.asJavaProperties()); + IntervalMetricReader.builder() + .setMetricExporter(metricExporter) + .setMetricProducers(Collections.singleton((SdkMeterProvider) GlobalMeterProvider.get())) + .buildAndStart(); + log.info("Installed metric exporter: {}", metricExporter.getClass().getName()); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AutoVersionResourceProvider.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AutoVersionResourceProvider.java new file mode 100644 index 000000000..83f25e648 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AutoVersionResourceProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import com.google.auto.service.AutoService; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.api.InstrumentationVersion; +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +@AutoService(ResourceProvider.class) +public class AutoVersionResourceProvider implements ResourceProvider { + + private static final AttributeKey TELEMETRY_AUTO_VERSION = + AttributeKey.stringKey("telemetry.auto.version"); + + @Override + public Resource createResource(ConfigProperties config) { + return InstrumentationVersion.VERSION == null + ? Resource.empty() + : Resource.create(Attributes.of(TELEMETRY_AUTO_VERSION, InstrumentationVersion.VERSION)); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Constants.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Constants.java new file mode 100644 index 000000000..b11cc59f2 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Constants.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Some useful constants. + * + *

Idea here is to keep this class safe to inject into client's class loader. + */ +public final class Constants { + + /** packages which will be loaded on the bootstrap classloader. */ + public static final List BOOTSTRAP_PACKAGE_PREFIXES = + Collections.unmodifiableList( + Arrays.asList( + "io.opentelemetry.javaagent.common.exec", + "io.opentelemetry.javaagent.slf4j", + "io.opentelemetry.javaagent.bootstrap", + "io.opentelemetry.javaagent.shaded", + "io.opentelemetry.javaagent.instrumentation.api")); + + private Constants() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExporterClassLoader.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExporterClassLoader.java new file mode 100644 index 000000000..b48780a45 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExporterClassLoader.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import static io.opentelemetry.javaagent.tooling.ShadingRemapper.rule; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import net.bytebuddy.jar.asm.ClassReader; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.commons.ClassRemapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Deprecated +public class ExporterClassLoader extends URLClassLoader { + + private static final Logger log = LoggerFactory.getLogger(ExporterClassLoader.class); + + // We need to prefix the names to prevent the gradle shadowJar relocation rules from touching + // them. It's possible to do this by excluding this class from shading, but it may cause issue + // with transitive dependencies down the line. + private static final ShadingRemapper remapper = + new ShadingRemapper( + rule("#io.opentelemetry.api", "#io.opentelemetry.javaagent.shaded.io.opentelemetry.api"), + rule( + "#io.opentelemetry.context", + "#io.opentelemetry.javaagent.shaded.io.opentelemetry.context"), + rule( + "#io.opentelemetry.extension.aws", + "#io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.aws"), + rule("#java.util.logging.Logger", "#io.opentelemetry.javaagent.bootstrap.PatchLogger"), + rule("#org.slf4j", "#io.opentelemetry.javaagent.slf4j")); + + private final Manifest manifest; + + public ExporterClassLoader(URL url, ClassLoader parent) { + super(new URL[] {url}, parent); + this.manifest = getManifest(url); + } + + @Override + public Enumeration getResources(String name) throws IOException { + // A small hack to prevent other exporters from being loaded by this classloader if they + // should happen to appear on the classpath. + if (name.equals("META-INF/services/io.opentelemetry.javaagent.spi.exporter.SpanExporterFactory") + || name.equals( + "META-INF/services/io.opentelemetry.javaagent.spi.exporter.MetricExporterFactory")) { + return findResources(name); + } + return super.getResources(name); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + // Use resource loading to get the class as a stream of bytes, then use ASM to transform it. + InputStream in = getResourceAsStream(name.replace('.', '/') + ".class"); + if (in == null) { + throw new ClassNotFoundException(name); + } + try { + byte[] bytes = remapClassBytes(in); + definePackageIfNeeded(name); + return defineClass(name, bytes, 0, bytes.length); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } finally { + try { + in.close(); + } catch (IOException e) { + log.debug(e.getMessage(), e); + } + } + } + + private void definePackageIfNeeded(String className) { + String packageName = getPackageName(className); + if (packageName == null) { + // default package + return; + } + if (isPackageDefined(packageName)) { + // package has already been defined + return; + } + try { + definePackage(packageName); + } catch (IllegalArgumentException e) { + // this exception is thrown when the package has already been defined, which is possible due + // to race condition with the check above + if (!isPackageDefined(packageName)) { + // this shouldn't happen however + log.error(e.getMessage(), e); + } + } + } + + private boolean isPackageDefined(String packageName) { + return getPackage(packageName) != null; + } + + private void definePackage(String packageName) { + if (manifest == null) { + definePackage(packageName, null, null, null, null, null, null, null); + } else { + definePackage(packageName, manifest, null); + } + } + + private static byte[] remapClassBytes(InputStream in) throws IOException { + ClassWriter cw = new ClassWriter(0); + ClassReader cr = new ClassReader(in); + cr.accept(new ClassRemapper(cw, remapper), ClassReader.EXPAND_FRAMES); + return cw.toByteArray(); + } + + private static String getPackageName(String className) { + int index = className.lastIndexOf('.'); + return index == -1 ? null : className.substring(0, index); + } + + private static Manifest getManifest(URL url) { + try (JarFile jarFile = new JarFile(url.toURI().getPath())) { + return jarFile.getManifest(); + } catch (IOException | URISyntaxException e) { + log.warn(e.getMessage(), e); + } + return null; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java new file mode 100644 index 000000000..ee165d4bf --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java @@ -0,0 +1,161 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import net.bytebuddy.dynamic.loading.MultipleParentClassLoader; + +/** + * This class creates a classloader which encapsulates arbitrary extensions for Otel Java + * instrumentation agent. Such extensions may include SDK components (exporters or propagators) and + * additional instrumentations. They have to be isolated and shaded to reduce interference with the + * user application and to make it compatible with shaded SDK used by the agent. Thus each extension + * jar gets a separate classloader and all of them are aggregated with the help of {@link + * MultipleParentClassLoader}. + */ +// TODO find a way to initialize logging before using this class +// Used by AgentInitializer +@SuppressWarnings({"unused", "SystemOut"}) +public class ExtensionClassLoader extends URLClassLoader { + // NOTE it's important not to use slf4j in this class, because this class is used before slf4j is + // configured, and so using slf4j here would initialize slf4j-simple before we have a chance to + // configure the logging levels + + static { + ClassLoader.registerAsParallelCapable(); + } + + public static ClassLoader getInstance(ClassLoader parent, File javaagentFile) { + List extensions = new ArrayList<>(); + + includeEmbeddedExtensionsIfFound(parent, extensions, javaagentFile); + + // TODO add support for old deprecated property otel.javaagent.experimental.exporter.jar + extensions.addAll( + parseLocation( + System.getProperty( + "otel.javaagent.experimental.extensions", + System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS")), + javaagentFile)); + + extensions.addAll( + parseLocation( + System.getProperty( + "otel.javaagent.experimental.initializer.jar", + System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_INITIALIZER_JAR")), + javaagentFile)); + // TODO when logging is configured add warning about deprecated property + + if (extensions.isEmpty()) { + return parent; + } + + List delegates = new ArrayList<>(extensions.size()); + for (URL url : extensions) { + delegates.add(getDelegate(parent, url)); + } + return new MultipleParentClassLoader(parent, delegates); + } + + private static void includeEmbeddedExtensionsIfFound( + ClassLoader parent, List extensions, File javaagentFile) { + try { + JarFile jarFile = new JarFile(javaagentFile, false); + Enumeration entryEnumeration = jarFile.entries(); + String prefix = "extensions/"; + File tempDirectory = null; + while (entryEnumeration.hasMoreElements()) { + JarEntry jarEntry = entryEnumeration.nextElement(); + + if (jarEntry.getName().startsWith(prefix) && !jarEntry.isDirectory()) { + tempDirectory = ensureTempDirectoryExists(tempDirectory); + + File tempFile = new File(tempDirectory, jarEntry.getName().substring(prefix.length())); + if (tempFile.createNewFile()) { + tempFile.deleteOnExit(); + extractFile(jarFile, jarEntry, tempFile); + addFileUrl(extensions, tempFile); + } else { + System.err.println("Failed to create temp file " + tempFile); + } + } + } + } catch (IOException ex) { + System.err.println("Failed to open embedded extensions " + ex.getMessage()); + } + } + + private static File ensureTempDirectoryExists(File tempDirectory) throws IOException { + if (tempDirectory == null) { + tempDirectory = Files.createTempDirectory("otel-extensions").toFile(); + tempDirectory.deleteOnExit(); + } + return tempDirectory; + } + + private static URLClassLoader getDelegate(ClassLoader parent, URL extensionUrl) { + return new ExtensionClassLoader(new URL[] {extensionUrl}, parent); + } + + private static List parseLocation(String locationName, File javaagentFile) { + List result = new ArrayList<>(); + + if (locationName == null) { + return result; + } + + File location = new File(locationName); + if (location.isFile()) { + addFileUrl(result, location); + } else if (location.isDirectory()) { + File[] files = location.listFiles(f -> f.isFile() && f.getName().endsWith(".jar")); + if (files != null) { + for (File file : files) { + if (!file.getAbsolutePath().equals(javaagentFile.getAbsolutePath())) { + addFileUrl(result, file); + } + } + } + } + return result; + } + + private static void addFileUrl(List result, File file) { + try { + URL wrappedUrl = new URL("otel", null, -1, "/", new RemappingUrlStreamHandler(file)); + result.add(wrappedUrl); + } catch (MalformedURLException ignored) { + System.err.println("Ignoring " + file); + } + } + + private static void extractFile(JarFile jarFile, JarEntry jarEntry, File outputFile) + throws IOException { + try (InputStream in = jarFile.getInputStream(jarEntry); + ReadableByteChannel rbc = Channels.newChannel(in); + FileOutputStream fos = new FileOutputStream(outputFile)) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } + } + + private ExtensionClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java new file mode 100644 index 000000000..99bc5f45c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java @@ -0,0 +1,259 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.BOOTSTRAP_CLASSLOADER; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import io.opentelemetry.javaagent.bootstrap.HelperResources; +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.nio.file.Files; +import java.security.SecureClassLoader; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import net.bytebuddy.agent.builder.AgentBuilder.Transformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.loading.ClassInjector; +import net.bytebuddy.utility.JavaModule; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Injects instrumentation helper classes into the user's classloader. */ +public class HelperInjector implements Transformer { + + private static final Logger log = LoggerFactory.getLogger(HelperInjector.class); + + // Need this because we can't put null into the injectedClassLoaders map. + private static final ClassLoader BOOTSTRAP_CLASSLOADER_PLACEHOLDER = + new SecureClassLoader(null) { + @Override + public String toString() { + return ""; + } + }; + + private static final Cache, Boolean> injectedClasses = + Cache.newBuilder().setWeakKeys().build(); + + private final String requestingName; + + private final Set helperClassNames; + private final Set helperResourceNames; + @Nullable private final ClassLoader helpersSource; + private final Map dynamicTypeMap = new LinkedHashMap<>(); + + private final Cache injectedClassLoaders = + Cache.newBuilder().setWeakKeys().build(); + + private final List> helperModules = new CopyOnWriteArrayList<>(); + + /** + * Construct HelperInjector. + * + * @param helperClassNames binary names of the helper classes to inject. These class names must be + * resolvable by the classloader returned by + * io.opentelemetry.javaagent.tooling.Utils#getAgentClassLoader(). Classes are injected in the + * order provided. This is important if there is interdependency between helper classes that + * requires them to be injected in a specific order. And be careful, the class's package in + * library will be renamed like 'io.opentelemetry.instrumentation' to + * 'io.opentelemetry.javaagent.shaded.instrumentation' + */ + public HelperInjector( + String requestingName, + List helperClassNames, + List helperResourceNames, + ClassLoader helpersSource) { + this.requestingName = requestingName; + + this.helperClassNames = new LinkedHashSet<>(helperClassNames); + this.helperResourceNames = new LinkedHashSet<>(helperResourceNames); + this.helpersSource = helpersSource; + } + + public HelperInjector(String requestingName, Map helperMap) { + this.requestingName = requestingName; + + this.helperClassNames = helperMap.keySet(); + this.dynamicTypeMap.putAll(helperMap); + + this.helperResourceNames = Collections.emptySet(); + this.helpersSource = null; + } + + public static HelperInjector forDynamicTypes( + String requestingName, Collection> helpers) { + Map bytes = new HashMap<>(helpers.size()); + for (DynamicType.Unloaded helper : helpers) { + bytes.put(helper.getTypeDescription().getName(), helper.getBytes()); + } + return new HelperInjector(requestingName, bytes); + } + + private Map getHelperMap() throws IOException { + if (dynamicTypeMap.isEmpty()) { + Map classnameToBytes = new LinkedHashMap<>(); + + ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(helpersSource); + + for (String helperClassName : helperClassNames) { + byte[] classBytes = locator.locate(helperClassName).resolve(); + classnameToBytes.put(helperClassName, classBytes); + } + + return classnameToBytes; + } else { + return dynamicTypeMap; + } + } + + @Override + public DynamicType.Builder transform( + DynamicType.Builder builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + if (!helperClassNames.isEmpty()) { + if (classLoader == BOOTSTRAP_CLASSLOADER) { + classLoader = BOOTSTRAP_CLASSLOADER_PLACEHOLDER; + } + + injectedClassLoaders.computeIfAbsent( + classLoader, + cl -> { + try { + log.debug("Injecting classes onto classloader {} -> {}", cl, helperClassNames); + + Map classnameToBytes = getHelperMap(); + Map> classes; + if (cl == BOOTSTRAP_CLASSLOADER_PLACEHOLDER) { + classes = injectBootstrapClassLoader(classnameToBytes); + } else { + classes = injectClassLoader(cl, classnameToBytes); + } + + classes.values().forEach(c -> injectedClasses.put(c, Boolean.TRUE)); + + // All agent helper classes are in the unnamed module + // And there's exactly one unnamed module per classloader + // Use the module of the first class for convenience + if (JavaModule.isSupported()) { + JavaModule javaModule = JavaModule.ofType(classes.values().iterator().next()); + helperModules.add(new WeakReference<>(javaModule.unwrap())); + } + } catch (Exception e) { + if (log.isErrorEnabled()) { + log.error( + "Error preparing helpers while processing {} for {}. Failed to inject helper classes into instance {}", + typeDescription, + requestingName, + cl, + e); + } + throw new IllegalStateException(e); + } + return true; + }); + + ensureModuleCanReadHelperModules(module); + } + + if (!helperResourceNames.isEmpty()) { + for (String resourceName : helperResourceNames) { + URL resource = helpersSource.getResource(resourceName); + if (resource == null) { + log.debug("Helper resource {} requested but not found.", resourceName); + continue; + } + + log.debug("Injecting resource onto classloader {} -> {}", classLoader, resourceName); + HelperResources.register(classLoader, resourceName, resource); + } + } + + return builder; + } + + private static Map> injectBootstrapClassLoader( + Map classnameToBytes) throws IOException { + // Mar 2020: Since we're proactively cleaning up tempDirs, we cannot share dirs per thread. + // If this proves expensive, we could do a per-process tempDir with + // a reference count -- but for now, starting simple. + + // Failures to create a tempDir are propagated as IOException and handled by transform + File tempDir = createTempDir(); + try { + return ClassInjector.UsingInstrumentation.of( + tempDir, + ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, + AgentInstaller.getInstrumentation()) + .injectRaw(classnameToBytes); + } finally { + // Delete fails silently + deleteTempDir(tempDir); + } + } + + private static Map> injectClassLoader( + ClassLoader classLoader, Map classnameToBytes) { + return new ClassInjector.UsingReflection(classLoader).injectRaw(classnameToBytes); + } + + // JavaModule.equals doesn't work for some reason + @SuppressWarnings("ReferenceEquality") + private void ensureModuleCanReadHelperModules(JavaModule target) { + if (JavaModule.isSupported() && target != JavaModule.UNSUPPORTED && target.isNamed()) { + for (WeakReference helperModuleReference : helperModules) { + Object realModule = helperModuleReference.get(); + if (realModule != null) { + JavaModule helperModule = JavaModule.of(realModule); + + if (!target.canRead(helperModule)) { + log.debug("Adding module read from {} to {}", target, helperModule); + ClassInjector.UsingInstrumentation.redefineModule( + AgentInstaller.getInstrumentation(), + target, + Collections.singleton(helperModule), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptySet(), + Collections.emptyMap()); + } + } + } + } + } + + private static File createTempDir() throws IOException { + return Files.createTempDirectory("opentelemetry-temp-jars").toFile(); + } + + private static void deleteTempDir(File file) { + // Not using Files.delete for deleting the directory because failures + // create Exceptions which may prove expensive. Instead using the + // older File API which simply returns a boolean. + boolean deleted = file.delete(); + if (!deleted) { + file.deleteOnExit(); + } + } + + public static boolean isInjectedClass(Class c) { + return Boolean.TRUE.equals(injectedClasses.get(c)); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/InputStreamUrlConnection.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/InputStreamUrlConnection.java new file mode 100644 index 000000000..53095f784 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/InputStreamUrlConnection.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.security.Permission; + +public class InputStreamUrlConnection extends URLConnection { + private final InputStream inputStream; + private final long contentLength; + + public InputStreamUrlConnection(URL url, InputStream inputStream, long contentLength) { + super(url); + this.inputStream = inputStream; + this.contentLength = contentLength; + } + + @Override + public void connect() { + connected = true; + } + + @Override + public InputStream getInputStream() { + return inputStream; + } + + @Override + public Permission getPermission() { + // No permissions needed because all classes are in memory + return null; + } + + @Override + public long getContentLengthLong() { + return contentLength; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/LoggingConfigurer.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/LoggingConfigurer.java new file mode 100644 index 000000000..5c63b6f8e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/LoggingConfigurer.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import java.util.Locale; + +final class LoggingConfigurer { + + private static final String SIMPLE_LOGGER_SHOW_DATE_TIME_PROPERTY = + "io.opentelemetry.javaagent.slf4j.simpleLogger.showDateTime"; + private static final String SIMPLE_LOGGER_DATE_TIME_FORMAT_PROPERTY = + "io.opentelemetry.javaagent.slf4j.simpleLogger.dateTimeFormat"; + private static final String SIMPLE_LOGGER_DATE_TIME_FORMAT_DEFAULT = + "'[otel.javaagent 'yyyy-MM-dd HH:mm:ss:SSS Z']'"; + private static final String SIMPLE_LOGGER_DEFAULT_LOG_LEVEL_PROPERTY = + "io.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel"; + private static final String SIMPLE_LOGGER_PREFIX = + "io.opentelemetry.javaagent.slf4j.simpleLogger.log."; + + static void configureLogger() { + setSystemPropertyDefault(SIMPLE_LOGGER_SHOW_DATE_TIME_PROPERTY, "true"); + setSystemPropertyDefault( + SIMPLE_LOGGER_DATE_TIME_FORMAT_PROPERTY, SIMPLE_LOGGER_DATE_TIME_FORMAT_DEFAULT); + + if (isDebugMode()) { + setSystemPropertyDefault(SIMPLE_LOGGER_DEFAULT_LOG_LEVEL_PROPERTY, "DEBUG"); + // suppress a couple of verbose ClassNotFoundException stack traces logged at debug level + setSystemPropertyDefault(SIMPLE_LOGGER_PREFIX + "io.perfmark.PerfMark", "INFO"); + setSystemPropertyDefault(SIMPLE_LOGGER_PREFIX + "io.grpc.Context", "INFO"); + setSystemPropertyDefault(SIMPLE_LOGGER_PREFIX + "io.grpc.internal.ServerImplBuilder", "INFO"); + setSystemPropertyDefault(SIMPLE_LOGGER_PREFIX + "io.grpc.ManagedChannelRegistry", "INFO"); + setSystemPropertyDefault( + SIMPLE_LOGGER_PREFIX + "io.netty.util.internal.NativeLibraryLoader", "INFO"); + setSystemPropertyDefault( + SIMPLE_LOGGER_PREFIX + "io.grpc.internal.ManagedChannelImplBuilder", "INFO"); + } else { + // by default muzzle warnings are turned off + setSystemPropertyDefault(SIMPLE_LOGGER_PREFIX + "muzzleMatcher", "OFF"); + } + } + + private static void setSystemPropertyDefault(String property, String value) { + if (System.getProperty(property) == null) { + System.setProperty(property, value); + } + } + + /** + * Determine if we should log in debug level according to otel.javaagent.debug + * + * @return true if we should + */ + private static boolean isDebugMode() { + String tracerDebugLevelSysprop = "otel.javaagent.debug"; + String tracerDebugLevelProp = System.getProperty(tracerDebugLevelSysprop); + + if (tracerDebugLevelProp != null) { + return Boolean.parseBoolean(tracerDebugLevelProp); + } + + String tracerDebugLevelEnv = + System.getenv(tracerDebugLevelSysprop.replace('.', '_').toUpperCase(Locale.ROOT)); + + if (tracerDebugLevelEnv != null) { + return Boolean.parseBoolean(tracerDebugLevelEnv); + } + return false; + } + + private LoggingConfigurer() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/OpenTelemetryInstaller.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/OpenTelemetryInstaller.java new file mode 100644 index 000000000..d94dce231 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/OpenTelemetryInstaller.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.config.ConfigBuilder; +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.javaagent.instrumentation.api.OpenTelemetrySdkAccess; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@AutoService(AgentListener.class) +public class OpenTelemetryInstaller implements AgentListener { + private static final Logger log = LoggerFactory.getLogger(OpenTelemetryInstaller.class); + + static final String JAVAAGENT_ENABLED_CONFIG = "otel.javaagent.enabled"; + + @Override + public void beforeAgent(Config config) { + installAgentTracer(config); + } + + /** + * Register agent tracer if no agent tracer is already registered. + * + * @param config Configuration instance + */ + @SuppressWarnings("unused") + public static synchronized void installAgentTracer(Config config) { + if (config.getBoolean(JAVAAGENT_ENABLED_CONFIG, true)) { + copySystemProperties(config); + + OpenTelemetrySdk sdk = OpenTelemetrySdkAutoConfiguration.initialize(); + OpenTelemetrySdkAccess.internalSetForceFlush( + (timeout, unit) -> sdk.getSdkTracerProvider().forceFlush().join(timeout, unit)); + } else { + log.info("Tracing is disabled."); + } + } + + // OpenTelemetrySdkAutoConfiguration currently only supports configuration from environment. We + // massage any properties we have that aren't in the environment to system properties. + // TODO(anuraaga): Make this less hacky + private static void copySystemProperties(Config config) { + Properties allProperties = config.asJavaProperties(); + Properties environmentProperties = + new ConfigBuilder() + .readEnvironmentVariables() + .readSystemProperties() + .build() + .asJavaProperties(); + + allProperties.forEach( + (key, value) -> { + String keyStr = (String) key; + if (!environmentProperties.containsKey(key) + && keyStr.startsWith("otel.") + && !keyStr.startsWith("otel.instrumentation")) { + System.setProperty(keyStr, (String) value); + } + }); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlConnection.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlConnection.java new file mode 100644 index 000000000..0d2da7956 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlConnection.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import static io.opentelemetry.javaagent.tooling.ShadingRemapper.rule; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.security.Permission; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import net.bytebuddy.jar.asm.ClassReader; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.commons.ClassRemapper; + +public class RemappingUrlConnection extends URLConnection { + // We need to prefix the names to prevent the gradle shadowJar relocation rules from touching + // them. It's possible to do this by excluding this class from shading, but it may cause issue + // with transitive dependencies down the line. + private static final ShadingRemapper remapper = + new ShadingRemapper( + rule("#io.opentelemetry.api", "#io.opentelemetry.javaagent.shaded.io.opentelemetry.api"), + rule( + "#io.opentelemetry.context", + "#io.opentelemetry.javaagent.shaded.io.opentelemetry.context"), + rule( + "#io.opentelemetry.instrumentation", + "#io.opentelemetry.javaagent.shaded.io.opentelemetry.instrumentation"), + rule( + "#io.opentelemetry.semconv", + "#io.opentelemetry.javaagent.shaded.io.opentelemetry.semconv"), + rule( + "#io.opentelemetry.extension.aws", + "#io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.aws"), + rule("#java.util.logging.Logger", "#io.opentelemetry.javaagent.bootstrap.PatchLogger"), + rule("#org.slf4j", "#io.opentelemetry.javaagent.slf4j")); + + private final JarFile delegateJarFile; + private final JarEntry entry; + + private byte[] cacheClassBytes; + + public RemappingUrlConnection(URL url, JarFile delegateJarFile, JarEntry entry) { + super(url); + this.delegateJarFile = delegateJarFile; + this.entry = entry; + } + + @Override + public void connect() { + connected = true; + } + + @Override + public InputStream getInputStream() throws IOException { + if (cacheClassBytes == null) { + cacheClassBytes = readAndRemap(); + } + + return new ByteArrayInputStream(cacheClassBytes); + } + + private byte[] readAndRemap() throws IOException { + try { + InputStream inputStream = delegateJarFile.getInputStream(entry); + return remapClassBytes(inputStream); + } catch (IOException e) { + throw new IOException( + String.format("Failed to remap bytes for %s: %s%n", url.toString(), e.getMessage())); + } + } + + private static byte[] remapClassBytes(InputStream in) throws IOException { + ClassReader cr = new ClassReader(in); + ClassWriter cw = new ClassWriter(cr, 0); + cr.accept(new ClassRemapper(cw, remapper), ClassReader.EXPAND_FRAMES); + return cw.toByteArray(); + } + + @Override + public Permission getPermission() { + // No permissions needed because all classes are in memory + return null; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java new file mode 100644 index 000000000..dab3681c0 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +class RemappingUrlStreamHandler extends URLStreamHandler { + private final JarFile delegateJarFile; + + public RemappingUrlStreamHandler(File delegateFile) { + try { + delegateJarFile = new JarFile(delegateFile, false); + } catch (IOException e) { + throw new IllegalStateException("Unable to read internal jar", e); + } + } + + /** {@inheritDoc} */ + @Override + protected URLConnection openConnection(URL url) throws IOException { + String file = url.getFile(); + if ("/".equals(file)) { + // "/" is used as the default url of the jar + // This is called by the SecureClassLoader trying to obtain permissions + // nullInputStream() is not available until Java 11 + return new InputStreamUrlConnection(url, new ByteArrayInputStream(new byte[0]), 0); + } + + if (file.startsWith("/")) { + file = file.substring(1); + } + JarEntry entry = delegateJarFile.getJarEntry(file); + if (entry == null) { + throw new FileNotFoundException( + "JAR entry " + file + " not found in " + delegateJarFile.getName()); + } + + // That will NOT remap the content of files under META-INF/services + if (file.endsWith(".class")) { + return new RemappingUrlConnection(url, delegateJarFile, entry); + } else { + InputStream is = delegateJarFile.getInputStream(entry); + return new InputStreamUrlConnection(url, is, entry.getSize()); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/SafeServiceLoader.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/SafeServiceLoader.java new file mode 100644 index 000000000..3b1942071 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/SafeServiceLoader.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import io.opentelemetry.javaagent.extension.Ordered; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class SafeServiceLoader { + + private static final Logger log = LoggerFactory.getLogger(SafeServiceLoader.class); + + /** + * Delegates to {@link ServiceLoader#load(Class, ClassLoader)} and then eagerly iterates over + * returned {@code Iterable}, ignoring any potential {@link UnsupportedClassVersionError}. + * + *

Those errors can happen when some classes returned by {@code ServiceLoader} were compiled + * for later java version than is used by currently running JVM. During normal course of business + * this should not happen. Please read CONTRIBUTING.md, section "Testing - Java versions" for a + * background info why this is Ok. + */ + // Because we want to catch exception per iteration + @SuppressWarnings("ForEachIterable") + public static List load(Class serviceClass) { + List result = new ArrayList<>(); + java.util.ServiceLoader services = ServiceLoader.load(serviceClass); + for (Iterator iter = services.iterator(); iter.hasNext(); ) { + try { + result.add(iter.next()); + } catch (UnsupportedClassVersionError e) { + log.debug("Unable to load instrumentation class: {}", e.getMessage()); + } + } + return result; + } + + /** + * Same as {@link #load(Class)}, but also orders the returned implementations by comparing their + * {@link Ordered#order()}. + */ + public static List loadOrdered(Class serviceClass) { + List result = load(serviceClass); + result.sort(Comparator.comparing(Ordered::order)); + return result; + } + + private SafeServiceLoader() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ShadingRemapper.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ShadingRemapper.java new file mode 100644 index 000000000..480ec234d --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ShadingRemapper.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import java.util.Map; +import java.util.TreeMap; +import net.bytebuddy.jar.asm.commons.Remapper; + +public class ShadingRemapper extends Remapper { + public static class Rule { + private final String from; + private final String to; + + public Rule(String from, String to) { + // Strip prefix added to prevent the build-time relocation from changing the names + if (from.startsWith("#")) { + from = from.substring(1); + } + if (to.startsWith("#")) { + to = to.substring(1); + } + this.from = from.replace('.', '/'); + this.to = to.replace('.', '/'); + } + } + + public static Rule rule(String from, String to) { + return new Rule(from, to); + } + + private final TreeMap map = new TreeMap<>(); + + public ShadingRemapper(Rule... rules) { + for (Rule rule : rules) { + map.put(rule.from, rule.to); + } + } + + @Override + public String map(String internalName) { + Map.Entry e = map.floorEntry(internalName); + if (e != null && internalName.startsWith(e.getKey())) { + return e.getValue() + internalName.substring(e.getKey().length()); + } + return super.map(internalName); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/TransformSafeLogger.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/TransformSafeLogger.java new file mode 100644 index 000000000..bc3573d22 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/TransformSafeLogger.java @@ -0,0 +1,170 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import static org.slf4j.event.Level.DEBUG; +import static org.slf4j.event.Level.TRACE; +import static org.slf4j.event.Level.WARN; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; + +/** + * Debug logging that is performed under class file transform needs to use this class, because + * gradle deadlocks sporadically under the following sequence: + *

  • Gradle triggers a class to load while it is holding a lock + *
  • Class file transform occurs (under this lock) and the agent writes to System.out + *
  • (Because gradle hijacks System.out), gradle is called from inside of the class file transform + *
  • Gradle tries to grab a different lock during it's implementation of System.out + */ +public final class TransformSafeLogger { + + private static final boolean ENABLE_TRANSFORM_SAFE_LOGGING = + Boolean.getBoolean("otel.javaagent.testing.transform-safe-logging.enabled"); + + @Nullable private static final BlockingQueue logMessageQueue; + + static { + if (ENABLE_TRANSFORM_SAFE_LOGGING) { + logMessageQueue = new ArrayBlockingQueue<>(1000); + Thread thread = new Thread(new LogMessageQueueReader()); + thread.setName("otel-javaagent-transform-safe-logger"); + thread.setDaemon(true); + thread.start(); + } else { + logMessageQueue = null; + } + } + + private final Logger logger; + + public static TransformSafeLogger getLogger(Class clazz) { + return new TransformSafeLogger(LoggerFactory.getLogger(clazz)); + } + + private TransformSafeLogger(Logger logger) { + this.logger = logger; + } + + public void debug(String format, Object arg) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(DEBUG, logger, format, arg)); + } else { + logger.debug(format, arg); + } + } + + public void debug(String format, Object arg1, Object arg2) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(DEBUG, logger, format, arg1, arg2)); + } else { + logger.debug(format, arg1, arg2); + } + } + + public void debug(String format, Object... arguments) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(DEBUG, logger, format, arguments)); + } else { + logger.debug(format, arguments); + } + } + + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + public void trace(String format, Object arg) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(TRACE, logger, format, arg)); + } else { + logger.trace(format, arg); + } + } + + public void trace(String format, Object arg1, Object arg2) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(TRACE, logger, format, arg1, arg2)); + } else { + logger.trace(format, arg1, arg2); + } + } + + public void trace(String format, Object... arguments) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(TRACE, logger, format, arguments)); + } else { + logger.trace(format, arguments); + } + } + + public void warn(String format, Object arg) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(WARN, logger, format, arg)); + } else { + logger.warn(format, arg); + } + } + + public void warn(String format, Object arg1, Object arg2) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(WARN, logger, format, arg1, arg2)); + } else { + logger.warn(format, arg1, arg2); + } + } + + public void warn(String format, Object... arguments) { + if (logMessageQueue != null) { + logMessageQueue.offer(new LogMessage(WARN, logger, format, arguments)); + } else { + logger.warn(format, arguments); + } + } + + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + private static class LogMessageQueueReader implements Runnable { + @Override + public void run() { + try { + while (true) { + LogMessage logMessage = logMessageQueue.take(); + if (logMessage.level == DEBUG) { + logMessage.logger.debug(logMessage.format, logMessage.arguments); + } else if (logMessage.level == TRACE) { + logMessage.logger.trace(logMessage.format, logMessage.arguments); + } else { + logMessage.logger.warn( + "level {} not implemented yet in TransformSafeLogger", logMessage.level); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + private static class LogMessage { + private final Level level; + private final Logger logger; + private final String format; + private final Object[] arguments; + + private LogMessage(Level level, Logger logger, String format, Object... arguments) { + this.level = level; + this.logger = logger; + this.format = format; + this.arguments = arguments; + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Utils.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Utils.java new file mode 100644 index 000000000..82b27cb7e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Utils.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.bootstrap.AgentClassLoader; +import io.opentelemetry.javaagent.bootstrap.AgentClassLoader.BootstrapClassLoaderProxy; +import io.opentelemetry.javaagent.bootstrap.AgentInitializer; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDefinition; + +public class Utils { + + private static final BootstrapClassLoaderProxy unitTestBootstrapProxy = + new BootstrapClassLoaderProxy(null); + + /** Return the classloader the core agent is running on. */ + public static ClassLoader getAgentClassLoader() { + return AgentInstaller.class.getClassLoader(); + } + + public static ClassLoader getExtensionsClassLoader() { + return AgentInitializer.getAgentClassLoader(); + } + + /** Return a classloader which can be used to look up bootstrap resources. */ + public static BootstrapClassLoaderProxy getBootstrapProxy() { + if (getAgentClassLoader() instanceof AgentClassLoader) { + return ((AgentClassLoader) getAgentClassLoader()).getBootstrapProxy(); + } + // in a unit test + return unitTestBootstrapProxy; + } + + /** com.foo.Bar to com/foo/Bar.class */ + public static String getResourceName(String className) { + return className.replace('.', '/') + ".class"; + } + + /** com/foo/Bar to com.foo.Bar */ + public static String getClassName(String internalName) { + return internalName.replace('/', '.'); + } + + /** com.foo.Bar to com/foo/Bar */ + public static String getInternalName(Class clazz) { + return clazz.getName().replace('.', '/'); + } + + /** + * Convert class name to a format that can be used as part of inner class name by replacing all + * '.'s with '$'s. + * + * @param className class named to be converted + * @return converted name + */ + public static String convertToInnerClassName(String className) { + return className.replace('.', '$'); + } + + /** + * Get method definition for given {@link TypeDefinition} and method name. + * + * @param type type + * @param methodName method name + * @return {@link MethodDescription} for given method + * @throws IllegalStateException if more then one method matches (i.e. in case of overloaded + * methods) or if no method found + */ + public static MethodDescription getMethodDefinition(TypeDefinition type, String methodName) { + return type.getDeclaredMethods().filter(named(methodName)).getOnly(); + } + + private Utils() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/VersionLogger.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/VersionLogger.java new file mode 100644 index 000000000..17b40a04d --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/VersionLogger.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import io.opentelemetry.instrumentation.api.InstrumentationVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class VersionLogger { + + private static final Logger log = LoggerFactory.getLogger(VersionLogger.class); + + public static void logAllVersions() { + log.info("opentelemetry-javaagent - version: {}", InstrumentationVersion.VERSION); + if (log.isDebugEnabled()) { + log.debug( + "Running on Java {}. JVM {} - {} - {}", + System.getProperty("java.version"), + System.getProperty("java.vm.name"), + System.getProperty("java.vm.vendor"), + System.getProperty("java.vm.version")); + } + } + + private VersionLogger() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/AgentCachingPoolStrategy.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/AgentCachingPoolStrategy.java new file mode 100644 index 000000000..9796c9d02 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/AgentCachingPoolStrategy.java @@ -0,0 +1,329 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import java.lang.ref.WeakReference; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.annotation.AnnotationList; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.description.type.TypeList; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.pool.TypePool; + +/** + * + * + *
      + * There two core parts to the cache... + *
    • a cache of ClassLoader to WeakReference<ClassLoader> + *
    • a single cache of TypeResolutions for all ClassLoaders - keyed by a custom composite key of + * ClassLoader and class name + *
    + * + *

    This design was chosen to create a single limited size cache that can be adjusted for the + * entire application -- without having to create a large number of WeakReference objects. + * + *

    Eviction is handled through a size restriction + */ +public class AgentCachingPoolStrategy implements AgentBuilder.PoolStrategy { + + // Many things are package visible for testing purposes -- + // others to avoid creation of synthetic accessors + + static final int TYPE_CAPACITY = 64; + + static final int BOOTSTRAP_HASH = 7236344; // Just a random number + + /** + * Cache of recent ClassLoader WeakReferences; used to... + * + *

      + *
    • Reduced number of WeakReferences created + *
    • Allow for quick fast path equivalence check of composite keys + *
    + */ + final Cache> loaderRefCache = + Cache.newBuilder().setWeakKeys().build(); + + /** + * Single shared Type.Resolution cache -- uses a composite key -- conceptually of loader & name + */ + final Cache sharedResolutionCache = + Cache.newBuilder().setMaximumSize(TYPE_CAPACITY).build(); + + // fast path for bootstrap + final SharedResolutionCacheAdapter bootstrapCacheProvider = + new SharedResolutionCacheAdapter(BOOTSTRAP_HASH, null, sharedResolutionCache); + + @Override + public final TypePool typePool(ClassFileLocator classFileLocator, ClassLoader classLoader) { + if (classLoader == null) { + return createCachingTypePool(bootstrapCacheProvider, classFileLocator); + } + + WeakReference loaderRef = + loaderRefCache.computeIfAbsent(classLoader, WeakReference::new); + + int loaderHash = classLoader.hashCode(); + return createCachingTypePool(loaderHash, loaderRef, classFileLocator); + } + + @Override + public final TypePool typePool( + ClassFileLocator classFileLocator, ClassLoader classLoader, String name) { + return typePool(classFileLocator, classLoader); + } + + private TypePool.CacheProvider createCacheProvider( + int loaderHash, WeakReference loaderRef) { + return new SharedResolutionCacheAdapter(loaderHash, loaderRef, sharedResolutionCache); + } + + private TypePool createCachingTypePool( + int loaderHash, WeakReference loaderRef, ClassFileLocator classFileLocator) { + return new TypePool.Default.WithLazyResolution( + createCacheProvider(loaderHash, loaderRef), + classFileLocator, + TypePool.Default.ReaderMode.FAST); + } + + private static TypePool createCachingTypePool( + TypePool.CacheProvider cacheProvider, ClassFileLocator classFileLocator) { + return new TypePool.Default.WithLazyResolution( + cacheProvider, classFileLocator, TypePool.Default.ReaderMode.FAST); + } + + /** + * TypeCacheKey is key for the sharedResolutionCache. Conceptually, it is a mix of ClassLoader & + * class name. + * + *

    For efficiency & GC purposes, it is actually composed of loaderHash & + * WeakReference<ClassLoader> + * + *

    The loaderHash exists to avoid calling get & strengthening the Reference. + */ + static final class TypeCacheKey { + private final int loaderHash; + private final WeakReference loaderRef; + private final String className; + + private final int hashCode; + + TypeCacheKey(int loaderHash, WeakReference loaderRef, String className) { + this.loaderHash = loaderHash; + this.loaderRef = loaderRef; + this.className = className; + + hashCode = 31 * loaderHash + className.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof TypeCacheKey)) { + return false; + } + + TypeCacheKey other = (TypeCacheKey) obj; + + if (loaderHash != other.loaderHash) { + return false; + } + + if (!className.equals(other.className)) { + return false; + } + + // Fastpath loaderRef equivalence -- works because of WeakReference cache used + // Also covers the bootstrap null loaderRef case + if (loaderRef == other.loaderRef) { + return true; + } + + // need to perform a deeper loader check -- requires calling Reference.get + // which can strengthen the Reference, so deliberately done last + + // If either reference has gone null, they aren't considered equivalent + // Technically, this is a bit of violation of equals semantics, since + // two equivalent references can become not equivalent. + + // In this case, it is fine because that means the ClassLoader is no + // longer live, so the entries will never match anyway and will fall + // out of the cache. + ClassLoader thisLoader = loaderRef.get(); + if (thisLoader == null) { + return false; + } + + ClassLoader otherLoader = other.loaderRef.get(); + if (otherLoader == null) { + return false; + } + + return thisLoader == otherLoader; + } + + @Override + public final int hashCode() { + return hashCode; + } + + @Override + public String toString() { + return "TypeCacheKey{" + + "loaderHash=" + + loaderHash + + ", loaderRef=" + + loaderRef + + ", className='" + + className + + '\'' + + '}'; + } + } + + static final class SharedResolutionCacheAdapter implements TypePool.CacheProvider { + private static final String OBJECT_NAME = "java.lang.Object"; + private static final TypePool.Resolution OBJECT_RESOLUTION = + new TypePool.Resolution.Simple(new CachingTypeDescription(TypeDescription.OBJECT)); + + private final int loaderHash; + private final WeakReference loaderRef; + private final Cache sharedResolutionCache; + + SharedResolutionCacheAdapter( + int loaderHash, + WeakReference loaderRef, + Cache sharedResolutionCache) { + this.loaderHash = loaderHash; + this.loaderRef = loaderRef; + this.sharedResolutionCache = sharedResolutionCache; + } + + @Override + public TypePool.Resolution find(String className) { + TypePool.Resolution existingResolution = + sharedResolutionCache.get(new TypeCacheKey(loaderHash, loaderRef, className)); + if (existingResolution != null) { + return existingResolution; + } + + if (OBJECT_NAME.equals(className)) { + return OBJECT_RESOLUTION; + } + + return null; + } + + @Override + public TypePool.Resolution register(String className, TypePool.Resolution resolution) { + if (OBJECT_NAME.equals(className)) { + return resolution; + } + + resolution = new CachingResolution(resolution); + + sharedResolutionCache.put(new TypeCacheKey(loaderHash, loaderRef, className), resolution); + return resolution; + } + + @Override + public void clear() { + // Allowing the high-level eviction policy make the clearing decisions + } + } + + private static class CachingResolution implements TypePool.Resolution { + private final TypePool.Resolution delegate; + private TypeDescription cachedResolution; + + public CachingResolution(TypePool.Resolution delegate) { + + this.delegate = delegate; + } + + @Override + public boolean isResolved() { + return delegate.isResolved(); + } + + @Override + public TypeDescription resolve() { + // Intentionally not "thread safe". Duplicate work deemed an acceptable trade-off. + if (cachedResolution == null) { + cachedResolution = new CachingTypeDescription(delegate.resolve()); + } + return cachedResolution; + } + } + + /** + * TypeDescription implementation that delegates and caches the results for the expensive calls + * commonly used by our instrumentation. + */ + private static class CachingTypeDescription + extends TypeDescription.AbstractBase.OfSimpleType.WithDelegation { + private final TypeDescription delegate; + + // These fields are intentionally not "thread safe". + // Duplicate work deemed an acceptable trade-off. + private Generic superClass; + private TypeList.Generic interfaces; + private AnnotationList annotations; + private MethodList methods; + + public CachingTypeDescription(TypeDescription delegate) { + this.delegate = delegate; + } + + @Override + protected TypeDescription delegate() { + return delegate; + } + + @Override + public Generic getSuperClass() { + if (superClass == null) { + superClass = delegate.getSuperClass(); + } + return superClass; + } + + @Override + public TypeList.Generic getInterfaces() { + if (interfaces == null) { + interfaces = delegate.getInterfaces(); + } + return interfaces; + } + + @Override + public AnnotationList getDeclaredAnnotations() { + if (annotations == null) { + annotations = delegate.getDeclaredAnnotations(); + } + return annotations; + } + + @Override + public MethodList getDeclaredMethods() { + if (methods == null) { + methods = delegate.getDeclaredMethods(); + } + return methods; + } + + @Override + public String getName() { + return delegate.getName(); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/AgentLocationStrategy.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/AgentLocationStrategy.java new file mode 100644 index 000000000..4c9b52253 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/AgentLocationStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy; + +import io.opentelemetry.javaagent.tooling.Utils; +import java.util.ArrayList; +import java.util.List; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.utility.JavaModule; + +/** + * Locate resources with the loading classloader. Because of a quirk with the way classes appended + * to the bootstrap classpath work, we first check our bootstrap proxy. If the loading classloader + * cannot find the desired resource, check up the classloader hierarchy until a resource is found. + */ +public class AgentLocationStrategy implements AgentBuilder.LocationStrategy { + + public ClassFileLocator classFileLocator(ClassLoader classLoader) { + return classFileLocator(classLoader, null); + } + + @Override + public ClassFileLocator classFileLocator(ClassLoader classLoader, JavaModule javaModule) { + List locators = new ArrayList<>(); + locators.add(ClassFileLocator.ForClassLoader.of(Utils.getBootstrapProxy())); + while (classLoader != null) { + locators.add(ClassFileLocator.ForClassLoader.WeaklyReferenced.of(classLoader)); + classLoader = classLoader.getParent(); + } + + return new ClassFileLocator.Compound(locators.toArray(new ClassFileLocator[0])); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/ExceptionHandlers.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/ExceptionHandlers.java new file mode 100644 index 000000000..0784275f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/ExceptionHandlers.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy; + +import io.opentelemetry.javaagent.bootstrap.ExceptionLogger; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.asm.Advice.ExceptionHandler; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.bytecode.StackManipulation; +import net.bytebuddy.jar.asm.Label; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.jar.asm.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ExceptionHandlers { + private static final String LOG_FACTORY_NAME = LoggerFactory.class.getName().replace('.', '/'); + private static final String LOGGER_NAME = Logger.class.getName().replace('.', '/'); + // Bootstrap ExceptionHandler.class will always be resolvable, so we'll use it in the log name + private static final String HANDLER_NAME = ExceptionLogger.class.getName().replace('.', '/'); + + private static final ExceptionHandler EXCEPTION_STACK_HANDLER = + new ExceptionHandler.Simple( + new StackManipulation() { + // Pops one Throwable off the stack. Maxes the stack to at least 3. + private final StackManipulation.Size size = new StackManipulation.Size(-1, 3); + + @Override + public boolean isValid() { + return true; + } + + @Override + public StackManipulation.Size apply(MethodVisitor mv, Implementation.Context context) { + String name = context.getInstrumentedType().getName(); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + // writes the following bytecode: + // try { + // org.slf4j.LoggerFactory.getLogger((Class)ExceptionLogger.class) + // .debug("exception in instrumentation", t); + // } catch (Throwable t2) { + // } + Label logStart = new Label(); + Label logEnd = new Label(); + Label eatException = new Label(); + Label handlerExit = new Label(); + + // Frames are only meaningful for class files in version 6 or later. + boolean frames = context.getClassFileVersion().isAtLeast(ClassFileVersion.JAVA_V6); + + mv.visitTryCatchBlock(logStart, logEnd, eatException, "java/lang/Throwable"); + + // stack: (top) throwable + mv.visitLabel(logStart); + mv.visitLdcInsn(Type.getType("L" + HANDLER_NAME + ";")); + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + LOG_FACTORY_NAME, + "getLogger", + "(Ljava/lang/Class;)L" + LOGGER_NAME + ";", + /* isInterface= */ false); + mv.visitInsn(Opcodes.SWAP); // stack: (top) throwable,logger + mv.visitLdcInsn( + "Failed to handle exception in instrumentation for " + + name + + " on " + + classLoader); + mv.visitInsn(Opcodes.SWAP); // stack: (top) throwable,string,logger + mv.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + LOGGER_NAME, + "debug", + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + /* isInterface= */ true); + mv.visitLabel(logEnd); + mv.visitJumpInsn(Opcodes.GOTO, handlerExit); + + // if the runtime can't reach our ExceptionHandler or logger, + // silently eat the exception + mv.visitLabel(eatException); + if (frames) { + mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"}); + } + mv.visitInsn(Opcodes.POP); + // mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Throwable", + // "printStackTrace", "()V", false); + + mv.visitLabel(handlerExit); + if (frames) { + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + } + + return size; + } + }); + + public static ExceptionHandler defaultExceptionHandler() { + return EXCEPTION_STACK_HANDLER; + } + + private ExceptionHandlers() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/ConfigInitializer.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/ConfigInitializer.java new file mode 100644 index 000000000..35abb217c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/ConfigInitializer.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.config; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.config.ConfigBuilder; +import io.opentelemetry.javaagent.spi.config.PropertySource; +import io.opentelemetry.javaagent.tooling.SafeServiceLoader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ConfigInitializer { + private static final Logger log = LoggerFactory.getLogger(ConfigInitializer.class); + + private static final String CONFIGURATION_FILE_PROPERTY = "otel.javaagent.configuration-file"; + private static final String CONFIGURATION_FILE_ENV_VAR = "OTEL_JAVAAGENT_CONFIGURATION_FILE"; + + public static void initialize() { + Config.internalInitializeConfig(create(loadSpiConfiguration(), loadConfigurationFile())); + } + + // visible for testing + static Config create(Properties spiConfiguration, Properties configurationFile) { + return new ConfigBuilder() + .readProperties(spiConfiguration) + .readProperties(configurationFile) + .readEnvironmentVariables() + .readSystemProperties() + .build(); + } + + /** Retrieves all default configuration overloads using SPI and initializes Config. */ + private static Properties loadSpiConfiguration() { + Properties propertiesFromSpi = new Properties(); + for (PropertySource propertySource : SafeServiceLoader.load(PropertySource.class)) { + propertiesFromSpi.putAll(propertySource.getProperties()); + } + return propertiesFromSpi; + } + + /** + * Loads the optional configuration properties file into the global {@link Properties} object. + * + * @return The {@link Properties} object. the returned instance might be empty of file does not + * exist or if it is in a wrong format. + */ + private static Properties loadConfigurationFile() { + Properties properties = new Properties(); + + // Reading from system property first and from env after + String configurationFilePath = System.getProperty(CONFIGURATION_FILE_PROPERTY); + if (configurationFilePath == null) { + configurationFilePath = System.getenv(CONFIGURATION_FILE_ENV_VAR); + } + if (configurationFilePath == null) { + return properties; + } + + // Normalizing tilde (~) paths for unix systems + configurationFilePath = + configurationFilePath.replaceFirst("^~", System.getProperty("user.home")); + + // Configuration properties file is optional + File configurationFile = new File(configurationFilePath); + if (!configurationFile.exists()) { + log.error("Configuration file '{}' not found.", configurationFilePath); + return properties; + } + + try (InputStreamReader reader = + new InputStreamReader(new FileInputStream(configurationFile), StandardCharsets.UTF_8)) { + properties.load(reader); + } catch (FileNotFoundException fnf) { + log.error("Configuration file '{}' not found.", configurationFilePath); + } catch (IOException ioe) { + log.error( + "Configuration file '{}' cannot be accessed or correctly parsed.", configurationFilePath); + } + + return properties; + } + + private ConfigInitializer() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParser.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParser.java new file mode 100644 index 000000000..5741fb1f8 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParser.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class MethodsConfigurationParser { + + private static final Logger log = LoggerFactory.getLogger(MethodsConfigurationParser.class); + + static final String PACKAGE_CLASS_NAME_REGEX = "[\\w.$]+"; + private static final String METHOD_LIST_REGEX = "\\s*(?:\\w+\\s*,)*\\s*(?:\\w+\\s*,?)\\s*"; + private static final String CONFIG_FORMAT = + "(?:\\s*" + + PACKAGE_CLASS_NAME_REGEX + + "\\[" + + METHOD_LIST_REGEX + + "]\\s*;)*\\s*" + + PACKAGE_CLASS_NAME_REGEX + + "\\[" + + METHOD_LIST_REGEX + + "]"; + + /** + * This method takes a string in a form of {@code + * "io.package.ClassName[method1,method2];my.example[someMethodName];"} and returns a map where + * keys are class names and corresponding value is a set of methods for that class. + * + *

    Strings of such format are used e.g. to configure {@code TraceConfigInstrumentation} + */ + public static Map> parse(String configString) { + if (configString == null || configString.trim().isEmpty()) { + return Collections.emptyMap(); + } else if (!validateConfigString(configString)) { + log.warn( + "Invalid trace method config '{}'. Must match 'package.Class$Name[method1,method2];*'.", + configString); + return Collections.emptyMap(); + } else { + Map> toTrace = new HashMap<>(); + String[] classMethods = configString.split(";", -1); + for (String classMethod : classMethods) { + if (classMethod.trim().isEmpty()) { + continue; + } + String[] splitClassMethod = classMethod.split("\\[", -1); + String className = splitClassMethod[0]; + String method = splitClassMethod[1].trim(); + String methodNames = method.substring(0, method.length() - 1); + String[] splitMethodNames = methodNames.split(",", -1); + Set trimmedMethodNames = new HashSet<>(splitMethodNames.length); + for (String methodName : splitMethodNames) { + String trimmedMethodName = methodName.trim(); + if (!trimmedMethodName.isEmpty()) { + trimmedMethodNames.add(trimmedMethodName); + } + } + if (!trimmedMethodNames.isEmpty()) { + toTrace.put(className.trim(), trimmedMethodNames); + } + } + return toTrace; + } + } + + private static boolean validateConfigString(String configString) { + for (String segment : configString.split(";")) { + if (!segment.trim().matches(CONFIG_FORMAT)) { + return false; + } + } + return true; + } + + private MethodsConfigurationParser() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/FieldBackedProvider.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/FieldBackedProvider.java new file mode 100644 index 000000000..b4ea387ea --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/FieldBackedProvider.java @@ -0,0 +1,1030 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.context; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType; +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.BOOTSTRAP_CLASSLOADER; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.HelperInjector; +import io.opentelemetry.javaagent.tooling.TransformSafeLogger; +import io.opentelemetry.javaagent.tooling.Utils; +import io.opentelemetry.javaagent.tooling.instrumentation.InstrumentationModuleInstaller; +import java.lang.reflect.Method; +import java.security.ProtectionDomain; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.modifier.TypeManifestation; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.FieldVisitor; +import net.bytebuddy.jar.asm.Label; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.jar.asm.Type; +import net.bytebuddy.pool.TypePool; +import net.bytebuddy.utility.JavaModule; + +/** + * InstrumentationContextProvider which stores context in a field that is injected into a class and + * falls back to global map if field was not injected. + * + *

    This is accomplished by + * + *

      + *
    1. Injecting a Dynamic Interface that provides getter and setter for context field + *
    2. Applying Dynamic Interface to a type needing context, implementing interface methods and + * adding context storage field + *
    3. Injecting a Dynamic Class created from {@link ContextStoreImplementationTemplate} to use + * injected field or fall back to a static map + *
    4. Rewriting calls to the context-store to access the specific dynamic {@link + * ContextStoreImplementationTemplate} + *
    + * + *

    Example:
    + * InstrumentationContext.get(Runnable.class, RunnableState.class)")
    + * is rewritten to:
    + * FieldBackedProvider$ContextStore$Runnable$RunnableState12345.getContextStore(runnableRunnable.class, + * RunnableState.class) + */ +public class FieldBackedProvider implements InstrumentationContextProvider { + + private static final TransformSafeLogger log = + TransformSafeLogger.getLogger(FieldBackedProvider.class); + + /** + * Note: the value here has to be inside on of the prefixes in + * io.opentelemetry.javaagent.tooling.Constants#BOOTSTRAP_PACKAGE_PREFIXES. This ensures that + * 'isolating' (or 'module') classloaders like jboss and osgi see injected classes. This works + * because we instrument those classloaders to load everything inside bootstrap packages. + */ + private static final String DYNAMIC_CLASSES_PACKAGE = + "io.opentelemetry.javaagent.bootstrap.instrumentation.context."; + + private static final String INJECTED_FIELDS_MARKER_CLASS_NAME = + Utils.getInternalName(FieldBackedContextStoreAppliedMarker.class); + + private static final Method CONTEXT_GET_METHOD; + private static final Method GET_CONTEXT_STORE_METHOD; + + static { + try { + CONTEXT_GET_METHOD = InstrumentationContext.class.getMethod("get", Class.class, Class.class); + GET_CONTEXT_STORE_METHOD = + ContextStoreImplementationTemplate.class.getMethod( + "getContextStore", Class.class, Class.class); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static final boolean FIELD_INJECTION_ENABLED = + Config.get().getBoolean("otel.javaagent.experimental.field-injection.enabled", true); + + private final Class instrumenterClass; + private final ByteBuddy byteBuddy; + private final Map contextStore; + + // fields-accessor-interface-name -> fields-accessor-interface-dynamic-type + private final Map> fieldAccessorInterfaces; + + private final AgentBuilder.Transformer fieldAccessorInterfacesInjector; + + // context-store-type-name -> context-store-type-name-dynamic-type + private final Map> contextStoreImplementations; + + private final AgentBuilder.Transformer contextStoreImplementationsInjector; + + public FieldBackedProvider(Class instrumenterClass, Map contextStore) { + this.instrumenterClass = instrumenterClass; + this.contextStore = contextStore; + byteBuddy = new ByteBuddy(); + fieldAccessorInterfaces = generateFieldAccessorInterfaces(); + fieldAccessorInterfacesInjector = bootstrapHelperInjector(fieldAccessorInterfaces.values()); + contextStoreImplementations = generateContextStoreImplementationClasses(); + contextStoreImplementationsInjector = + bootstrapHelperInjector(contextStoreImplementations.values()); + } + + @Override + public AgentBuilder.Identified.Extendable instrumentationTransformer( + AgentBuilder.Identified.Extendable builder) { + if (!contextStore.isEmpty()) { + /* + * Install transformer that rewrites accesses to context store with specialized bytecode that + * invokes appropriate storage implementation. + */ + builder = + builder.transform(getTransformerForAsmVisitor(getContextStoreReadsRewritingVisitor())); + builder = injectHelpersIntoBootstrapClassloader(builder); + } + return builder; + } + + private AsmVisitorWrapper getContextStoreReadsRewritingVisitor() { + return new AsmVisitorWrapper() { + @Override + public int mergeWriter(int flags) { + return flags | ClassWriter.COMPUTE_MAXS; + } + + @Override + public int mergeReader(int flags) { + return flags; + } + + @Override + public ClassVisitor wrap( + TypeDescription instrumentedType, + ClassVisitor classVisitor, + Implementation.Context implementationContext, + TypePool typePool, + FieldList fields, + MethodList methods, + int writerFlags, + int readerFlags) { + return new ClassVisitor(Opcodes.ASM7, classVisitor) { + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + return new MethodVisitor(Opcodes.ASM7, mv) { + /** The most recent objects pushed to the stack. */ + private final Object[] stack = {null, null}; + /** Most recent instructions. */ + private final int[] insnStack = {-1, -1, -1}; + + @Override + public void visitMethodInsn( + int opcode, String owner, String name, String descriptor, boolean isInterface) { + pushOpcode(opcode); + if (Utils.getInternalName(CONTEXT_GET_METHOD.getDeclaringClass()).equals(owner) + && CONTEXT_GET_METHOD.getName().equals(name) + && Type.getMethodDescriptor(CONTEXT_GET_METHOD).equals(descriptor)) { + log.trace("Found context-store access in {}", instrumenterClass.getName()); + /* + The idea here is that the rest if this method visitor collects last three instructions in `insnStack` + variable. Once we get here we check if those last three instructions constitute call that looks like + `InstrumentationContext.get(K.class, V.class)`. If it does the inside of this if rewrites it to call + dynamically injected context store implementation instead. + */ + if ((insnStack[0] == Opcodes.INVOKESTATIC + && insnStack[1] == Opcodes.LDC + && insnStack[2] == Opcodes.LDC) + && (stack[0] instanceof Type && stack[1] instanceof Type)) { + String contextClassName = ((Type) stack[0]).getClassName(); + String keyClassName = ((Type) stack[1]).getClassName(); + TypeDescription contextStoreImplementationClass = + getContextStoreImplementation(keyClassName, contextClassName); + if (log.isTraceEnabled()) { + log.trace( + "Rewriting context-store map fetch for instrumenter {}: {} -> {}", + instrumenterClass.getName(), + keyClassName, + contextClassName); + } + if (contextStoreImplementationClass == null) { + throw new IllegalStateException( + String.format( + "Incorrect Context Api Usage detected. Cannot find map holder class for %s context %s. Was that class defined in contextStore for instrumentation %s?", + keyClassName, contextClassName, instrumenterClass.getName())); + } + if (!contextClassName.equals(contextStore.get(keyClassName))) { + throw new IllegalStateException( + String.format( + "Incorrect Context Api Usage detected. Incorrect context class %s, expected %s for instrumentation %s", + contextClassName, + contextStore.get(keyClassName), + instrumenterClass.getName())); + } + // stack: contextClass | keyClass + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + contextStoreImplementationClass.getInternalName(), + GET_CONTEXT_STORE_METHOD.getName(), + Type.getMethodDescriptor(GET_CONTEXT_STORE_METHOD), + /* isInterface= */ false); + return; + } + throw new IllegalStateException( + "Incorrect Context Api Usage detected. Key and context class must be class-literals. Example of correct usage: InstrumentationContext.get(Runnable.class, RunnableContext.class)"); + } else { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + + /** Tracking the most recently used opcodes to assert proper api usage. */ + private void pushOpcode(int opcode) { + System.arraycopy(insnStack, 0, insnStack, 1, insnStack.length - 1); + insnStack[0] = opcode; + } + + /** + * Tracking the most recently pushed objects on the stack to assert proper api usage. + */ + private void pushStack(Object o) { + System.arraycopy(stack, 0, stack, 1, stack.length - 1); + stack[0] = o; + } + + @Override + public void visitInsn(int opcode) { + pushOpcode(opcode); + super.visitInsn(opcode); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + pushOpcode(opcode); + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitIntInsn(int opcode, int operand) { + pushOpcode(opcode); + super.visitIntInsn(opcode, operand); + } + + @Override + public void visitVarInsn(int opcode, int var) { + pushOpcode(opcode); + pushStack(var); + super.visitVarInsn(opcode, var); + } + + @Override + public void visitLdcInsn(Object value) { + pushOpcode(Opcodes.LDC); + pushStack(value); + super.visitLdcInsn(value); + } + }; + } + }; + } + }; + } + + private AgentBuilder.Identified.Extendable injectHelpersIntoBootstrapClassloader( + AgentBuilder.Identified.Extendable builder) { + /* + * We inject into bootstrap classloader because field accessor interfaces are needed by context + * store implementations. Unfortunately this forces us to remove stored type checking because + * actual classes may not be available at this point. + */ + builder = builder.transform(fieldAccessorInterfacesInjector); + + /* + * We inject context store implementation into bootstrap classloader because same implementation + * may be used by different instrumentations and it has to use same static map in case of + * fallback to map-backed storage. + */ + builder = builder.transform(contextStoreImplementationsInjector); + return builder; + } + + /** Get transformer that forces helper injection onto bootstrap classloader. */ + private AgentBuilder.Transformer bootstrapHelperInjector( + Collection> helpers) { + // TODO: Better to pass through the context of the Instrumenter + return new AgentBuilder.Transformer() { + final HelperInjector injector = + HelperInjector.forDynamicTypes(getClass().getSimpleName(), helpers); + + @Override + public DynamicType.Builder transform( + DynamicType.Builder builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return injector.transform( + builder, + typeDescription, + // context store implementation classes will always go to the bootstrap + BOOTSTRAP_CLASSLOADER, + module); + } + }; + } + + /* + Set of pairs (context holder, context class) for which we have matchers installed. + We use this to make sure we do not install matchers repeatedly for cases when same + context class is used by multiple instrumentations. + */ + private static final Set> INSTALLED_CONTEXT_MATCHERS = new HashSet<>(); + + /** Clear set that prevents multiple matchers for same context class. */ + public static void resetContextMatchers() { + synchronized (INSTALLED_CONTEXT_MATCHERS) { + INSTALLED_CONTEXT_MATCHERS.clear(); + } + } + + @Override + public AgentBuilder.Identified.Extendable additionalInstrumentation( + AgentBuilder.Identified.Extendable builder) { + + if (FIELD_INJECTION_ENABLED) { + for (Map.Entry entry : contextStore.entrySet()) { + /* + * For each context store defined in a current instrumentation we create an agent builder + * that injects necessary fields. + * Note: this synchronization should not have any impact on performance + * since this is done when agent builder is being made, it doesn't affect actual + * class transformation. + */ + synchronized (INSTALLED_CONTEXT_MATCHERS) { + if (INSTALLED_CONTEXT_MATCHERS.contains(entry)) { + log.trace("Skipping builder for {} {}", instrumenterClass.getName(), entry); + continue; + } + + log.trace("Making builder for {} {}", instrumenterClass.getName(), entry); + INSTALLED_CONTEXT_MATCHERS.add(entry); + + /* + * For each context store defined in a current instrumentation we create an agent builder + * that injects necessary fields. + */ + builder = + builder + .type(not(isAbstract()).and(safeHasSuperType(named(entry.getKey())))) + .and(safeToInjectFieldsMatcher()) + .and(InstrumentationModuleInstaller.NOT_DECORATOR_MATCHER) + .transform(NoOpTransformer.INSTANCE); + + /* + * We inject helpers here as well as when instrumentation is applied to ensure that + * helpers are present even if instrumented classes are not loaded, but classes with state + * fields added are loaded (e.g. sun.net.www.protocol.https.HttpsURLConnectionImpl). + */ + builder = injectHelpersIntoBootstrapClassloader(builder); + + builder = + builder.transform( + getTransformerForAsmVisitor( + getFieldInjectionVisitor(entry.getKey(), entry.getValue()))); + } + } + } + return builder; + } + + private static AgentBuilder.RawMatcher safeToInjectFieldsMatcher() { + return new AgentBuilder.RawMatcher() { + @Override + public boolean matches( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + Class classBeingRedefined, + ProtectionDomain protectionDomain) { + /* + * The idea here is that we can add fields if class is just being loaded + * (classBeingRedefined == null) and we have to add same fields again if class we added + * fields before is being transformed again. Note: here we assume that Class#getInterfaces() + * returns list of interfaces defined immediately on a given class, not inherited from its + * parents. It looks like current JVM implementation does exactly this but javadoc is not + * explicit about that. + */ + return classBeingRedefined == null + || Arrays.asList(classBeingRedefined.getInterfaces()) + .contains(FieldBackedContextStoreAppliedMarker.class); + } + }; + } + + private AsmVisitorWrapper getFieldInjectionVisitor(String keyClassName, String contextClassName) { + return new AsmVisitorWrapper() { + + @Override + public int mergeWriter(int flags) { + return flags | ClassWriter.COMPUTE_MAXS; + } + + @Override + public int mergeReader(int flags) { + return flags; + } + + @Override + public ClassVisitor wrap( + TypeDescription instrumentedType, + ClassVisitor classVisitor, + Implementation.Context implementationContext, + TypePool typePool, + FieldList fields, + MethodList methods, + int writerFlags, + int readerFlags) { + return new ClassVisitor(Opcodes.ASM7, classVisitor) { + // We are using Object class name instead of contextClassName here because this gets + // injected onto Bootstrap classloader where context class may be unavailable + private final TypeDescription contextType = + new TypeDescription.ForLoadedType(Object.class); + private final String fieldName = getContextFieldName(keyClassName); + private final String getterMethodName = getContextGetterName(keyClassName); + private final String setterMethodName = getContextSetterName(keyClassName); + private final TypeDescription interfaceType = + getFieldAccessorInterface(keyClassName, contextClassName); + private boolean foundField = false; + private boolean foundGetter = false; + private boolean foundSetter = false; + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + if (interfaces == null) { + interfaces = new String[] {}; + } + Set set = new LinkedHashSet<>(Arrays.asList(interfaces)); + set.add(INJECTED_FIELDS_MARKER_CLASS_NAME); + set.add(interfaceType.getInternalName()); + super.visit(version, access, name, signature, superName, set.toArray(new String[] {})); + } + + @Override + public FieldVisitor visitField( + int access, String name, String descriptor, String signature, Object value) { + if (name.equals(fieldName)) { + foundField = true; + } + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + if (name.equals(getterMethodName)) { + foundGetter = true; + } + if (name.equals(setterMethodName)) { + foundSetter = true; + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public void visitEnd() { + // Checking only for field existence is not enough as libraries like CGLIB only copy + // public/protected methods and not fields (neither public nor private ones) when + // they enhance a class. + // For this reason we check separately for the field and for the two accessors. + if (!foundField) { + cv.visitField( + // Field should be transient to avoid being serialized with the object. + Opcodes.ACC_PRIVATE | Opcodes.ACC_TRANSIENT, + fieldName, + contextType.getDescriptor(), + null, + null); + } + if (!foundGetter) { + addGetter(); + } + if (!foundSetter) { + addSetter(); + } + super.visitEnd(); + } + + // just 'standard' getter implementation + private void addGetter() { + MethodVisitor mv = getAccessorMethodVisitor(getterMethodName); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn( + Opcodes.GETFIELD, + instrumentedType.getInternalName(), + fieldName, + contextType.getDescriptor()); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + // just 'standard' setter implementation + private void addSetter() { + MethodVisitor mv = getAccessorMethodVisitor(setterMethodName); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitFieldInsn( + Opcodes.PUTFIELD, + instrumentedType.getInternalName(), + fieldName, + contextType.getDescriptor()); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + private MethodVisitor getAccessorMethodVisitor(String methodName) { + return cv.visitMethod( + Opcodes.ACC_PUBLIC, + methodName, + Utils.getMethodDefinition(interfaceType, methodName).getDescriptor(), + null, + null); + } + }; + } + }; + } + + private TypeDescription getContextStoreImplementation( + String keyClassName, String contextClassName) { + DynamicType.Unloaded type = + contextStoreImplementations.get( + getContextStoreImplementationClassName(keyClassName, contextClassName)); + if (type == null) { + return null; + } else { + return type.getTypeDescription(); + } + } + + private Map> generateContextStoreImplementationClasses() { + Map> contextStoreImplementations = + new HashMap<>(contextStore.size()); + for (Map.Entry entry : contextStore.entrySet()) { + DynamicType.Unloaded type = + makeContextStoreImplementationClass(entry.getKey(), entry.getValue()); + contextStoreImplementations.put(type.getTypeDescription().getName(), type); + } + return Collections.unmodifiableMap(contextStoreImplementations); + } + + /** + * Generate an 'implementation' of a context store class for given key class name and context + * class name. + * + * @param keyClassName key class name + * @param contextClassName context class name + * @return unloaded dynamic type containing generated class + */ + private DynamicType.Unloaded makeContextStoreImplementationClass( + String keyClassName, String contextClassName) { + return byteBuddy + .rebase(ContextStoreImplementationTemplate.class) + .modifiers(Visibility.PUBLIC, TypeManifestation.FINAL) + .name(getContextStoreImplementationClassName(keyClassName, contextClassName)) + .visit(getContextStoreImplementationVisitor(keyClassName, contextClassName)) + .make(); + } + + /** + * Returns a visitor that 'fills in' missing methods into concrete implementation of + * ContextStoreImplementationTemplate for given key class name and context class name. + * + * @param keyClassName key class name + * @param contextClassName context class name + * @return visitor that adds implementation for methods that need to be generated + */ + private AsmVisitorWrapper getContextStoreImplementationVisitor( + String keyClassName, String contextClassName) { + return new AsmVisitorWrapper() { + + @Override + public int mergeWriter(int flags) { + return flags | ClassWriter.COMPUTE_MAXS; + } + + @Override + public int mergeReader(int flags) { + return flags; + } + + @Override + public ClassVisitor wrap( + TypeDescription instrumentedType, + ClassVisitor classVisitor, + Implementation.Context implementationContext, + TypePool typePool, + FieldList fields, + MethodList methods, + int writerFlags, + int readerFlags) { + return new ClassVisitor(Opcodes.ASM7, classVisitor) { + + private final TypeDescription accessorInterface = + getFieldAccessorInterface(keyClassName, contextClassName); + private final String accessorInterfaceInternalName = accessorInterface.getInternalName(); + private final String instrumentedTypeInternalName = instrumentedType.getInternalName(); + private final boolean frames = + implementationContext.getClassFileVersion().isAtLeast(ClassFileVersion.JAVA_V6); + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + if ("realGet".equals(name)) { + generateRealGetMethod(name); + return null; + } else if ("realPut".equals(name)) { + generateRealPutMethod(name); + return null; + } else if ("realSynchronizeInstance".equals(name)) { + generateRealSynchronizeInstanceMethod(name); + return null; + } else { + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + } + + /** + * Provides implementation for {@code realGet} method that looks like below. + * + *

    + * + *
    +           * private Object realGet(final Object key) {
    +           *   if (key instanceof $accessorInterfaceInternalName) {
    +           *     return (($accessorInterfaceInternalName) key).$getterName();
    +           *   } else {
    +           *     return mapGet(key);
    +           *   }
    +           * }
    +           * 
    + * + *
    + * + * @param name name of the method being visited + */ + private void generateRealGetMethod(String name) { + String getterName = getContextGetterName(keyClassName); + Label elseLabel = new Label(); + MethodVisitor mv = getMethodVisitor(name); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.INSTANCEOF, accessorInterfaceInternalName); + mv.visitJumpInsn(Opcodes.IFEQ, elseLabel); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.CHECKCAST, accessorInterfaceInternalName); + mv.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + accessorInterfaceInternalName, + getterName, + Utils.getMethodDefinition(accessorInterface, getterName).getDescriptor(), + /* isInterface= */ true); + mv.visitInsn(Opcodes.ARETURN); + mv.visitLabel(elseLabel); + if (frames) { + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + } + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn( + Opcodes.INVOKESPECIAL, + instrumentedTypeInternalName, + "mapGet", + Utils.getMethodDefinition(instrumentedType, "mapGet").getDescriptor(), + /* isInterface= */ false); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + /** + * Provides implementation for {@code realPut} method that looks like below. + * + *
    + * + *
    +           * private void realPut(final Object key, final Object value) {
    +           *   if (key instanceof $accessorInterfaceInternalName) {
    +           *     (($accessorInterfaceInternalName) key).$setterName(value);
    +           *   } else {
    +           *     mapPut(key, value);
    +           *   }
    +           * }
    +           * 
    + * + *
    + * + * @param name name of the method being visited + */ + private void generateRealPutMethod(String name) { + String setterName = getContextSetterName(keyClassName); + Label elseLabel = new Label(); + Label endLabel = new Label(); + MethodVisitor mv = getMethodVisitor(name); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.INSTANCEOF, accessorInterfaceInternalName); + mv.visitJumpInsn(Opcodes.IFEQ, elseLabel); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.CHECKCAST, accessorInterfaceInternalName); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + accessorInterfaceInternalName, + setterName, + Utils.getMethodDefinition(accessorInterface, setterName).getDescriptor(), + /* isInterface= */ true); + mv.visitJumpInsn(Opcodes.GOTO, endLabel); + mv.visitLabel(elseLabel); + if (frames) { + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + } + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitMethodInsn( + Opcodes.INVOKESPECIAL, + instrumentedTypeInternalName, + "mapPut", + Utils.getMethodDefinition(instrumentedType, "mapPut").getDescriptor(), + /* isInterface= */ false); + mv.visitLabel(endLabel); + if (frames) { + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + } + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + /** + * Provides implementation for {@code realSynchronizeInstance} method that looks like + * below. + * + *
    + * + *
    +           * private Object realSynchronizeInstance(final Object key) {
    +           *   if (key instanceof $accessorInterfaceInternalName) {
    +           *     return key;
    +           *   } else {
    +           *     return mapSynchronizeInstance(key);
    +           *   }
    +           * }
    +           * 
    + * + *
    + * + * @param name name of the method being visited + */ + private void generateRealSynchronizeInstanceMethod(String name) { + MethodVisitor mv = getMethodVisitor(name); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.INSTANCEOF, accessorInterfaceInternalName); + Label elseLabel = new Label(); + mv.visitJumpInsn(Opcodes.IFEQ, elseLabel); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitInsn(Opcodes.ARETURN); + mv.visitLabel(elseLabel); + if (frames) { + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + } + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn( + Opcodes.INVOKESPECIAL, + instrumentedTypeInternalName, + "mapSynchronizeInstance", + Utils.getMethodDefinition(instrumentedType, "mapSynchronizeInstance") + .getDescriptor(), + /* isInterface= */ false); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + private MethodVisitor getMethodVisitor(String methodName) { + return cv.visitMethod( + Opcodes.ACC_PRIVATE, + methodName, + Utils.getMethodDefinition(instrumentedType, methodName).getDescriptor(), + null, + null); + } + }; + } + }; + } + + /** + * Template class used to generate the class that accesses stored context using either key + * instance's own injected field or global hash map if field is not available. + */ + // Called from generated code + @SuppressWarnings({"UnusedMethod", "UnusedVariable", "MethodCanBeStatic"}) + private static final class ContextStoreImplementationTemplate + implements ContextStore { + private static final ContextStoreImplementationTemplate INSTANCE = + new ContextStoreImplementationTemplate(Cache.newBuilder().setWeakKeys().build()); + + private final Cache map; + + private ContextStoreImplementationTemplate(Cache map) { + this.map = map; + } + + @Override + public Object get(Object key) { + return realGet(key); + } + + @Override + public Object putIfAbsent(Object key, Object context) { + Object existingContext = realGet(key); + if (null != existingContext) { + return existingContext; + } + synchronized (realSynchronizeInstance(key)) { + existingContext = realGet(key); + if (null != existingContext) { + return existingContext; + } + realPut(key, context); + return context; + } + } + + @Override + public Object putIfAbsent(Object key, Factory contextFactory) { + Object existingContext = realGet(key); + if (null != existingContext) { + return existingContext; + } + synchronized (realSynchronizeInstance(key)) { + existingContext = realGet(key); + if (null != existingContext) { + return existingContext; + } + Object context = contextFactory.create(); + realPut(key, context); + return context; + } + } + + @Override + public void put(Object key, Object context) { + realPut(key, context); + } + + private Object realGet(Object key) { + // to be generated + return null; + } + + private void realPut(Object key, Object value) { + // to be generated + } + + private Object realSynchronizeInstance(Object key) { + // to be generated + return null; + } + + private Object mapGet(Object key) { + return map.get(key); + } + + private void mapPut(Object key, Object value) { + if (value == null) { + map.remove(key); + } else { + map.put(key, value); + } + } + + private Object mapSynchronizeInstance(Object key) { + return map; + } + + public static ContextStore getContextStore(Class keyClass, Class contextClass) { + // We do not actually check the keyClass here - but that should be fine since compiler would + // check things for us. + return INSTANCE; + } + } + + private TypeDescription getFieldAccessorInterface(String keyClassName, String contextClassName) { + DynamicType.Unloaded type = + fieldAccessorInterfaces.get( + getContextAccessorInterfaceName(keyClassName, contextClassName)); + if (type == null) { + return null; + } else { + return type.getTypeDescription(); + } + } + + private Map> generateFieldAccessorInterfaces() { + Map> fieldAccessorInterfaces = + new HashMap<>(contextStore.size()); + for (Map.Entry entry : contextStore.entrySet()) { + DynamicType.Unloaded type = makeFieldAccessorInterface(entry.getKey(), entry.getValue()); + fieldAccessorInterfaces.put(type.getTypeDescription().getName(), type); + } + return Collections.unmodifiableMap(fieldAccessorInterfaces); + } + + /** + * Generate an interface that provides field accessor methods for given key class name and context + * class name. + * + * @param keyClassName key class name + * @param contextClassName context class name + * @return unloaded dynamic type containing generated interface + */ + private DynamicType.Unloaded makeFieldAccessorInterface( + String keyClassName, String contextClassName) { + // We are using Object class name instead of contextClassName here because this gets injected + // onto Bootstrap classloader where context class may be unavailable + TypeDescription contextType = new TypeDescription.ForLoadedType(Object.class); + return byteBuddy + .makeInterface() + .name(getContextAccessorInterfaceName(keyClassName, contextClassName)) + .defineMethod(getContextGetterName(keyClassName), contextType, Visibility.PUBLIC) + .withoutCode() + .defineMethod(getContextSetterName(keyClassName), TypeDescription.VOID, Visibility.PUBLIC) + .withParameter(contextType, "value") + .withoutCode() + .make(); + } + + private static AgentBuilder.Transformer getTransformerForAsmVisitor(AsmVisitorWrapper visitor) { + return (builder, typeDescription, classLoader, module) -> builder.visit(visitor); + } + + private String getContextStoreImplementationClassName( + String keyClassName, String contextClassName) { + return DYNAMIC_CLASSES_PACKAGE + + getClass().getSimpleName() + + "$ContextStore$" + + Utils.convertToInnerClassName(keyClassName) + + "$" + + Utils.convertToInnerClassName(contextClassName); + } + + private String getContextAccessorInterfaceName(String keyClassName, String contextClassName) { + return DYNAMIC_CLASSES_PACKAGE + + getClass().getSimpleName() + + "$ContextAccessor$" + + Utils.convertToInnerClassName(keyClassName) + + "$" + + Utils.convertToInnerClassName(contextClassName); + } + + private static String getContextFieldName(String keyClassName) { + return "__opentelemetryContext$" + Utils.convertToInnerClassName(keyClassName); + } + + private static String getContextGetterName(String keyClassName) { + return "get" + getContextFieldName(keyClassName); + } + + private static String getContextSetterName(String key) { + return "set" + getContextFieldName(key); + } + + // Originally found in AgentBuilder.Transformer.NoOp, but removed in 1.10.7 + enum NoOpTransformer implements AgentBuilder.Transformer { + INSTANCE; + + @Override + public DynamicType.Builder transform( + DynamicType.Builder builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return builder; + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/InstrumentationContextProvider.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/InstrumentationContextProvider.java new file mode 100644 index 000000000..16e8860d6 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/InstrumentationContextProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.context; + +import net.bytebuddy.agent.builder.AgentBuilder; + +public interface InstrumentationContextProvider { + + /** + * Hook to provide an agent builder after advice is applied to target class. Used to implement + * context-store lookup. + */ + AgentBuilder.Identified.Extendable instrumentationTransformer( + AgentBuilder.Identified.Extendable builder); + + /** Hook to define additional instrumentation. Run at instrumentation advice is hooked up. */ + AgentBuilder.Identified.Extendable additionalInstrumentation( + AgentBuilder.Identified.Extendable builder); +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/NoopContextProvider.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/NoopContextProvider.java new file mode 100644 index 000000000..6573fcc7a --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/context/NoopContextProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.context; + +import net.bytebuddy.agent.builder.AgentBuilder.Identified.Extendable; + +public class NoopContextProvider implements InstrumentationContextProvider { + + public static final NoopContextProvider INSTANCE = new NoopContextProvider(); + + private NoopContextProvider() {} + + @Override + public Extendable instrumentationTransformer(Extendable builder) { + return builder; + } + + @Override + public Extendable additionalInstrumentation(Extendable builder) { + return builder; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java new file mode 100644 index 000000000..4ec29d3e5 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java @@ -0,0 +1,268 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; + +/** + * Additional global ignore settings that are used to reduce number of classes we try to apply + * expensive matchers to. + * + *

    This is separated from {@link GlobalIgnoredTypesConfigurer} to allow for better testing. The + * idea is that we should be able to remove this matcher from the agent and all tests should still + * pass. Moreover, no classes matched by this matcher should be modified during test run. + */ +@AutoService(IgnoredTypesConfigurer.class) +public class AdditionalLibraryIgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + // We set this system property when running the agent with unit tests to allow verifying that we + // don't ignore libraries that we actually attempt to instrument. It means either the list is + // wrong or a type matcher is. + private static final String ADDITIONAL_LIBRARY_IGNORES_ENABLED = + "otel.javaagent.testing.additional-library-ignores.enabled"; + + @Override + public void configure(Config config, IgnoredTypesBuilder builder) { + if (config.getBoolean(ADDITIONAL_LIBRARY_IGNORES_ENABLED, true)) { + configure(builder); + } + } + + // only used by tests (to bypass the ignores check) + public void configure(IgnoredTypesBuilder builder) { + builder + .ignoreClass("com.beust.jcommander.") + .ignoreClass("com.fasterxml.classmate.") + .ignoreClass("com.github.mustachejava.") + .ignoreClass("com.jayway.jsonpath.") + .ignoreClass("com.lightbend.lagom.") + .ignoreClass("javax.el.") + .ignoreClass("org.apache.lucene.") + .ignoreClass("org.apache.tartarus.") + .ignoreClass("org.json.simple.") + .ignoreClass("org.yaml.snakeyaml."); + + builder.ignoreClass("net.sf.cglib.").allowClass("net.sf.cglib.core.internal.LoadingCache$2"); + + builder + .ignoreClass("org.springframework.aop.") + .ignoreClass("org.springframework.cache.") + .ignoreClass("org.springframework.dao.") + .ignoreClass("org.springframework.ejb.") + .ignoreClass("org.springframework.expression.") + .ignoreClass("org.springframework.format.") + .ignoreClass("org.springframework.jca.") + .ignoreClass("org.springframework.jdbc.") + .ignoreClass("org.springframework.jmx.") + .ignoreClass("org.springframework.jndi.") + .ignoreClass("org.springframework.lang.") + .ignoreClass("org.springframework.objenesis.") + .ignoreClass("org.springframework.orm.") + .ignoreClass("org.springframework.remoting.") + .ignoreClass("org.springframework.scripting.") + .ignoreClass("org.springframework.stereotype.") + .ignoreClass("org.springframework.transaction.") + .ignoreClass("org.springframework.ui.") + .ignoreClass("org.springframework.validation."); + + builder + .ignoreClass("org.springframework.data.") + .allowClass("org.springframework.data.repository.core.support.RepositoryFactorySupport") + .allowClass("org.springframework.data.convert.ClassGeneratingEntityInstantiator$") + .allowClass("org.springframework.data.jpa.repository.config.InspectionClassLoader"); + + builder + .ignoreClass("org.springframework.amqp.") + .allowClass("org.springframework.amqp.rabbit.connection.") + .allowClass("org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer") + // these implement Runnable, so tests currently force these allows + // though not sure if it's important or not that they get instrumented + .allowClass( + "org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer") + .allowClass( + "org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry$AggregatingCallback"); + + builder + .ignoreClass("org.springframework.beans.") + .allowClass("org.springframework.beans.factory.support.DisposableBeanAdapter") + .allowClass("org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader$"); + + builder + .ignoreClass("org.springframework.boot.") + .allowClass("org.springframework.boot.context.web.") + .allowClass("org.springframework.boot.logging.logback.") + .allowClass("org.springframework.boot.web.filter.") + .allowClass("org.springframework.boot.web.servlet.") + .allowClass("org.springframework.boot.autoconfigure.BackgroundPreinitializer$") + .allowClass("org.springframework.boot.autoconfigure.condition.OnClassCondition$") + .allowClass("org.springframework.boot.web.embedded.netty.NettyWebServer$") + .allowClass( + "org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer$") + .allowClass( + "org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader") + .allowClass("org.springframework.boot.context.embedded.EmbeddedWebApplicationContext") + .allowClass( + "org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext") + // spring boot 2 classes + .allowClass( + "org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext") + .allowClass( + "org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext") + .allowClass("org.springframework.boot.web.embedded.tomcat.TomcatWebServer$") + .allowClass("org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader") + .allowClass("org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$") + .allowClass("org.springframework.boot.StartupInfoLogger$") + .allowClass("org.springframework.boot.SpringApplicationShutdownHook"); + + builder + .ignoreClass("org.springframework.cglib.") + // This class contains nested Callable instance that we'd happily not touch, but + // unfortunately our field injection code is not flexible enough to realize that, so instead + // we instrument this Callable to make tests happy. + .allowClass("org.springframework.cglib.core.internal.LoadingCache$"); + + builder + .ignoreClass("org.springframework.context.") + // More runnables to deal with + .allowClass("org.springframework.context.support.AbstractApplicationContext$") + .allowClass("org.springframework.context.support.ContextTypeMatchClassLoader") + // Allow instrumenting ApplicationContext implementations - to inject beans + .allowClass("org.springframework.context.annotation.AnnotationConfigApplicationContext") + .allowClass("org.springframework.context.support.AbstractApplicationContext") + .allowClass("org.springframework.context.support.GenericApplicationContext"); + + builder + .ignoreClass("org.springframework.core.") + .allowClass("org.springframework.core.task.") + .allowClass("org.springframework.core.DecoratingClassLoader") + .allowClass("org.springframework.core.OverridingClassLoader") + .allowClass("org.springframework.core.ReactiveAdapterRegistry$EmptyCompletableFuture"); + + builder + .ignoreClass("org.springframework.instrument.") + .allowClass("org.springframework.instrument.classloading.SimpleThrowawayClassLoader") + .allowClass("org.springframework.instrument.classloading.ShadowingClassLoader"); + + builder + .ignoreClass("org.springframework.http.") + // There are some Mono implementation that get instrumented + .allowClass("org.springframework.http.server.reactive."); + + builder + .ignoreClass("org.springframework.jms.") + .allowClass("org.springframework.jms.listener.") + .allowClass( + "org.springframework.jms.config.JmsListenerEndpointRegistry$AggregatingCallback"); + + builder + .ignoreClass("org.springframework.messaging.") + .allowClass("org.springframework.messaging.support.ExecutorSubscribableChannel$SendTask") + .allowClass("org.springframework.messaging.support.MessageHandlingRunnable"); + + builder + .ignoreClass("org.springframework.util.") + .allowClass("org.springframework.util.concurrent."); + + builder + .ignoreClass("org.springframework.web.") + .allowClass("org.springframework.web.servlet.") + .allowClass("org.springframework.web.filter.") + .allowClass("org.springframework.web.multipart.") + .allowClass("org.springframework.web.reactive.") + .allowClass("org.springframework.web.context.request.async.") + .allowClass( + "org.springframework.web.context.support.AbstractRefreshableWebApplicationContext") + .allowClass("org.springframework.web.context.support.GenericWebApplicationContext") + .allowClass("org.springframework.web.context.support.XmlWebApplicationContext"); + + // xml-apis, xerces, xalan, but not xml web-services + builder + .ignoreClass("javax.xml.") + .allowClass("javax.xml.ws.") + .ignoreClass("org.apache.bcel.") + .ignoreClass("org.apache.html.") + .ignoreClass("org.apache.regexp.") + .ignoreClass("org.apache.wml.") + .ignoreClass("org.apache.xalan.") + .ignoreClass("org.apache.xerces.") + .ignoreClass("org.apache.xml.") + .ignoreClass("org.apache.xpath.") + .ignoreClass("org.xml."); + + builder + .ignoreClass("ch.qos.logback.") + // We instrument this Runnable + .allowClass("ch.qos.logback.core.AsyncAppenderBase$Worker") + // Allow instrumenting loggers & events + .allowClass("ch.qos.logback.classic.Logger") + .allowClass("ch.qos.logback.classic.spi.LoggingEvent") + .allowClass("ch.qos.logback.classic.spi.LoggingEventVO"); + + builder + .ignoreClass("com.codahale.metrics.") + // We instrument servlets + .allowClass("com.codahale.metrics.servlets."); + + builder + .ignoreClass("com.couchbase.client.deps.") + // Couchbase library includes some packaged dependencies, unfortunately some of them are + // instrumented by executors instrumentation + .allowClass("com.couchbase.client.deps.io.netty.") + .allowClass("com.couchbase.client.deps.org.LatencyUtils.") + .allowClass("com.couchbase.client.deps.com.lmax.disruptor."); + + builder + .ignoreClass("com.google.cloud.") + .ignoreClass("com.google.instrumentation.") + .ignoreClass("com.google.j2objc.") + .ignoreClass("com.google.gson.") + .ignoreClass("com.google.logging.") + .ignoreClass("com.google.longrunning.") + .ignoreClass("com.google.protobuf.") + .ignoreClass("com.google.rpc.") + .ignoreClass("com.google.thirdparty.") + .ignoreClass("com.google.type."); + + builder + .ignoreClass("com.google.common.") + .allowClass("com.google.common.util.concurrent.") + .allowClass("com.google.common.base.internal.Finalizer"); + + builder + .ignoreClass("com.google.inject.") + // We instrument Runnable there + .allowClass("com.google.inject.internal.AbstractBindingProcessor$") + .allowClass("com.google.inject.internal.BytecodeGen$") + .allowClass("com.google.inject.internal.cglib.core.internal.$LoadingCache$"); + + builder.ignoreClass("com.google.api.").allowClass("com.google.api.client.http.HttpRequest"); + + builder + .ignoreClass("org.h2.") + .allowClass("org.h2.Driver") + .allowClass("org.h2.jdbc.") + .allowClass("org.h2.jdbcx.") + // Some runnables that get instrumented + .allowClass("org.h2.util.Task") + .allowClass("org.h2.store.FileLock") + .allowClass("org.h2.engine.DatabaseCloser") + .allowClass("org.h2.engine.OnExitDatabaseCloser"); + + builder + .ignoreClass("com.carrotsearch.hppc.") + .allowClass("com.carrotsearch.hppc.HashOrderMixing$"); + + builder + .ignoreClass("com.fasterxml.jackson.") + .allowClass("com.fasterxml.jackson.module.afterburner.util.MyClassLoader"); + + // kotlin, note we do not ignore kotlinx because we instrument coroutines code + builder.ignoreClass("kotlin.").allowClass("kotlin.coroutines.jvm.internal.DebugProbesKt"); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java new file mode 100644 index 000000000..5724e2ef6 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.bootstrap.AgentClassLoader; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.javaagent.tooling.ExporterClassLoader; +import io.opentelemetry.javaagent.tooling.ExtensionClassLoader; + +@AutoService(IgnoredTypesConfigurer.class) +public class GlobalIgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(Config config, IgnoredTypesBuilder builder) { + configureIgnoredTypes(builder); + configureIgnoredClassLoaders(builder); + } + + private static void configureIgnoredTypes(IgnoredTypesBuilder builder) { + builder + .ignoreClass("org.gradle.") + .ignoreClass("net.bytebuddy.") + .ignoreClass("jdk.") + .ignoreClass("org.aspectj.") + .ignoreClass("datadog.") + .ignoreClass("com.intellij.rt.debugger.") + .ignoreClass("com.p6spy.") + .ignoreClass("com.dynatrace.") + .ignoreClass("com.jloadtrace.") + .ignoreClass("com.appdynamics.") + .ignoreClass("com.newrelic.agent.") + .ignoreClass("com.newrelic.api.agent.") + .ignoreClass("com.nr.agent.") + .ignoreClass("com.singularity.") + .ignoreClass("com.jinspired.") + .ignoreClass("org.jinspired."); + + // allow JDK HttpClient + builder.allowClass("jdk.internal.net.http."); + + // groovy + builder + .ignoreClass("org.groovy.") + .ignoreClass("org.apache.groovy.") + .ignoreClass("org.codehaus.groovy.") + // We seem to instrument some classes in runtime + .allowClass("org.codehaus.groovy.runtime."); + + // clojure + builder.ignoreClass("clojure.").ignoreClass("$fn__"); + + builder + .ignoreClass("io.opentelemetry.javaagent.") + // FIXME: We should remove this once + // https://github.com/raphw/byte-buddy/issues/558 is fixed + .allowClass("io.opentelemetry.javaagent.instrumentation.api.concurrent.RunnableWrapper") + .allowClass("io.opentelemetry.javaagent.instrumentation.api.concurrent.CallableWrapper"); + + builder + .ignoreClass("java.") + .allowClass("java.net.URL") + .allowClass("java.net.HttpURLConnection") + .allowClass("java.net.URLClassLoader") + .allowClass("java.rmi.") + .allowClass("java.util.concurrent.") + .allowClass("java.lang.reflect.Proxy") + .allowClass("java.lang.ClassLoader") + // Concurrent instrumentation modifies the structure of + // Cleaner class incompatibly with java9+ modules. + // Working around until a long-term fix for modules can be + // put in place. + .allowClass("java.util.logging.") + .ignoreClass("java.util.logging.LogManager$Cleaner"); + + builder + .ignoreClass("com.sun.") + .allowClass("com.sun.messaging.") + .allowClass("com.sun.jersey.api.client") + .allowClass("com.sun.appserv") + .allowClass("com.sun.faces") + .allowClass("com.sun.xml.ws"); + + builder + .ignoreClass("sun.") + .allowClass("sun.net.www.protocol.") + .allowClass("sun.rmi.server") + .allowClass("sun.rmi.transport") + .allowClass("sun.net.www.http.HttpClient"); + + builder.ignoreClass("org.slf4j.").allowClass("org.slf4j.MDC"); + + builder + .ignoreClass("org.springframework.core.$Proxy") + // Tapestry Proxy, check only specific class that we know would be instrumented since there + // is no common prefix for its proxies other than "$". ByteBuddy fails to instrument this + // proxy, and as there is no reason why it should be instrumented anyway, exclude it. + .ignoreClass("$HttpServletRequest_"); + } + + private static void configureIgnoredClassLoaders(IgnoredTypesBuilder builder) { + builder + .ignoreClassLoader("org.codehaus.groovy.runtime.callsite.CallSiteClassLoader") + .ignoreClassLoader("sun.reflect.DelegatingClassLoader") + .ignoreClassLoader("jdk.internal.reflect.DelegatingClassLoader") + .ignoreClassLoader("clojure.lang.DynamicClassLoader") + .ignoreClassLoader("org.apache.cxf.common.util.ASMHelper$TypeHelperClassLoader") + .ignoreClassLoader("sun.misc.Launcher$ExtClassLoader") + .ignoreClassLoader(AgentClassLoader.class.getName()) + .ignoreClassLoader(ExporterClassLoader.class.getName()) + .ignoreClassLoader(ExtensionClassLoader.class.getName()); + + builder + .ignoreClassLoader("datadog.") + .ignoreClassLoader("com.dynatrace.") + .ignoreClassLoader("com.appdynamics.") + .ignoreClassLoader("com.newrelic.agent.") + .ignoreClassLoader("com.newrelic.api.agent.") + .ignoreClassLoader("com.nr.agent."); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoreAllow.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoreAllow.java new file mode 100644 index 000000000..5b6eef002 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoreAllow.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +public enum IgnoreAllow { + IGNORE, + ALLOW +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredClassLoadersMatcher.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredClassLoadersMatcher.java new file mode 100644 index 000000000..8309f552b --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredClassLoadersMatcher.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import io.opentelemetry.javaagent.bootstrap.PatchLogger; +import io.opentelemetry.javaagent.tooling.ignore.trie.Trie; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.matcher.ElementMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IgnoredClassLoadersMatcher extends ElementMatcher.Junction.AbstractBase { + private static final Logger log = LoggerFactory.getLogger(IgnoredClassLoadersMatcher.class); + + /* Cache of classloader-instance -> (true|false). True = skip instrumentation. False = safe to instrument. */ + private static final Cache skipCache = + Cache.newBuilder().setWeakKeys().build(); + + private final Trie ignoredClassLoaders; + + public IgnoredClassLoadersMatcher(Trie ignoredClassLoaders) { + this.ignoredClassLoaders = ignoredClassLoaders; + } + + @Override + public boolean matches(ClassLoader cl) { + if (cl == ClassLoadingStrategy.BOOTSTRAP_LOADER) { + // Don't skip bootstrap loader + return false; + } + + String name = cl.getClass().getName(); + + IgnoreAllow ignored = ignoredClassLoaders.getOrNull(name); + if (ignored == IgnoreAllow.ALLOW) { + return false; + } else if (ignored == IgnoreAllow.IGNORE) { + return true; + } + + return skipCache.computeIfAbsent( + cl, + c -> { + // when ClassloadingInstrumentation is active, checking delegatesToBootstrap() below is + // not + // required, because ClassloadingInstrumentation forces all class loaders to load all of + // the + // classes in Constants.BOOTSTRAP_PACKAGE_PREFIXES directly from the bootstrap class + // loader + // + // however, at this time we don't want to introduce the concept of a required + // instrumentation, + // and we don't want to introduce the concept of the tooling code depending on whether or + // not + // a particular instrumentation is active (mainly because this particular use case doesn't + // seem to justify introducing either of these new concepts) + return !delegatesToBootstrap(cl); + }); + } + + /** + * TODO: this turns out to be useless with OSGi: {@code + * org.eclipse.osgi.internal.loader.BundleLoader#isRequestFromVM} returns {@code true} when class + * loading is issued from this check and {@code false} for 'real' class loads. We should come up + * with some sort of hack to avoid this problem. + */ + private static boolean delegatesToBootstrap(ClassLoader loader) { + boolean delegates = true; + if (!loadsExpectedClass(loader, PatchLogger.class)) { + log.debug("loader {} failed to delegate bootstrap agent class", loader); + delegates = false; + } + return delegates; + } + + private static boolean loadsExpectedClass(ClassLoader loader, Class expectedClass) { + try { + return loader.loadClass(expectedClass.getName()) == expectedClass; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesBuilderImpl.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesBuilderImpl.java new file mode 100644 index 000000000..6706f6268 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesBuilderImpl.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.tooling.ignore.trie.Trie; + +public class IgnoredTypesBuilderImpl implements IgnoredTypesBuilder { + private final Trie.Builder ignoredTypesTrie = Trie.newBuilder(); + private final Trie.Builder ignoredClassLoadersTrie = Trie.newBuilder(); + + @Override + public IgnoredTypesBuilder ignoreClass(String classNameOrPrefix) { + ignoredTypesTrie.put(classNameOrPrefix, IgnoreAllow.IGNORE); + return this; + } + + @Override + public IgnoredTypesBuilder allowClass(String classNameOrPrefix) { + ignoredTypesTrie.put(classNameOrPrefix, IgnoreAllow.ALLOW); + return this; + } + + @Override + public IgnoredTypesBuilder ignoreClassLoader(String classNameOrPrefix) { + ignoredClassLoadersTrie.put(classNameOrPrefix, IgnoreAllow.IGNORE); + return this; + } + + @Override + public IgnoredTypesBuilder allowClassLoader(String classNameOrPrefix) { + ignoredClassLoadersTrie.put(classNameOrPrefix, IgnoreAllow.ALLOW); + return this; + } + + @Override + public IgnoredTypesBuilder ignoreTaskClass(String className) { + // TODO: collect task classes into a separate trie + throw new UnsupportedOperationException("not implemented yet"); + } + + @Override + public IgnoredTypesBuilder allowTaskClass(String className) { + // TODO: collect task classes into a separate trie + throw new UnsupportedOperationException("not implemented yet"); + } + + public Trie buildIgnoredTypesTrie() { + return ignoredTypesTrie.build(); + } + + public Trie buildIgnoredClassLoadersTrie() { + return ignoredClassLoadersTrie.build(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesMatcher.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesMatcher.java new file mode 100644 index 000000000..a10fd4152 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesMatcher.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import io.opentelemetry.javaagent.tooling.ignore.trie.Trie; +import java.util.regex.Pattern; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class IgnoredTypesMatcher extends ElementMatcher.Junction.AbstractBase { + + private static final Pattern COM_MCHANGE_PROXY = + Pattern.compile("com\\.mchange\\.v2\\.c3p0\\..*Proxy"); + + private final Trie ignoredTypes; + + public IgnoredTypesMatcher(Trie ignoredTypes) { + this.ignoredTypes = ignoredTypes; + } + + @Override + public boolean matches(TypeDescription target) { + String name = target.getActualName(); + + IgnoreAllow ignored = ignoredTypes.getOrNull(name); + if (ignored == IgnoreAllow.ALLOW) { + return false; + } else if (ignored == IgnoreAllow.IGNORE) { + return true; + } + + // bytecode proxies typically have $$ in their name + if (name.contains("$$")) { + // allow scala anonymous classes + return !name.contains("$$anon$"); + } + + if (name.contains("$JaxbAccessor") + || name.contains("CGLIB$$") + || name.contains("javassist") + || name.contains(".asm.") + || name.contains("$__sisu") + || name.contains("$$EnhancerByProxool$$") + // glassfish ejb proxy + // We skip instrumenting these because some instrumentations e.g. jax-rs instrument methods + // that are annotated with @Path in an interface implemented by the class. We don't really + // want to instrument these methods in generated classes as this would create spans that + // have the generated class name in them instead of the actual class that handles the call. + || name.contains("__EJB31_Generated__")) { + return true; + } + + return COM_MCHANGE_PROXY.matcher(name).matches(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassesConfigurer.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassesConfigurer.java new file mode 100644 index 000000000..c69c166ae --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassesConfigurer.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import java.util.List; + +@AutoService(IgnoredTypesConfigurer.class) +public class UserExcludedClassesConfigurer implements IgnoredTypesConfigurer { + + // visible for tests + static final String EXCLUDED_CLASSES_CONFIG = "otel.javaagent.exclude-classes"; + + @Override + public void configure(Config config, IgnoredTypesBuilder builder) { + List excludedClasses = config.getList(EXCLUDED_CLASSES_CONFIG); + for (String excludedClass : excludedClasses) { + excludedClass = excludedClass.trim(); + // remove the trailing * + if (excludedClass.endsWith("*")) { + excludedClass = excludedClass.substring(0, excludedClass.length() - 1); + } + builder.ignoreClass(excludedClass); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/trie/Trie.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/trie/Trie.java new file mode 100644 index 000000000..87830900a --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/trie/Trie.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore.trie; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A prefix tree that maps from the longest matching prefix to a value {@code V}. */ +public interface Trie { + + /** Start building a trie. */ + static Builder newBuilder() { + return new TrieImpl.BuilderImpl<>(); + } + + /** + * Returns the value associated with the longest matched prefix, or null if there wasn't a match. + * For example: for a trie containing an {@code ("abc", 10)} entry {@code trie.getOrNull("abcd")} + * will return {@code 10}. + */ + @Nullable + V getOrNull(CharSequence str); + + interface Builder { + + /** Associate {@code value} with the string {@code str}. */ + Builder put(CharSequence str, V value); + + Trie build(); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/trie/TrieImpl.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/trie/TrieImpl.java new file mode 100644 index 000000000..bba637ded --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/trie/TrieImpl.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore.trie; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class TrieImpl implements Trie { + + private final Node root; + + private TrieImpl(Node root) { + this.root = root; + } + + @Override + public V getOrNull(CharSequence str) { + Node node = root; + V lastMatchedValue = null; + + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + Node next = node.getNext(c); + if (next == null) { + return lastMatchedValue; + } + node = next; + // next node matched, use its value if it's defined + lastMatchedValue = next.value != null ? next.value : lastMatchedValue; + } + + return lastMatchedValue; + } + + static final class Node { + final char[] chars; + final Node[] children; + final V value; + + Node(char[] chars, Node[] children, V value) { + this.chars = chars; + this.children = children; + this.value = value; + } + + @Nullable + Node getNext(char c) { + int index = Arrays.binarySearch(chars, c); + if (index < 0) { + return null; + } + return children[index]; + } + } + + static final class BuilderImpl implements Builder { + + private final NodeBuilder root = new NodeBuilder<>(); + + @Override + public Builder put(CharSequence str, V value) { + put(root, str, 0, value); + return this; + } + + private void put(NodeBuilder node, CharSequence str, int i, V value) { + if (str.length() == i) { + node.value = value; + return; + } + char c = str.charAt(i); + NodeBuilder next = node.children.computeIfAbsent(c, k -> new NodeBuilder<>()); + put(next, str, i + 1, value); + } + + @Override + public Trie build() { + return new TrieImpl<>(root.build()); + } + } + + static final class NodeBuilder { + final Map> children = new HashMap<>(); + V value; + + Node build() { + int size = children.size(); + char[] chars = new char[size]; + Node[] nodes = new Node[size]; + + int i = 0; + Iterator>> it = + this.children.entrySet().stream().sorted(Map.Entry.comparingByKey()).iterator(); + while (it.hasNext()) { + Map.Entry> e = it.next(); + chars[i] = e.getKey(); + nodes[i++] = e.getValue().build(); + } + + return new Node<>(chars, nodes, value); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/ConstantAdjuster.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/ConstantAdjuster.java new file mode 100644 index 000000000..eed700620 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/ConstantAdjuster.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.TypeConstantAdjustment; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; + +/** + * This {@link AgentBuilder.Transformer} ensures that class files of a version previous to Java 5 do + * not store class entries in the generated class's constant pool. + * + * @see ConstantAdjuster The ASM visitor that does the actual work. + */ +final class ConstantAdjuster implements AgentBuilder.Transformer { + private static final ConstantAdjuster INSTANCE = new ConstantAdjuster(); + + static AgentBuilder.Transformer instance() { + return INSTANCE; + } + + private ConstantAdjuster() {} + + @Override + public DynamicType.Builder transform( + DynamicType.Builder builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return builder.visit(TypeConstantAdjustment.INSTANCE); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationLoader.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationLoader.java new file mode 100644 index 000000000..1e9baa4d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationLoader.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation; + +import static io.opentelemetry.javaagent.tooling.SafeServiceLoader.loadOrdered; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.AgentExtension; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import net.bytebuddy.agent.builder.AgentBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@AutoService(AgentExtension.class) +public class InstrumentationLoader implements AgentExtension { + private static final Logger log = LoggerFactory.getLogger(InstrumentationLoader.class); + + private final InstrumentationModuleInstaller instrumentationModuleInstaller = + new InstrumentationModuleInstaller(); + + @Override + public AgentBuilder extend(AgentBuilder agentBuilder) { + int numberOfLoadedModules = 0; + for (InstrumentationModule instrumentationModule : loadOrdered(InstrumentationModule.class)) { + log.debug( + "Loading instrumentation {} [class {}]", + instrumentationModule.instrumentationName(), + instrumentationModule.getClass().getName()); + try { + agentBuilder = instrumentationModuleInstaller.install(instrumentationModule, agentBuilder); + numberOfLoadedModules++; + } catch (Exception | LinkageError e) { + log.error( + "Unable to load instrumentation {} [class {}]", + instrumentationModule.instrumentationName(), + instrumentationModule.getClass().getName(), + e); + } + } + log.debug("Installed {} instrumenter(s)", numberOfLoadedModules); + + return agentBuilder; + } + + @Override + public String extensionName() { + return "instrumentation-loader"; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java new file mode 100644 index 000000000..890d609e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.failSafe; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.tooling.HelperInjector; +import io.opentelemetry.javaagent.tooling.TransformSafeLogger; +import io.opentelemetry.javaagent.tooling.Utils; +import io.opentelemetry.javaagent.tooling.context.FieldBackedProvider; +import io.opentelemetry.javaagent.tooling.context.InstrumentationContextProvider; +import io.opentelemetry.javaagent.tooling.context.NoopContextProvider; +import io.opentelemetry.javaagent.tooling.muzzle.matcher.Mismatch; +import io.opentelemetry.javaagent.tooling.muzzle.matcher.ReferenceMatcher; +import java.security.ProtectionDomain; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.annotation.AnnotationSource; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.utility.JavaModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class InstrumentationModuleInstaller { + private static final TransformSafeLogger log = + TransformSafeLogger.getLogger(InstrumentationModule.class); + private static final Logger muzzleLog = LoggerFactory.getLogger("muzzleMatcher"); + + // Added here instead of AgentInstaller's ignores because it's relatively + // expensive. https://github.com/DataDog/dd-trace-java/pull/1045 + public static final ElementMatcher.Junction NOT_DECORATOR_MATCHER = + not(isAnnotatedWith(named("javax.decorator.Decorator"))); + + AgentBuilder install( + InstrumentationModule instrumentationModule, AgentBuilder parentAgentBuilder) { + // 简化排除module的jvm参数 + boolean exclude = InstrumentationModule.EXCLUDE_MODULE.contains(instrumentationModule.instrumentationName()); + log.warn("Instrumentation:{} {}",instrumentationModule.instrumentationName(),instrumentationModule.isEnabled() && !exclude); + if (!instrumentationModule.isEnabled() || exclude) { + log.debug("Instrumentation {} is disabled", instrumentationModule.instrumentationName()); + return parentAgentBuilder; + } + List helperClassNames = instrumentationModule.getMuzzleHelperClassNames(); + List helperResourceNames = instrumentationModule.helperResourceNames(); + List typeInstrumentations = instrumentationModule.typeInstrumentations(); + if (typeInstrumentations.isEmpty()) { + if (!helperClassNames.isEmpty() || !helperResourceNames.isEmpty()) { + log.warn( + "Helper classes and resources won't be injected if no types are instrumented: {}", + instrumentationModule.instrumentationName()); + } + + return parentAgentBuilder; + } + + ElementMatcher.Junction moduleClassLoaderMatcher = + instrumentationModule.classLoaderMatcher(); + MuzzleMatcher muzzleMatcher = new MuzzleMatcher(instrumentationModule, helperClassNames); + AgentBuilder.Transformer helperInjector = + new HelperInjector( + instrumentationModule.instrumentationName(), + helperClassNames, + helperResourceNames, + Utils.getExtensionsClassLoader()); + InstrumentationContextProvider contextProvider = + createInstrumentationContextProvider(instrumentationModule); + + AgentBuilder agentBuilder = parentAgentBuilder; + for (TypeInstrumentation typeInstrumentation : typeInstrumentations) { + AgentBuilder.Identified.Extendable extendableAgentBuilder = + agentBuilder + .type( + failSafe( + typeInstrumentation.typeMatcher(), + "Instrumentation type matcher unexpected exception: " + getClass().getName()), + failSafe( + moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization()), + "Instrumentation class loader matcher unexpected exception: " + + getClass().getName())) + .and(NOT_DECORATOR_MATCHER) + .and(muzzleMatcher) + .transform(ConstantAdjuster.instance()) + .transform(helperInjector); + extendableAgentBuilder = contextProvider.instrumentationTransformer(extendableAgentBuilder); + TypeTransformerImpl typeTransformer = new TypeTransformerImpl(extendableAgentBuilder); + typeInstrumentation.transform(typeTransformer); + extendableAgentBuilder = typeTransformer.getAgentBuilder(); + extendableAgentBuilder = contextProvider.additionalInstrumentation(extendableAgentBuilder); + + agentBuilder = extendableAgentBuilder; + } + + return agentBuilder; + } + + private static InstrumentationContextProvider createInstrumentationContextProvider( + InstrumentationModule instrumentationModule) { + Map contextStore = instrumentationModule.getMuzzleContextStoreClasses(); + if (!contextStore.isEmpty()) { + return new FieldBackedProvider(instrumentationModule.getClass(), contextStore); + } else { + return NoopContextProvider.INSTANCE; + } + } + + /** + * A ByteBuddy matcher that decides whether this instrumentation should be applied. Calls + * generated {@link ReferenceMatcher}: if any mismatch with the passed {@code classLoader} is + * found this instrumentation is skipped. + */ + private static class MuzzleMatcher implements AgentBuilder.RawMatcher { + private final InstrumentationModule instrumentationModule; + private final List helperClassNames; + private final AtomicBoolean initialized = new AtomicBoolean(false); + private volatile ReferenceMatcher referenceMatcher; + + private MuzzleMatcher( + InstrumentationModule instrumentationModule, List helperClassNames) { + this.instrumentationModule = instrumentationModule; + this.helperClassNames = helperClassNames; + } + + @Override + public boolean matches( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + Class classBeingRedefined, + ProtectionDomain protectionDomain) { + ReferenceMatcher muzzle = getReferenceMatcher(); + boolean isMatch = muzzle.matches(classLoader); + + if (!isMatch) { + if (muzzleLog.isWarnEnabled()) { + muzzleLog.warn( + "Instrumentation skipped, mismatched references were found: {} [class {}] on {}", + instrumentationModule.instrumentationName(), + instrumentationModule.getClass().getName(), + classLoader); + List mismatches = muzzle.getMismatchedReferenceSources(classLoader); + for (Mismatch mismatch : mismatches) { + muzzleLog.warn("-- {}", mismatch); + } + } + } else { + if (log.isDebugEnabled()) { + log.debug( + "Applying instrumentation: {} [class {}] on {}", + instrumentationModule.instrumentationName(), + instrumentationModule.getClass().getName(), + classLoader); + } + } + + return isMatch; + } + + // ReferenceMatcher internally caches the muzzle check results per classloader, that's why we + // keep its instance in a field + // it is lazily created to avoid unnecessarily loading the muzzle references from the module + // during the agent setup + private ReferenceMatcher getReferenceMatcher() { + if (initialized.compareAndSet(false, true)) { + referenceMatcher = + new ReferenceMatcher( + helperClassNames, + instrumentationModule.getMuzzleReferences(), + instrumentationModule::isHelperClass); + } + return referenceMatcher; + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java new file mode 100644 index 000000000..1f1059f7e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.tooling.Utils; +import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class TypeTransformerImpl implements TypeTransformer { + private AgentBuilder.Identified.Extendable agentBuilder; + + TypeTransformerImpl(AgentBuilder.Identified.Extendable agentBuilder) { + this.agentBuilder = agentBuilder; + } + + @Override + public void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName) { + agentBuilder = + agentBuilder.transform( + new AgentBuilder.Transformer.ForAdvice() + .include( + Utils.getBootstrapProxy(), + Utils.getAgentClassLoader(), + Utils.getExtensionsClassLoader()) + .withExceptionHandler(ExceptionHandlers.defaultExceptionHandler()) + .advice(methodMatcher, adviceClassName)); + } + + @Override + public void applyTransformer(AgentBuilder.Transformer transformer) { + agentBuilder = agentBuilder.transform(transformer); + } + + AgentBuilder.Identified.Extendable getAgentBuilder() { + return agentBuilder; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/InstrumentationClassPredicate.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/InstrumentationClassPredicate.java new file mode 100644 index 000000000..19a0beb55 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/InstrumentationClassPredicate.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle; + +import java.util.function.Predicate; + +public final class InstrumentationClassPredicate { + // javaagent instrumentation packages + private static final String JAVAAGENT_INSTRUMENTATION_PACKAGE = + "io.opentelemetry.javaagent.instrumentation."; + private static final String JAVAAGENT_API_PACKAGE = + "io.opentelemetry.javaagent.instrumentation.api."; + + // library instrumentation packages (both shaded in the agent) + private static final String LIBRARY_INSTRUMENTATION_PACKAGE = "io.opentelemetry.instrumentation."; + private static final String INSTRUMENTATION_API_PACKAGE = "io.opentelemetry.instrumentation.api."; + + private final Predicate additionalLibraryInstrumentationPredicate; + + public InstrumentationClassPredicate( + Predicate additionalLibraryInstrumentationPredicate) { + this.additionalLibraryInstrumentationPredicate = additionalLibraryInstrumentationPredicate; + } + + /** + * Defines which classes are treated by muzzle as "internal", "helper" instrumentation classes. + * + *

    This set of classes is defined by a package naming convention: all javaagent and library + * instrumentation classes are treated as "helper" classes and are subjected to the reference + * collection process. All others (including {@code instrumentation-api} and {@code javaagent-api} + * modules are not scanned for references (but references to them are collected). + * + *

    Aside from "standard" instrumentation helper class packages, instrumentation modules can + * pass an additional predicate to include instrumentation helper classes from 3rd party packages. + */ + public boolean isInstrumentationClass(String className) { + return isJavaagentInstrumentationClass(className) + || isLibraryInstrumentationClass(className) + || additionalLibraryInstrumentationPredicate.test(className); + } + + public boolean isProvidedByLibrary(String className) { + return !isInstrumentationClass(className) && !isProvidedByJavaagent(className); + } + + private static boolean isProvidedByJavaagent(String className) { + return className.startsWith(JAVAAGENT_API_PACKAGE) + || className.startsWith(INSTRUMENTATION_API_PACKAGE) + || className.startsWith("io.opentelemetry.javaagent.bootstrap.") + || className.startsWith("io.opentelemetry.api.") + || className.startsWith("io.opentelemetry.context.") + || className.startsWith("io.opentelemetry.semconv.") + || className.startsWith("org.slf4j."); + } + + private static boolean isJavaagentInstrumentationClass(String className) { + return className.startsWith(JAVAAGENT_INSTRUMENTATION_PACKAGE) + && !className.startsWith(JAVAAGENT_API_PACKAGE); + } + + private static boolean isLibraryInstrumentationClass(String className) { + return className.startsWith(LIBRARY_INSTRUMENTATION_PACKAGE) + && !className.startsWith(INSTRUMENTATION_API_PACKAGE); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/AdviceClassNameCollector.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/AdviceClassNameCollector.java new file mode 100644 index 000000000..9e13434d4 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/AdviceClassNameCollector.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.collector; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.HashSet; +import java.util.Set; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class AdviceClassNameCollector implements TypeTransformer { + private final Set adviceClassNames = new HashSet<>(); + + @Override + public void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName) { + adviceClassNames.add(adviceClassName); + } + + @Override + public void applyTransformer(AgentBuilder.Transformer transformer) {} + + Set getAdviceClassNames() { + return adviceClassNames; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCodeGenerationPlugin.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCodeGenerationPlugin.java new file mode 100644 index 000000000..f1897435f --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCodeGenerationPlugin.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.collector; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import net.bytebuddy.build.Plugin; +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.dynamic.DynamicType; + +/** + * This class is a ByteBuddy build plugin that is responsible for generating actual implementation + * of some {@link InstrumentationModule} methods. Auto-generated methods have the word "muzzle" in + * their names. + * + *

    This class is used in the gradle build scripts, referenced by each instrumentation module. + */ +public class MuzzleCodeGenerationPlugin implements Plugin { + + private static final TypeDescription instrumentationModuleType = + new TypeDescription.ForLoadedType(InstrumentationModule.class); + + @Override + public boolean matches(TypeDescription target) { + if (target.isAbstract()) { + return false; + } + // AutoService annotation is not retained at runtime. Check for InstrumentationModule supertype + boolean isInstrumentationModule = false; + TypeDefinition instrumentation = target.getSuperClass(); + while (instrumentation != null) { + if (instrumentation.equals(instrumentationModuleType)) { + isInstrumentationModule = true; + break; + } + instrumentation = instrumentation.getSuperClass(); + } + return isInstrumentationModule; + } + + @Override + public DynamicType.Builder apply( + DynamicType.Builder builder, + TypeDescription typeDescription, + ClassFileLocator classFileLocator) { + return builder.visit(new MuzzleCodeGenerator()); + } + + @Override + public void close() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCodeGenerator.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCodeGenerator.java new file mode 100644 index 000000000..fb50fdda8 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCodeGenerator.java @@ -0,0 +1,550 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.collector; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.muzzle.ClassRef; +import io.opentelemetry.javaagent.extension.muzzle.ClassRefBuilder; +import io.opentelemetry.javaagent.extension.muzzle.FieldRef; +import io.opentelemetry.javaagent.extension.muzzle.Flag; +import io.opentelemetry.javaagent.extension.muzzle.MethodRef; +import io.opentelemetry.javaagent.extension.muzzle.Source; +import io.opentelemetry.javaagent.tooling.Utils; +import java.net.URLClassLoader; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.jar.asm.Type; +import net.bytebuddy.pool.TypePool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class generates the actual implementation of the {@link + * InstrumentationModule#getMuzzleReferences()} method. It collects references from all advice + * classes defined in an instrumentation and writes them as Java bytecode in the generated {@link + * InstrumentationModule#getMuzzleReferences()} method. + * + *

    This class is run at compile time by the {@link MuzzleCodeGenerationPlugin} ByteBuddy plugin. + */ +class MuzzleCodeGenerator implements AsmVisitorWrapper { + private static final Logger log = LoggerFactory.getLogger(MuzzleCodeGenerator.class); + + private static final String MUZZLE_REFERENCES_METHOD_NAME = "getMuzzleReferences"; + private static final String MUZZLE_HELPER_CLASSES_METHOD_NAME = "getMuzzleHelperClassNames"; + private static final String MUZZLE_CONTEXT_STORE_CLASSES_METHOD_NAME = + "getMuzzleContextStoreClasses"; + + @Override + public int mergeWriter(int flags) { + return flags | ClassWriter.COMPUTE_MAXS; + } + + @Override + public int mergeReader(int flags) { + return flags; + } + + @Override + public ClassVisitor wrap( + TypeDescription instrumentedType, + ClassVisitor classVisitor, + Implementation.Context implementationContext, + TypePool typePool, + FieldList fields, + MethodList methods, + int writerFlags, + int readerFlags) { + return new GenerateMuzzleMethodsAndFields(classVisitor); + } + + private static class GenerateMuzzleMethodsAndFields extends ClassVisitor { + + private String instrumentationClassName; + private InstrumentationModule instrumentationModule; + + private boolean generateReferencesMethod = true; + private boolean generateHelperClassNamesMethod = true; + private boolean generateContextStoreClassesMethod = true; + + public GenerateMuzzleMethodsAndFields(ClassVisitor classVisitor) { + super(Opcodes.ASM7, classVisitor); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + this.instrumentationClassName = name; + try { + instrumentationModule = + (InstrumentationModule) + MuzzleCodeGenerator.class + .getClassLoader() + .loadClass(Utils.getClassName(instrumentationClassName)) + .getDeclaredConstructor() + .newInstance(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + if (MUZZLE_REFERENCES_METHOD_NAME.equals(name)) { + generateReferencesMethod = false; + log.info( + "The '{}' method was already found in class '{}'. Muzzle will not generate it again", + MUZZLE_REFERENCES_METHOD_NAME, + instrumentationClassName); + } + if (MUZZLE_HELPER_CLASSES_METHOD_NAME.equals(name)) { + generateHelperClassNamesMethod = false; + log.info( + "The '{}' method was already found in class '{}'. Muzzle will not generate it again", + MUZZLE_HELPER_CLASSES_METHOD_NAME, + instrumentationClassName); + } + if (MUZZLE_CONTEXT_STORE_CLASSES_METHOD_NAME.equals(name)) { + generateContextStoreClassesMethod = false; + log.info( + "The '{}' method was already found in class '{}'. Muzzle will not generate it again", + MUZZLE_CONTEXT_STORE_CLASSES_METHOD_NAME, + instrumentationClassName); + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public void visitEnd() { + ReferenceCollector collector = collectReferences(); + if (generateReferencesMethod) { + generateMuzzleReferencesMethod(collector); + } + if (generateHelperClassNamesMethod) { + generateMuzzleHelperClassNamesMethod(collector); + } + if (generateContextStoreClassesMethod) { + generateMuzzleContextStoreClassesMethod(collector); + } + super.visitEnd(); + } + + private ReferenceCollector collectReferences() { + AdviceClassNameCollector adviceClassNameCollector = new AdviceClassNameCollector(); + for (TypeInstrumentation typeInstrumentation : instrumentationModule.typeInstrumentations()) { + typeInstrumentation.transform(adviceClassNameCollector); + } + + // the classloader has a parent including the Gradle classpath, such as buildSrc dependencies. + // These may have resources take precedence over ones we define, so we need to make sure to + // not include them when loading resources. + ClassLoader resourceLoader = + new URLClassLoader( + ((URLClassLoader) MuzzleCodeGenerator.class.getClassLoader()).getURLs(), null); + ReferenceCollector collector = + new ReferenceCollector(instrumentationModule::isHelperClass, resourceLoader); + for (String adviceClass : adviceClassNameCollector.getAdviceClassNames()) { + collector.collectReferencesFromAdvice(adviceClass); + } + for (String resource : instrumentationModule.helperResourceNames()) { + collector.collectReferencesFromResource(resource); + } + collector.prune(); + return collector; + } + + private void generateMuzzleReferencesMethod(ReferenceCollector collector) { + Type referenceType = Type.getType(ClassRef.class); + Type referenceBuilderType = Type.getType(ClassRefBuilder.class); + Type referenceFlagType = Type.getType(Flag.class); + Type referenceFlagArrayType = Type.getType(Flag[].class); + Type referenceSourceArrayType = Type.getType(Source[].class); + Type stringType = Type.getType(String.class); + Type typeType = Type.getType(Type.class); + Type typeArrayType = Type.getType(Type[].class); + + /* + * public Map getMuzzleReferences() { + * Map references = new HashMap<>(...); + * references.put("reference class name", ClassRef.newBuilder(...) + * ... + * .build()); + * return references; + * } + */ + MethodVisitor mv = + super.visitMethod( + Opcodes.ACC_PUBLIC, MUZZLE_REFERENCES_METHOD_NAME, "()Ljava/util/Map;", null, null); + mv.visitCode(); + + Collection references = collector.getReferences().values(); + + writeNewMap(mv, references.size()); + // stack: map + mv.visitVarInsn(Opcodes.ASTORE, 1); + // stack: + + references.forEach( + reference -> { + mv.visitVarInsn(Opcodes.ALOAD, 1); + // stack: map + mv.visitLdcInsn(reference.getClassName()); + // stack: map, className + + mv.visitLdcInsn(reference.getClassName()); + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + referenceType.getInternalName(), + "newBuilder", + Type.getMethodDescriptor(referenceBuilderType, stringType), + /* isInterface= */ false); + // stack: map, className, builder + + for (Source source : reference.getSources()) { + mv.visitLdcInsn(source.getName()); + mv.visitLdcInsn(source.getLine()); + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + referenceBuilderType.getInternalName(), + "addSource", + Type.getMethodDescriptor(referenceBuilderType, stringType, Type.INT_TYPE), + /* isInterface= */ false); + } + // stack: map, className, builder + for (Flag flag : reference.getFlags()) { + String enumClassName = getEnumClassInternalName(flag); + mv.visitFieldInsn( + Opcodes.GETSTATIC, enumClassName, flag.name(), "L" + enumClassName + ";"); + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + referenceBuilderType.getInternalName(), + "addFlag", + Type.getMethodDescriptor(referenceBuilderType, referenceFlagType), + /* isInterface= */ false); + } + // stack: map, className, builder + if (null != reference.getSuperClassName()) { + mv.visitLdcInsn(reference.getSuperClassName()); + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + referenceBuilderType.getInternalName(), + "setSuperClassName", + Type.getMethodDescriptor(referenceBuilderType, stringType), + /* isInterface= */ false); + } + // stack: map, className, builder + for (String interfaceName : reference.getInterfaceNames()) { + mv.visitLdcInsn(interfaceName); + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + referenceBuilderType.getInternalName(), + "addInterfaceName", + Type.getMethodDescriptor(referenceBuilderType, stringType), + /* isInterface= */ false); + } + // stack: map, className, builder + for (FieldRef field : reference.getFields()) { + writeSourcesArray(mv, field.getSources()); + writeFlagsArray(mv, field.getFlags()); + // field name + mv.visitLdcInsn(field.getName()); + writeType(mv, field.getDescriptor()); + // declared flag + mv.visitLdcInsn(field.isDeclared()); + + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + referenceBuilderType.getInternalName(), + "addField", + Type.getMethodDescriptor( + referenceBuilderType, + referenceSourceArrayType, + referenceFlagArrayType, + stringType, + typeType, + Type.BOOLEAN_TYPE), + /* isInterface= */ false); + } + // stack: map, className, builder + for (MethodRef method : reference.getMethods()) { + writeSourcesArray(mv, method.getSources()); + writeFlagsArray(mv, method.getFlags()); + // method name + mv.visitLdcInsn(method.getName()); + // method return and argument types + { + // we cannot pass the whole method descriptor string as it won't be shaded, so + // we + // have to pass the return and parameter types separately - strings in + // Type.getType() + // calls will be shaded correctly + Type methodType = Type.getMethodType(method.getDescriptor()); + + writeType(mv, methodType.getReturnType().getDescriptor()); + + mv.visitLdcInsn(methodType.getArgumentTypes().length); + mv.visitTypeInsn(Opcodes.ANEWARRAY, typeType.getInternalName()); + int i = 0; + for (Type parameterType : methodType.getArgumentTypes()) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + writeType(mv, parameterType.getDescriptor()); + mv.visitInsn(Opcodes.AASTORE); + i++; + } + } + + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + referenceBuilderType.getInternalName(), + "addMethod", + Type.getMethodDescriptor( + referenceBuilderType, + referenceSourceArrayType, + referenceFlagArrayType, + stringType, + typeType, + typeArrayType), + /* isInterface= */ false); + } + // stack: map, className, builder + mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + referenceBuilderType.getInternalName(), + "build", + Type.getMethodDescriptor(referenceType), + /* isInterface= */ false); + // stack: map, className, classRef + + mv.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + "java/util/Map", + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + /* isInterface= */ true); + // stack: previousValue + mv.visitInsn(Opcodes.POP); + // stack: + }); + + mv.visitVarInsn(Opcodes.ALOAD, 1); + // stack: map + mv.visitInsn(Opcodes.ARETURN); + + mv.visitMaxs(0, 0); // recomputed + mv.visitEnd(); + } + + private static void writeNewMap(MethodVisitor mv, int size) { + mv.visitTypeInsn(Opcodes.NEW, "java/util/HashMap"); + // stack: map + mv.visitInsn(Opcodes.DUP); + // stack: map, map + // pass bigger size to avoid resizes; same formula as in e.g. HashSet(Collection) + // 0.75 is the default load factor + mv.visitLdcInsn((int) (size / 0.75f) + 1); + // stack: map, map, size + mv.visitLdcInsn(0.75f); + // stack: map, map, size, loadFactor + mv.visitMethodInsn( + Opcodes.INVOKESPECIAL, "java/util/HashMap", "", "(IF)V", /* isInterface= */ false); + } + + private static void writeSourcesArray(MethodVisitor mv, Set sources) { + Type referenceSourceType = Type.getType(Source.class); + + mv.visitLdcInsn(sources.size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, referenceSourceType.getInternalName()); + + int i = 0; + for (Source source : sources) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + + mv.visitTypeInsn(Opcodes.NEW, referenceSourceType.getInternalName()); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(source.getName()); + mv.visitLdcInsn(source.getLine()); + mv.visitMethodInsn( + Opcodes.INVOKESPECIAL, + referenceSourceType.getInternalName(), + "", + "(Ljava/lang/String;I)V", + /* isInterface= */ false); + + mv.visitInsn(Opcodes.AASTORE); + ++i; + } + } + + private static void writeFlagsArray(MethodVisitor mv, Set flags) { + Type referenceFlagType = Type.getType(Flag.class); + + mv.visitLdcInsn(flags.size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, referenceFlagType.getInternalName()); + + int i = 0; + for (Flag flag : flags) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + String enumClassName = getEnumClassInternalName(flag); + mv.visitFieldInsn(Opcodes.GETSTATIC, enumClassName, flag.name(), "L" + enumClassName + ";"); + mv.visitInsn(Opcodes.AASTORE); + ++i; + } + } + + private static final Pattern ANONYMOUS_ENUM_CONSTANT_CLASS = + Pattern.compile("(?.*)\\$[0-9]+$"); + + // drops "$1" suffix for enum constants that override/implement super class methods + private static String getEnumClassInternalName(Flag flag) { + String fullInternalName = Utils.getInternalName(flag.getClass()); + Matcher m = ANONYMOUS_ENUM_CONSTANT_CLASS.matcher(fullInternalName); + return m.matches() ? m.group("enumClass") : fullInternalName; + } + + private static void writeType(MethodVisitor mv, String descriptor) { + Type typeType = Type.getType(Type.class); + + mv.visitLdcInsn(descriptor); + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + typeType.getInternalName(), + "getType", + Type.getMethodDescriptor(typeType, Type.getType(String.class)), + /* isInterface= */ false); + } + + private void generateMuzzleHelperClassNamesMethod(ReferenceCollector collector) { + /* + * public List getMuzzleHelperClassNames() { + * List helperClassNames = new ArrayList<>(...); + * helperClassNames.add(...); + * return helperClassNames; + * } + */ + MethodVisitor mv = + super.visitMethod( + Opcodes.ACC_PUBLIC, + MUZZLE_HELPER_CLASSES_METHOD_NAME, + "()Ljava/util/List;", + null, + null); + mv.visitCode(); + + List helperClassNames = collector.getSortedHelperClasses(); + + mv.visitTypeInsn(Opcodes.NEW, "java/util/ArrayList"); + // stack: list + mv.visitInsn(Opcodes.DUP); + // stack: list, list + mv.visitLdcInsn(helperClassNames.size()); + // stack: list, list, size + mv.visitMethodInsn( + Opcodes.INVOKESPECIAL, "java/util/ArrayList", "", "(I)V", /* isInterface= */ false); + // stack: list + mv.visitVarInsn(Opcodes.ASTORE, 1); + // stack: + + helperClassNames.forEach( + helperClassName -> { + mv.visitVarInsn(Opcodes.ALOAD, 1); + // stack: list + mv.visitLdcInsn(helperClassName); + // stack: list, helperClassName + mv.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + "java/util/List", + "add", + "(Ljava/lang/Object;)Z", + /* isInterface= */ true); + // stack: added + mv.visitInsn(Opcodes.POP); + // stack: + }); + + mv.visitVarInsn(Opcodes.ALOAD, 1); + // stack: list + mv.visitInsn(Opcodes.ARETURN); + + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + private void generateMuzzleContextStoreClassesMethod(ReferenceCollector collector) { + /* + * public Map getMuzzleContextStoreClasses() { + * Map contextStore = new HashMap<>(...); + * contextStore.put(..., ...); + * return contextStore; + * } + */ + MethodVisitor mv = + super.visitMethod( + Opcodes.ACC_PUBLIC, + MUZZLE_CONTEXT_STORE_CLASSES_METHOD_NAME, + "()Ljava/util/Map;", + null, + null); + mv.visitCode(); + + Map contextStoreClasses = collector.getContextStoreClasses(); + + writeNewMap(mv, contextStoreClasses.size()); + // stack: map + mv.visitVarInsn(Opcodes.ASTORE, 1); + // stack: + + contextStoreClasses.forEach( + (className, contextClassName) -> { + mv.visitVarInsn(Opcodes.ALOAD, 1); + // stack: map + mv.visitLdcInsn(className); + // stack: map, className + mv.visitLdcInsn(contextClassName); + // stack: map, className, contextClassName + mv.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + "java/util/Map", + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + /* isInterface= */ true); + // stack: previousValue + mv.visitInsn(Opcodes.POP); + // stack: + }); + + mv.visitVarInsn(Opcodes.ALOAD, 1); + // stack: map + mv.visitInsn(Opcodes.ARETURN); + + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCompilationException.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCompilationException.java new file mode 100644 index 000000000..10f69f4be --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/MuzzleCompilationException.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.collector; + +public class MuzzleCompilationException extends RuntimeException { + public MuzzleCompilationException(String message) { + super(message); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/ReferenceCollectingClassVisitor.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/ReferenceCollectingClassVisitor.java new file mode 100644 index 000000000..480725e7d --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/ReferenceCollectingClassVisitor.java @@ -0,0 +1,568 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.collector; + +import com.google.common.collect.EvictingQueue; +import io.opentelemetry.javaagent.extension.muzzle.ClassRef; +import io.opentelemetry.javaagent.extension.muzzle.Flag; +import io.opentelemetry.javaagent.extension.muzzle.Flag.ManifestationFlag; +import io.opentelemetry.javaagent.extension.muzzle.Flag.MinimumVisibilityFlag; +import io.opentelemetry.javaagent.extension.muzzle.Flag.OwnershipFlag; +import io.opentelemetry.javaagent.extension.muzzle.Flag.VisibilityFlag; +import io.opentelemetry.javaagent.extension.muzzle.Source; +import io.opentelemetry.javaagent.tooling.Utils; +import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationClassPredicate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.FieldVisitor; +import net.bytebuddy.jar.asm.Handle; +import net.bytebuddy.jar.asm.Label; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.jar.asm.Type; + +/** Visit a class and collect all references made by the visited class. */ +// Additional things we could check +// - annotations on class +// - outer class +// - inner class +// - cast opcodes in method bodies +class ReferenceCollectingClassVisitor extends ClassVisitor { + + /** + * Get the package of an internal class name. + * + *

    foo/bar/Baz -> foo/bar/ + */ + private static String internalPackageName(String internalName) { + return internalName.replaceAll("/[^/]+$", ""); + } + + /** + * Compute the minimum required access for FROM class to access the TO class. + * + * @return A reference flag with the required level of access. + */ + private static MinimumVisibilityFlag computeMinimumClassAccess(Type from, Type to) { + if (from.getInternalName().equalsIgnoreCase(to.getInternalName())) { + return MinimumVisibilityFlag.PRIVATE_OR_HIGHER; + } else if (internalPackageName(from.getInternalName()) + .equals(internalPackageName(to.getInternalName()))) { + return MinimumVisibilityFlag.PACKAGE_OR_HIGHER; + } else { + return MinimumVisibilityFlag.PUBLIC; + } + } + + /** + * Compute the minimum required access for FROM class to access a field on the TO class. + * + * @return A reference flag with the required level of access. + */ + private static MinimumVisibilityFlag computeMinimumFieldAccess(Type from, Type to) { + if (from.getInternalName().equalsIgnoreCase(to.getInternalName())) { + return MinimumVisibilityFlag.PRIVATE_OR_HIGHER; + } else if (internalPackageName(from.getInternalName()) + .equals(internalPackageName(to.getInternalName()))) { + return MinimumVisibilityFlag.PACKAGE_OR_HIGHER; + } else { + // Additional references: check the type hierarchy of FROM to distinguish public from + // protected + return MinimumVisibilityFlag.PROTECTED_OR_HIGHER; + } + } + + /** + * Compute the minimum required access for FROM class to access METHODTYPE on the TO class. + * + * @return A reference flag with the required level of access. + */ + private static MinimumVisibilityFlag computeMinimumMethodAccess(Type from, Type to) { + if (from.getInternalName().equalsIgnoreCase(to.getInternalName())) { + return MinimumVisibilityFlag.PRIVATE_OR_HIGHER; + } else { + // Additional references: check the type hierarchy of FROM to distinguish public from + // protected + return MinimumVisibilityFlag.PROTECTED_OR_HIGHER; + } + } + + /** + * If TYPE is an array, returns the underlying type. If TYPE is not an array simply return the + * type. + */ + private static Type underlyingType(Type type) { + while (type.getSort() == Type.ARRAY) { + type = type.getElementType(); + } + return type; + } + + private final InstrumentationClassPredicate instrumentationClassPredicate; + private final boolean isAdviceClass; + + private final Map references = new LinkedHashMap<>(); + private final Set helperClasses = new HashSet<>(); + // helper super classes which are themselves also helpers + // this is needed for injecting the helper classes into the class loader in the correct order + private final Set helperSuperClasses = new HashSet<>(); + private final Map contextStoreClasses = new LinkedHashMap<>(); + private String refSourceClassName; + private Type refSourceType; + + ReferenceCollectingClassVisitor( + InstrumentationClassPredicate instrumentationClassPredicate, boolean isAdviceClass) { + super(Opcodes.ASM7); + this.instrumentationClassPredicate = instrumentationClassPredicate; + this.isAdviceClass = isAdviceClass; + } + + Map getReferences() { + return references; + } + + Set getHelperClasses() { + return helperClasses; + } + + Set getHelperSuperClasses() { + return helperSuperClasses; + } + + Map getContextStoreClasses() { + return contextStoreClasses; + } + + private void addExtendsReference(ClassRef ref) { + addReference(ref); + if (instrumentationClassPredicate.isInstrumentationClass(ref.getClassName())) { + helperSuperClasses.add(ref.getClassName()); + } + } + + private void addReference(ClassRef ref) { + if (!ref.getClassName().startsWith("java.")) { + ClassRef reference = references.get(ref.getClassName()); + if (null == reference) { + references.put(ref.getClassName(), ref); + } else { + references.put(ref.getClassName(), reference.merge(ref)); + } + } + if (instrumentationClassPredicate.isInstrumentationClass(ref.getClassName())) { + helperClasses.add(ref.getClassName()); + } + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + refSourceClassName = Utils.getClassName(name); + refSourceType = Type.getType("L" + name + ";"); + + // class references are not generated for advice classes, only for helper classes + if (!isAdviceClass) { + String fixedSuperClassName = Utils.getClassName(superName); + + addExtendsReference( + ClassRef.newBuilder(fixedSuperClassName).addSource(refSourceClassName).build()); + + List fixedInterfaceNames = new ArrayList<>(interfaces.length); + for (String interfaceName : interfaces) { + String fixedInterfaceName = Utils.getClassName(interfaceName); + fixedInterfaceNames.add(fixedInterfaceName); + + addExtendsReference( + ClassRef.newBuilder(fixedInterfaceName).addSource(refSourceClassName).build()); + } + + addReference( + ClassRef.newBuilder(refSourceClassName) + .addSource(refSourceClassName) + .setSuperClassName(fixedSuperClassName) + .addInterfaceNames(fixedInterfaceNames) + .addFlag(computeTypeManifestationFlag(access)) + .build()); + } + + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField( + int access, String name, String descriptor, String signature, Object value) { + // Additional references we could check + // - annotations on field + + Type fieldType = Type.getType(descriptor); + + // remember that this field was declared in the currently visited helper class + addReference( + ClassRef.newBuilder(refSourceClassName) + .addSource(refSourceClassName) + .addField(new Source[0], new Flag[0], name, fieldType, /* isFieldDeclared= */ true) + .build()); + + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + + // declared method references are not generated for advice classes, only for helper classes + if (!isAdviceClass) { + Type methodType = Type.getMethodType(descriptor); + + Flag visibilityFlag = computeVisibilityFlag(access); + Flag ownershipFlag = computeOwnershipFlag(access); + Flag manifestationFlag = computeTypeManifestationFlag(access); + + addReference( + ClassRef.newBuilder(refSourceClassName) + .addSource(refSourceClassName) + .addMethod( + new Source[0], + new Flag[] {visibilityFlag, ownershipFlag, manifestationFlag}, + name, + methodType.getReturnType(), + methodType.getArgumentTypes()) + .build()); + } + + // Additional references we could check + // - Classes in signature (return type, params) and visible from this package + return new AdviceReferenceMethodVisitor( + new InstrumentationContextMethodVisitor( + super.visitMethod(access, name, descriptor, signature, exceptions))); + } + + private static VisibilityFlag computeVisibilityFlag(int access) { + if (VisibilityFlag.PUBLIC.matches(access)) { + return VisibilityFlag.PUBLIC; + } else if (VisibilityFlag.PROTECTED.matches(access)) { + return VisibilityFlag.PROTECTED; + } else if (VisibilityFlag.PACKAGE.matches(access)) { + return VisibilityFlag.PACKAGE; + } else { + return VisibilityFlag.PRIVATE; + } + } + + private static OwnershipFlag computeOwnershipFlag(int access) { + if (OwnershipFlag.STATIC.matches(access)) { + return OwnershipFlag.STATIC; + } else { + return OwnershipFlag.NON_STATIC; + } + } + + private static ManifestationFlag computeTypeManifestationFlag(int access) { + if (ManifestationFlag.ABSTRACT.matches(access)) { + return ManifestationFlag.ABSTRACT; + } else if (ManifestationFlag.FINAL.matches(access)) { + return ManifestationFlag.FINAL; + } else { + return ManifestationFlag.NON_FINAL; + } + } + + private class AdviceReferenceMethodVisitor extends MethodVisitor { + private int currentLineNumber = -1; + + public AdviceReferenceMethodVisitor(MethodVisitor methodVisitor) { + super(Opcodes.ASM7, methodVisitor); + } + + @Override + public void visitLineNumber(int line, Label start) { + currentLineNumber = line; + super.visitLineNumber(line, start); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + // Additional references we could check + // * DONE owner class + // * DONE owner class has a field (name) + // * DONE field is static or non-static + // * DONE field's visibility from this point (NON_PRIVATE?) + // * DONE owner class's visibility from this point (NON_PRIVATE?) + // + // * DONE field-source class (descriptor) + // * DONE field-source visibility from this point (PRIVATE?) + + Type ownerType = + owner.startsWith("[") + ? underlyingType(Type.getType(owner)) + : Type.getType("L" + owner + ";"); + Type fieldType = Type.getType(descriptor); + + List fieldFlags = new ArrayList<>(); + fieldFlags.add(computeMinimumFieldAccess(refSourceType, ownerType)); + fieldFlags.add( + opcode == Opcodes.GETSTATIC || opcode == Opcodes.PUTSTATIC + ? OwnershipFlag.STATIC + : OwnershipFlag.NON_STATIC); + + addReference( + ClassRef.newBuilder(ownerType.getClassName()) + .addSource(refSourceClassName, currentLineNumber) + .addFlag(computeMinimumClassAccess(refSourceType, ownerType)) + .addField( + new Source[] {new Source(refSourceClassName, currentLineNumber)}, + fieldFlags.toArray(new Flag[0]), + name, + fieldType, + /* isFieldDeclared= */ false) + .build()); + + Type underlyingFieldType = underlyingType(Type.getType(descriptor)); + if (underlyingFieldType.getSort() == Type.OBJECT) { + addReference( + ClassRef.newBuilder(underlyingFieldType.getClassName()) + .addSource(refSourceClassName, currentLineNumber) + .addFlag(computeMinimumClassAccess(refSourceType, underlyingFieldType)) + .build()); + } + + super.visitFieldInsn(opcode, owner, name, descriptor); + } + + @Override + public void visitMethodInsn( + int opcode, String owner, String name, String descriptor, boolean isInterface) { + // Additional references we could check + // * DONE name of method owner's class + // * DONE is the owner an interface? + // * DONE owner's access from here (PRIVATE?) + // * DONE method on the owner class + // * DONE is the method static? Is it visible from here? + // * Class names from the method descriptor + // * params classes + // * return type + Type methodType = Type.getMethodType(descriptor); + + Type ownerType = + owner.startsWith("[") + ? underlyingType(Type.getType(owner)) + : Type.getType("L" + owner + ";"); + + { // ref for method return type + Type returnType = underlyingType(methodType.getReturnType()); + if (returnType.getSort() == Type.OBJECT) { + addReference( + ClassRef.newBuilder(returnType.getClassName()) + .addSource(refSourceClassName, currentLineNumber) + .addFlag(computeMinimumClassAccess(refSourceType, returnType)) + .build()); + } + } + // refs for method param types + for (Type paramType : methodType.getArgumentTypes()) { + paramType = underlyingType(paramType); + if (paramType.getSort() == Type.OBJECT) { + addReference( + ClassRef.newBuilder(paramType.getClassName()) + .addSource(refSourceClassName, currentLineNumber) + .addFlag(computeMinimumClassAccess(refSourceType, paramType)) + .build()); + } + } + + List methodFlags = new ArrayList<>(); + methodFlags.add( + opcode == Opcodes.INVOKESTATIC ? OwnershipFlag.STATIC : OwnershipFlag.NON_STATIC); + methodFlags.add(computeMinimumMethodAccess(refSourceType, ownerType)); + + addReference( + ClassRef.newBuilder(ownerType.getClassName()) + .addSource(refSourceClassName, currentLineNumber) + .addFlag(isInterface ? ManifestationFlag.INTERFACE : ManifestationFlag.NON_INTERFACE) + .addFlag(computeMinimumClassAccess(refSourceType, ownerType)) + .addMethod( + new Source[] {new Source(refSourceClassName, currentLineNumber)}, + methodFlags.toArray(new Flag[0]), + name, + methodType.getReturnType(), + methodType.getArgumentTypes()) + .build()); + + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + Type typeObj = underlyingType(Type.getObjectType(type)); + if (typeObj.getSort() == Type.OBJECT) { + addReference( + ClassRef.newBuilder(typeObj.getClassName()) + .addSource(refSourceClassName, currentLineNumber) + .addFlag(computeMinimumClassAccess(refSourceType, typeObj)) + .build()); + } + + super.visitTypeInsn(opcode, type); + } + + @Override + public void visitInvokeDynamicInsn( + String name, + String descriptor, + Handle bootstrapMethodHandle, + Object... bootstrapMethodArguments) { + // This part might be unnecessary... + addReference( + ClassRef.newBuilder(Utils.getClassName(bootstrapMethodHandle.getOwner())) + .addSource(refSourceClassName, currentLineNumber) + .addFlag( + computeMinimumClassAccess( + refSourceType, Type.getObjectType(bootstrapMethodHandle.getOwner()))) + .build()); + for (Object arg : bootstrapMethodArguments) { + if (arg instanceof Handle) { + Handle handle = (Handle) arg; + addReference( + ClassRef.newBuilder(Utils.getClassName(handle.getOwner())) + .addSource(refSourceClassName, currentLineNumber) + .addFlag( + computeMinimumClassAccess( + refSourceType, Type.getObjectType(handle.getOwner()))) + .build()); + } + } + super.visitInvokeDynamicInsn( + name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + + @Override + public void visitLdcInsn(Object value) { + if (value instanceof Type) { + Type type = underlyingType((Type) value); + if (type.getSort() == Type.OBJECT) { + addReference( + ClassRef.newBuilder(type.getClassName()) + .addSource(refSourceClassName, currentLineNumber) + .addFlag(computeMinimumClassAccess(refSourceType, type)) + .build()); + } + } + super.visitLdcInsn(value); + } + } + + private class InstrumentationContextMethodVisitor extends MethodVisitor { + // this data structure will remember last two LDC instructions before + // InstrumentationContext.get() call + private final EvictingQueue lastTwoClassConstants = EvictingQueue.create(2); + + InstrumentationContextMethodVisitor(MethodVisitor methodVisitor) { + super(Opcodes.ASM7, methodVisitor); + } + + @Override + public void visitInsn(int opcode) { + registerOpcode(opcode, null); + super.visitInsn(opcode); + } + + @Override + public void visitIntInsn(int opcode, int operand) { + registerOpcode(opcode, null); + super.visitIntInsn(opcode, operand); + } + + @Override + public void visitVarInsn(int opcode, int var) { + registerOpcode(opcode, null); + super.visitVarInsn(opcode, var); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + registerOpcode(opcode, null); + super.visitTypeInsn(opcode, type); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + registerOpcode(opcode, null); + super.visitFieldInsn(opcode, owner, name, descriptor); + } + + @Override + public void visitMethodInsn( + int opcode, String owner, String name, String descriptor, boolean isInterface) { + + Type methodType = Type.getMethodType(descriptor); + Type ownerType = Type.getType("L" + owner + ";"); + + // remember used context classes if this is an InstrumentationContext.get() call + if ("io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext" + .equals(ownerType.getClassName()) + && "get".equals(name) + && methodType.getArgumentTypes().length == 2) { + // in case of invalid scenario (not using .class ref directly) don't store anything and + // clear the last LDC stack + // note that FieldBackedProvider also check for an invalid context call in the runtime + if (lastTwoClassConstants.remainingCapacity() == 0) { + String className = lastTwoClassConstants.poll(); + String contextClassName = lastTwoClassConstants.poll(); + contextStoreClasses.put(className, contextClassName); + } else { + throw new MuzzleCompilationException( + "Invalid InstrumentationContext#get(Class, Class) usage: you cannot pass variables," + + " method parameters, compute classes; class references need to be passed" + + " directly to the get() method"); + } + } + + registerOpcode(opcode, null); + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + registerOpcode(opcode, null); + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitLdcInsn(Object value) { + registerOpcode(Opcodes.LDC, value); + super.visitLdcInsn(value); + } + + private void registerOpcode(int opcode, Object value) { + // check if this is an LDC instruction; if so, remember the class that was used + // we need to remember last two LDC instructions that were executed before + // InstrumentationContext.get() call + if (opcode == Opcodes.LDC) { + if (value instanceof Type) { + Type type = (Type) value; + if (type.getSort() == Type.OBJECT) { + lastTwoClassConstants.add(type.getClassName()); + return; + } + } + } + + // instruction other than LDC visited; pop the first element if present - this will + // prevent adding wrong context key pairs in case of an invalid scenario + lastTwoClassConstants.poll(); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/ReferenceCollector.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/ReferenceCollector.java new file mode 100644 index 000000000..bdde1db05 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/collector/ReferenceCollector.java @@ -0,0 +1,340 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.collector; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singleton; + +import com.google.common.base.Strings; +import com.google.common.graph.Graph; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.Graphs; +import com.google.common.graph.MutableGraph; +import io.opentelemetry.javaagent.extension.muzzle.ClassRef; +import io.opentelemetry.javaagent.extension.muzzle.Flag; +import io.opentelemetry.javaagent.tooling.Utils; +import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationClassPredicate; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URLConnection; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.jar.asm.ClassReader; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * {@link LinkedHashMap} is used for reference map to guarantee a deterministic order of iteration, + * so that bytecode generated based on it would also be deterministic. + * + *

    This class is only called at compile time by the {@link MuzzleCodeGenerationPlugin} ByteBuddy + * plugin. + */ +public class ReferenceCollector { + + private final Map references = new LinkedHashMap<>(); + private final MutableGraph helperSuperClassGraph = GraphBuilder.directed().build(); + private final Map contextStoreClasses = new LinkedHashMap<>(); + private final Set visitedClasses = new HashSet<>(); + private final InstrumentationClassPredicate instrumentationClassPredicate; + private final ClassLoader resourceLoader; + + // only used by tests + public ReferenceCollector(Predicate libraryInstrumentationPredicate) { + this(libraryInstrumentationPredicate, ReferenceCollector.class.getClassLoader()); + } + + public ReferenceCollector( + Predicate libraryInstrumentationPredicate, ClassLoader resourceLoader) { + this.instrumentationClassPredicate = + new InstrumentationClassPredicate(libraryInstrumentationPredicate); + this.resourceLoader = resourceLoader; + } + + /** + * If passed {@code resource} path points to an SPI file (either Java {@link + * java.util.ServiceLoader} or AWS SDK {@code ExecutionInterceptor}) reads the file and adds every + * implementation as a reference, traversing the graph of classes until a non-instrumentation + * (external) class is encountered. + * + * @param resource path to the resource file, same as in {@link ClassLoader#getResource(String)} + * @see InstrumentationClassPredicate + */ + public void collectReferencesFromResource(String resource) { + if (!isSpiFile(resource)) { + return; + } + + List spiImplementations = new ArrayList<>(); + try (InputStream stream = getResourceStream(resource)) { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, UTF_8)); + while (reader.ready()) { + String line = reader.readLine(); + if (!Strings.isNullOrEmpty(line)) { + spiImplementations.add(line); + } + } + } catch (IOException e) { + throw new IllegalStateException("Error reading resource " + resource, e); + } + + visitClassesAndCollectReferences(spiImplementations, /* startsFromAdviceClass= */ false); + } + + private static final Pattern AWS_SDK_V2_SERVICE_INTERCEPTOR_SPI = + Pattern.compile("software/amazon/awssdk/services/\\w+(/\\w+)?/execution.interceptors"); + + private static final Pattern AWS_SDK_V1_SERVICE_INTERCEPTOR_SPI = + Pattern.compile("com/amazonaws/services/\\w+(/\\w+)?/request.handler2s"); + + private static boolean isSpiFile(String resource) { + return resource.startsWith("META-INF/services/") + || resource.equals("software/amazon/awssdk/global/handlers/execution.interceptors") + || resource.equals("com/amazonaws/global/handlers/request.handler2s") + || AWS_SDK_V2_SERVICE_INTERCEPTOR_SPI.matcher(resource).matches() + || AWS_SDK_V1_SERVICE_INTERCEPTOR_SPI.matcher(resource).matches(); + } + + /** + * Traverse a graph of classes starting from {@code adviceClassName} and collect all references to + * both internal (instrumentation) and external classes. + * + *

    The graph of classes is traversed until a non-instrumentation (external) class is + * encountered. + * + * @param adviceClassName Starting point for generating references. + * @see InstrumentationClassPredicate + */ + public void collectReferencesFromAdvice(String adviceClassName) { + visitClassesAndCollectReferences(singleton(adviceClassName), /* startsFromAdviceClass= */ true); + } + + private void visitClassesAndCollectReferences( + Collection startingClasses, boolean startsFromAdviceClass) { + Queue instrumentationQueue = new ArrayDeque<>(startingClasses); + boolean isAdviceClass = startsFromAdviceClass; + + while (!instrumentationQueue.isEmpty()) { + String visitedClassName = instrumentationQueue.remove(); + visitedClasses.add(visitedClassName); + + try (InputStream in = getClassFileStream(visitedClassName)) { + // only start from method bodies for the advice class (skips class/method references) + ReferenceCollectingClassVisitor cv = + new ReferenceCollectingClassVisitor(instrumentationClassPredicate, isAdviceClass); + ClassReader reader = new ClassReader(in); + reader.accept(cv, ClassReader.SKIP_FRAMES); + + for (Map.Entry entry : cv.getReferences().entrySet()) { + String refClassName = entry.getKey(); + ClassRef reference = entry.getValue(); + + // Don't generate references created outside of the instrumentation package. + if (!visitedClasses.contains(refClassName) + && instrumentationClassPredicate.isInstrumentationClass(refClassName)) { + instrumentationQueue.add(refClassName); + } + addReference(refClassName, reference); + } + collectHelperClasses( + isAdviceClass, visitedClassName, cv.getHelperClasses(), cv.getHelperSuperClasses()); + + contextStoreClasses.putAll(cv.getContextStoreClasses()); + } catch (IOException e) { + throw new IllegalStateException("Error reading class " + visitedClassName, e); + } + + if (isAdviceClass) { + isAdviceClass = false; + } + } + } + + private InputStream getClassFileStream(String className) throws IOException { + return getResourceStream(Utils.getResourceName(className)); + } + + private InputStream getResourceStream(String resource) throws IOException { + URLConnection connection = + checkNotNull(resourceLoader.getResource(resource), "Couldn't find resource %s", resource) + .openConnection(); + + // Since the JarFile cache is not per class loader, but global with path as key, using cache may + // cause the same instance of JarFile being used for consecutive builds, even if the file has + // been changed. There is still another cache in ZipFile.Source which checks last modified time + // as well, so the zip index is not scanned again on every class. + connection.setUseCaches(false); + return connection.getInputStream(); + } + + private void addReference(String refClassName, ClassRef reference) { + if (references.containsKey(refClassName)) { + references.put(refClassName, references.get(refClassName).merge(reference)); + } else { + references.put(refClassName, reference); + } + } + + private void collectHelperClasses( + boolean isAdviceClass, + String className, + Set helperClasses, + Set helperSuperClasses) { + for (String helperClass : helperClasses) { + helperSuperClassGraph.addNode(helperClass); + } + if (!isAdviceClass) { + for (String helperSuperClass : helperSuperClasses) { + helperSuperClassGraph.putEdge(className, helperSuperClass); + } + } + } + + public Map getReferences() { + return references; + } + + public void prune() { + // helper classes that may help another helper class implement an abstract library method + // must be retained + // for example if helper class A extends helper class B, and A also implements a library + // interface L, then B needs to be retained so that it can be used at runtime to verify that A + // implements all of L's methods. + // Super types of A that are not also helper classes do not need to be retained because they can + // be looked up on the classpath at runtime, see HelperReferenceWrapper.create(). + Set helperClassesParticipatingInLibrarySuperType = + getHelperClassesParticipatingInLibrarySuperType(); + + for (Iterator i = references.values().iterator(); i.hasNext(); ) { + ClassRef reference = i.next(); + if (instrumentationClassPredicate.isProvidedByLibrary(reference.getClassName())) { + // these are the references to library classes which need to be checked at runtime + continue; + } + if (helperClassesParticipatingInLibrarySuperType.contains(reference)) { + // these need to be kept in order to check that abstract methods are implemented, + // and to check that declared super class fields are present + // + // can at least prune constructors, private, and static methods, since those cannot be used + // to help implement an abstract library method + reference + .getMethods() + .removeIf( + method -> + method.getName().equals(MethodDescription.CONSTRUCTOR_INTERNAL_NAME) + || method.getFlags().contains(Flag.VisibilityFlag.PRIVATE) + || method.getFlags().contains(Flag.OwnershipFlag.STATIC)); + continue; + } + i.remove(); + } + } + + private Set getHelperClassesParticipatingInLibrarySuperType() { + Set helperClassesParticipatingInLibrarySuperType = new HashSet<>(); + for (ClassRef reference : getHelperClassesWithLibrarySuperType()) { + addSuperTypesThatAreAlsoHelperClasses( + reference.getClassName(), helperClassesParticipatingInLibrarySuperType); + } + return helperClassesParticipatingInLibrarySuperType; + } + + private Set getHelperClassesWithLibrarySuperType() { + Set helperClassesWithLibrarySuperType = new HashSet<>(); + for (ClassRef reference : references.values()) { + if (instrumentationClassPredicate.isInstrumentationClass(reference.getClassName()) + && hasLibrarySuperType(reference.getClassName())) { + helperClassesWithLibrarySuperType.add(reference); + } + } + return helperClassesWithLibrarySuperType; + } + + private void addSuperTypesThatAreAlsoHelperClasses( + @Nullable String className, Set superTypes) { + if (className != null && instrumentationClassPredicate.isInstrumentationClass(className)) { + ClassRef reference = references.get(className); + superTypes.add(reference); + + addSuperTypesThatAreAlsoHelperClasses(reference.getSuperClassName(), superTypes); + // need to keep interfaces too since they may have default methods + for (String superType : reference.getInterfaceNames()) { + addSuperTypesThatAreAlsoHelperClasses(superType, superTypes); + } + } + } + + private boolean hasLibrarySuperType(@Nullable String typeName) { + if (typeName == null || typeName.startsWith("java.")) { + return false; + } + if (instrumentationClassPredicate.isProvidedByLibrary(typeName)) { + return true; + } + ClassRef reference = references.get(typeName); + if (hasLibrarySuperType(reference.getSuperClassName())) { + return true; + } + for (String type : reference.getInterfaceNames()) { + if (hasLibrarySuperType(type)) { + return true; + } + } + return false; + } + + // see https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + public List getSortedHelperClasses() { + MutableGraph dependencyGraph = Graphs.copyOf(Graphs.transpose(helperSuperClassGraph)); + List helperClasses = new ArrayList<>(dependencyGraph.nodes().size()); + + Queue helpersWithNoDeps = findAllHelperClassesWithoutDependencies(dependencyGraph); + + while (!helpersWithNoDeps.isEmpty()) { + String helperClass = helpersWithNoDeps.remove(); + helperClasses.add(helperClass); + + Set dependencies = new HashSet<>(dependencyGraph.successors(helperClass)); + for (String dependency : dependencies) { + dependencyGraph.removeEdge(helperClass, dependency); + if (dependencyGraph.predecessors(dependency).isEmpty()) { + helpersWithNoDeps.add(dependency); + } + } + } + + return helperClasses; + } + + private static Queue findAllHelperClassesWithoutDependencies( + Graph dependencyGraph) { + Queue helpersWithNoDeps = new LinkedList<>(); + for (String helperClass : dependencyGraph.nodes()) { + if (dependencyGraph.predecessors(helperClass).isEmpty()) { + helpersWithNoDeps.add(helperClass); + } + } + return helpersWithNoDeps; + } + + public Map getContextStoreClasses() { + return contextStoreClasses; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/HelperReferenceWrapper.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/HelperReferenceWrapper.java new file mode 100644 index 000000000..5e32150fe --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/HelperReferenceWrapper.java @@ -0,0 +1,314 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.matcher; + +import static net.bytebuddy.description.method.MethodDescription.CONSTRUCTOR_INTERNAL_NAME; + +import io.opentelemetry.javaagent.extension.muzzle.ClassRef; +import io.opentelemetry.javaagent.extension.muzzle.FieldRef; +import io.opentelemetry.javaagent.extension.muzzle.Flag.ManifestationFlag; +import io.opentelemetry.javaagent.extension.muzzle.Flag.OwnershipFlag; +import io.opentelemetry.javaagent.extension.muzzle.Flag.VisibilityFlag; +import io.opentelemetry.javaagent.extension.muzzle.MethodRef; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.method.MethodDescription.InDefinedShape; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.pool.TypePool; +import net.bytebuddy.pool.TypePool.Resolution; + +/** This class provides a common interface for {@link ClassRef} and {@link TypeDescription}. */ +interface HelperReferenceWrapper { + boolean isAbstract(); + + /** + * Returns true if the wrapped type extends any class other than {@link Object} or implements any + * interface. + */ + boolean hasSuperTypes(); + + /** + * Returns an iterable containing the wrapped type's super class (if exists) and implemented + * interfaces. + */ + Stream getSuperTypes(); + + /** Returns an iterable with all non-private, non-static methods declared in the wrapped type. */ + Stream getMethods(); + + /** Returns an iterable with all non-private fields declared in the wrapped type. */ + Stream getFields(); + + final class Method { + private final boolean isAbstract; + private final String declaringClass; + private final String name; + private final String descriptor; + + public Method(boolean isAbstract, String declaringClass, String name, String descriptor) { + this.isAbstract = isAbstract; + this.declaringClass = declaringClass; + this.name = name; + this.descriptor = descriptor; + } + + public boolean isAbstract() { + return isAbstract; + } + + public String getDeclaringClass() { + return declaringClass; + } + + public String getName() { + return name; + } + + public String getDescriptor() { + return descriptor; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Method)) { + return false; + } + Method other = (Method) obj; + return Objects.equals(name, other.name) && Objects.equals(descriptor, other.descriptor); + } + + @Override + public int hashCode() { + return Objects.hash(name, descriptor); + } + + @Override + public String toString() { + return "Method{" + + "isAbstract=" + + isAbstract + + ", declaringClass='" + + declaringClass + + '\'' + + ", name='" + + name + + '\'' + + ", descriptor='" + + descriptor + + '\'' + + '}'; + } + } + + final class Field { + private final String name; + private final String descriptor; + + public Field(String name, String descriptor) { + this.name = name; + this.descriptor = descriptor; + } + + public String getName() { + return name; + } + + public String getDescriptor() { + return descriptor; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Field field = (Field) o; + return Objects.equals(name, field.name) && Objects.equals(descriptor, field.descriptor); + } + + @Override + public int hashCode() { + return Objects.hash(name, descriptor); + } + + @Override + public String toString() { + return "Field{" + "name='" + name + '\'' + ", descriptor='" + descriptor + '\'' + '}'; + } + } + + class Factory { + private final TypePool classpathPool; + private final Map helperReferences; + + public Factory(TypePool classpathPool, Map helperReferences) { + this.classpathPool = classpathPool; + this.helperReferences = helperReferences; + } + + public HelperReferenceWrapper create(ClassRef reference) { + return new ReferenceType(reference); + } + + private HelperReferenceWrapper create(String className) { + Resolution resolution = classpathPool.describe(className); + if (resolution.isResolved()) { + return new ClasspathType(resolution.resolve()); + } + // checking helper references is needed when one helper class A extends another helper class B + // and the subclass A also implements a library interface C + // B needs to be resolved as part of checking that A implements all required methods of C + // but B cannot be resolved on the classpath, so B needs to be resolved from helper references + if (helperReferences.containsKey(className)) { + return new ReferenceType(helperReferences.get(className)); + } + throw new IllegalStateException("Missing class " + className); + } + + private final class ReferenceType implements HelperReferenceWrapper { + private final ClassRef reference; + + private ReferenceType(ClassRef reference) { + this.reference = reference; + } + + @Override + public boolean isAbstract() { + return reference.getFlags().contains(ManifestationFlag.ABSTRACT); + } + + @Override + public boolean hasSuperTypes() { + return hasActualSuperType() || reference.getInterfaceNames().size() > 0; + } + + @Override + public Stream getSuperTypes() { + Stream superClass = Stream.empty(); + if (hasActualSuperType()) { + superClass = Stream.of(Factory.this.create(reference.getSuperClassName())); + } + + Stream interfaces = + reference.getInterfaceNames().stream().map(Factory.this::create); + + return Stream.concat(superClass, interfaces); + } + + private boolean hasActualSuperType() { + return reference.getSuperClassName() != null; + } + + @Override + public Stream getMethods() { + return reference.getMethods().stream().filter(this::isOverrideable).map(this::toMethod); + } + + private boolean isOverrideable(MethodRef method) { + return !(method.getFlags().contains(OwnershipFlag.STATIC) + || method.getFlags().contains(VisibilityFlag.PRIVATE) + || CONSTRUCTOR_INTERNAL_NAME.equals(method.getName())); + } + + private Method toMethod(MethodRef method) { + return new Method( + method.getFlags().contains(ManifestationFlag.ABSTRACT), + reference.getClassName(), + method.getName(), + method.getDescriptor()); + } + + @Override + public Stream getFields() { + return reference.getFields().stream() + .filter(this::isDeclaredAndNotPrivate) + .map(this::toField); + } + + private boolean isDeclaredAndNotPrivate(FieldRef field) { + return field.isDeclared() && !field.getFlags().contains(VisibilityFlag.PRIVATE); + } + + private Field toField(FieldRef field) { + return new Field(field.getName(), field.getDescriptor()); + } + } + + private static final class ClasspathType implements HelperReferenceWrapper { + private final TypeDescription type; + + private ClasspathType(TypeDescription type) { + this.type = type; + } + + @Override + public boolean isAbstract() { + return type.isAbstract(); + } + + @Override + public boolean hasSuperTypes() { + return hasActualSuperType() || type.getInterfaces().size() > 0; + } + + private boolean hasActualSuperType() { + return type.getSuperClass() != null; + } + + @Override + public Stream getSuperTypes() { + Stream superClass = Stream.empty(); + if (hasActualSuperType()) { + superClass = Stream.of(new ClasspathType(type.getSuperClass().asErasure())); + } + + Stream interfaces = + type.getInterfaces().asErasures().stream().map(ClasspathType::new); + + return Stream.concat(superClass, interfaces); + } + + @Override + public Stream getMethods() { + return type.getDeclaredMethods().stream() + .filter(ClasspathType::isOverrideable) + .map(this::toMethod); + } + + private static boolean isOverrideable(InDefinedShape method) { + return !(method.isStatic() || method.isPrivate() || method.isConstructor()); + } + + private Method toMethod(InDefinedShape method) { + return new Method( + method.isAbstract(), type.getName(), method.getInternalName(), method.getDescriptor()); + } + + @Override + public Stream getFields() { + return type.getDeclaredFields().stream() + .filter(ClasspathType::isNotPrivate) + .map(ClasspathType::toField); + } + + private static boolean isNotPrivate(FieldDescription.InDefinedShape field) { + return !field.isPrivate(); + } + + private static Field toField(FieldDescription.InDefinedShape field) { + return new Field(field.getName(), field.getDescriptor()); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/Mismatch.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/Mismatch.java new file mode 100644 index 000000000..bdc9782d3 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/Mismatch.java @@ -0,0 +1,169 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.matcher; + +import io.opentelemetry.javaagent.extension.muzzle.ClassRef; +import io.opentelemetry.javaagent.extension.muzzle.FieldRef; +import io.opentelemetry.javaagent.extension.muzzle.Flag; +import io.opentelemetry.javaagent.extension.muzzle.MethodRef; +import io.opentelemetry.javaagent.extension.muzzle.Source; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collection; +import java.util.Collections; +import net.bytebuddy.jar.asm.Type; + +/** + * A mismatch between a {@link ClassRef} and a runtime class. + * + *

    This class' {@link #toString()} returns a human-readable description of the mismatch along + * with the first source code location of the reference which caused the mismatch. + */ +public abstract class Mismatch { + /** Instrumentation sources which caused the mismatch. */ + private final Collection mismatchSources; + + private Mismatch(Collection mismatchSources) { + this.mismatchSources = mismatchSources; + } + + @Override + public String toString() { + if (mismatchSources.size() > 0) { + return mismatchSources.iterator().next().toString() + " " + getMismatchDetails(); + } else { + return " " + getMismatchDetails(); + } + } + + /** Human-readable string describing the mismatch. */ + abstract String getMismatchDetails(); + + public static class MissingClass extends Mismatch { + private final String className; + + public MissingClass(ClassRef classRef) { + super(classRef.getSources()); + this.className = classRef.getClassName(); + } + + public MissingClass(ClassRef classRef, String className) { + super(classRef.getSources()); + this.className = className; + } + + @Override + String getMismatchDetails() { + return "Missing class " + className; + } + } + + public static class MissingFlag extends Mismatch { + private final Flag expectedFlag; + private final String classMethodOrFieldDesc; + private final int foundAccess; + + public MissingFlag( + Collection sources, + String classMethodOrFieldDesc, + Flag expectedFlag, + int foundAccess) { + super(sources); + this.classMethodOrFieldDesc = classMethodOrFieldDesc; + this.expectedFlag = expectedFlag; + this.foundAccess = foundAccess; + } + + @Override + String getMismatchDetails() { + return classMethodOrFieldDesc + " requires flag " + expectedFlag + " found " + foundAccess; + } + } + + public static class MissingField extends Mismatch { + private final String className; + private final String fieldName; + private final String fieldDescriptor; + + MissingField(ClassRef classRef, FieldRef fieldRef) { + super(fieldRef.getSources()); + this.className = classRef.getClassName(); + this.fieldName = fieldRef.getName(); + this.fieldDescriptor = fieldRef.getDescriptor(); + } + + MissingField(ClassRef classRef, HelperReferenceWrapper.Field field) { + super(classRef.getSources()); + this.className = classRef.getClassName(); + this.fieldName = field.getName(); + this.fieldDescriptor = field.getDescriptor(); + } + + @Override + String getMismatchDetails() { + return "Missing field " + + Type.getType(fieldDescriptor).getClassName() + + " " + + fieldName + + " in class " + + className; + } + } + + public static class MissingMethod extends Mismatch { + private final String className; + private final String methodName; + private final String methodDescriptor; + + public MissingMethod(ClassRef classRef, MethodRef methodRef) { + super(methodRef.getSources()); + this.className = classRef.getClassName(); + this.methodName = methodRef.getName(); + this.methodDescriptor = methodRef.getDescriptor(); + } + + public MissingMethod(ClassRef classRef, HelperReferenceWrapper.Method method) { + super(classRef.getSources()); + this.className = method.getDeclaringClass(); + this.methodName = method.getName(); + this.methodDescriptor = method.getDescriptor(); + } + + @Override + String getMismatchDetails() { + return "Missing method " + className + "#" + methodName + methodDescriptor; + } + } + + /** Fallback mismatch in case an unexpected exception occurs during reference checking. */ + public static class ReferenceCheckError extends Mismatch { + private final Exception referenceCheckException; + private final ClassRef referenceBeingChecked; + private final ClassLoader classLoaderBeingChecked; + + public ReferenceCheckError( + Exception e, ClassRef referenceBeingChecked, ClassLoader classLoaderBeingChecked) { + super(Collections.emptyList()); + referenceCheckException = e; + this.referenceBeingChecked = referenceBeingChecked; + this.classLoaderBeingChecked = classLoaderBeingChecked; + } + + @Override + String getMismatchDetails() { + StringWriter sw = new StringWriter(); + sw.write("Failed to generate reference check for: "); + sw.write(referenceBeingChecked.toString()); + sw.write(" on classloader "); + sw.write(classLoaderBeingChecked.toString()); + sw.write("\n"); + // add exception message and stack trace + PrintWriter pw = new PrintWriter(sw); + referenceCheckException.printStackTrace(pw); + return sw.toString(); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/MuzzleGradlePluginUtil.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/MuzzleGradlePluginUtil.java new file mode 100644 index 000000000..1ab473bc0 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/MuzzleGradlePluginUtil.java @@ -0,0 +1,179 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.matcher; + +import static java.lang.System.lineSeparator; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.muzzle.ClassRef; +import io.opentelemetry.javaagent.extension.muzzle.FieldRef; +import io.opentelemetry.javaagent.extension.muzzle.MethodRef; +import io.opentelemetry.javaagent.extension.muzzle.Source; +import io.opentelemetry.javaagent.tooling.HelperInjector; +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import net.bytebuddy.dynamic.ClassFileLocator; + +/** Entry point for the muzzle gradle plugin. */ +// Runs in special classloader so tedious to provide access to the Gradle logger. +@SuppressWarnings("SystemOut") +public final class MuzzleGradlePluginUtil { + private static final String INDENT = " "; + + /** + * Verifies that all instrumentations present in the {@code agentClassLoader} can be safely + * applied to the passed {@code userClassLoader}. + * + *

    This method throws whenever one of the following step fails (and {@code assertPass} is + * true): + * + *

      + *
    1. {@code userClassLoader} is not matched by the {@link + * InstrumentationModule#classLoaderMatcher()} method + *
    2. {@link ReferenceMatcher} of any instrumentation module finds any mismatch + *
    3. any helper class defined in {@link InstrumentationModule#getMuzzleHelperClassNames()} + * fails to be injected into {@code userClassLoader} + *
    + * + *

    When {@code assertPass = false} this method behaves in an opposite way: failure in any of + * the first two steps is expected (helper classes are not injected at all). + * + *

    This method is repeatedly called by the {@code :muzzle} gradle task - each tested dependency + * version passes different {@code userClassLoader}. + */ + public static void assertInstrumentationMuzzled( + ClassLoader agentClassLoader, ClassLoader userClassLoader, boolean assertPass) + throws Exception { + // muzzle validate all instrumenters + int validatedModulesCount = 0; + for (InstrumentationModule instrumentationModule : + ServiceLoader.load(InstrumentationModule.class, agentClassLoader)) { + ReferenceMatcher muzzle = + new ReferenceMatcher( + instrumentationModule.getMuzzleHelperClassNames(), + instrumentationModule.getMuzzleReferences(), + instrumentationModule::isHelperClass); + List mismatches = muzzle.getMismatchedReferenceSources(userClassLoader); + + boolean classLoaderMatch = + instrumentationModule.classLoaderMatcher().matches(userClassLoader); + boolean passed = mismatches.isEmpty() && classLoaderMatch; + + if (passed && !assertPass) { + System.err.println( + "MUZZLE PASSED " + + instrumentationModule.getClass().getSimpleName() + + " BUT FAILURE WAS EXPECTED"); + throw new IllegalStateException("Instrumentation unexpectedly passed Muzzle validation"); + } else if (!passed && assertPass) { + System.err.println( + "FAILED MUZZLE VALIDATION: " + + instrumentationModule.getClass().getName() + + " mismatches:"); + + if (!classLoaderMatch) { + System.err.println("-- classloader mismatch"); + } + + for (Mismatch mismatch : mismatches) { + System.err.println("-- " + mismatch); + } + throw new IllegalStateException("Instrumentation failed Muzzle validation"); + } + + validatedModulesCount++; + } + // run helper injector on all instrumentation modules + if (assertPass) { + for (InstrumentationModule instrumentationModule : + ServiceLoader.load(InstrumentationModule.class, agentClassLoader)) { + try { + // verify helper injector works + List allHelperClasses = instrumentationModule.getMuzzleHelperClassNames(); + if (!allHelperClasses.isEmpty()) { + new HelperInjector( + MuzzleGradlePluginUtil.class.getSimpleName(), + createHelperMap(allHelperClasses, agentClassLoader)) + .transform(null, null, userClassLoader, null); + } + } catch (RuntimeException e) { + System.err.println( + "FAILED HELPER INJECTION. Are Helpers being injected in the correct order?"); + throw e; + } + } + } + if (validatedModulesCount == 0) { + String errorMessage = "Did not found any InstrumentationModule to validate!"; + System.err.println(errorMessage); + throw new IllegalStateException(errorMessage); + } + } + + private static Map createHelperMap( + Collection helperClassNames, ClassLoader agentClassLoader) throws IOException { + Map helperMap = new LinkedHashMap<>(helperClassNames.size()); + for (String helperName : helperClassNames) { + ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(agentClassLoader); + byte[] classBytes = locator.locate(helperName).resolve(); + helperMap.put(helperName, classBytes); + } + return helperMap; + } + + /** + * Prints all references from all instrumentation modules present in the passed {@code + * instrumentationClassLoader}. + * + *

    Called by the {@code printMuzzleReferences} gradle task. + */ + public static void printMuzzleReferences(ClassLoader instrumentationClassLoader) { + for (InstrumentationModule instrumentationModule : + ServiceLoader.load(InstrumentationModule.class, instrumentationClassLoader)) { + try { + System.out.println(instrumentationModule.getClass().getName()); + for (ClassRef ref : instrumentationModule.getMuzzleReferences().values()) { + System.out.print(prettyPrint(ref)); + } + } catch (RuntimeException e) { + String message = + "Unexpected exception printing references for " + + instrumentationModule.getClass().getName(); + System.out.println(message); + throw new IllegalStateException(message, e); + } + } + } + + private static String prettyPrint(ClassRef ref) { + StringBuilder builder = new StringBuilder(INDENT).append(ref).append(lineSeparator()); + if (!ref.getSources().isEmpty()) { + builder.append(INDENT).append(INDENT).append("Sources:").append(lineSeparator()); + for (Source source : ref.getSources()) { + builder + .append(INDENT) + .append(INDENT) + .append(INDENT) + .append("at: ") + .append(source) + .append(lineSeparator()); + } + } + for (FieldRef field : ref.getFields()) { + builder.append(INDENT).append(INDENT).append(field).append(lineSeparator()); + } + for (MethodRef method : ref.getMethods()) { + builder.append(INDENT).append(INDENT).append(method).append(lineSeparator()); + } + return builder.toString(); + } + + private MuzzleGradlePluginUtil() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/ReferenceMatcher.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/ReferenceMatcher.java new file mode 100644 index 000000000..3e5ee2d79 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/matcher/ReferenceMatcher.java @@ -0,0 +1,327 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle.matcher; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.BOOTSTRAP_LOADER; + +import io.opentelemetry.instrumentation.api.caching.Cache; +import io.opentelemetry.javaagent.extension.muzzle.ClassRef; +import io.opentelemetry.javaagent.extension.muzzle.FieldRef; +import io.opentelemetry.javaagent.extension.muzzle.Flag; +import io.opentelemetry.javaagent.extension.muzzle.MethodRef; +import io.opentelemetry.javaagent.tooling.AgentTooling; +import io.opentelemetry.javaagent.tooling.Utils; +import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationClassPredicate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.jar.asm.Type; +import net.bytebuddy.pool.TypePool; + +/** Matches a set of references against a classloader. */ +public final class ReferenceMatcher { + + private final Cache mismatchCache = + Cache.newBuilder().setWeakKeys().build(); + private final Map references; + private final Set helperClassNames; + private final InstrumentationClassPredicate instrumentationClassPredicate; + + public ReferenceMatcher( + List helperClassNames, + Map references, + Predicate libraryInstrumentationPredicate) { + this.references = references; + this.helperClassNames = new HashSet<>(helperClassNames); + this.instrumentationClassPredicate = + new InstrumentationClassPredicate(libraryInstrumentationPredicate); + } + + /** + * Matcher used by ByteBuddy. Fails fast and only caches empty results, or complete results + * + * @param userClassLoader Classloader to validate against (or null for bootstrap) + * @return true if all references match the classpath of loader + */ + public boolean matches(ClassLoader userClassLoader) { + if (userClassLoader == BOOTSTRAP_LOADER) { + userClassLoader = Utils.getBootstrapProxy(); + } + return mismatchCache.computeIfAbsent(userClassLoader, this::doesMatch); + } + + private boolean doesMatch(ClassLoader loader) { + TypePool typePool = createTypePool(loader); + for (ClassRef reference : references.values()) { + if (!checkMatch(reference, typePool, loader).isEmpty()) { + return false; + } + } + return true; + } + + /** + * Loads the full list of mismatches. Used in debug contexts only + * + * @param loader Classloader to validate against (or null for bootstrap) + * @return A list of all mismatches between this ReferenceMatcher and loader's classpath. + */ + public List getMismatchedReferenceSources(ClassLoader loader) { + if (loader == BOOTSTRAP_LOADER) { + loader = Utils.getBootstrapProxy(); + } + TypePool typePool = createTypePool(loader); + + List mismatches = emptyList(); + + for (ClassRef reference : references.values()) { + mismatches = lazyAddAll(mismatches, checkMatch(reference, typePool, loader)); + } + + return mismatches; + } + + private static TypePool createTypePool(ClassLoader loader) { + return AgentTooling.poolStrategy() + .typePool(AgentTooling.locationStrategy().classFileLocator(loader), loader); + } + + /** + * Check a reference against a classloader's classpath. + * + * @return A list of mismatched sources. A list of size 0 means the reference matches the class. + */ + private List checkMatch(ClassRef reference, TypePool typePool, ClassLoader loader) { + try { + if (instrumentationClassPredicate.isInstrumentationClass(reference.getClassName())) { + // make sure helper class is registered + if (!helperClassNames.contains(reference.getClassName())) { + return singletonList(new Mismatch.MissingClass(reference)); + } + // helper classes get their own check: whether they implement all abstract methods + return checkHelperClassMatch(reference, typePool); + } else { + TypePool.Resolution resolution = typePool.describe(reference.getClassName()); + if (!resolution.isResolved()) { + return singletonList(new Mismatch.MissingClass(reference)); + } + return checkThirdPartyTypeMatch(reference, resolution.resolve()); + } + } catch (RuntimeException e) { + if (e.getMessage().startsWith("Cannot resolve type description for ")) { + // bytebuddy throws an illegal state exception with this message if it cannot resolve types + // TODO: handle missing type resolutions without catching bytebuddy's exceptions + String className = e.getMessage().replace("Cannot resolve type description for ", ""); + return singletonList(new Mismatch.MissingClass(reference, className)); + } else { + // Shouldn't happen. Fail the reference check and add a mismatch for debug logging. + return singletonList(new Mismatch.ReferenceCheckError(e, reference, loader)); + } + } + } + + // for helper classes we make sure that all abstract methods from super classes and interfaces are + // implemented and that all accessed fields are defined somewhere in the type hierarchy + private List checkHelperClassMatch(ClassRef helperClass, TypePool typePool) { + List mismatches = emptyList(); + + HelperReferenceWrapper helperWrapper = + new HelperReferenceWrapper.Factory(typePool, references).create(helperClass); + + Set undeclaredFields = + helperClass.getFields().stream() + .filter(f -> !f.isDeclared()) + .map(f -> new HelperReferenceWrapper.Field(f.getName(), f.getDescriptor())) + .collect(Collectors.toSet()); + + // if there are any fields in this helper class that's not declared here, check the type + // hierarchy + if (!undeclaredFields.isEmpty()) { + Set superClassFields = new HashSet<>(); + collectFieldsFromTypeHierarchy(helperWrapper, superClassFields); + + undeclaredFields.removeAll(superClassFields); + for (HelperReferenceWrapper.Field missingField : undeclaredFields) { + mismatches = lazyAdd(mismatches, new Mismatch.MissingField(helperClass, missingField)); + } + } + + // skip abstract method check if this type does not have super type or is abstract + if (!helperWrapper.hasSuperTypes() || helperWrapper.isAbstract()) { + return mismatches; + } + + // treat the helper type as a bag of methods: collect all methods defined in the helper class, + // all superclasses and interfaces and check if all abstract methods are implemented somewhere + Set abstractMethods = new HashSet<>(); + Set plainMethods = new HashSet<>(); + collectMethodsFromTypeHierarchy(helperWrapper, abstractMethods, plainMethods); + + abstractMethods.removeAll(plainMethods); + for (HelperReferenceWrapper.Method unimplementedMethod : abstractMethods) { + mismatches = + lazyAdd(mismatches, new Mismatch.MissingMethod(helperClass, unimplementedMethod)); + } + + return mismatches; + } + + private static void collectFieldsFromTypeHierarchy( + HelperReferenceWrapper type, Set fields) { + + type.getFields().forEach(fields::add); + type.getSuperTypes().forEach(superType -> collectFieldsFromTypeHierarchy(superType, fields)); + } + + private static void collectMethodsFromTypeHierarchy( + HelperReferenceWrapper type, + Set abstractMethods, + Set plainMethods) { + + type.getMethods() + .forEach(method -> (method.isAbstract() ? abstractMethods : plainMethods).add(method)); + + type.getSuperTypes() + .forEach( + superType -> collectMethodsFromTypeHierarchy(superType, abstractMethods, plainMethods)); + } + + private static List checkThirdPartyTypeMatch( + ClassRef reference, TypeDescription typeOnClasspath) { + List mismatches = Collections.emptyList(); + + for (Flag flag : reference.getFlags()) { + if (!flag.matches(typeOnClasspath.getActualModifiers(false))) { + String desc = reference.getClassName(); + mismatches = + lazyAdd( + mismatches, + new Mismatch.MissingFlag( + reference.getSources(), desc, flag, typeOnClasspath.getActualModifiers(false))); + } + } + + for (FieldRef fieldRef : reference.getFields()) { + FieldDescription.InDefinedShape fieldDescription = findField(fieldRef, typeOnClasspath); + if (fieldDescription == null) { + mismatches = lazyAdd(mismatches, new Mismatch.MissingField(reference, fieldRef)); + } else { + for (Flag flag : fieldRef.getFlags()) { + if (!flag.matches(fieldDescription.getModifiers())) { + String desc = + reference.getClassName() + + "#" + + fieldRef.getName() + + Type.getType(fieldRef.getDescriptor()).getInternalName(); + mismatches = + lazyAdd( + mismatches, + new Mismatch.MissingFlag( + fieldRef.getSources(), desc, flag, fieldDescription.getModifiers())); + } + } + } + } + + for (MethodRef methodRef : reference.getMethods()) { + MethodDescription.InDefinedShape methodDescription = findMethod(methodRef, typeOnClasspath); + if (methodDescription == null) { + mismatches = lazyAdd(mismatches, new Mismatch.MissingMethod(reference, methodRef)); + } else { + for (Flag flag : methodRef.getFlags()) { + if (!flag.matches(methodDescription.getModifiers())) { + String desc = + reference.getClassName() + "#" + methodRef.getName() + methodRef.getDescriptor(); + mismatches = + lazyAdd( + mismatches, + new Mismatch.MissingFlag( + methodRef.getSources(), desc, flag, methodDescription.getModifiers())); + } + } + } + } + return mismatches; + } + + private static FieldDescription.InDefinedShape findField( + FieldRef fieldRef, TypeDescription typeOnClasspath) { + for (FieldDescription.InDefinedShape fieldType : typeOnClasspath.getDeclaredFields()) { + if (fieldType.getName().equals(fieldRef.getName()) + && fieldType.getDescriptor().equals(fieldRef.getDescriptor())) { + return fieldType; + } + } + if (typeOnClasspath.getSuperClass() != null) { + FieldDescription.InDefinedShape fieldOnSupertype = + findField(fieldRef, typeOnClasspath.getSuperClass().asErasure()); + if (fieldOnSupertype != null) { + return fieldOnSupertype; + } + } + for (TypeDescription.Generic interfaceType : typeOnClasspath.getInterfaces()) { + FieldDescription.InDefinedShape fieldOnSupertype = + findField(fieldRef, interfaceType.asErasure()); + if (fieldOnSupertype != null) { + return fieldOnSupertype; + } + } + return null; + } + + private static MethodDescription.InDefinedShape findMethod( + MethodRef methodRef, TypeDescription typeOnClasspath) { + for (MethodDescription.InDefinedShape methodDescription : + typeOnClasspath.getDeclaredMethods()) { + if (methodDescription.getInternalName().equals(methodRef.getName()) + && methodDescription.getDescriptor().equals(methodRef.getDescriptor())) { + return methodDescription; + } + } + if (typeOnClasspath.getSuperClass() != null) { + MethodDescription.InDefinedShape methodOnSupertype = + findMethod(methodRef, typeOnClasspath.getSuperClass().asErasure()); + if (methodOnSupertype != null) { + return methodOnSupertype; + } + } + for (TypeDescription.Generic interfaceType : typeOnClasspath.getInterfaces()) { + MethodDescription.InDefinedShape methodOnSupertype = + findMethod(methodRef, interfaceType.asErasure()); + if (methodOnSupertype != null) { + return methodOnSupertype; + } + } + return null; + } + + // optimization to avoid ArrayList allocation in the common case when there are no mismatches + private static List lazyAdd(List mismatches, Mismatch mismatch) { + List result = mismatches.isEmpty() ? new ArrayList<>() : mismatches; + result.add(mismatch); + return result; + } + + // optimization to avoid ArrayList allocation in the common case when there are no mismatches + private static List lazyAddAll(List mismatches, List toAdd) { + if (!toAdd.isEmpty()) { + List result = mismatches.isEmpty() ? new ArrayList<>() : mismatches; + result.addAll(toAdd); + return result; + } + return mismatches; + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/ExceptionHandlerTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/ExceptionHandlerTest.groovy new file mode 100644 index 000000000..72ff3defc --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/ExceptionHandlerTest.groovy @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.test + +import static net.bytebuddy.matcher.ElementMatchers.isMethod +import static net.bytebuddy.matcher.ElementMatchers.named +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.core.read.ListAppender +import io.opentelemetry.javaagent.bootstrap.ExceptionLogger +import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers +import net.bytebuddy.agent.ByteBuddyAgent +import net.bytebuddy.agent.builder.AgentBuilder +import net.bytebuddy.agent.builder.ResettableClassFileTransformer +import net.bytebuddy.dynamic.ClassFileLocator +import org.slf4j.LoggerFactory +import spock.lang.Shared +import spock.lang.Specification + +class ExceptionHandlerTest extends Specification { + @Shared + ListAppender testAppender = new ListAppender() + @Shared + ResettableClassFileTransformer transformer + + def setupSpec() { + AgentBuilder builder = new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .type(named(getClass().getName() + '$SomeClass')) + .transform( + new AgentBuilder.Transformer.ForAdvice() + .with(new AgentBuilder.LocationStrategy.Simple(ClassFileLocator.ForClassLoader.of(BadAdvice.getClassLoader()))) + .withExceptionHandler(ExceptionHandlers.defaultExceptionHandler()) + .advice( + isMethod().and(named("isInstrumented")), + BadAdvice.getName())) + .transform( + new AgentBuilder.Transformer.ForAdvice() + .with(new AgentBuilder.LocationStrategy.Simple(ClassFileLocator.ForClassLoader.of(BadAdvice.getClassLoader()))) + .withExceptionHandler(ExceptionHandlers.defaultExceptionHandler()) + .advice( + isMethod().and(namedOneOf("smallStack", "largeStack")), + BadAdvice.NoOpAdvice.getName())) + + ByteBuddyAgent.install() + transformer = builder.installOn(ByteBuddyAgent.getInstrumentation()) + + Logger logger = (Logger) LoggerFactory.getLogger(ExceptionLogger) + testAppender.setContext(logger.getLoggerContext()) + logger.addAppender(testAppender) + testAppender.start() + } + + def cleanupSpec() { + testAppender.stop() + transformer.reset(ByteBuddyAgent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + } + + def "exception handler invoked"() { + setup: + int initLogEvents = testAppender.list.size() + expect: + SomeClass.isInstrumented() + testAppender.list.size() == initLogEvents + 1 + testAppender.list.get(testAppender.list.size() - 1).getLevel() == Level.DEBUG + // Make sure the log event came from our error handler. + // If the log message changes in the future, it's fine to just + // update the test's hardcoded message + testAppender.list.get(testAppender.list.size() - 1).getMessage().startsWith("Failed to handle exception in instrumentation for") + } + + def "exception on non-delegating classloader"() { + setup: + int initLogEvents = testAppender.list.size() + URL[] classpath = [SomeClass.getProtectionDomain().getCodeSource().getLocation(), + GroovyObject.getProtectionDomain().getCodeSource().getLocation()] + URLClassLoader loader = new URLClassLoader(classpath, (ClassLoader) null) + when: + loader.loadClass(LoggerFactory.getName()) + then: + thrown ClassNotFoundException + + when: + Class someClazz = loader.loadClass(SomeClass.getName()) + then: + someClazz.getClassLoader() == loader + someClazz.getMethod("isInstrumented").invoke(null) + testAppender.list.size() == initLogEvents + } + + def "exception handler sets the correct stack size"() { + when: + SomeClass.smallStack() + SomeClass.largeStack() + + then: + noExceptionThrown() + } + + static class SomeClass { + static boolean isInstrumented() { + return false + } + + static void smallStack() { + // a method with a max stack of 0 + } + + static void largeStack() { + // a method with a max stack of 6 + long l = 22l + int i = 3 + double d = 32.2d + Object o = new Object() + println "large stack: $l $i $d $o" + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy new file mode 100644 index 000000000..0556b6ad0 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.test + +import static io.opentelemetry.instrumentation.test.utils.ClasspathUtils.isClassLoaded +import static io.opentelemetry.instrumentation.test.utils.GcUtils.awaitGc +import static io.opentelemetry.javaagent.extension.matcher.ClassLoaderMatcher.BOOTSTRAP_CLASSLOADER + +import io.opentelemetry.javaagent.tooling.AgentInstaller +import io.opentelemetry.javaagent.tooling.HelperInjector +import io.opentelemetry.javaagent.tooling.Utils +import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicReference +import net.bytebuddy.agent.ByteBuddyAgent +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.dynamic.ClassFileLocator +import net.bytebuddy.dynamic.loading.ClassInjector +import spock.lang.Specification + +class HelperInjectionTest extends Specification { + + def "helpers injected to non-delegating classloader"() { + setup: + URL[] helpersSourceUrls = new URL[1] + helpersSourceUrls[0] = HelperClass.getProtectionDomain().getCodeSource().getLocation() + ClassLoader helpersSourceLoader = new URLClassLoader(helpersSourceUrls) + + String helperClassName = HelperInjectionTest.getPackage().getName() + '.HelperClass' + HelperInjector injector = new HelperInjector("test", [helperClassName], [], helpersSourceLoader) + AtomicReference emptyLoader = new AtomicReference<>(new URLClassLoader(new URL[0], (ClassLoader) null)) + + when: + emptyLoader.get().loadClass(helperClassName) + then: + thrown ClassNotFoundException + + when: + injector.transform(null, null, emptyLoader.get(), null) + emptyLoader.get().loadClass(helperClassName) + then: + isClassLoaded(helperClassName, emptyLoader.get()) + // injecting into emptyLoader should not cause helper class to be load in the helper source classloader + !isClassLoaded(helperClassName, helpersSourceLoader) + + when: "references to emptyLoader are gone" + emptyLoader.get().close() // cleanup + def ref = new WeakReference(emptyLoader.get()) + emptyLoader.set(null) + + awaitGc(ref) + + then: "HelperInjector doesn't prevent it from being collected" + null == ref.get() + } + + def "helpers injected on bootstrap classloader"() { + setup: + ByteBuddyAgent.install() + AgentInstaller.installBytebuddyAgent(ByteBuddyAgent.getInstrumentation()) + String helperClassName = HelperInjectionTest.getPackage().getName() + '.HelperClass' + HelperInjector injector = new HelperInjector("test", [helperClassName], [], this.class.classLoader) + URLClassLoader bootstrapChild = new URLClassLoader(new URL[0], (ClassLoader) null) + + when: + bootstrapChild.loadClass(helperClassName) + then: + thrown ClassNotFoundException + + when: + injector.transform(null, null, BOOTSTRAP_CLASSLOADER, null) + Class helperClass = bootstrapChild.loadClass(helperClassName) + then: + helperClass.getClassLoader() == BOOTSTRAP_CLASSLOADER + } + + def "check hard references on class injection"() { + setup: + String helperClassName = HelperInjectionTest.getPackage().getName() + '.HelperClass' + + // Copied from HelperInjector: + ClassFileLocator locator = + ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader()) + byte[] classBytes = locator.locate(helperClassName).resolve() + TypeDescription typeDesc = + new TypeDescription.Latent( + helperClassName, 0, null, Collections. emptyList()) + + AtomicReference emptyLoader = new AtomicReference<>(new URLClassLoader(new URL[0], (ClassLoader) null)) + AtomicReference injector = new AtomicReference<>(new ClassInjector.UsingReflection(emptyLoader.get())) + injector.get().inject([(typeDesc): classBytes]) + + when: + def injectorRef = new WeakReference(injector.get()) + injector.set(null) + + awaitGc(injectorRef) + + then: + null == injectorRef.get() + + when: + def loaderRef = new WeakReference(emptyLoader.get()) + emptyLoader.set(null) + + awaitGc(loaderRef) + + then: + null == loaderRef.get() + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/ResourceLocatingTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/ResourceLocatingTest.groovy new file mode 100644 index 000000000..8bd705e82 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/ResourceLocatingTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.test + +import io.opentelemetry.javaagent.tooling.bytebuddy.AgentLocationStrategy +import java.util.concurrent.atomic.AtomicReference +import net.bytebuddy.agent.builder.AgentBuilder +import spock.lang.Shared +import spock.lang.Specification + +class ResourceLocatingTest extends Specification { + @Shared + def lastLookup = new AtomicReference() + @Shared + def childLoader = new ClassLoader(this.getClass().getClassLoader()) { + @Override + URL getResource(String name) { + lastLookup.set(name) + // do not delegate resource lookup + return findResource(name) + } + } + + def cleanup() { + lastLookup.set(null) + } + + def "finds resources from parent classloader"() { + expect: + locator.locate("java/lang/Object").isResolved() == usesProvidedClassloader + // lastLookup verifies that the given classloader is only used when expected + lastLookup.get() == usesProvidedClassloader ? null : "java/lang/Object.class" + + and: + !locator.locate("java/lang/InvalidClass").isResolved() + lastLookup.get() == "java/lang/InvalidClass.class" + + where: + locator | usesProvidedClassloader + new AgentLocationStrategy().classFileLocator(childLoader, null) | true + AgentBuilder.LocationStrategy.ForClassLoader.STRONG.classFileLocator(childLoader, null) | false + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessorTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessorTest.groovy new file mode 100644 index 000000000..704179c4a --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessorTest.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling + +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.context.Context +import io.opentelemetry.sdk.trace.ReadWriteSpan +import spock.lang.Specification + +class AddThreadDetailsSpanProcessorTest extends Specification { + def span = Mock(ReadWriteSpan) + + def processor = new AddThreadDetailsSpanProcessor() + + def "should require onStart call"() { + expect: + processor.isStartRequired() + } + + def "should set thread attributes on span start"() { + given: + def currentThreadName = Thread.currentThread().name + def currentThreadId = Thread.currentThread().id + + when: + processor.onStart(Context.root(), span) + + then: + 1 * span.setAttribute(SemanticAttributes.THREAD_ID, currentThreadId) + 1 * span.setAttribute(SemanticAttributes.THREAD_NAME, currentThreadName) + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/CacheProviderTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/CacheProviderTest.groovy new file mode 100644 index 000000000..0f58a7874 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/CacheProviderTest.groovy @@ -0,0 +1,181 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling + +import io.opentelemetry.javaagent.tooling.bytebuddy.AgentCachingPoolStrategy +import java.lang.ref.WeakReference +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.dynamic.ClassFileLocator +import net.bytebuddy.pool.TypePool +import spock.lang.Specification + +class CacheProviderTest extends Specification { + def "key bootstrap equivalence"() { + // def loader = null + def loaderHash = AgentCachingPoolStrategy.BOOTSTRAP_HASH + def loaderRef = null + + def key1 = new AgentCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + def key2 = new AgentCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + + expect: + key1.hashCode() == key2.hashCode() + key1.equals(key2) + } + + def "key same ref equivalence"() { + setup: + def loader = newClassLoader() + def loaderHash = loader.hashCode() + def loaderRef = new WeakReference(loader) + + def key1 = new AgentCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + def key2 = new AgentCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + + expect: + key1.hashCode() == key2.hashCode() + key1.equals(key2) + } + + def "key different ref equivalence"() { + setup: + def loader = newClassLoader() + def loaderHash = loader.hashCode() + def loaderRef1 = new WeakReference(loader) + def loaderRef2 = new WeakReference(loader) + + def key1 = new AgentCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef1, "foo") + def key2 = new AgentCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef2, "foo") + + expect: + loaderRef1 != loaderRef2 + + key1.hashCode() == key2.hashCode() + key1.equals(key2) + } + + def "key mismatch -- same loader - diff name"() { + setup: + def loader = newClassLoader() + def loaderHash = loader.hashCode() + def loaderRef = new WeakReference(loader) + def fooKey = new AgentCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + def barKey = new AgentCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "bar") + + expect: + // not strictly guaranteed -- but important for performance + fooKey.hashCode() != barKey.hashCode() + !fooKey.equals(barKey) + } + + def "key mismatch -- same name - diff loader"() { + setup: + def loader1 = newClassLoader() + def loader1Hash = loader1.hashCode() + def loaderRef1 = new WeakReference(loader1) + + def loader2 = newClassLoader() + def loader2Hash = loader2.hashCode() + def loaderRef2 = new WeakReference(loader2) + + def fooKey1 = new AgentCachingPoolStrategy.TypeCacheKey(loader1Hash, loaderRef1, "foo") + def fooKey2 = new AgentCachingPoolStrategy.TypeCacheKey(loader2Hash, loaderRef2, "foo") + + expect: + // not strictly guaranteed -- but important for performance + fooKey1.hashCode() != fooKey2.hashCode() + !fooKey1.equals(fooKey2) + } + + def "test basic caching"() { + setup: + def poolStrat = new AgentCachingPoolStrategy() + + def loader = newClassLoader() + def loaderHash = loader.hashCode() + def loaderRef = new WeakReference(loader) + + def cacheProvider = poolStrat.createCacheProvider(loaderHash, loaderRef) + + when: + cacheProvider.register("foo", new TypePool.Resolution.Simple(TypeDescription.VOID)) + + then: + // not strictly guaranteed, but fine for this test + cacheProvider.find("foo") != null + } + + def "test loader equivalence"() { + setup: + def poolStrat = new AgentCachingPoolStrategy() + + def loader1 = newClassLoader() + def loaderHash1 = loader1.hashCode() + def loaderRef1A = new WeakReference(loader1) + def loaderRef1B = new WeakReference(loader1) + + def cacheProvider1A = poolStrat.createCacheProvider(loaderHash1, loaderRef1A) + def cacheProvider1B = poolStrat.createCacheProvider(loaderHash1, loaderRef1B) + + when: + cacheProvider1A.register("foo", newVoid()) + + then: + // not strictly guaranteed, but fine for this test + cacheProvider1A.find("foo") != null + cacheProvider1B.find("foo") != null + + cacheProvider1A.find("foo").is(cacheProvider1B.find("foo")) + } + + def "test loader separation"() { + setup: + def poolStrat = new AgentCachingPoolStrategy() + + def loader1 = newClassLoader() + def loaderHash1 = loader1.hashCode() + def loaderRef1 = new WeakReference(loader1) + + def loader2 = newClassLoader() + def loaderHash2 = loader2.hashCode() + def loaderRef2 = new WeakReference(loader2) + + def cacheProvider1 = poolStrat.createCacheProvider(loaderHash1, loaderRef1) + def cacheProvider2 = poolStrat.createCacheProvider(loaderHash2, loaderRef2) + + when: + cacheProvider1.register("foo", newVoid()) + cacheProvider2.register("foo", newVoid()) + + then: + // not strictly guaranteed, but fine for this test + cacheProvider1.find("foo") != null + cacheProvider2.find("foo") != null + + !cacheProvider1.find("foo").is(cacheProvider2.find("foo")) + } + + static newVoid() { + return new TypePool.Resolution.Simple(TypeDescription.VOID) + } + + static newClassLoader() { + return new URLClassLoader([] as URL[], (ClassLoader) null) + } + + static newLocator() { + return new ClassFileLocator() { + @Override + ClassFileLocator.Resolution locate(String name) throws IOException { + return null + } + + @Override + void close() throws IOException { + } + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/ExporterClassLoaderTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/ExporterClassLoaderTest.groovy new file mode 100644 index 000000000..4866ae899 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/ExporterClassLoaderTest.groovy @@ -0,0 +1,253 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling + +import groovy.transform.CompileStatic +import io.opentelemetry.javaagent.spi.exporter.MetricExporterFactory +import io.opentelemetry.javaagent.spi.exporter.SpanExporterFactory +import io.opentelemetry.sdk.metrics.export.MetricExporter +import io.opentelemetry.sdk.trace.export.SpanExporter +import java.nio.charset.StandardCharsets +import java.util.jar.Attributes +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream +import java.util.jar.Manifest +import spock.lang.Specification + +class ExporterClassLoaderTest extends Specification { + + // Verifies https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/542 + def "does not look in parent classloader for metric exporters"() { + setup: + def parentClassloader = new ParentClassLoader([createJarWithClasses(MetricExporterFactoryParent)] as URL[]) + def childClassloader = new ExporterClassLoader(createJarWithClasses(MetricExporterFactoryChild), parentClassloader) + + when: + ServiceLoader serviceLoader = ServiceLoader.load(MetricExporterFactory, childClassloader) + + then: + serviceLoader.size() == 1 + + and: + childClassloader.manifest != null + + when: + MetricExporterFactory instance = serviceLoader.iterator().next() + Class clazz = instance.getClass() + + then: + clazz.getClassLoader() == childClassloader + } + + def "does not look in parent classloader for span exporters"() { + setup: + def parentClassloader = new ParentClassLoader([createJarWithClasses(SpanExporterFactoryParent)] as URL[]) + def childClassloader = new ExporterClassLoader(createJarWithClasses(SpanExporterFactoryChild), parentClassloader) + + when: + ServiceLoader serviceLoader = ServiceLoader.load(SpanExporterFactory, childClassloader) + + then: + serviceLoader.size() == 1 + + and: + childClassloader.manifest != null + + when: + SpanExporterFactory instance = serviceLoader.iterator().next() + Class clazz = instance.getClass() + + then: + clazz.getClassLoader() == childClassloader + } + + // Verifies that loading of exporter jar succeeds when there is a space in path to exporter jar + def "load jar with space in path"() { + setup: + def parentClassloader = new ParentClassLoader() + // " .jar" is used to make path to jar contain a space + def childClassloader = new ExporterClassLoader(createJarWithClasses(" .jar", MetricExporterFactoryChild), parentClassloader) + + when: + ServiceLoader serviceLoader = ServiceLoader.load(MetricExporterFactory, childClassloader) + + then: + serviceLoader.size() == 1 + + and: + childClassloader.manifest != null + + when: + MetricExporterFactory instance = serviceLoader.iterator().next() + Class clazz = instance.getClass() + + then: + clazz.getClassLoader() == childClassloader + + and: + clazz.getPackage().getImplementationVersion() == "test-implementation-version" + } + + static class MetricExporterFactoryParent implements MetricExporterFactory { + + @Override + MetricExporter fromConfig(Properties config) { + return null + } + + @Override + Set getNames() { + return null + } + } + + static class MetricExporterFactoryChild implements MetricExporterFactory { + + @Override + MetricExporter fromConfig(Properties config) { + return null + } + + @Override + Set getNames() { + return null + } + } + + static class SpanExporterFactoryParent implements SpanExporterFactory { + + @Override + SpanExporter fromConfig(Properties config) { + return null + } + + @Override + Set getNames() { + return null + } + } + + static class SpanExporterFactoryChild implements SpanExporterFactory { + + @Override + SpanExporter fromConfig(Properties config) { + return null + } + + @Override + Set getNames() { + return null + } + } + + static URL createJarWithClasses(final Class... classes) { + createJarWithClasses(".jar", classes) + } + + static URL createJarWithClasses(final String suffix, final Class... classes) + throws IOException { + File tmpJar = File.createTempFile(UUID.randomUUID().toString() + "-", suffix) + tmpJar.deleteOnExit() + + JarOutputStream target = new JarOutputStream(new FileOutputStream(tmpJar)) + for (Class clazz : classes) { + addToJar(clazz, clazz.getInterfaces()[0], target) + } + + Manifest manifest = new Manifest() + Attributes attributes = manifest.getMainAttributes() + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0") + attributes.put(Attributes.Name.SPECIFICATION_TITLE, "test-specification-title") + attributes.put(Attributes.Name.SPECIFICATION_VERSION, "test-specification-version") + attributes.put(Attributes.Name.SPECIFICATION_VENDOR, "test-specification-vendor") + attributes.put(Attributes.Name.IMPLEMENTATION_TITLE, "test-implementation-title") + attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, "test-implementation-version") + attributes.put(Attributes.Name.IMPLEMENTATION_VENDOR, "test-implementation-vendor") + + JarEntry manifestEntry = new JarEntry(JarFile.MANIFEST_NAME) + target.putNextEntry(manifestEntry) + manifest.write(target) + target.closeEntry() + + target.close() + + return tmpJar.toURI().toURL() + } + + //This is mostly copy-pasted from IntegrationTestUtils, but we need to save service files as well + private static void addToJar(final Class clazz, final Class serviceInterface, final JarOutputStream jarOutputStream) + throws IOException { + String resourceName = getResourceName(clazz.getName()) + + ClassLoader loader = clazz.getClassLoader() + if (null == loader) { + // bootstrap resources can be fetched through the system loader + loader = ClassLoader.getSystemClassLoader() + } + + InputStream inputStream = null + try { + JarEntry entry = new JarEntry(resourceName) + jarOutputStream.putNextEntry(entry) + inputStream = loader.getResourceAsStream(resourceName) + + byte[] buffer = new byte[1024] + while (true) { + int count = inputStream.read(buffer) + if (count == -1) { + break + } + jarOutputStream.write(buffer, 0, count) + } + jarOutputStream.closeEntry() + + JarEntry serviceEntry = new JarEntry("META-INF/services/" + serviceInterface.getName()) + jarOutputStream.putNextEntry(serviceEntry) + jarOutputStream.write(clazz.getName().getBytes(StandardCharsets.UTF_8)) + jarOutputStream.closeEntry() + } finally { + if (inputStream != null) { + inputStream.close() + } + } + } + + /** com.foo.Bar -> com/foo/Bar.class */ + private static String getResourceName(final String className) { + return className.replace('.', '/') + ".class" + } + + @CompileStatic + private static class ParentClassLoader extends URLClassLoader { + + ParentClassLoader() { + super() + } + + ParentClassLoader(URL[] urls) { + super(urls) + } + + @Override + Package getPackage(String name) { + // ExporterClassLoader uses getPackage to check whether package has already been + // defined. As getPackage also searches packages from parent class loader we return + // null here to ensure that package is defined in ExporterClassLoader. + null + } + + @Override + Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + // test classes are available in system class loader filter them so that + // they would be loaded by ExporterClassLoader + if (name.startsWith(ExporterClassLoaderTest.getName())) { + throw new ClassNotFoundException(name) + } + return super.loadClass(name, resolve) + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/UtilsTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/UtilsTest.groovy new file mode 100644 index 000000000..3107bb0e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/UtilsTest.groovy @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling + +import spock.lang.Specification + +class UtilsTest extends Specification { + + def "getResourceName() adds suffix and converts dots to slashes"() { + setup: + def result = Utils.getResourceName("com.example.Something") + expect: + result == "com/example/Something.class" + } + + def "getClassName() converts slashes to dots"() { + setup: + def result = Utils.getClassName("com/example/Something") + expect: + result == "com.example.Something" + } + + def "getInternalName() converts slashes to dots"() { + setup: + def result = Utils.getInternalName(UtilsTest) + expect: + result == "io/opentelemetry/javaagent/tooling/UtilsTest" + } + + def "convertToInnerClassName() makes dollar into dots"() { + setup: + def result = Utils.convertToInnerClassName("com/example/MyOuter.MyInner") + expect: + result == 'com/example/MyOuter$MyInner' + } + +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ExtendsClassMatcherTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ExtendsClassMatcherTest.groovy new file mode 100644 index 000000000..2a9ea83ad --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ExtendsClassMatcherTest.groovy @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass +import static net.bytebuddy.matcher.ElementMatchers.named + +import io.opentelemetry.javaagent.tooling.AgentTooling +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.A +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.B +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.F +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.G +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.jar.asm.Opcodes +import spock.lang.Shared +import spock.lang.Specification + +class ExtendsClassMatcherTest extends Specification { + @Shared + def typePool = + AgentTooling.poolStrategy() + .typePool(AgentTooling.locationStrategy().classFileLocator(this.class.classLoader, null), this.class.classLoader) + + def "test matcher #matcherClass.simpleName -> #type.simpleName"() { + expect: + extendsClass(matcher).matches(argument) == result + + where: + matcherClass | type | result + A | B | false + A | F | false + G | F | false + F | F | true + F | G | true + + matcher = named(matcherClass.name) + argument = typePool.describe(type.name).resolve() + } + + def "test traversal exceptions"() { + setup: + def type = Mock(TypeDescription) + def typeGeneric = Mock(TypeDescription.Generic) + def matcher = extendsClass(named(Object.name)) + + when: + def result = matcher.matches(type) + + then: + !result // default to false + noExceptionThrown() + 1 * type.getModifiers() >> Opcodes.ACC_ABSTRACT + 1 * type.asGenericType() >> typeGeneric + 1 * type.getTypeName() >> "type-name" + 1 * typeGeneric.asErasure() >> { throw new Exception("asErasure exception") } + 1 * typeGeneric.getTypeName() >> "typeGeneric-name" + 1 * type.getSuperClass() >> { throw new Exception("getSuperClass exception") } + 0 * _ + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/HasInterfaceMatcherTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/HasInterfaceMatcherTest.groovy new file mode 100644 index 000000000..ec80c3388 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/HasInterfaceMatcherTest.groovy @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface +import static net.bytebuddy.matcher.ElementMatchers.named + +import io.opentelemetry.javaagent.tooling.AgentTooling +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.A +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.B +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.E +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.F +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.G +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.description.type.TypeList +import spock.lang.Shared +import spock.lang.Specification + +class HasInterfaceMatcherTest extends Specification { + @Shared + def typePool = + AgentTooling.poolStrategy() + .typePool(AgentTooling.locationStrategy().classFileLocator(this.class.classLoader, null), this.class.classLoader) + + def "test matcher #matcherClass.simpleName -> #type.simpleName"() { + expect: + implementsInterface(matcher).matches(argument) == result + + where: + matcherClass | type | result + A | A | true + A | B | true + B | A | false + A | E | true + A | F | true + A | G | true + F | A | false + F | F | false + F | G | false + + matcher = named(matcherClass.name) + argument = typePool.describe(type.name).resolve() + } + + def "test traversal exceptions"() { + setup: + def type = Mock(TypeDescription) + def typeGeneric = Mock(TypeDescription.Generic) + def matcher = implementsInterface(named(Object.name)) + def interfaces = Mock(TypeList.Generic) + def it = new ThrowOnFirstElement() + + when: + def result = matcher.matches(type) + + then: + !result // default to false + noExceptionThrown() + 1 * type.isInterface() >> true + 1 * type.asGenericType() >> typeGeneric + 1 * typeGeneric.asErasure() >> { throw new Exception("asErasure exception") } + 1 * typeGeneric.getTypeName() >> "typeGeneric-name" + 1 * type.getInterfaces() >> interfaces + 1 * interfaces.iterator() >> it + 1 * type.getSuperClass() >> { throw new Exception("getSuperClass exception") } + 2 * type.getTypeName() >> "type-name" + 0 * _ + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/HasSuperMethodMatcherTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/HasSuperMethodMatcherTest.groovy new file mode 100644 index 000000000..75ab0f94b --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/HasSuperMethodMatcherTest.groovy @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperMethod +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith +import static net.bytebuddy.matcher.ElementMatchers.none + +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.* +import net.bytebuddy.description.method.MethodDescription +import spock.lang.Specification + +class HasSuperMethodMatcherTest extends Specification { + + def "test matcher #type.simpleName #method"() { + expect: + hasSuperMethod(isAnnotatedWith(Trace)).matches(argument) == result + + where: + type | method | result + A | "a" | false + B | "b" | true + C | "c" | false + F | "f" | true + G | "g" | false + TracedClass | "a" | true + UntracedClass | "a" | false + UntracedClass | "b" | true + + argument = new MethodDescription.ForLoadedMethod(type.getDeclaredMethod(method)) + } + + def "test constructor never matches"() { + setup: + def method = Mock(MethodDescription) + def matcher = hasSuperMethod(none()) + + when: + def result = matcher.matches(method) + + then: + !result + 1 * method.isConstructor() >> true + 0 * _ + } + + def "test traversal exceptions"() { + setup: + def method = Mock(MethodDescription) + def matcher = hasSuperMethod(none()) + def sigToken = new MethodDescription.ForLoadedMethod(A.getDeclaredMethod("a")).asSignatureToken() + + when: + def result = matcher.matches(method) + + then: + !result // default to false + 1 * method.isConstructor() >> false + 1 * method.asSignatureToken() >> sigToken + 1 * method.getDeclaringType() >> null + 0 * _ + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ImplementsInterfaceMatcherTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ImplementsInterfaceMatcherTest.groovy new file mode 100644 index 000000000..d73269869 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ImplementsInterfaceMatcherTest.groovy @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface +import static net.bytebuddy.matcher.ElementMatchers.named + +import io.opentelemetry.javaagent.tooling.AgentTooling +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.A +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.B +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.E +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.F +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.G +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.description.type.TypeList +import spock.lang.Shared +import spock.lang.Specification + +class ImplementsInterfaceMatcherTest extends Specification { + @Shared + def typePool = + AgentTooling.poolStrategy() + .typePool(AgentTooling.locationStrategy().classFileLocator(this.class.classLoader, null), this.class.classLoader) + + def "test matcher #matcherClass.simpleName -> #type.simpleName"() { + expect: + implementsInterface(matcher).matches(argument) == result + + where: + matcherClass | type | result + A | A | true + A | B | true + B | A | false + A | E | true + A | F | true + A | G | true + F | A | false + F | F | false + F | G | false + + matcher = named(matcherClass.name) + argument = typePool.describe(type.name).resolve() + } + + def "test exception getting interfaces"() { + setup: + def type = Mock(TypeDescription) + def typeGeneric = Mock(TypeDescription.Generic) + def matcher = implementsInterface(named(Object.name)) + + when: + def result = matcher.matches(type) + + then: + !result // default to false + noExceptionThrown() + 1 * type.isInterface() >> true + 1 * type.asGenericType() >> typeGeneric + 1 * typeGeneric.asErasure() >> { throw new Exception("asErasure exception") } + 1 * typeGeneric.getTypeName() >> "typeGeneric-name" + 1 * type.getInterfaces() >> { throw new Exception("getInterfaces exception") } + 1 * type.getSuperClass() >> { throw new Exception("getSuperClass exception") } + 2 * type.getTypeName() >> "type-name" + 0 * _ + } + + def "test traversal exceptions"() { + setup: + def type = Mock(TypeDescription) + def typeGeneric = Mock(TypeDescription.Generic) + def matcher = implementsInterface(named(Object.name)) + def interfaces = Mock(TypeList.Generic) + def it = new ThrowOnFirstElement() + + when: + def result = matcher.matches(type) + + then: + !result // default to false + noExceptionThrown() + 1 * type.isInterface() >> true + 1 * type.asGenericType() >> typeGeneric + 1 * typeGeneric.asErasure() >> { throw new Exception("asErasure exception") } + 1 * typeGeneric.getTypeName() >> "typeGeneric-name" + 1 * type.getInterfaces() >> interfaces + 1 * interfaces.iterator() >> it + 2 * type.getTypeName() >> "type-name" + 1 * type.getSuperClass() >> { throw new Exception("getSuperClass exception") } + 0 * _ + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/SafeHasSuperTypeMatcherTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/SafeHasSuperTypeMatcherTest.groovy new file mode 100644 index 000000000..bb0c7388c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/SafeHasSuperTypeMatcherTest.groovy @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.safeHasSuperType +import static net.bytebuddy.matcher.ElementMatchers.named + +import io.opentelemetry.javaagent.tooling.AgentTooling +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.A +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.B +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.E +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.F +import io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses.G +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.description.type.TypeList +import spock.lang.Shared +import spock.lang.Specification + +class SafeHasSuperTypeMatcherTest extends Specification { + @Shared + def typePool = + AgentTooling.poolStrategy() + .typePool(AgentTooling.locationStrategy().classFileLocator(this.class.classLoader, null), this.class.classLoader) + + def "test matcher #matcherClass.simpleName -> #type.simpleName"() { + expect: + safeHasSuperType(matcher).matches(argument) == result + + where: + matcherClass | type | result + A | A | true + A | B | true + B | A | false + A | E | true + A | F | true + B | G | true + F | A | false + F | F | true + F | G | true + + matcher = named(matcherClass.name) + argument = typePool.describe(type.name).resolve() + } + + def "test exception getting interfaces"() { + setup: + def type = Mock(TypeDescription) + def typeGeneric = Mock(TypeDescription.Generic) + def matcher = safeHasSuperType(named(Object.name)) + + when: + def result = matcher.matches(type) + + then: + !result // default to false + noExceptionThrown() + 1 * type.asGenericType() >> typeGeneric + 1 * typeGeneric.asErasure() >> { throw new Exception("asErasure exception") } + 1 * typeGeneric.getTypeName() >> "typeGeneric-name" + 1 * type.getInterfaces() >> { throw new Exception("getInterfaces exception") } + 1 * type.getSuperClass() >> { throw new Exception("getSuperClass exception") } + 2 * type.getTypeName() >> "type-name" + 0 * _ + } + + def "test traversal exceptions"() { + setup: + def type = Mock(TypeDescription) + def typeGeneric = Mock(TypeDescription.Generic) + def matcher = safeHasSuperType(named(Object.name)) + def interfaces = Mock(TypeList.Generic) + def it = new ThrowOnFirstElement() + + when: + def result = matcher.matches(type) + + then: + !result // default to false + noExceptionThrown() + 1 * type.getInterfaces() >> interfaces + 1 * interfaces.iterator() >> it + 1 * type.asGenericType() >> typeGeneric + 1 * typeGeneric.asErasure() >> { throw new Exception("asErasure exception") } + 1 * typeGeneric.getTypeName() >> "typeGeneric-name" + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/SafeMatcherTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/SafeMatcherTest.groovy new file mode 100644 index 000000000..980655a61 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/SafeMatcherTest.groovy @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.failSafe + +import net.bytebuddy.matcher.ElementMatcher +import spock.lang.Specification + +class SafeMatcherTest extends Specification { + + def mockMatcher = Mock(ElementMatcher) + + def "test matcher"() { + setup: + def matcher = failSafe(mockMatcher, "test") + + when: + def result = matcher.matches(new Object()) + + then: + 1 * mockMatcher.matches(_) >> match + result == match + + where: + match << [true, false] + } + + def "test matcher exception"() { + setup: + def matcher = failSafe(mockMatcher, "test") + + when: + def result = matcher.matches(new Object()) + + then: + 1 * mockMatcher.matches(_) >> { throw new Exception("matcher exception") } + 0 * _ + noExceptionThrown() + !result // default to false + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ThrowOnFirstElement.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ThrowOnFirstElement.groovy new file mode 100644 index 000000000..f9012bb8d --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ThrowOnFirstElement.groovy @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher + +class ThrowOnFirstElement implements Iterator { + + int i = 0 + + @Override + boolean hasNext() { + return i++ < 1 + } + + @Override + Object next() { + throw new Exception("iteration exception") + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/ConfigInitializerTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/ConfigInitializerTest.groovy new file mode 100644 index 000000000..10a54d9b0 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/ConfigInitializerTest.groovy @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.config + + +import org.junit.Rule +import org.junit.contrib.java.lang.system.EnvironmentVariables +import org.junit.contrib.java.lang.system.RestoreSystemProperties +import spock.lang.Specification + +class ConfigInitializerTest extends Specification { + @Rule + public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties() + @Rule + public final EnvironmentVariables environmentVariables = new EnvironmentVariables() + + def "should use SPI properties"() { + given: + def spiConfiguration = new Properties() + spiConfiguration.put("property1", "spi-1") + spiConfiguration.put("property2", "spi-2") + spiConfiguration.put("property3", "spi-3") + spiConfiguration.put("property4", "spi-4") + + when: + def config = ConfigInitializer.create(spiConfiguration, new Properties()) + + then: + config.getProperty("property1") == "spi-1" + config.getProperty("property2") == "spi-2" + config.getProperty("property3") == "spi-3" + config.getProperty("property4") == "spi-4" + } + + def "should use configuration file properties (takes precedence over SPI)"() { + given: + def spiConfiguration = new Properties() + spiConfiguration.put("property1", "spi-1") + spiConfiguration.put("property2", "spi-2") + spiConfiguration.put("property3", "spi-3") + spiConfiguration.put("property4", "spi-4") + + def configurationFile = new Properties() + configurationFile.put("property1", "cf-1") + configurationFile.put("property2", "cf-2") + configurationFile.put("property3", "cf-3") + + when: + def config = ConfigInitializer.create(spiConfiguration, configurationFile) + + then: + config.getProperty("property1") == "cf-1" + config.getProperty("property2") == "cf-2" + config.getProperty("property3") == "cf-3" + config.getProperty("property4") == "spi-4" + } + + def "should use environment variables (takes precedence over configuration file)"() { + given: + def spiConfiguration = new Properties() + spiConfiguration.put("property1", "spi-1") + spiConfiguration.put("property2", "spi-2") + spiConfiguration.put("property3", "spi-3") + spiConfiguration.put("property4", "spi-4") + + def configurationFile = new Properties() + configurationFile.put("property1", "cf-1") + configurationFile.put("property2", "cf-2") + configurationFile.put("property3", "cf-3") + + environmentVariables.set("property1", "env-1") + environmentVariables.set("property2", "env-2") + + when: + def config = ConfigInitializer.create(spiConfiguration, configurationFile) + + then: + config.getProperty("property1") == "env-1" + config.getProperty("property2") == "env-2" + config.getProperty("property3") == "cf-3" + config.getProperty("property4") == "spi-4" + } + + def "should use system properties (takes precedence over environment variables)"() { + given: + def spiConfiguration = new Properties() + spiConfiguration.put("property1", "spi-1") + spiConfiguration.put("property2", "spi-2") + spiConfiguration.put("property3", "spi-3") + spiConfiguration.put("property4", "spi-4") + + def configurationFile = new Properties() + configurationFile.put("property1", "cf-1") + configurationFile.put("property2", "cf-2") + configurationFile.put("property3", "cf-3") + + environmentVariables.set("property1", "env-1") + environmentVariables.set("property2", "env-2") + + System.setProperty("property1", "sp-1") + + when: + def config = ConfigInitializer.create(spiConfiguration, configurationFile) + + then: + config.getProperty("property1") == "sp-1" + config.getProperty("property2") == "env-2" + config.getProperty("property3") == "cf-3" + config.getProperty("property4") == "spi-4" + } + + def "should normalize property names"() { + given: + def spiConfiguration = new Properties() + spiConfiguration.put("otel.some-property.from-spi", "value") + + def configurationFile = new Properties() + configurationFile.put("otel.some-property.from-file", "value") + + environmentVariables.set("OTEL_SOME_ENV_VAR", "value") + + System.setProperty("otel.some-system-property", "value") + + when: + def config = ConfigInitializer.create(spiConfiguration, configurationFile) + + then: + config.getProperty("otel.some-property.from-spi") == "value" + config.getProperty("otel.some-property.from-file") == "value" + config.getProperty("otel.some-env-var") == "value" + config.getProperty("otel.some-system-property") == "value" + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParserTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParserTest.groovy new file mode 100644 index 000000000..3f8cbf4c9 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParserTest.groovy @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.config + + +import spock.lang.Specification + +class MethodsConfigurationParserTest extends Specification { + + def "test configuration #value"() { + expect: + MethodsConfigurationParser.parse(value) == expected + + where: + value | expected + null | [:] + " " | [:] + "some.package.ClassName" | [:] + "some.package.ClassName[ , ]" | [:] + "some.package.ClassName[ , method]" | [:] + "some.package.Class\$Name[ method , ]" | ["some.package.Class\$Name": ["method"].toSet()] + "ClassName[ method1,]" | ["ClassName": ["method1"].toSet()] + "ClassName[method1 , method2]" | ["ClassName": ["method1", "method2"].toSet()] + "Class\$1[method1 ] ; Class\$2[ method2];" | ["Class\$1": ["method1"].toSet(), "Class\$2": ["method2"].toSet()] + "Duplicate[method1] ; Duplicate[method2] ;Duplicate[method3];" | ["Duplicate": ["method3"].toSet()] + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/muzzle/InstrumentationClassPredicateTest.groovy b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/muzzle/InstrumentationClassPredicateTest.groovy new file mode 100644 index 000000000..8a6adb7ab --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/muzzle/InstrumentationClassPredicateTest.groovy @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.muzzle + +import spock.lang.Specification +import spock.lang.Unroll + +class InstrumentationClassPredicateTest extends Specification { + @Unroll + def "should collect references for #desc"() { + setup: + def predicate = new InstrumentationClassPredicate({ it.startsWith("com.example.instrumentation.library") }) + + expect: + predicate.isInstrumentationClass(className) + + where: + desc | className + "javaagent instrumentation class" | "io.opentelemetry.javaagent.instrumentation.some_instrumentation.Advice" + "library instrumentation class" | "io.opentelemetry.instrumentation.LibraryClass" + "additional library instrumentation class" | "com.example.instrumentation.library.ThirdPartyExternalInstrumentation" + } + + @Unroll + def "should not collect references for #desc"() { + setup: + def predicate = new InstrumentationClassPredicate({ false }) + + expect: + !predicate.isInstrumentationClass(className) + + where: + desc | className + "Java SDK class" | "java.util.ArrayList" + "javaagent-tooling class" | "io.opentelemetry.javaagent.tooling.Constants" + "instrumentation-api class" | "io.opentelemetry.instrumentation.api.InstrumentationVersion" + "javaagent-api class" | "io.opentelemetry.javaagent.instrumentation.api.ContextStore" + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/test/BadAdvice.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/test/BadAdvice.java new file mode 100644 index 000000000..32b443350 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/test/BadAdvice.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.test; + +import net.bytebuddy.asm.Advice; + +public class BadAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void throwAnException(@Advice.Return(readOnly = false) boolean returnVal) { + returnVal = true; + throw new IllegalStateException("Test Exception"); + } + + public static class NoOpAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void doNothing() { + System.currentTimeMillis(); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/test/HelperClass.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/test/HelperClass.java new file mode 100644 index 000000000..f926da318 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/test/HelperClass.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.test; + +/** Used by {@link HelperInjectionTest}. */ +class HelperClass {} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/A.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/A.java new file mode 100644 index 000000000..3ff6a3854 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/A.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface A { + void a(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/B.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/B.java new file mode 100644 index 000000000..17cd17e7e --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/B.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface B extends A { + @Trace + void b(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/C.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/C.java new file mode 100644 index 000000000..0f033f0f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/C.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface C extends A, B { + void c(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/D.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/D.java new file mode 100644 index 000000000..20de394cb --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/D.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface D extends A, B, C { + @Trace + void d(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/E.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/E.java new file mode 100644 index 000000000..4a66756f2 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/E.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public interface E extends B, C, D { + void e(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/F.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/F.java new file mode 100644 index 000000000..9e7e0f901 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/F.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public abstract class F implements E { + @Trace + public abstract void f(); +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/G.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/G.java new file mode 100644 index 000000000..650769c0c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/G.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public abstract class G extends F { + public void g() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/Trace.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/Trace.java new file mode 100644 index 000000000..1b0af6d56 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/Trace.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +public @interface Trace {} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/TracedClass.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/TracedClass.java new file mode 100644 index 000000000..40d9e7981 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/TracedClass.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +public class TracedClass extends UntracedClass { + @Trace + @Override + public void g() {} + + @Trace + @Override + public void f() {} + + @Trace + @Override + public void e() {} + + @Trace + @Override + public void d() {} + + @Trace + @Override + public void c() {} + + @Trace + @Override + public void b() {} + + @Trace + @Override + public void a() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/UntracedClass.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/UntracedClass.java new file mode 100644 index 000000000..2fd9de343 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/testclasses/UntracedClass.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.bytebuddy.matcher.testclasses; + +public class UntracedClass extends G { + @Override + public void g() {} + + @Override + public void f() {} + + @Override + public void e() {} + + @Override + public void d() {} + + @Override + public void c() {} + + @Override + public void b() {} + + @Override + public void a() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassesConfigurerTest.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassesConfigurerTest.java new file mode 100644 index 000000000..f652a370d --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassesConfigurerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import static io.opentelemetry.javaagent.tooling.ignore.UserExcludedClassesConfigurer.EXCLUDED_CLASSES_CONFIG; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.instrumentation.api.config.ConfigBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserExcludedClassesConfigurerTest { + @Mock IgnoredTypesBuilder builder; + + IgnoredTypesConfigurer underTest = new UserExcludedClassesConfigurer(); + + @Test + void shouldAddNothingToBuilderWhenPropertyIsEmpty() { + // when + underTest.configure(Config.create(emptyMap()), builder); + + // then + verifyNoInteractions(builder); + } + + @Test + void shouldIgnoreClassesAndPackages() { + // given + Config config = + new ConfigBuilder() + .readProperties( + singletonMap( + EXCLUDED_CLASSES_CONFIG, + "com.example.IgnoredClass,com.example.ignored.*,com.another_ignore")) + .build(); + + // when + underTest.configure(config, builder); + + // then + verify(builder).ignoreClass("com.example.IgnoredClass"); + verify(builder).ignoreClass("com.example.ignored."); + verify(builder).ignoreClass("com.another_ignore"); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/trie/TrieTest.java b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/trie/TrieTest.java new file mode 100644 index 000000000..a1660f4a5 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/trie/TrieTest.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore.trie; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class TrieTest { + @Test + void shouldMatchExactString() { + Trie trie = + Trie.newBuilder().put("abc", 0).put("abcd", 10).put("abcde", 20).build(); + + assertNull(trie.getOrNull("ab")); + assertEquals(0, trie.getOrNull("abc")); + assertEquals(10, trie.getOrNull("abcd")); + assertEquals(20, trie.getOrNull("abcde")); + } + + @Test + void shouldReturnLastMatchedValue() { + Trie trie = + Trie.newBuilder().put("abc", 0).put("abcde", 10).put("abcdfgh", 20).build(); + + assertNull(trie.getOrNull("ababababa")); + assertEquals(0, trie.getOrNull("abcd")); + assertEquals(10, trie.getOrNull("abcdefgh")); + assertEquals(20, trie.getOrNull("abcdfghjkl")); + } + + @Test + void shouldOverwritePreviousValue() { + Trie trie = Trie.newBuilder().put("abc", 0).put("abc", 12).build(); + + assertEquals(12, trie.getOrNull("abc")); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/README.md b/opentelemetry-java-instrumentation/javaagent/README.md new file mode 100644 index 000000000..99102af4c --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/README.md @@ -0,0 +1 @@ +# Java Agent for Auto Instrumentation diff --git a/opentelemetry-java-instrumentation/javaagent/gradle.properties b/opentelemetry-java-instrumentation/javaagent/gradle.properties new file mode 100644 index 000000000..45d64bec2 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/gradle.properties @@ -0,0 +1 @@ +otel.stable=true diff --git a/opentelemetry-java-instrumentation/javaagent/javaagent.gradle b/opentelemetry-java-instrumentation/javaagent/javaagent.gradle new file mode 100644 index 000000000..c73282e5f --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/javaagent.gradle @@ -0,0 +1,153 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.github.jk1.license.filter.LicenseBundleNormalizer +import com.github.jk1.license.render.InventoryMarkdownReportRenderer + +plugins { + id "otel.shadow-conventions" + id "com.github.jk1.dependency-license-report" version "1.16" +} + +description = 'OpenTelemetry Javaagent' + +group = 'io.opentelemetry.javaagent' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +configurations { + shadowInclude { + canBeResolved = true + canBeConsumed = false + } +} + +processResources { + from(rootProject.file("licenses")) { + into("META-INF/licenses") + } +} + +jar { + manifest { + attributes( + "Main-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Agent-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Premain-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Can-Redefine-Classes": true, + "Can-Retransform-Classes": true, + ) + } +} + +CopySpec isolateSpec(Collection projectsWithShadowJar) { + return copySpec { + from({ projectsWithShadowJar.tasks.shadowJar.collect { zipTree(it.archiveFile) } }) { + // important to keep prefix 'inst' short, as it is prefixed to lots of strings in runtime mem + into 'inst' + rename '(^.*)\\.class$', '$1.class' + // Rename LICENSE file since it clashes with license dir on non-case sensitive FSs (i.e. Mac) + rename '^LICENSE$', 'LICENSE.renamed' + } + } +} + +//Includes everything needed for OOTB experience +shadowJar { + def projectsWithShadowJar = [project(':instrumentation'), project(":javaagent-exporters")] + projectsWithShadowJar.each { + dependsOn("${it.path}:shadowJar") + } + with isolateSpec(projectsWithShadowJar) +} + +//Includes instrumentations, but not exporters +task lightShadow(type: ShadowJar) { + dependsOn ':instrumentation:shadowJar' + def projectsWithShadowJar = [project(':instrumentation')] + with isolateSpec(projectsWithShadowJar) +} + +publishing { + publications { + maven(MavenPublication) { + artifact lightShadow + } + } +} + +tasks.withType(ShadowJar).configureEach { + configurations = [project.configurations.shadowInclude] + + manifest { + inheritFrom project.tasks.jar.manifest + } +} + +configurations { + licenseReportDependencies +} + +dependencies { + testCompileOnly project(':javaagent-bootstrap') + testCompileOnly project(':javaagent-api') + + testImplementation "com.google.guava:guava" + + testImplementation 'io.opentracing.contrib.dropwizard:dropwizard-opentracing:0.2.2' + + shadowInclude project(path: ':javaagent-bootstrap') + + // We only have compileOnly dependencies on these to make sure they don't leak into POMs. + licenseReportDependencies("com.github.ben-manes.caffeine:caffeine") { + transitive = false + } + licenseReportDependencies "com.blogspot.mydailyjava:weak-lock-free" + // TODO ideally this would be :instrumentation instead of :javaagent-tooling + // in case there are dependencies (accidentally) pulled in by instrumentation modules + // but I couldn't get that to work + licenseReportDependencies project(':javaagent-tooling') + licenseReportDependencies project(':javaagent-extension-api') + licenseReportDependencies project(':javaagent-bootstrap') +} + +tasks.withType(Test).configureEach { + inputs.file(shadowJar.archiveFile) + + jvmArgs "-Dotel.javaagent.debug=true" + + doFirst { + // Defining here to allow jacoco to be first on the command line. + jvmArgs "-javaagent:${shadowJar.archivePath}" + } + + testLogging { + events "started" + } + + dependsOn shadowJar +} +assemble.dependsOn lightShadow +assemble.dependsOn shadowJar + +licenseReport { + outputDir = rootProject.file("licenses") + + renderers = [new InventoryMarkdownReportRenderer()] + + configurations = ["licenseReportDependencies"] + + excludeGroups = [ + "io.opentelemetry.instrumentation", + "io.opentelemetry.javaagent" + ] + + filters = [new LicenseBundleNormalizer(bundlePath: "$projectDir/license-normalizer-bundle.json")] +} + +def cleanLicenses = tasks.register("cleanLicenses", Delete) { + delete(rootProject.file("licenses")) +} + +tasks.named("generateLicenseReport").configure { + dependsOn(cleanLicenses) +} diff --git a/opentelemetry-java-instrumentation/javaagent/license-normalizer-bundle.json b/opentelemetry-java-instrumentation/javaagent/license-normalizer-bundle.json new file mode 100644 index 000000000..e194358a8 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/license-normalizer-bundle.json @@ -0,0 +1,27 @@ +{ + "bundles": [ + { + "bundleName": "apache2", + "licenseName": "Apache License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0" + } + ], + "transformationRules": [ + { + "bundleName": "apache2", + "licenseNamePattern": "Apache 2.0" + }, + { + "bundleName": "apache2", + "licenseNamePattern": "Apache-2.0" + }, + { + "bundleName": "apache2", + "licenseNamePattern": "The Apache License, Version 2.0" + }, + { + "bundleName": "apache2", + "licenseNamePattern": "The Apache Software License, Version 2.0" + } + ] +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.groovy b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.groovy new file mode 100644 index 000000000..0ae4e2327 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.groovy @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent + +import jvmbootstraptest.AgentLoadedChecker +import jvmbootstraptest.MyClassLoaderIsNotBootstrap +import spock.lang.Specification + +class AgentLoadedIntoBootstrapTest extends Specification { + + def "Agent loads in when separate jvm is launched"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(AgentLoadedChecker.getName() + , "" as String[] + , "" as String[] + , [:] + , true) == 0 + } + + // this tests the case where someone adds the contents of opentelemetry-javaagent.jar by mistake + // to their application's "uber.jar" + // + // the reason this can cause issues is because we locate the agent jar based on the CodeSource of + // the OpenTelemetryAgent class, and then we add that jar file to the bootstrap class path + // + // but if we find the OpenTelemetryAgent class in an uber jar file, and we add that (whole) uber + // jar file to the bootstrap class loader, that can cause some applications to break, as there's a + // lot of application and library code that doesn't handle getClassLoader() returning null + // (e.g. https://github.com/qos-ch/logback/pull/291) + def "application uber jar should not be added to the bootstrap class loader"() { + setup: + def mainClassName = MyClassLoaderIsNotBootstrap.getName() + def pathToJar = IntegrationTestUtils.createJarWithClasses(mainClassName, + MyClassLoaderIsNotBootstrap, + OpenTelemetryAgent).getPath() + + expect: + IntegrationTestUtils.runOnSeparateJvm(mainClassName + , "" as String[] + , "" as String[] + , [:] + , pathToJar as String + , true) == 0 + + cleanup: + new File(pathToJar).delete() + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/LogLevelTest.groovy b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/LogLevelTest.groovy new file mode 100644 index 000000000..87fffce55 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/LogLevelTest.groovy @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent + +import jvmbootstraptest.LogLevelChecker +import spock.lang.Specification + +class LogLevelTest extends Specification { + + + /* Priority: io.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel > opentelemetry.javaagent.debug > OTEL_JAVAAGENT_DEBUG + 1: INFO LOGS + 0: DEBUG Logs + */ + + def "otel.javaagent.debug false"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() + , ["-Dotel.javaagent.debug=false", "-Dotel.javaagent.enabled=false"] as String[] + , "" as String[] + , [:] + , true) == 1 + } + + def "SLF4J DEBUG && otel.javaagent.debug is false"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() + , ["-Dotel.javaagent.debug=false", "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=debug", "-Dotel.javaagent.enabled=false"] as String[] + , "" as String[] + , [:] + , true) == 0 + } + + def "otel.javaagent.debug is false && OTEL_JAVAAGENT_DEBUG is true"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() + , ["-Dotel.javaagent.debug=false", "-Dotel.javaagent.enabled=false"] as String[] + , "" as String[] + , ["OTEL_JAVAAGENT_DEBUG": "true"] + , true) == 1 + } + + def "otel.javaagent.debug is true"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() + , ["-Dotel.javaagent.debug=true", "-Dotel.javaagent.enabled=false"] as String[] + , "" as String[] + , [:] + , true) == 0 + } + + + def "OTEL_JAVAAGENT_DEBUG is true"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() + , ["-Dotel.javaagent.enabled=false"] as String[] + , "" as String[] + , ["OTEL_JAVAAGENT_DEBUG": "true"] + , true) == 0 + } + + def "otel.javaagent.debug is true && OTEL_JAVAAGENT_DEBUG is false"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() + , ["-Dotel.javaagent.debug=true", "-Dotel.javaagent.enabled=false"] as String[] + , "" as String[] + , ["OTEL_JAVAAGENT_DEBUG": "false"] + , true) == 0 + } + + + def "SLF4J DEBUG && OTEL_JAVAAGENT_DEBUG is false"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() + , ["-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=debug", "-Dotel.javaagent.enabled=false"] as String[] + , "" as String[] + , ["OTEL_JAVAAGENT_DEBUG": "false"] + , true) == 0 + } + + def "SLF4J INFO && OTEL_JAVAAGENT_DEBUG is true"() { + expect: + IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() + , ["-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=info", "-Dotel.javaagent.enabled=false"] as String[] + , "" as String[] + , ["OTEL_JAVAAGENT_DEBUG": "true"] + , true) == 1 + } + +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy new file mode 100644 index 000000000..510057551 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.classloading + +import static io.opentelemetry.javaagent.IntegrationTestUtils.createJarWithClasses + +import io.opentelemetry.javaagent.util.GcUtils +import io.opentelemetry.test.ClassToInstrument +import io.opentelemetry.test.ClassToInstrumentChild +import java.lang.ref.WeakReference +import spock.lang.Specification + +class ClassLoadingTest extends Specification { + + final URL[] classpath = [createJarWithClasses(ClassToInstrument, ClassToInstrumentChild)] + + /** Assert that we can instrument classloaders which cannot resolve agent advice classes. */ + def "instrument classloader without agent classes"() { + setup: + URLClassLoader loader = new URLClassLoader(classpath, (ClassLoader) null) + + when: + loader.loadClass("io.opentelemetry.javaagent.instrumentation.trace_annotation.TraceAdvice") + then: + thrown ClassNotFoundException + + when: + Class instrumentedClass = loader.loadClass(ClassToInstrument.getName()) + then: + instrumentedClass.getClassLoader() == loader + } + + def "make sure ByteBuddy does not hold strong references to ClassLoader"() { + setup: + URLClassLoader loader = new URLClassLoader(classpath, (ClassLoader) null) + WeakReference ref = new WeakReference<>(loader) + + when: + loader.loadClass(ClassToInstrument.getName()) + loader = null + + GcUtils.awaitGc(ref) + + then: + null == ref.get() + } + + // We are doing this because Groovy cannot properly resolve constructor argument types in anonymous classes + static class CountingClassLoader extends URLClassLoader { + public int count = 0 + + CountingClassLoader(URL[] urls) { + super(urls, (ClassLoader) null) + } + + @Override + URL getResource(String name) { + count++ + return super.getResource(name) + } + } + + def "make sure that ByteBuddy reads the class bytes only once"() { + setup: + CountingClassLoader loader = new CountingClassLoader(classpath) + + when: + //loader.loadClass("aaa") + loader.loadClass(ClassToInstrument.getName()) + int countAfterFirstLoad = loader.count + loader.loadClass(ClassToInstrumentChild.getName()) + + then: + // ClassToInstrumentChild won't cause an additional getResource() because its TypeDescription is created from transformation bytes. + loader.count > 0 + loader.count == countAfterFirstLoad + } + + def "make sure that ByteBuddy doesn't reuse cached type descriptions between different classloaders"() { + setup: + CountingClassLoader loader1 = new CountingClassLoader(classpath) + CountingClassLoader loader2 = new CountingClassLoader(classpath) + + when: + loader1.loadClass(ClassToInstrument.getName()) + loader2.loadClass(ClassToInstrument.getName()) + + then: + loader1.count > 0 + loader2.count > 0 + loader1.count == loader2.count + } + + def "can find classes but not resources loaded onto the bootstrap classpath"() { + expect: + Class.forName(name) != null + + // Resources from bootstrap injected jars can't be loaded. + // https://github.com/raphw/byte-buddy/pull/496 + if (onTestClasspath) { + assert ClassLoader.getSystemClassLoader().getResource(resource) != null + } else { + assert ClassLoader.getSystemClassLoader().getResource(resource) == null + } + + + where: + name | onTestClasspath + "io.opentelemetry.javaagent.instrumentation.api.concurrent.State" | true + // This test case fails on ibm j9. Perhaps this rule only applies to OpenJdk based jvms? +// "io.opentelemetry.javaagent.instrumentation.api.concurrent.State" | false + resource = name.replace(".", "/") + ".class" + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ShadowPackageRenamingTest.groovy b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ShadowPackageRenamingTest.groovy new file mode 100644 index 000000000..0c95e4762 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ShadowPackageRenamingTest.groovy @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.classloading + +import com.google.common.collect.MapMaker +import com.google.common.reflect.ClassPath +import io.opentelemetry.javaagent.IntegrationTestUtils +import spock.lang.Specification + +class ShadowPackageRenamingTest extends Specification { + + + static final String[] AGENT_PACKAGE_PREFIXES = [ + "io.opentelemetry.instrumentation.api", + // guava + "com.google.auto", + "com.google.common", + "com.google.thirdparty.publicsuffix", + // bytebuddy + "net.bytebuddy", + "org.yaml.snakeyaml", + // disruptor + "com.lmax.disruptor", + // okHttp + "okhttp3", + "okio", + "jnr", + "org.objectweb.asm", + "com.kenai", + // Custom RxJava Utility + "rx.__OpenTelemetryTracingUtil" + ] + + def "agent dependencies renamed"() { + setup: + Class clazz = + IntegrationTestUtils.getAgentClassLoader() + .loadClass("io.opentelemetry.javaagent.tooling.AgentInstaller") + URL userGuava = + MapMaker.getProtectionDomain().getCodeSource().getLocation() + URL agentGuavaDep = + clazz + .getClassLoader() + .loadClass("com.google.common.collect.MapMaker") + .getProtectionDomain() + .getCodeSource() + .getLocation() + URL agentSource = + clazz.getProtectionDomain().getCodeSource().getLocation() + + expect: + agentSource.getFile().endsWith(".jar") + agentSource.getProtocol() == "file" + agentSource == agentGuavaDep + agentSource.getFile() != userGuava.getFile() + } + + def "agent classes not visible"() { + when: + ClassLoader.getSystemClassLoader().loadClass("io.opentelemetry.javaagent.tooling.AgentInstaller") + then: + thrown ClassNotFoundException + } + + def "agent jar contains no bootstrap classes"() { + setup: + ClassPath agentClasspath = ClassPath.from(IntegrationTestUtils.getAgentClassLoader()) + + ClassPath bootstrapClasspath = ClassPath.from(IntegrationTestUtils.getBootstrapProxy()) + Set bootstrapClasses = new HashSet<>() + List bootstrapPrefixes = IntegrationTestUtils.getBootstrapPackagePrefixes() + List badBootstrapPrefixes = [] + for (ClassPath.ClassInfo info : bootstrapClasspath.getAllClasses()) { + bootstrapClasses.add(info.getName()) + // make sure all bootstrap classes can be loaded from system + ClassLoader.getSystemClassLoader().loadClass(info.getName()) + boolean goodPrefix = false + for (int i = 0; i < bootstrapPrefixes.size(); ++i) { + if (info.getName().startsWith(bootstrapPrefixes[i])) { + goodPrefix = true + break + } + } + if (info.getName() == 'io.opentelemetry.javaagent.OpenTelemetryAgent') { + // io.opentelemetry.javaagent.OpenTelemetryAgent isn't needed in the bootstrap prefixes + // because it doesn't live in the bootstrap class loader, but it's still "good" for the + // purpose of this test which is just checking all the classes sitting directly inside of + // the agent jar + goodPrefix = true + } + if (!goodPrefix) { + badBootstrapPrefixes.add(info.getName()) + } + } + + List agentDuplicateClassFile = new ArrayList<>() + List badAgentPrefixes = [] + // TODO (trask) agentClasspath.getAllClasses() is empty + // so this part of the test doesn't verify what it thinks it is verifying + for (ClassPath.ClassInfo classInfo : agentClasspath.getAllClasses()) { + if (bootstrapClasses.contains(classInfo.getName())) { + agentDuplicateClassFile.add(classInfo) + } + boolean goodPrefix = false + for (int i = 0; i < AGENT_PACKAGE_PREFIXES.length; ++i) { + if (classInfo.getName().startsWith(AGENT_PACKAGE_PREFIXES[i])) { + goodPrefix = true + break + } + } + if (!goodPrefix) { + badAgentPrefixes.add(classInfo.getName()) + } + } + + expect: + agentDuplicateClassFile == [] + badBootstrapPrefixes == [] + badAgentPrefixes == [] + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.groovy b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.groovy new file mode 100644 index 000000000..032cb9941 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/groovy/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.groovy @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.muzzle + +import io.opentelemetry.javaagent.IntegrationTestUtils +import java.lang.reflect.Field +import java.lang.reflect.Method +import spock.lang.Specification + +class MuzzleBytecodeTransformTest extends Specification { + + def "muzzle fields added to all instrumentation"() { + setup: + List unMuzzledClasses = [] + List nonLazyFields = [] + List unInitFields = [] + def instrumentationModuleClass = IntegrationTestUtils.getAgentClassLoader().loadClass("io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule") + for (Object instrumenter : ServiceLoader.load(instrumentationModuleClass)) { + if (!instrumentationModuleClass.isAssignableFrom(instrumenter.getClass())) { + // muzzle only applies to default instrumenters + continue + } + Field f + Method m + try { + f = instrumenter.getClass().getDeclaredField("muzzleReferences") + f.setAccessible(true) + if (f.get(instrumenter) != null) { + nonLazyFields.add(instrumenter.getClass()) + } + m = instrumenter.getClass().getDeclaredMethod("getMuzzleReferences") + m.setAccessible(true) + m.invoke(instrumenter) + if (f.get(instrumenter) == null) { + unInitFields.add(instrumenter.getClass()) + } + } catch (NoSuchFieldException | NoSuchMethodException e) { + unMuzzledClasses.add(instrumenter.getClass()) + } finally { + if (null != f) { + f.setAccessible(false) + } + if (null != m) { + m.setAccessible(false) + } + } + } + expect: + unMuzzledClasses == [] + nonLazyFields == [] + unInitFields == [] + } + +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/CommonTest.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/CommonTest.java new file mode 100644 index 000000000..05ba036dc --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/CommonTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent; + +import org.junit.Test; + +/** + * @author goodjava@qq.com + */ +public class CommonTest { + + @SuppressWarnings("SystemOut") + @Test + public void test1() { + System.out.println("test"); + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/DemoRun.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/DemoRun.java new file mode 100644 index 000000000..f74ba6e57 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/DemoRun.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent; + +/** + * @author goodjava@qq.com + */ +public class DemoRun { + + + @SuppressWarnings("SystemOut") + public static void main(String[] args) { + System.out.println("demo run"); + } + +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/IntegrationTestUtils.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/IntegrationTestUtils.java new file mode 100644 index 000000000..9f96c50ad --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/IntegrationTestUtils.java @@ -0,0 +1,262 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IntegrationTestUtils { + + private static final Logger logger = LoggerFactory.getLogger(IntegrationTestUtils.class); + + /** Returns the classloader the core agent is running on. */ + public static ClassLoader getAgentClassLoader() { + return getAgentFieldClassloader("agentClassLoader"); + } + + private static ClassLoader getAgentFieldClassloader(String fieldName) { + Field classloaderField = null; + try { + Class agentClass = + ClassLoader.getSystemClassLoader() + .loadClass("io.opentelemetry.javaagent.bootstrap.AgentInitializer"); + classloaderField = agentClass.getDeclaredField(fieldName); + classloaderField.setAccessible(true); + return (ClassLoader) classloaderField.get(null); + } catch (Exception e) { + throw new IllegalStateException(e); + } finally { + if (null != classloaderField) { + classloaderField.setAccessible(false); + } + } + } + + // TODO this works only accidentally now, because we don't have extensions in tests. + /** Returns the URL to the jar the agent appended to the bootstrap classpath. */ + public static ClassLoader getBootstrapProxy() throws Exception { + ClassLoader agentClassLoader = getAgentClassLoader(); + Method getBootstrapProxy = agentClassLoader.getClass().getMethod("getBootstrapProxy"); + return (ClassLoader) getBootstrapProxy.invoke(agentClassLoader); + } + + /** See {@link IntegrationTestUtils#createJarWithClasses(String, Class[])}. */ + public static URL createJarWithClasses(Class... classes) throws IOException { + return createJarWithClasses(null, classes); + } + + /** + * Create a temporary jar on the filesystem with the bytes of the given classes. + * + *

    The jar file will be removed when the jvm exits. + * + * @param mainClassname The name of the class to use for Main-Class and Premain-Class. May be null + * @param classes classes to package into the jar. + * @return the location of the newly created jar. + */ + public static URL createJarWithClasses(String mainClassname, Class... classes) + throws IOException { + File tmpJar = File.createTempFile(UUID.randomUUID() + "-", ".jar"); + tmpJar.deleteOnExit(); + + Manifest manifest = new Manifest(); + if (mainClassname != null) { + Attributes mainAttributes = manifest.getMainAttributes(); + mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + mainAttributes.put(Attributes.Name.MAIN_CLASS, mainClassname); + } + JarOutputStream target = new JarOutputStream(new FileOutputStream(tmpJar), manifest); + for (Class clazz : classes) { + addToJar(clazz, target); + } + target.close(); + + return tmpJar.toURI().toURL(); + } + + private static void addToJar(Class clazz, JarOutputStream jarOutputStream) throws IOException { + InputStream inputStream = null; + ClassLoader loader = clazz.getClassLoader(); + if (null == loader) { + // bootstrap resources can be fetched through the system loader + loader = ClassLoader.getSystemClassLoader(); + } + try { + JarEntry entry = new JarEntry(getResourceName(clazz.getName())); + jarOutputStream.putNextEntry(entry); + inputStream = loader.getResourceAsStream(getResourceName(clazz.getName())); + + byte[] buffer = new byte[1024]; + while (true) { + int count = inputStream.read(buffer); + if (count == -1) { + break; + } + jarOutputStream.write(buffer, 0, count); + } + jarOutputStream.closeEntry(); + } finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + + // com.foo.Bar -> com/foo/Bar.class + public static String getResourceName(String className) { + return className.replace('.', '/') + ".class"; + } + + public static List getBootstrapPackagePrefixes() throws Exception { + Field f = + getAgentClassLoader() + .loadClass("io.opentelemetry.javaagent.tooling.Constants") + .getField("BOOTSTRAP_PACKAGE_PREFIXES"); + return (List) f.get(null); + } + + private static String getAgentArgument() { + RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean(); + for (String arg : runtimeMxBean.getInputArguments()) { + if (arg.startsWith("-javaagent")) { + return arg; + } + } + + throw new IllegalStateException("Agent jar not found"); + } + + public static int runOnSeparateJvm( + String mainClassName, + String[] jvmArgs, + String[] mainMethodArgs, + Map envVars, + boolean printOutputStreams) + throws Exception { + String classPath = System.getProperty("java.class.path"); + return runOnSeparateJvm( + mainClassName, jvmArgs, mainMethodArgs, envVars, classPath, printOutputStreams); + } + + /** + * On a separate JVM, run the main method for a given class. + * + * @param mainClassName The name of the entry point class. Must declare a main method. + * @param printOutputStreams if true, print stdout and stderr of the child jvm + * @return the return code of the child jvm + */ + public static int runOnSeparateJvm( + String mainClassName, + String[] jvmArgs, + String[] mainMethodArgs, + Map envVars, + String classpath, + boolean printOutputStreams) + throws Exception { + + String separator = System.getProperty("file.separator"); + String path = System.getProperty("java.home") + separator + "bin" + separator + "java"; + + List vmArgsList = new ArrayList<>(Arrays.asList(jvmArgs)); + vmArgsList.add(getAgentArgument()); + + List commands = new ArrayList<>(); + commands.add(path); + commands.addAll(vmArgsList); + commands.add("-cp"); + commands.add(classpath); + commands.add(mainClassName); + commands.addAll(Arrays.asList(mainMethodArgs)); + ProcessBuilder processBuilder = new ProcessBuilder(commands.toArray(new String[0])); + processBuilder.environment().putAll(envVars); + + Process process = processBuilder.start(); + + StreamGobbler errorGobbler = + new StreamGobbler(process.getErrorStream(), "ERROR", printOutputStreams); + StreamGobbler outputGobbler = + new StreamGobbler(process.getInputStream(), "OUTPUT", printOutputStreams); + outputGobbler.start(); + errorGobbler.start(); + + waitFor(process, 30, TimeUnit.SECONDS); + + outputGobbler.join(); + errorGobbler.join(); + + return process.exitValue(); + } + + private static void waitFor(Process process, long timeout, TimeUnit unit) + throws InterruptedException, TimeoutException { + long startTime = System.nanoTime(); + long rem = unit.toNanos(timeout); + + do { + try { + process.exitValue(); + return; + } catch (IllegalThreadStateException ex) { + if (rem > 0) { + Thread.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100)); + } + } + rem = unit.toNanos(timeout) - (System.nanoTime() - startTime); + } while (rem > 0); + throw new TimeoutException(); + } + + private static class StreamGobbler extends Thread { + final InputStream stream; + final String type; + final boolean print; + + private StreamGobbler(InputStream stream, String type, boolean print) { + this.stream = stream; + this.type = type; + this.print = print; + } + + @Override + public void run() { + try { + BufferedReader reader = + new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); + String line = null; + while ((line = reader.readLine()) != null) { + if (print) { + logger.info("{}> {}", type, line); + } + } + } catch (IOException e) { + logger.warn("Error gobbling.", e); + } + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/util/GcUtils.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/util/GcUtils.java new file mode 100644 index 000000000..c3dafe56b --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/javaagent/util/GcUtils.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.util; + +import java.lang.ref.WeakReference; + +public final class GcUtils { + public static void awaitGc(WeakReference ref) throws InterruptedException { + while (ref.get() != null) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + System.gc(); + System.runFinalization(); + } + } + + private GcUtils() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/test/ClassToInstrument.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/test/ClassToInstrument.java new file mode 100644 index 000000000..d897c87e2 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/test/ClassToInstrument.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test; + +import io.opentracing.contrib.dropwizard.Trace; + +/** + * Note: this has to stay outside of 'io.opentelemetry.javaagent' package to be considered for + * instrumentation + */ +public class ClassToInstrument { + @Trace + public static void someMethod() {} +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/test/ClassToInstrumentChild.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/test/ClassToInstrumentChild.java new file mode 100644 index 000000000..13875c2ca --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/io/opentelemetry/test/ClassToInstrumentChild.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test; + +/** + * Note: this has to stay outside of 'io.opentelemetry.javaagent' package to be considered for + * instrumentation + */ +public class ClassToInstrumentChild extends ClassToInstrument {} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/AgentLoadedChecker.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/AgentLoadedChecker.java new file mode 100644 index 000000000..fd82db546 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/AgentLoadedChecker.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jvmbootstraptest; + +import java.net.URL; +import java.net.URLClassLoader; + +public class AgentLoadedChecker { + public static void main(String[] args) throws ClassNotFoundException { + // Empty classloader that delegates to bootstrap + URLClassLoader emptyClassLoader = new URLClassLoader(new URL[] {}, null); + Class agentClass = + emptyClassLoader.loadClass("io.opentelemetry.javaagent.bootstrap.AgentInitializer"); + + if (agentClass.getClassLoader() != null) { + throw new IllegalStateException( + "Agent loaded into classloader other than bootstrap: " + agentClass.getClassLoader()); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/LogLevelChecker.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/LogLevelChecker.java new file mode 100644 index 000000000..842644a58 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/LogLevelChecker.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jvmbootstraptest; + +public class LogLevelChecker { + // returns an exception if logs are not in DEBUG + public static void main(String[] args) { + + String str = + System.getProperty("io.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel"); + + if ((str == null) || (str != null && !str.equalsIgnoreCase("debug"))) { + throw new IllegalStateException("debug mode not set"); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/MyClassLoaderIsNotBootstrap.java b/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/MyClassLoaderIsNotBootstrap.java new file mode 100644 index 000000000..126796f9a --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/java/jvmbootstraptest/MyClassLoaderIsNotBootstrap.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package jvmbootstraptest; + +public class MyClassLoaderIsNotBootstrap { + public static void main(String[] args) { + if (MyClassLoaderIsNotBootstrap.class.getClassLoader() == null) { + throw new IllegalStateException( + "Application level class was loaded by bootstrap classloader"); + } + } +} diff --git a/opentelemetry-java-instrumentation/javaagent/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/javaagent/src/test/resources/logback.xml new file mode 100644 index 000000000..6875217a6 --- /dev/null +++ b/opentelemetry-java-instrumentation/javaagent/src/test/resources/logback.xml @@ -0,0 +1,18 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/licenses/byte-buddy-1.10.18.jar/META-INF/LICENSE b/opentelemetry-java-instrumentation/licenses/byte-buddy-1.10.18.jar/META-INF/LICENSE new file mode 100644 index 000000000..3dd2c50e8 --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/byte-buddy-1.10.18.jar/META-INF/LICENSE @@ -0,0 +1,180 @@ +This product bundles ASM 9.0, which is available under a "3-clause BSD" +license. For details, see licenses/ASM. For more information visit https://asm.ow2.io. + +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 + diff --git a/opentelemetry-java-instrumentation/licenses/byte-buddy-1.10.18.jar/META-INF/NOTICE b/opentelemetry-java-instrumentation/licenses/byte-buddy-1.10.18.jar/META-INF/NOTICE new file mode 100644 index 000000000..1d6251013 --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/byte-buddy-1.10.18.jar/META-INF/NOTICE @@ -0,0 +1,13 @@ +Copyright 2014 - 2020 Rafael Winterhalter + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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/opentelemetry-java-instrumentation/licenses/byte-buddy-agent-1.10.18.jar/META-INF/LICENSE b/opentelemetry-java-instrumentation/licenses/byte-buddy-agent-1.10.18.jar/META-INF/LICENSE new file mode 100644 index 000000000..d0381d6d0 --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/byte-buddy-agent-1.10.18.jar/META-INF/LICENSE @@ -0,0 +1,176 @@ +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 diff --git a/opentelemetry-java-instrumentation/licenses/byte-buddy-agent-1.10.18.jar/META-INF/NOTICE b/opentelemetry-java-instrumentation/licenses/byte-buddy-agent-1.10.18.jar/META-INF/NOTICE new file mode 100644 index 000000000..1d6251013 --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/byte-buddy-agent-1.10.18.jar/META-INF/NOTICE @@ -0,0 +1,13 @@ +Copyright 2014 - 2020 Rafael Winterhalter + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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/opentelemetry-java-instrumentation/licenses/caffeine-2.9.0.jar/META-INF/LICENSE b/opentelemetry-java-instrumentation/licenses/caffeine-2.9.0.jar/META-INF/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/caffeine-2.9.0.jar/META-INF/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/opentelemetry-java-instrumentation/licenses/licenses.md b/opentelemetry-java-instrumentation/licenses/licenses.md new file mode 100644 index 000000000..92608f1a7 --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/licenses.md @@ -0,0 +1,508 @@ + +#javaagent +##Dependency License Report +_2021-03-22 18:06:07 JST_ +## Apache License, Version 2.0 + +**1** **Group:** `com.blogspot.mydailyjava` **Name:** `weak-lock-free` **Version:** `0.18` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM Project URL**: [https://github.com/raphw/weak-lock-free](https://github.com/raphw/weak-lock-free) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**2** **Group:** `com.blogspot.mydailyjava` **Name:** `weak-lock-free` **Version:** `0.18` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM Project URL**: [https://github.com/raphw/weak-lock-free](https://github.com/raphw/weak-lock-free) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**3** **Group:** `com.github.ben-manes.caffeine` **Name:** `caffeine` **Version:** `2.9.0` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM Project URL**: [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [caffeine-2.9.0.jar/META-INF/LICENSE](caffeine-2.9.0.jar/META-INF/LICENSE) + +**4** **Group:** `com.github.ben-manes.caffeine` **Name:** `caffeine` **Version:** `2.9.0` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM Project URL**: [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [caffeine-2.9.0.jar/META-INF/LICENSE](caffeine-2.9.0.jar/META-INF/LICENSE) + +**5** **Group:** `com.google.android` **Name:** `annotations` **Version:** `4.1.1.4` +> - **POM Project URL**: [http://source.android.com/](http://source.android.com/) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**6** **Group:** `com.google.api.grpc` **Name:** `proto-google-common-protos` **Version:** `2.0.1` +> - **POM Project URL**: [https://github.com/googleapis/java-iam/proto-google-common-protos](https://github.com/googleapis/java-iam/proto-google-common-protos) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**7** **Group:** `com.google.auto` **Name:** `auto-common` **Version:** `0.10` +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**8** **Group:** `com.google.auto.service` **Name:** `auto-service` **Version:** `1.0-rc7` +> - **POM Project URL**: [https://github.com/google/auto/tree/master/service](https://github.com/google/auto/tree/master/service) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**9** **Group:** `com.google.auto.service` **Name:** `auto-service-annotations` **Version:** `1.0-rc7` +> - **POM Project URL**: [https://github.com/google/auto/tree/master/service](https://github.com/google/auto/tree/master/service) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**10** **Group:** `com.google.code.findbugs` **Name:** `jsr305` **Version:** `3.0.2` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM Project URL**: [http://findbugs.sourceforge.net/](http://findbugs.sourceforge.net/) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**11** **Group:** `com.google.code.findbugs` **Name:** `jsr305` **Version:** `3.0.2` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM Project URL**: [http://findbugs.sourceforge.net/](http://findbugs.sourceforge.net/) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**12** **Group:** `com.google.code.gson` **Name:** `gson` **Version:** `2.8.6` +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**13** **Group:** `com.google.errorprone` **Name:** `error_prone_annotations` **Version:** `2.4.0` +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**14** **Group:** `com.google.guava` **Name:** `failureaccess` **Version:** `1.0.1` +> - **Manifest Project URL**: [https://github.com/google/guava/](https://github.com/google/guava/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**15** **Group:** `com.google.guava` **Name:** `failureaccess` **Version:** `1.0.1` +> - **Manifest Project URL**: [https://github.com/google/guava/](https://github.com/google/guava/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**16** **Group:** `com.google.guava` **Name:** `guava` **Version:** `30.0-android` +> - **Manifest Project URL**: [https://github.com/google/guava/](https://github.com/google/guava/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**17** **Group:** `com.google.guava` **Name:** `guava` **Version:** `30.0-android` +> - **Manifest Project URL**: [https://github.com/google/guava/](https://github.com/google/guava/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**18** **Group:** `com.google.guava` **Name:** `listenablefuture` **Version:** `9999.0-empty-to-avoid-conflict-with-guava` +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**19** **Group:** `com.google.j2objc` **Name:** `j2objc-annotations` **Version:** `1.3` +> - **POM Project URL**: [https://github.com/google/j2objc/](https://github.com/google/j2objc/) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**20** **Group:** `com.squareup.okhttp3` **Name:** `okhttp` **Version:** `3.14.9` +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [okhttp-3.14.9.jar/okhttp3/internal/publicsuffix/NOTICE](okhttp-3.14.9.jar/okhttp3/internal/publicsuffix/NOTICE) + +**21** **Group:** `com.squareup.okio` **Name:** `okio` **Version:** `1.17.2` +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**22** **Group:** `io.grpc` **Name:** `grpc-api` **Version:** `1.35.0` +> - **POM Project URL**: [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**23** **Group:** `io.grpc` **Name:** `grpc-context` **Version:** `1.35.0` +> - **POM Project URL**: [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**24** **Group:** `io.grpc` **Name:** `grpc-core` **Version:** `1.34.1` +> - **POM Project URL**: [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**25** **Group:** `io.grpc` **Name:** `grpc-netty` **Version:** `1.34.1` +> - **POM Project URL**: [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**26** **Group:** `io.grpc` **Name:** `grpc-protobuf` **Version:** `1.35.0` +> - **POM Project URL**: [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**27** **Group:** `io.grpc` **Name:** `grpc-protobuf-lite` **Version:** `1.35.0` +> - **POM Project URL**: [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**28** **Group:** `io.grpc` **Name:** `grpc-stub` **Version:** `1.35.0` +> - **POM Project URL**: [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**29** **Group:** `io.netty` **Name:** `netty-buffer` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**30** **Group:** `io.netty` **Name:** `netty-buffer` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**31** **Group:** `io.netty` **Name:** `netty-buffer` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**32** **Group:** `io.netty` **Name:** `netty-codec` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**33** **Group:** `io.netty` **Name:** `netty-codec` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**34** **Group:** `io.netty` **Name:** `netty-codec` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**35** **Group:** `io.netty` **Name:** `netty-codec-http` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**36** **Group:** `io.netty` **Name:** `netty-codec-http` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**37** **Group:** `io.netty` **Name:** `netty-codec-http` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**38** **Group:** `io.netty` **Name:** `netty-codec-http2` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**39** **Group:** `io.netty` **Name:** `netty-codec-http2` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**40** **Group:** `io.netty` **Name:** `netty-codec-http2` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**41** **Group:** `io.netty` **Name:** `netty-codec-socks` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**42** **Group:** `io.netty` **Name:** `netty-codec-socks` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**43** **Group:** `io.netty` **Name:** `netty-codec-socks` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**44** **Group:** `io.netty` **Name:** `netty-common` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**45** **Group:** `io.netty` **Name:** `netty-common` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**46** **Group:** `io.netty` **Name:** `netty-common` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**47** **Group:** `io.netty` **Name:** `netty-handler` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**48** **Group:** `io.netty` **Name:** `netty-handler` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**49** **Group:** `io.netty` **Name:** `netty-handler` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**50** **Group:** `io.netty` **Name:** `netty-handler-proxy` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**51** **Group:** `io.netty` **Name:** `netty-handler-proxy` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**52** **Group:** `io.netty` **Name:** `netty-handler-proxy` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**53** **Group:** `io.netty` **Name:** `netty-resolver` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**54** **Group:** `io.netty` **Name:** `netty-resolver` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**55** **Group:** `io.netty` **Name:** `netty-resolver` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**56** **Group:** `io.netty` **Name:** `netty-transport` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**57** **Group:** `io.netty` **Name:** `netty-transport` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**58** **Group:** `io.netty` **Name:** `netty-transport` **Version:** `4.1.51.Final` +> - **Manifest Project URL**: [https://netty.io/](https://netty.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +**59** **Group:** `io.opentelemetry` **Name:** `opentelemetry-api` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**60** **Group:** `io.opentelemetry` **Name:** `opentelemetry-api-metrics` **Version:** `1.0.1-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**61** **Group:** `io.opentelemetry` **Name:** `opentelemetry-context` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**62** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-jaeger` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**63** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-logging` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**64** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-otlp` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**65** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-otlp-common` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**66** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-otlp-metrics` **Version:** `1.0.1-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**67** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-otlp-trace` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**68** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-prometheus` **Version:** `1.0.1-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**69** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-zipkin` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**70** **Group:** `io.opentelemetry` **Name:** `opentelemetry-extension-aws` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**71** **Group:** `io.opentelemetry` **Name:** `opentelemetry-extension-kotlin` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**72** **Group:** `io.opentelemetry` **Name:** `opentelemetry-extension-trace-propagators` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**73** **Group:** `io.opentelemetry` **Name:** `opentelemetry-proto` **Version:** `1.0.1-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**74** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**75** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-common` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**76** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-autoconfigure` **Version:** `1.0.1-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**77** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-resources` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**78** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-metrics` **Version:** `1.0.1-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**79** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-trace` **Version:** `1.0.1` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**80** **Group:** `io.opentelemetry` **Name:** `opentelemetry-semconv` **Version:** `1.0.1-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**81** **Group:** `io.perfmark` **Name:** `perfmark-api` **Version:** `0.19.0` +> - **POM Project URL**: [https://github.com/perfmark/perfmark](https://github.com/perfmark/perfmark) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**82** **Group:** `io.prometheus` **Name:** `simpleclient` **Version:** `0.10.0` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**83** **Group:** `io.prometheus` **Name:** `simpleclient` **Version:** `0.10.0` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**84** **Group:** `io.prometheus` **Name:** `simpleclient_common` **Version:** `0.9.0` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**85** **Group:** `io.prometheus` **Name:** `simpleclient_common` **Version:** `0.9.0` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**86** **Group:** `io.prometheus` **Name:** `simpleclient_httpserver` **Version:** `0.9.0` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**87** **Group:** `io.prometheus` **Name:** `simpleclient_httpserver` **Version:** `0.9.0` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**88** **Group:** `io.zipkin.reporter2` **Name:** `zipkin-reporter` **Version:** `2.16.3` +> - **Manifest Project URL**: [https://zipkin.io/](https://zipkin.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [zipkin-reporter-2.16.3.jar/META-INF/LICENSE](zipkin-reporter-2.16.3.jar/META-INF/LICENSE) + +**89** **Group:** `io.zipkin.reporter2` **Name:** `zipkin-reporter` **Version:** `2.16.3` +> - **Manifest Project URL**: [https://zipkin.io/](https://zipkin.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [zipkin-reporter-2.16.3.jar/META-INF/LICENSE](zipkin-reporter-2.16.3.jar/META-INF/LICENSE) + +**90** **Group:** `io.zipkin.reporter2` **Name:** `zipkin-sender-okhttp3` **Version:** `2.16.3` +> - **Manifest Project URL**: [https://zipkin.io/](https://zipkin.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE](zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE) + +**91** **Group:** `io.zipkin.reporter2` **Name:** `zipkin-sender-okhttp3` **Version:** `2.16.3` +> - **Manifest Project URL**: [https://zipkin.io/](https://zipkin.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE](zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE) + +**92** **Group:** `io.zipkin.zipkin2` **Name:** `zipkin` **Version:** `2.23.2` +> - **Manifest Project URL**: [http://zipkin.io/](http://zipkin.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [zipkin-2.23.2.jar/META-INF/LICENSE](zipkin-2.23.2.jar/META-INF/LICENSE) + +**93** **Group:** `io.zipkin.zipkin2` **Name:** `zipkin` **Version:** `2.23.2` +> - **Manifest Project URL**: [http://zipkin.io/](http://zipkin.io/) +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [zipkin-2.23.2.jar/META-INF/LICENSE](zipkin-2.23.2.jar/META-INF/LICENSE) + +**94** **Group:** `net.bytebuddy` **Name:** `byte-buddy` **Version:** `1.10.18` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [byte-buddy-1.10.18.jar/META-INF/LICENSE](byte-buddy-1.10.18.jar/META-INF/LICENSE) + - [byte-buddy-1.10.18.jar/META-INF/NOTICE](byte-buddy-1.10.18.jar/META-INF/NOTICE) + +**95** **Group:** `net.bytebuddy` **Name:** `byte-buddy` **Version:** `1.10.18` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [byte-buddy-1.10.18.jar/META-INF/LICENSE](byte-buddy-1.10.18.jar/META-INF/LICENSE) + - [byte-buddy-1.10.18.jar/META-INF/NOTICE](byte-buddy-1.10.18.jar/META-INF/NOTICE) + +**96** **Group:** `net.bytebuddy` **Name:** `byte-buddy-agent` **Version:** `1.10.18` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [byte-buddy-agent-1.10.18.jar/META-INF/LICENSE](byte-buddy-agent-1.10.18.jar/META-INF/LICENSE) + - [byte-buddy-agent-1.10.18.jar/META-INF/NOTICE](byte-buddy-agent-1.10.18.jar/META-INF/NOTICE) + +**97** **Group:** `net.bytebuddy` **Name:** `byte-buddy-agent` **Version:** `1.10.18` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **Embedded license files**: [byte-buddy-agent-1.10.18.jar/META-INF/LICENSE](byte-buddy-agent-1.10.18.jar/META-INF/LICENSE) + - [byte-buddy-agent-1.10.18.jar/META-INF/NOTICE](byte-buddy-agent-1.10.18.jar/META-INF/NOTICE) + +**98** **Group:** `org.codehaus.mojo` **Name:** `animal-sniffer-annotations` **Version:** `1.19` +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **POM License**: MIT License - [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) + +## GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception + +**99** **Group:** `org.checkerframework` **Name:** `checker-compat-qual` **Version:** `2.5.5` +> - **POM Project URL**: [https://checkerframework.org](https://checkerframework.org) +> - **POM License**: GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception - [https://openjdk.java.net/legal/gplv2+ce.html](https://openjdk.java.net/legal/gplv2+ce.html) +> - **POM License**: MIT License - [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) + +## MIT License + +**100** **Group:** `org.checkerframework` **Name:** `checker-compat-qual` **Version:** `2.5.5` +> - **POM Project URL**: [https://checkerframework.org](https://checkerframework.org) +> - **POM License**: GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception - [https://openjdk.java.net/legal/gplv2+ce.html](https://openjdk.java.net/legal/gplv2+ce.html) +> - **POM License**: MIT License - [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) + +**101** **Group:** `org.codehaus.mojo` **Name:** `animal-sniffer-annotations` **Version:** `1.19` +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +> - **POM License**: MIT License - [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) + +**102** **Group:** `org.slf4j` **Name:** `slf4j-api` **Version:** `1.7.30` +> - **POM Project URL**: [http://www.slf4j.org](http://www.slf4j.org) +> - **POM License**: MIT License - [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) + +**103** **Group:** `org.slf4j` **Name:** `slf4j-simple` **Version:** `1.7.30` +> - **POM Project URL**: [http://www.slf4j.org](http://www.slf4j.org) +> - **POM License**: MIT License - [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) + +## The 3-Clause BSD License + +**104** **Group:** `com.google.protobuf` **Name:** `protobuf-java` **Version:** `3.14.0` +> - **Manifest Project URL**: [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) +> - **Manifest License**: The 3-Clause BSD License (Not Packaged) +> - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) + +**105** **Group:** `com.google.protobuf` **Name:** `protobuf-java` **Version:** `3.14.0` +> - **Manifest Project URL**: [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) +> - **Manifest License**: The 3-Clause BSD License (Not Packaged) +> - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) + +**106** **Group:** `com.google.protobuf` **Name:** `protobuf-java-util` **Version:** `3.14.0` +> - **Manifest Project URL**: [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) +> - **Manifest License**: The 3-Clause BSD License (Not Packaged) +> - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) + +**107** **Group:** `com.google.protobuf` **Name:** `protobuf-java-util` **Version:** `3.14.0` +> - **Manifest Project URL**: [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) +> - **Manifest License**: The 3-Clause BSD License (Not Packaged) +> - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) + +## Unknown + +**108** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-bom` **Version:** `1.4.21` + + diff --git a/opentelemetry-java-instrumentation/licenses/okhttp-3.14.9.jar/okhttp3/internal/publicsuffix/NOTICE b/opentelemetry-java-instrumentation/licenses/okhttp-3.14.9.jar/okhttp3/internal/publicsuffix/NOTICE new file mode 100644 index 000000000..94973fde8 --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/okhttp-3.14.9.jar/okhttp3/internal/publicsuffix/NOTICE @@ -0,0 +1,5 @@ +Note that publicsuffixes.gz is compiled from The Public Suffix List: +https://publicsuffix.org/list/public_suffix_list.dat + +It is subject to the terms of the Mozilla Public License, v. 2.0: +https://mozilla.org/MPL/2.0/ diff --git a/opentelemetry-java-instrumentation/licenses/zipkin-2.23.2.jar/META-INF/LICENSE b/opentelemetry-java-instrumentation/licenses/zipkin-2.23.2.jar/META-INF/LICENSE new file mode 100644 index 000000000..0c1111bc7 --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/zipkin-2.23.2.jar/META-INF/LICENSE @@ -0,0 +1,216 @@ + 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. + +This product contains a modified part of Gson, distributed by Google: + + * License: Apache License v2.0 + * Homepage: https://github.com/google/gson + +This product contains a modified part of Guava, distributed by Google: + + * License: Apache License v2.0 + * Homepage: https://github.com/google/guava + +This product contains a modified part of Okio, distributed by Square: + + * License: Apache License v2.0 + * Homepage: https://github.com/square/okio diff --git a/opentelemetry-java-instrumentation/licenses/zipkin-reporter-2.16.3.jar/META-INF/LICENSE b/opentelemetry-java-instrumentation/licenses/zipkin-reporter-2.16.3.jar/META-INF/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/zipkin-reporter-2.16.3.jar/META-INF/LICENSE @@ -0,0 +1,201 @@ + 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/opentelemetry-java-instrumentation/licenses/zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE b/opentelemetry-java-instrumentation/licenses/zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/opentelemetry-java-instrumentation/licenses/zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE @@ -0,0 +1,201 @@ + 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/opentelemetry-java-instrumentation/opentelemetry-api-shaded-for-instrumenting/opentelemetry-api-shaded-for-instrumenting.gradle b/opentelemetry-java-instrumentation/opentelemetry-api-shaded-for-instrumenting/opentelemetry-api-shaded-for-instrumenting.gradle new file mode 100644 index 000000000..48df4355c --- /dev/null +++ b/opentelemetry-java-instrumentation/opentelemetry-api-shaded-for-instrumenting/opentelemetry-api-shaded-for-instrumenting.gradle @@ -0,0 +1,17 @@ +plugins { + id "com.github.johnrengelman.shadow" +} + +apply plugin: "otel.java-conventions" + +dependencies { + implementation "run.mone:opentelemetry-api" + implementation "run.mone:opentelemetry-api-metrics" +} + +// OpenTelemetry API shaded so that it can be used in instrumentation of OpenTelemetry API itself, +// and then its usage can be unshaded after OpenTelemetry API is shaded +// (see more explanation in opentelemetry-api-1.0.gradle) +shadowJar { + relocate "io.opentelemetry", "application.io.opentelemetry" +} diff --git a/opentelemetry-java-instrumentation/opentelemetry-ext-annotations-shaded-for-instrumenting/opentelemetry-ext-annotations-shaded-for-instrumenting.gradle b/opentelemetry-java-instrumentation/opentelemetry-ext-annotations-shaded-for-instrumenting/opentelemetry-ext-annotations-shaded-for-instrumenting.gradle new file mode 100644 index 000000000..f3f457539 --- /dev/null +++ b/opentelemetry-java-instrumentation/opentelemetry-ext-annotations-shaded-for-instrumenting/opentelemetry-ext-annotations-shaded-for-instrumenting.gradle @@ -0,0 +1,18 @@ +plugins { + id "com.github.johnrengelman.shadow" +} + +apply plugin: "otel.java-conventions" + +dependencies { + implementation "run.mone:opentelemetry-extension-annotations" + implementation "run.mone:opentelemetry-context" +} + +// OpenTelemetry API shaded so that it can be used in instrumentation of OpenTelemetry API itself, +// and then its usage can be unshaded after OpenTelemetry API is shaded +// (see more explanation in opentelemetry-api-1.0.gradle) +shadowJar { + + relocate "io.opentelemetry", "application.io.opentelemetry" +} diff --git a/opentelemetry-java-instrumentation/settings.gradle b/opentelemetry-java-instrumentation/settings.gradle new file mode 100644 index 000000000..314824186 --- /dev/null +++ b/opentelemetry-java-instrumentation/settings.gradle @@ -0,0 +1,304 @@ +pluginManagement { + plugins { + id 'com.github.ben-manes.versions' version '0.39.0' + id "io.github.gradle-nexus.publish-plugin" version "1.0.0" + id "me.champeau.jmh" version "0.6.4" + id "net.ltgt.nullaway" version "1.1.0" + id 'org.jetbrains.kotlin.jvm' version '1.5.10' + id 'org.unbroken-dome.test-sets' version '4.0.0' + id "nebula.release" version "15.3.0" + } +} + +plugins { + id 'com.gradle.enterprise' version '3.6.1' + id 'com.github.burrunan.s3-build-cache' version '1.1' +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + mavenLocal() + } +} + +def isCI = System.getenv("CI") != null +def skipBuildscan = Boolean.valueOf(System.getenv("SKIP_BUILDSCAN")) +gradleEnterprise { + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + + if (isCI && !skipBuildscan) { + publishAlways() + tag 'CI' + } + } +} + +apply plugin: 'com.github.burrunan.s3-build-cache' + +def awsAccessKeyId = System.getenv("S3_BUILD_CACHE_ACCESS_KEY_ID") + +buildCache { + remote(com.github.burrunan.s3cache.AwsS3BuildCache) { + region = 'us-west-2' + bucket = 'opentelemetry-java-instrumentation-gradle-cache' + push = isCI && awsAccessKeyId != null && !awsAccessKeyId.isEmpty() + } +} + +rootProject.name = 'opentelemetry-java-instrumentation' + +// agent projects +include ':opentelemetry-api-shaded-for-instrumenting' +include ':opentelemetry-ext-annotations-shaded-for-instrumenting' +include ':javaagent-bootstrap' +include ':javaagent-bootstrap-tests' +include ':javaagent-exporters' +include ':javaagent-extension-api' +include ':javaagent-tooling' +include ':javaagent' + +include ':bom-alpha' +include ':instrumentation-api' +include ':instrumentation-api-caching' +include ':javaagent-api' + +// misc +include ':dependencyManagement' +include ':testing:agent-exporter' +include ':testing:agent-for-testing' +include ':testing:armeria-shaded-for-testing' +include ':testing-common' +include ':testing-common:integration-tests' +include ':testing-common:library-for-integration-tests' + +// smoke tests +include ':smoke-tests' + +include ':instrumentation:apache-dubbo-2.7:javaagent' +include ':instrumentation:apache-dubbo-2.7:library' +include ':instrumentation:apache-dubbo-2.7:testing' +//include ':instrumentation:apache-httpasyncclient-4.1:javaagent' +include ':instrumentation:apache-httpclient:apache-httpclient-2.0:javaagent' +include ':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent' +include ':instrumentation:apache-httpclient:apache-httpclient-5.0:javaagent' +include ':instrumentation:docean:javaagent' +include ':instrumentation:internal:internal-class-loader:javaagent' +include ':instrumentation:internal:internal-class-loader:javaagent-integration-tests' +include ':instrumentation:internal:internal-eclipse-osgi-3.6:javaagent' +include ':instrumentation:internal:internal-proxy:javaagent' +include ':instrumentation:internal:internal-proxy:javaagent-unit-tests' +include ':instrumentation:internal:internal-url-class-loader:javaagent' +include ':instrumentation:internal:internal-url-class-loader:javaagent-integration-tests' +include ':instrumentation:executors:javaagent' +include ':instrumentation:external-annotations:javaagent' +include ':instrumentation:external-annotations:javaagent-unit-tests' +//include ':instrumentation:finatra-2.9:javaagent' +//include ':instrumentation:geode-1.4:javaagent' +//include ':instrumentation:google-http-client-1.19:javaagent' +//include ':instrumentation:grails-3.0:javaagent' +//include ':instrumentation:grizzly-2.0:javaagent' +include ':instrumentation:grpc-1.6:javaagent' +include ':instrumentation:grpc-1.6:library' +include ':instrumentation:grpc-1.6:testing' +//include ':instrumentation:guava-10.0:javaagent' +//include ':instrumentation:guava-10.0:library' +//include ':instrumentation:gwt-2.0:javaagent' +include ':instrumentation:hera:javaagent' +//include ':instrumentation:hibernate:hibernate-3.3:javaagent' +//include ':instrumentation:hibernate:hibernate-4.0:javaagent' +//include ':instrumentation:hibernate:hibernate-common:javaagent' +//include ':instrumentation:hibernate:hibernate-procedure-call-4.3:javaagent' +//include ':instrumentation:http-url-connection:javaagent' +//include ':instrumentation:hystrix-1.4:javaagent' +include ':instrumentation:java-http-client:javaagent' +//include ':instrumentation:jaxrs:jaxrs-1.0:javaagent' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-arquillian-testing' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-cxf-3.2:javaagent' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-jersey-2.0:javaagent' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-payara-testing' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-3.0:javaagent' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-3.1:javaagent' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-common:javaagent' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-testing' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-tomee-testing' +//include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-wildfly-testing' +//include ':instrumentation:jaxrs-client:jaxrs-client-1.1:javaagent' +//include ':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-common:javaagent' +//include ':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-cxf-3.0:javaagent' +//include ':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-jersey-2.0:javaagent' +//include ':instrumentation:jaxrs-client:jaxrs-client-2.0:jaxrs-client-2.0-resteasy-3.0:javaagent' +//include ':instrumentation:jaxws:jaxws-2.0:javaagent' +//include ':instrumentation:jaxws:jaxws-2.0-axis2-1.6:javaagent' +//include ':instrumentation:jaxws:jaxws-2.0-axis2-1.6:library' +//include ':instrumentation:jaxws:jaxws-2.0-cxf-3.0:javaagent' +//include ':instrumentation:jaxws:jaxws-2.0-cxf-3.0:library' +//include ':instrumentation:jaxws:jaxws-2.0-metro-2.2:javaagent' +//include ':instrumentation:jaxws:jaxws-2.0-testing' +//include ':instrumentation:jaxws:jaxws-common:library' +//include ':instrumentation:jaxws:jws-1.1:javaagent' +include ':instrumentation:jdbc:javaagent' +include ':instrumentation:jdbc:javaagent-unit-tests' +include ':instrumentation:jedis:jedis-1.4:javaagent' +include ':instrumentation:jedis:jedis-3.0:javaagent' +include ':instrumentation:jedis:jedis-4.0:javaagent' +include ':instrumentation:jetty:jetty-8.0:javaagent' +include ':instrumentation:jetty:jetty-11.0:javaagent' +include ':instrumentation:jetty:jetty-common:javaagent' +//include ':instrumentation:jms-1.1:javaagent' +//include ':instrumentation:jms-1.1:javaagent-unit-tests' +//include ':instrumentation:jsf:jsf-common:library' +//include ':instrumentation:jsf:jsf-testing-common' +//include ':instrumentation:jsf:mojarra-1.2:javaagent' +//include ':instrumentation:jsf:myfaces-1.2:javaagent' +//include ':instrumentation:jsp-2.3:javaagent' +//include ':instrumentation:kafka-clients-0.11:javaagent' +//include ':instrumentation:kafka-streams-0.11:javaagent' +//include ':instrumentation:kotlinx-coroutines:javaagent' +//include ':instrumentation:kubernetes-client-7.0:javaagent' +//include ':instrumentation:kubernetes-client-7.0:javaagent-unit-tests' +include ':instrumentation:lettuce:lettuce-common:library' +include ':instrumentation:lettuce:lettuce-4.0:javaagent' +include ':instrumentation:lettuce:lettuce-5.0:javaagent' +include ':instrumentation:lettuce:lettuce-5.1:javaagent' +include ':instrumentation:lettuce:lettuce-5.1:library' +include ':instrumentation:lettuce:lettuce-5.1:testing' +//include ':instrumentation:liberty:compile-stub' +//include ':instrumentation:liberty:liberty:javaagent' +//include ':instrumentation:liberty:liberty-dispatcher:javaagent' +//include ':instrumentation:log4j:log4j-1.2:javaagent' +//include ':instrumentation:log4j:log4j-2.7:javaagent' +//include ':instrumentation:log4j:log4j-2.13.2:javaagent' +//include ':instrumentation:log4j:log4j-2.13.2:library' +//include ':instrumentation:log4j:log4j-2-testing' +include ':instrumentation:logback-1.0:javaagent' +include ':instrumentation:logback-1.0:library' +include ':instrumentation:logback-1.0:testing' +include ':instrumentation:methods:javaagent' +include ':instrumentation:mongo:mongo-3.1:javaagent' +include ':instrumentation:mongo:mongo-3.1:library' +include ':instrumentation:mongo:mongo-3.1:testing' +include ':instrumentation:mongo:mongo-3.7:javaagent' +include ':instrumentation:mongo:mongo-4.0:javaagent' +include ':instrumentation:mongo:mongo-async-3.3:javaagent' +include ':instrumentation:mongo:mongo-testing' +//include ':instrumentation:netty:netty-3.8:javaagent' +include ':instrumentation:netty:netty-4.0:javaagent' +include ':instrumentation:netty:netty-4.1:library' +include ':instrumentation:netty:netty-4.1:javaagent' +include ':instrumentation:netty:netty-4-common:javaagent' +include ':instrumentation:okhttp:okhttp-2.2:javaagent' +include ':instrumentation:okhttp:okhttp-3.0:javaagent' +include ':instrumentation:okhttp:okhttp-3.0:library' +include ':instrumentation:okhttp:okhttp-3.0:testing' +include ':instrumentation:opentelemetry-annotations-1.0:javaagent' +include ':instrumentation:opentelemetry-api-1.0:javaagent' +include ':instrumentation:opentelemetry-api-metrics-1.0:javaagent' +//include ':instrumentation:oshi:javaagent' +//include ':instrumentation:oshi:library' +//include ':instrumentation:play:play-2.4:javaagent' +//include ':instrumentation:play:play-2.6:javaagent' +//include ':instrumentation:play-ws:play-ws-1.0:javaagent' +//include ':instrumentation:play-ws:play-ws-2.0:javaagent' +//include ':instrumentation:play-ws:play-ws-2.1:javaagent' +//include ':instrumentation:play-ws:play-ws-common:javaagent' +//include ':instrumentation:play-ws:play-ws-testing' +//include ':instrumentation:rabbitmq-2.7:javaagent' +//include ':instrumentation:ratpack-1.4:javaagent' +include ':instrumentation:reactor-3.1:javaagent' +include ':instrumentation:reactor-3.1:library' +include ':instrumentation:reactor-3.1:testing' +//include ':instrumentation:reactor-netty:reactor-netty-0.9:javaagent' +//include ':instrumentation:reactor-netty:reactor-netty-1.0:javaagent' +//include ':instrumentation:rediscala-1.8:javaagent' +//include ':instrumentation:redisson-3.0:javaagent' +//include ':instrumentation:rmi:javaagent' +include ':instrumentation:rocketmq-client-4.8:javaagent' +include ':instrumentation:rocketmq-client-4.8:library' +include ':instrumentation:rocketmq-client-4.8:testing' +include ':instrumentation:runtime-metrics:javaagent' +include ':instrumentation:runtime-metrics:library' +//include ':instrumentation:rxjava:rxjava-1.0:library' +//include ':instrumentation:rxjava:rxjava-2.0:library' +//include ':instrumentation:rxjava:rxjava-2.0:testing' +//include ':instrumentation:rxjava:rxjava-2.0:javaagent' +//include ':instrumentation:rxjava:rxjava-3.0:library' +//include ':instrumentation:rxjava:rxjava-3.0:testing' +//include ':instrumentation:rxjava:rxjava-3.0:javaagent' +//include ':instrumentation:scala-executors:javaagent' +include ':instrumentation:servlet:servlet-common:library' +include ':instrumentation:servlet:servlet-common:javaagent' +include ':instrumentation:servlet:servlet-javax-common:library' +include ':instrumentation:servlet:servlet-javax-common:javaagent' +include ':instrumentation:servlet:servlet-2.2:library' +include ':instrumentation:servlet:servlet-2.2:javaagent' +include ':instrumentation:servlet:servlet-3.0:library' +include ':instrumentation:servlet:servlet-3.0:javaagent' +include ':instrumentation:servlet:servlet-5.0:library' +include ':instrumentation:servlet:servlet-5.0:javaagent' +//include ':instrumentation:spark-2.3:javaagent' +//include ':instrumentation:spring:spring-batch-3.0:javaagent' +//include ':instrumentation:spring:spring-core-2.0:javaagent' +//include ':instrumentation:spring:spring-data-1.8:javaagent' +//include ':instrumentation:spring:spring-integration-4.1:javaagent' +//include ':instrumentation:spring:spring-integration-4.1:library' +//include ':instrumentation:spring:spring-integration-4.1:testing' +//include ':instrumentation:spring:spring-scheduling-3.1:javaagent' +//include ':instrumentation:spring:spring-web-3.1:library' +include ':instrumentation:spring:spring-webmvc-3.1:javaagent' +include ':instrumentation:spring:spring-webmvc-3.1:library' +//include ':instrumentation:spring:spring-webflux-5.0:javaagent' +//include ':instrumentation:spring:spring-webflux-5.0:library' +//include ':instrumentation:spring:spring-ws-2.0:javaagent' +//include ':instrumentation:spring:spring-boot-autoconfigure' +//include ':instrumentation:spring:starters:spring-starter' +//include ':instrumentation:spring:starters:jaeger-exporter-starter' +//include ':instrumentation:spring:starters:otlp-exporter-starter' +//include ':instrumentation:spring:starters:zipkin-exporter-starter' +//include ':instrumentation:spymemcached-2.12:javaagent' +//include ':instrumentation:struts-2.3:javaagent' +//include ':instrumentation:tapestry-5.4:javaagent' +include ':instrumentation:tomcat:tomcat-7.0:javaagent' +include ':instrumentation:tomcat:tomcat-10.0:javaagent' +include ':instrumentation:tomcat:tomcat-common:javaagent' +//include ':instrumentation:tesla:javaagent' +//include ':instrumentation:twilio-6.6:javaagent' +//include ':instrumentation:undertow-1.4:javaagent' +//include ':instrumentation:vaadin-14.2:javaagent' +//include ':instrumentation:vaadin-14.2:testing' +//include ':instrumentation:vertx-web-3.0:javaagent' +//include ':instrumentation:vertx-reactive-3.5:javaagent' +//include ':instrumentation:wicket-8.0:javaagent' + +// benchmark +include ':benchmark' +include ':benchmark-integration' +include ':benchmark-integration:jetty-perftest' +include ':benchmark-e2e' + +def setBuildFile(project) { + // javaagent-unittests modules are needed until those projects have library modules + // at which time those unittests can be moved to the library modules + // + // javaagent-integration-tests modules are only needed by "internal-" instrumentation + // which needs to be tested by creating some other "test" instrumentation + if (['javaagent', 'javaagent-unit-tests', 'javaagent-integration-tests', 'library', 'testing'].contains(project.projectDir.name) && project.path != ':javaagent') { + project.buildFileName = "${project.projectDir.parentFile.name}-${project.projectDir.name}.gradle" + } else { + project.buildFileName = "${project.name}.gradle" + } + if (!(project.buildFile.exists())) { + project.buildFileName += ".kts" + } + project.children.each { + setBuildFile(it) + } +} + +rootProject.children.each { + setBuildFile(it) +} + diff --git a/opentelemetry-java-instrumentation/smoke-tests/README.md b/opentelemetry-java-instrumentation/smoke-tests/README.md new file mode 100644 index 000000000..ffcc8ac26 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/README.md @@ -0,0 +1,5 @@ +# Smoke Tests +Assert that various applications will start up with the JavaAgent without any obvious ill effects. + +Each subproject underneath `smoke-tests` produces one or more docker images containing some application +under the test. Various tests in the main module then use them to run the appropriate tests. diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/build.gradle b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/build.gradle new file mode 100644 index 000000000..20e8aff06 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/build.gradle @@ -0,0 +1,96 @@ +import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage +import com.bmuschko.gradle.docker.tasks.image.DockerPushImage + +plugins { + id 'java' + id 'com.google.cloud.tools.jib' version '2.5.0' + id "com.bmuschko.docker-remote-api" version "6.7.0" + id 'com.github.johnrengelman.shadow' version '6.1.0' + id "de.undercouch.download" version "4.1.1" +} + +group = 'io.opentelemetry' +version = '0.0.1-SNAPSHOT' + +repositories { + mavenCentral() +} + +compileJava { + options.release.set(11) +} + +dependencies { + implementation("com.linecorp.armeria:armeria-grpc:1.0.0") + implementation("io.opentelemetry:opentelemetry-proto:1.3.0-alpha") + implementation("org.slf4j:slf4j-simple:1.7.30") +} + +shadowJar { + manifest { + attributes 'Main-Class': 'io.opentelemetry.smoketest.fakebackend.FakeBackendMain' + } +} + +ext { + extraTag = findProperty("extraTag") ?: new Date().format("yyyyMMdd.HHmmSS") +} + +jib { + from.image = "gcr.io/distroless/java-debian10:11" + to.image = "ghcr.io/open-telemetry/java-test-containers:smoke-fake-backend-$extraTag" +} + + +//windows containers are built manually since jib does not support windows containers yet +def backendDockerBuildDir = new File(project.buildDir, "docker-backend") + +task windowsBackendImagePrepare(type: Copy) { + dependsOn(shadowJar) + into(backendDockerBuildDir) + from("src/docker/backend") + from(shadowJar.outputs) { + rename { _ -> "fake-backend.jar" } + } +} + +task windowsBackendImageBuild(type: DockerBuildImage) { + dependsOn(windowsBackendImagePrepare) + inputDir = backendDockerBuildDir + + it.images.add "ghcr.io/open-telemetry/java-test-containers:smoke-fake-backend-windows-$extraTag" + it.dockerFile = new File(backendDockerBuildDir, "windows.dockerfile") +} + +def collectorDockerBuildDir = new File(project.buildDir, "docker-collector") + +task windowsCollectorBinaryDownload(type: Download) { + doFirst { + collectorDockerBuildDir.mkdirs() + } + + src("https://github.com/open-telemetry/opentelemetry-collector/releases/latest/download/otelcol_windows_amd64.exe") + dest(collectorDockerBuildDir) +} + +task windowsCollectorImagePrepare(type: Copy) { + dependsOn(windowsCollectorBinaryDownload) + into(collectorDockerBuildDir) + from("src/docker/collector") +} + +task windowsCollectorImageBuild(type: DockerBuildImage) { + dependsOn(windowsCollectorImagePrepare) + inputDir = collectorDockerBuildDir + + it.images.add "ghcr.io/open-telemetry/java-test-containers:collector-windows-$extraTag" + it.dockerFile = new File(collectorDockerBuildDir, "windows.dockerfile") +} + +tasks.create("dockerPush", DockerPushImage) { + group = "publishing" + description = "Push all Docker images for the test backend" + dependsOn(windowsBackendImageBuild, windowsCollectorImageBuild) + images.set(["ghcr.io/open-telemetry/java-test-containers:smoke-fake-backend-windows-$extraTag", + "ghcr.io/open-telemetry/java-test-containers:collector-windows-$extraTag"]) +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..c9d55ea1c --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637 +size 59203 diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..2cd63bbe0 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionSha256Sum=e6f83508f0970452f56197f610d13c5f593baaf43c0e3c6a571e5967be754025 \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradlew b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradlew.bat b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/opentelemetry-java-instrumentation/smoke-tests/fake-backend/settings.gradle b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/settings.gradle new file mode 100644 index 000000000..781836a90 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'fake-backend' diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/docker/backend/windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/docker/backend/windows.dockerfile new file mode 100644 index 000000000..e57d22214 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/docker/backend/windows.dockerfile @@ -0,0 +1,3 @@ +FROM winamd64/openjdk:11.0.11-jdk-windowsservercore-1809 +COPY fake-backend.jar /fake-backend.jar +CMD ["java", "-jar", "/fake-backend.jar"] diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/docker/collector/windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/docker/collector/windows.dockerfile new file mode 100644 index 000000000..dc2ad5855 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/docker/collector/windows.dockerfile @@ -0,0 +1,4 @@ +FROM mcr.microsoft.com/windows/servercore:ltsc2019 +COPY otelcol_windows_amd64.exe /otelcol_windows_amd64.exe +ENV NO_WINDOWS_SERVICE=1 +ENTRYPOINT /otelcol_windows_amd64.exe diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeBackendMain.java b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeBackendMain.java new file mode 100644 index 000000000..790dd047a --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeBackendMain.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.smoketest.fakebackend; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.module.SimpleSerializers; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.linecorp.armeria.common.HttpData; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.MediaType; +import com.linecorp.armeria.server.Server; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.server.healthcheck.HealthCheckService; +import io.netty.buffer.ByteBufOutputStream; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import java.io.IOException; +import java.io.OutputStream; +import org.curioswitch.common.protobuf.json.MessageMarshaller; + +public class FakeBackendMain { + + private static final JsonMapper OBJECT_MAPPER; + + static { + var marshaller = + MessageMarshaller.builder() + .register(ExportTraceServiceRequest.getDefaultInstance()) + .register(ExportMetricsServiceRequest.getDefaultInstance()) + .build(); + + var mapper = JsonMapper.builder(); + var module = new SimpleModule(); + var serializers = new SimpleSerializers(); + serializers.addSerializer( + new StdSerializer<>(ExportTraceServiceRequest.class) { + @Override + public void serialize( + ExportTraceServiceRequest value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + marshaller.writeValue(value, gen); + } + }); + serializers.addSerializer( + new StdSerializer<>(ExportMetricsServiceRequest.class) { + @Override + public void serialize( + ExportMetricsServiceRequest value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + marshaller.writeValue(value, gen); + } + }); + module.setSerializers(serializers); + mapper.addModule(module); + OBJECT_MAPPER = mapper.build(); + } + + public static void main(String[] args) { + var traceCollector = new FakeTraceCollectorService(); + var metricsCollector = new FakeMetricsCollectorService(); + var server = + Server.builder() + .http(8080) + .service(GrpcService.builder() + .addService(traceCollector) + .addService(metricsCollector) + .build()) + .service( + "/clear", + (ctx, req) -> { + traceCollector.clearRequests(); + metricsCollector.clearRequests(); + return HttpResponse.of(HttpStatus.OK); + }) + .service( + "/get-traces", + (ctx, req) -> { + var requests = traceCollector.getRequests(); + var buf = new ByteBufOutputStream(ctx.alloc().buffer()); + OBJECT_MAPPER.writeValue((OutputStream) buf, requests); + return HttpResponse.of( + HttpStatus.OK, MediaType.JSON, HttpData.wrap(buf.buffer())); + }) + .service( + "/get-metrics", + (ctx, req) -> { + var requests = metricsCollector.getRequests(); + var buf = new ByteBufOutputStream(ctx.alloc().buffer()); + OBJECT_MAPPER.writeValue((OutputStream) buf, requests); + return HttpResponse.of( + HttpStatus.OK, MediaType.JSON, HttpData.wrap(buf.buffer())); + }) + .service("/health", HealthCheckService.of()) + .build(); + + server.start().join(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> server.stop().join())); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeMetricsCollectorService.java b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeMetricsCollectorService.java new file mode 100644 index 000000000..8f7b7b0fb --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeMetricsCollectorService.java @@ -0,0 +1,33 @@ +package io.opentelemetry.smoketest.fakebackend; + +import com.google.common.collect.ImmutableList; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +class FakeMetricsCollectorService extends MetricsServiceGrpc.MetricsServiceImplBase { + + private final BlockingQueue exportRequests = + new LinkedBlockingDeque<>(); + + List getRequests() { + return ImmutableList.copyOf(exportRequests); + } + + void clearRequests() { + exportRequests.clear(); + } + + @Override + public void export( + ExportMetricsServiceRequest request, + StreamObserver responseObserver) { + exportRequests.add(request); + responseObserver.onNext(ExportMetricsServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeTraceCollectorService.java b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeTraceCollectorService.java new file mode 100644 index 000000000..54214d872 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/fake-backend/src/main/java/io/opentelemetry/smoketest/fakebackend/FakeTraceCollectorService.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.smoketest.fakebackend; + +import com.google.common.collect.ImmutableList; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +class FakeTraceCollectorService extends TraceServiceGrpc.TraceServiceImplBase { + + private final BlockingQueue exportRequests = + new LinkedBlockingDeque<>(); + + List getRequests() { + return ImmutableList.copyOf(exportRequests); + } + + void clearRequests() { + exportRequests.clear(); + } + + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + exportRequests.add(request); + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/grpc/build.gradle b/opentelemetry-java-instrumentation/smoke-tests/grpc/build.gradle new file mode 100644 index 000000000..d99c3ead8 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/build.gradle @@ -0,0 +1,47 @@ +plugins { + id "java" + id "com.google.cloud.tools.jib" version "2.6.0" +} + +group = "io.opentelemetry" +version = "0.0.1-SNAPSHOT" + +repositories { + mavenCentral() + // this is only needed for the working against unreleased otel-java snapshots + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + content { + includeGroup "io.opentelemetry" + } + } +} + +dependencies { + implementation platform("io.grpc:grpc-bom:1.33.1") + implementation platform("run.mone:opentelemetry-bom") + implementation platform("run.mone:opentelemetry-bom-alpha") + implementation platform("org.apache.logging.log4j:log4j-bom:2.13.2") + + implementation "io.grpc:grpc-netty-shaded" + implementation "io.grpc:grpc-protobuf" + implementation "io.grpc:grpc-stub" + implementation "run.mone:opentelemetry-proto" + implementation "run.mone:opentelemetry-extension-annotations" + implementation "org.apache.logging.log4j:log4j-core" + + runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl") +} + +compileJava { + options.release = 8 +} + +def targetJDK = project.hasProperty("targetJDK") ? project.targetJDK : 11 + +def tag = findProperty("tag") ?: new Date().format("yyyyMMdd.HHmmSS") + +jib { + from.image = "bellsoft/liberica-openjdk-alpine:$targetJDK" + to.image = "ghcr.io/open-telemetry/java-test-containers:smoke-grpc-jdk$targetJDK-$tag" +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/grpc/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java-instrumentation/smoke-tests/grpc/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..c9d55ea1c --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637 +size 59203 diff --git a/opentelemetry-java-instrumentation/smoke-tests/grpc/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java-instrumentation/smoke-tests/grpc/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..6c9a22477 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/opentelemetry-java-instrumentation/smoke-tests/grpc/gradlew b/opentelemetry-java-instrumentation/smoke-tests/grpc/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java-instrumentation/smoke-tests/grpc/gradlew.bat b/opentelemetry-java-instrumentation/smoke-tests/grpc/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/opentelemetry-java-instrumentation/smoke-tests/grpc/settings.gradle b/opentelemetry-java-instrumentation/smoke-tests/grpc/settings.gradle new file mode 100644 index 000000000..e8421fcbd --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.6/userguide/multi_project_builds.html + */ + +rootProject.name = 'grpc' diff --git a/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/java/io/opentelemetry/smoketest/grpc/TestMain.java b/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/java/io/opentelemetry/smoketest/grpc/TestMain.java new file mode 100644 index 000000000..d64c7941e --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/java/io/opentelemetry/smoketest/grpc/TestMain.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.grpc; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import io.grpc.Server; +import io.grpc.ServerBuilder; + +public class TestMain { + + private static final Logger logger = LogManager.getLogger(); + + public static void main(String[] args) throws Exception { + TestService service = new TestService(); + Server server = ServerBuilder.forPort(8080).addService(service).directExecutor().build().start(); + Runtime.getRuntime().addShutdownHook(new Thread(server::shutdownNow)); + logger.info("Server started at port 8080."); + server.awaitTermination(); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/java/io/opentelemetry/smoketest/grpc/TestService.java b/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/java/io/opentelemetry/smoketest/grpc/TestService.java new file mode 100644 index 000000000..63cd4eacf --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/java/io/opentelemetry/smoketest/grpc/TestService.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.grpc; + +import io.grpc.stub.StreamObserver; +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TestService extends TraceServiceGrpc.TraceServiceImplBase { + + private static final Logger logger = LogManager.getLogger(); + + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + logger.info("Request received"); + withSpan(); + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + + @WithSpan + public String withSpan() { + return "Hi"; + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/resources/log4j2.xml b/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/resources/log4j2.xml new file mode 100644 index 000000000..5e66a3a98 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/grpc/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/README.md b/opentelemetry-java-instrumentation/smoke-tests/matrix/README.md new file mode 100644 index 000000000..de5f1a6db --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/README.md @@ -0,0 +1,6 @@ +# Smoke Test Environment Matrix +This project builds docker images containing a simple test web application deployed to various +application servers or servlet containers. For each server several relevant versions are chosen. +In addition we build separate images for several support major java versions. +This way we can test our agent with many different combinations of runtime environment, +its version and running on different JVM versions from different vendors. \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/build.gradle b/opentelemetry-java-instrumentation/smoke-tests/matrix/build.gradle new file mode 100644 index 000000000..6a088b150 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/build.gradle @@ -0,0 +1,150 @@ +import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage +import com.bmuschko.gradle.docker.tasks.image.DockerPushImage + +plugins { + id "com.bmuschko.docker-remote-api" version "6.7.0" +} + +def buildLinuxTestImagesTask = tasks.create("buildLinuxTestImages") { + group = "build" + description = "Builds all Linux Docker images for the test matrix" + +} + +def buildWindowsTestImagesTask = tasks.create("buildWindowsTestImages") { + group = "build" + description = "Builds all Windows Docker images for the test matrix" +} + +ext { + matrix = [] +} + +tasks.create("pushMatrix", DockerPushImage) { + group = "publishing" + description = "Push all Docker images for the test matrix" + images.set(project.ext.matrix) +} + +// Each line under appserver describes one matrix of (version x vm x jdk), dockerfile key overrides +// Dockerfile name, args key passes raw arguments to docker build +def linuxTargets = [ + "jetty": [ + [version: ["9.4.35"], vm: ["hotspot", "openj9"], jdk: ["8", "11", "15"]], + [version: ["10.0.0"], vm: ["hotspot", "openj9"], jdk: ["11", "15"]], + [version: ["11.0.1"], vm: ["hotspot", "openj9"], jdk: ["11", "15"], war: "servlet-5.0"] + ], + "tomcat": [ + [version: ["7.0.107"], vm: ["hotspot", "openj9"], jdk: ["8"]], + [version: ["8.5.60", "9.0.40"], vm: ["hotspot", "openj9"], jdk: ["8", "11"]], + [version: ["10.0.4"], vm: ["hotspot", "openj9"], jdk: ["11", "15"], war: "servlet-5.0"] + ], + "tomee": [ + [version: ["7.0.0"], vm: ["hotspot"], jdk: ["8"]], + [version: ["7.0.0"], vm: ["openj9"], jdk: ["8"], dockerfile: "tomee-custom"], + [version: ["8.0.6"], vm: ["hotspot"], jdk: ["8", "11"]], + [version: ["8.0.6"], vm: ["openj9"], jdk: ["8", "11"], dockerfile: "tomee-custom"] + ], + "payara": [ + [version: ["5.2020.6"], vm: ["hotspot"], jdk: ["8"], args: [tagSuffix: ""]], + [version: ["5.2020.6"], vm: ["hotspot"], jdk: ["11"], args: [tagSuffix: "-jdk11"]], + [version: ["5.2020.6"], vm: ["openj9"], jdk: ["8", "11"], dockerfile: "payara-custom-5.2020.6"] + ], + "wildfly": [ + [version: ["13.0.0.Final"], vm: ["hotspot", "openj9"], jdk: ["8"]], + [version: ["17.0.1.Final", "21.0.0.Final"], vm: ["hotspot", "openj9"], jdk: ["8", "11", "15"]] + ], + "liberty": [ + [version: ["20.0.0.12"], vm: ["hotspot", "openj9"], jdk: ["8", "11", "15"]] + ] +] + +def windowsTargets = [ + "jetty" : [ + [version: ["9.4.35"], vm: ["hotspot", "openj9"], jdk: ["8", "11", "15"], args: [sourceVersion: "9.4.35.v20201120"]], + [version: ["10.0.0"], vm: ["hotspot", "openj9"], jdk: ["11", "15"], dockerfile: "jetty-split", args: [sourceVersion: "10.0.0.beta3"]], + [version: ["11.0.1"], vm: ["hotspot", "openj9"], jdk: ["11", "15"], dockerfile: "jetty-split", args: [sourceVersion: "11.0.1"], war: "servlet-5.0"] + ], + "tomcat" : [ + [version: ["7.0.107"], vm: ["hotspot", "openj9"], jdk: ["8"], args: [majorVersion: "7"]], + [version: ["8.5.60"], vm: ["hotspot", "openj9"], jdk: ["8", "11"], args: [majorVersion: "8"]], + [version: ["9.0.40"], vm: ["hotspot", "openj9"], jdk: ["8", "11"], args: [majorVersion: "9"]], + [version: ["10.0.4"], vm: ["hotspot", "openj9"], jdk: ["11", "15"], args: [majorVersion: "10"], war: "servlet-5.0"] + ], + "tomee" : [ + [version: ["7.0.0"], vm: ["hotspot", "openj9"], jdk: ["8"]], + [version: ["8.0.6"], vm: ["hotspot", "openj9"], jdk: ["8", "11"]] + ], + "payara": [ + [version: ["5.2020.6"], vm: ["hotspot", "openj9"], jdk: ["8", "11"]] + ], + "wildfly" : [ + [version: ["13.0.0.Final"], vm: ["hotspot", "openj9"], jdk: ["8"]], + [version: ["17.0.1.Final", "21.0.0.Final"], vm: ["hotspot", "openj9"], jdk: ["8", "11"]] + ], + "liberty" : [ + [version: ["20.0.0.12"], vm: ["hotspot", "openj9"], jdk: ["8", "11", "15"], args: [release: "2020-11-11_0736"]] + ] +] + +createDockerTasks(buildLinuxTestImagesTask, linuxTargets, false) +createDockerTasks(buildWindowsTestImagesTask, windowsTargets, true) + +def configureImage(Task parentTask, server, dockerfile, version, vm, jdk, warProject, Map extraArgs, isWindows = false) { + // Using separate build directory for different war files allows using the same app.war filename + def dockerWorkingDir = new File(project.buildDir, "docker-$warProject") + def dockerFileName = isWindows ? "${dockerfile}.windows.dockerfile" : "${dockerfile}.dockerfile" + def platformSuffix = isWindows ? "-windows" : "" + + def prepareTask = tasks.register("${server}ImagePrepare-$version-jdk$jdk-$vm$platformSuffix", Copy) { + def warTask = warProject != null ? project(":$warProject").tasks["war"] : project.tasks.war + it.dependsOn(warTask) + it.into(dockerWorkingDir) + it.from("src/$dockerFileName") + it.from("src/main/docker/$server") + it.from(warTask.archiveFile) { + rename { _ -> "app.war" } + } + } + + def extraTag = findProperty("extraTag") ?: new Date().format("yyyyMMdd.HHmmSS") + def vmSuffix = vm == "hotspot" ? "" : "-$vm" + def image = "ghcr.io/open-telemetry/java-test-containers:$server-$version-jdk$jdk$vmSuffix$platformSuffix-$extraTag" + + def buildTask = tasks.register("${server}Image-$version-jdk$jdk$vmSuffix$platformSuffix", DockerBuildImage) { + it.dependsOn(prepareTask) + group = "build" + description = "Builds Docker image with $server $version on JDK $jdk-$vm${isWindows ? ' on Windows' : ''}" + + it.inputDir.set(dockerWorkingDir) + it.images.add(image) + it.dockerFile.set(new File(dockerWorkingDir, dockerFileName)) + it.buildArgs.set(extraArgs + [jdk: jdk, vm: vm, version: version]) + it.doLast { + project.ext.matrix.add(image) + } + } + + parentTask.dependsOn(buildTask) + return image +} + +def createDockerTasks(Task parentTask, targets, isWindows) { + Set resultImages = [] + targets.each { server, matrices -> + matrices.forEach { entry -> + def dockerfile = entry["dockerfile"]?.toString() ?: server + def extraArgs = (entry["args"] ?: [:]) as Map + def warProject = entry["war"] ?: "servlet-3.0" + + entry.version.forEach { version -> + entry.vm.forEach { vm -> + entry.jdk.forEach { jdk -> + resultImages.add(configureImage(parentTask, server, dockerfile, version, vm, jdk, warProject, extraArgs, isWindows)) + } + } + } + } + } + return resultImages +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java-instrumentation/smoke-tests/matrix/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..912744eeb --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857 +size 58910 diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java-instrumentation/smoke-tests/matrix/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..77c9546f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionSha256Sum=3239b5ed86c3838a37d983ac100573f64c1f3fd8e1eb6c89fa5f9529b5ec091d \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/gradlew b/opentelemetry-java-instrumentation/smoke-tests/matrix/gradlew new file mode 100755 index 000000000..fbd7c5158 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/gradlew.bat b/opentelemetry-java-instrumentation/smoke-tests/matrix/gradlew.bat new file mode 100644 index 000000000..5093609d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/build.gradle b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/build.gradle new file mode 100644 index 000000000..1ea70d97a --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/build.gradle @@ -0,0 +1,15 @@ +plugins { + id "war" +} + +compileJava { + options.release.set(8) +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("javax.servlet:javax.servlet-api:3.0.1") +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java new file mode 100644 index 000000000..d246738c6 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import javax.servlet.AsyncContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +public class AsyncGreetingServlet extends GreetingServlet { + private static final BlockingQueue jobQueue = new LinkedBlockingQueue<>(); + private static final ExecutorService executor = Executors.newFixedThreadPool(2); + + @Override + public void init() throws ServletException { + executor.submit(new Runnable() { + @Override + public void run() { + try { + while (true) { + AsyncContext ac = jobQueue.take(); + executor.submit(() -> handleRequest(ac)); + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + } + + @Override + public void destroy() { + executor.shutdownNow(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + AsyncContext ac = req.startAsync(req, resp); + jobQueue.add(ac); + } + + private void handleRequest(AsyncContext ac) { + ac.dispatch("/greeting"); + } + +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionRequestListener.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionRequestListener.java new file mode 100644 index 000000000..468949f89 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionRequestListener.java @@ -0,0 +1,21 @@ +package io.opentelemetry.smoketest.matrix; + +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; + +public class ExceptionRequestListener implements ServletRequestListener { + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + if ("true".equals(sre.getServletRequest().getParameter("throwOnRequestDestroyed"))) { + throw new IllegalStateException("This is expected"); + } + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + if ("true".equals(sre.getServletRequest().getParameter("throwOnRequestInitialized"))) { + throw new IllegalStateException("This is expected"); + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionServlet.java new file mode 100644 index 000000000..3b5560929 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionServlet.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import java.io.IOException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class ExceptionServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + throw new IllegalStateException("This is expected"); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ForwardServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ForwardServlet.java new file mode 100644 index 000000000..6b401d84f --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/ForwardServlet.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class ForwardServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher("/hello.txt").forward(req, resp); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/GreetingServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/GreetingServlet.java new file mode 100644 index 000000000..c0d5cb236 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/GreetingServlet.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Objects; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class GreetingServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String path = (req.getContextPath() + "/headers").replace("//", "/"); + URL url = new URL("http", "localhost", req.getLocalPort(), path); + URLConnection urlConnection = url.openConnection(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (InputStream remoteInputStream = urlConnection.getInputStream()) { + long bytesRead = transfer(remoteInputStream, buffer); + String responseBody = buffer.toString("UTF-8"); + ServletOutputStream outputStream = resp.getOutputStream(); + outputStream.print( + bytesRead + " bytes read by " + urlConnection.getClass().getName() + "\n" + responseBody); + outputStream.flush(); + } + } + + // We have to run on Java 8, so no Java 9 stream transfer goodies for us. + private long transfer(InputStream from, OutputStream to) throws IOException { + Objects.requireNonNull(to, "out"); + long transferred = 0; + byte[] buffer = new byte[65535]; + int read; + while ((read = from.read(buffer, 0, buffer.length)) >= 0) { + to.write(buffer, 0, read); + transferred += read; + } + return transferred; + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/HeaderDumpingServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/HeaderDumpingServlet.java new file mode 100644 index 000000000..db47c4b8a --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/HeaderDumpingServlet.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class HeaderDumpingServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + PrintWriter response = resp.getWriter(); + Enumeration headerNames = req.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + response.write(headerName + ": "); + + List headers = Collections.list(req.getHeaders(headerName)); + if (headers.size() == 1) { + response.write(headers.get(0)); + } else { + response.write("["); + for (String header : headers) { + response.write(" " + header + ",\n"); + } + response.write("]"); + } + response.write("\n"); + } + + response.flush(); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/IncludeServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/IncludeServlet.java new file mode 100644 index 000000000..a2576716b --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/IncludeServlet.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class IncludeServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher("/hello.txt").include(req, resp); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/JspServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/JspServlet.java new file mode 100644 index 000000000..5221471e6 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/JspServlet.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import java.io.IOException; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class JspServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + RequestDispatcher resultView = req.getRequestDispatcher("test.jsp"); + resultView.forward(req, resp); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/WEB-INF/web.xml b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..845e30374 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,66 @@ + + + io.opentelemetry.smoketest.matrix.ExceptionRequestListener + + + Headers + io.opentelemetry.smoketest.matrix.HeaderDumpingServlet + + + Greeting + io.opentelemetry.smoketest.matrix.GreetingServlet + + + AsyncGreeting + io.opentelemetry.smoketest.matrix.AsyncGreetingServlet + true + + + Exception + io.opentelemetry.smoketest.matrix.ExceptionServlet + + + Forward + io.opentelemetry.smoketest.matrix.ForwardServlet + + + Include + io.opentelemetry.smoketest.matrix.IncludeServlet + + + Jsp + io.opentelemetry.smoketest.matrix.JspServlet + + + Headers + /headers + + + Greeting + /greeting + + + AsyncGreeting + /asyncgreeting + + + Exception + /exception + + + Forward + /forward + + + Include + /include + + + Jsp + /jsp + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/hello.txt b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/hello.txt new file mode 100644 index 000000000..5ab2f8a43 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/hello.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/test.jsp b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/test.jsp new file mode 100644 index 000000000..bc2bb7ae2 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-3.0/src/main/webapp/test.jsp @@ -0,0 +1,10 @@ + +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Successful JSP test + + + This JSP demonstrates that Otel instrumentation agent does not break JSP compilation and loading. + + diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/build.gradle b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/build.gradle new file mode 100644 index 000000000..180d58505 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/build.gradle @@ -0,0 +1,15 @@ +plugins { + id "war" +} + +compileJava { + options.release.set(11) +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java new file mode 100644 index 000000000..284e528dc --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +public class AsyncGreetingServlet extends GreetingServlet { + private static final BlockingQueue jobQueue = new LinkedBlockingQueue<>(); + private static final ExecutorService executor = Executors.newFixedThreadPool(2); + + @Override + public void init() throws ServletException { + executor.submit(new Runnable() { + @Override + public void run() { + try { + while (true) { + AsyncContext ac = jobQueue.take(); + executor.submit(() -> handleRequest(ac)); + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + } + + @Override + public void destroy() { + executor.shutdownNow(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + AsyncContext ac = req.startAsync(req, resp); + jobQueue.add(ac); + } + + private void handleRequest(AsyncContext ac) { + ac.dispatch("/greeting"); + } + +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionRequestListener.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionRequestListener.java new file mode 100644 index 000000000..435fc5c8e --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionRequestListener.java @@ -0,0 +1,21 @@ +package io.opentelemetry.smoketest.matrix; + +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; + +public class ExceptionRequestListener implements ServletRequestListener { + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + if ("true".equals(sre.getServletRequest().getParameter("throwOnRequestDestroyed"))) { + throw new IllegalStateException("This is expected"); + } + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + if ("true".equals(sre.getServletRequest().getParameter("throwOnRequestInitialized"))) { + throw new IllegalStateException("This is expected"); + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionServlet.java new file mode 100644 index 000000000..ae4e4c4fb --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ExceptionServlet.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ExceptionServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + throw new IllegalStateException("This is expected"); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ForwardServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ForwardServlet.java new file mode 100644 index 000000000..add8bec40 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/ForwardServlet.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ForwardServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher("/hello.txt").forward(req, resp); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/GreetingServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/GreetingServlet.java new file mode 100644 index 000000000..e4b9ead08 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/GreetingServlet.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Objects; + +public class GreetingServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String path = (req.getContextPath() + "/headers").replace("//", "/"); + URL url = new URL("http", "localhost", req.getLocalPort(), path); + URLConnection urlConnection = url.openConnection(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (InputStream remoteInputStream = urlConnection.getInputStream()) { + long bytesRead = transfer(remoteInputStream, buffer); + String responseBody = buffer.toString("UTF-8"); + ServletOutputStream outputStream = resp.getOutputStream(); + outputStream.print( + bytesRead + " bytes read by " + urlConnection.getClass().getName() + "\n" + responseBody); + outputStream.flush(); + } + } + + // We have to run on Java 8, so no Java 9 stream transfer goodies for us. + private long transfer(InputStream from, OutputStream to) throws IOException { + Objects.requireNonNull(to, "out"); + long transferred = 0; + byte[] buffer = new byte[65535]; + int read; + while ((read = from.read(buffer, 0, buffer.length)) >= 0) { + to.write(buffer, 0, read); + transferred += read; + } + return transferred; + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/HeaderDumpingServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/HeaderDumpingServlet.java new file mode 100644 index 000000000..75a6e17fa --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/HeaderDumpingServlet.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +public class HeaderDumpingServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + PrintWriter response = resp.getWriter(); + Enumeration headerNames = req.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + response.write(headerName + ": "); + + List headers = Collections.list(req.getHeaders(headerName)); + if (headers.size() == 1) { + response.write(headers.get(0)); + } else { + response.write("["); + for (String header : headers) { + response.write(" " + header + ",\n"); + } + response.write("]"); + } + response.write("\n"); + } + + response.flush(); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/IncludeServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/IncludeServlet.java new file mode 100644 index 000000000..7973cc26f --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/IncludeServlet.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class IncludeServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher("/hello.txt").include(req, resp); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/JspServlet.java b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/JspServlet.java new file mode 100644 index 000000000..aa140d16e --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/JspServlet.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.matrix; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class JspServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + RequestDispatcher resultView = req.getRequestDispatcher("test.jsp"); + resultView.forward(req, resp); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/WEB-INF/web.xml b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..4ee1f5472 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,66 @@ + + + io.opentelemetry.smoketest.matrix.ExceptionRequestListener + + + Headers + io.opentelemetry.smoketest.matrix.HeaderDumpingServlet + + + Greeting + io.opentelemetry.smoketest.matrix.GreetingServlet + + + AsyncGreeting + io.opentelemetry.smoketest.matrix.AsyncGreetingServlet + true + + + Exception + io.opentelemetry.smoketest.matrix.ExceptionServlet + + + Forward + io.opentelemetry.smoketest.matrix.ForwardServlet + + + Include + io.opentelemetry.smoketest.matrix.IncludeServlet + + + Jsp + io.opentelemetry.smoketest.matrix.JspServlet + + + Headers + /headers + + + Greeting + /greeting + + + AsyncGreeting + /asyncgreeting + + + Exception + /exception + + + Forward + /forward + + + Include + /include + + + Jsp + /jsp + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/hello.txt b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/hello.txt new file mode 100644 index 000000000..5ab2f8a43 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/hello.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/test.jsp b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/test.jsp new file mode 100644 index 000000000..bc2bb7ae2 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/servlet-5.0/src/main/webapp/test.jsp @@ -0,0 +1,10 @@ + +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Successful JSP test + + + This JSP demonstrates that Otel instrumentation agent does not break JSP compilation and loading. + + diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/settings.gradle b/opentelemetry-java-instrumentation/smoke-tests/matrix/settings.gradle new file mode 100644 index 000000000..655ace030 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'matrix' + +include ':servlet-3.0' +include ':servlet-5.0' diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty-split.windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty-split.windows.dockerfile new file mode 100644 index 000000000..38af1676a --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty-split.windows.dockerfile @@ -0,0 +1,21 @@ +ARG jdk +ARG vm +ARG sourceVersion + +# Unzip in a separate container so that zip file layer is not part of final image +FROM mcr.microsoft.com/windows/servercore:1809 as builder +ARG sourceVersion +ADD https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/${sourceVersion}/jetty-home-${sourceVersion}.zip /server.zip +RUN ["powershell", "-Command", "expand-archive -Path /server.zip -DestinationPath /server"] + +FROM adoptopenjdk:${jdk}-jdk-${vm}-windowsservercore-1809 +ARG sourceVersion +# Make /server the base directory to simplify all further paths +COPY --from=builder /server/jetty-home-${sourceVersion} /server +RUN ["powershell", "-Command", "New-Item -Path / -Name base -ItemType directory"] +WORKDIR /base +ENV JETTY_HOME=/server +ENV JETTY_BASE=/base +RUN java -jar /server/start.jar --add-module=ext,server,jsp,resources,deploy,jstl,websocket,http +COPY app.war /base/webapps/ +CMD java -jar /server/start.jar diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty.dockerfile new file mode 100644 index 000000000..29725aadb --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty.dockerfile @@ -0,0 +1,23 @@ +ARG version +ARG jdk +ARG vm +FROM jetty:${version}-jre11-slim as jetty + +FROM adoptopenjdk:${jdk}-jdk-${vm} +ENV JETTY_HOME /usr/local/jetty +ENV JETTY_BASE /var/lib/jetty +ENV TMPDIR /tmp/jetty +ENV PATH $JETTY_HOME/bin:$PATH + +COPY --from=jetty $JETTY_HOME $JETTY_HOME +COPY --from=jetty $JETTY_BASE $JETTY_BASE +COPY --from=jetty $TMPDIR $TMPDIR + +WORKDIR $JETTY_BASE +COPY --from=jetty docker-entrypoint.sh generate-jetty-start.sh / + +COPY app.war $JETTY_BASE/webapps/ + +EXPOSE 8080 +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["java","-jar","/usr/local/jetty/start.jar"] \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty.windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty.windows.dockerfile new file mode 100644 index 000000000..fd0eaeb9c --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/jetty.windows.dockerfile @@ -0,0 +1,20 @@ +ARG jdk +ARG vm +ARG sourceVersion + +# Unzip in a separate container so that zip file layer is not part of final image +FROM mcr.microsoft.com/windows/servercore:1809 as builder +ARG sourceVersion +ADD https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-distribution/${sourceVersion}/jetty-distribution-${sourceVersion}.zip /server.zip +RUN ["powershell", "-Command", "expand-archive -Path /server.zip -DestinationPath /server"] + +FROM adoptopenjdk:${jdk}-jdk-${vm}-windowsservercore-1809 +ARG sourceVersion +# Make /server the base directory to simplify all further paths +COPY --from=builder /server/jetty-distribution-${sourceVersion} /server +COPY app.war /server/webapps/ +RUN ["powershell", "-Command", "New-Item -Path /server -Name base -ItemType directory"] +WORKDIR /server +ENV JETTY_HOME=/server +ENV JETTY_BASE=/server +CMD java -jar /server/start.jar diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/liberty.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/liberty.dockerfile new file mode 100644 index 000000000..fe0ca57f9 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/liberty.dockerfile @@ -0,0 +1,24 @@ +ARG version +ARG jdk +ARG vm +FROM open-liberty:${version}-full-java11-openj9 as liberty + +FROM adoptopenjdk:${jdk}-jdk-${vm} +ENV CONFIG /config +ENV LIBERTY /opt/ol +ENV PATH=/opt/ol/wlp/bin:/opt/ol/docker/:/opt/ol/helpers/build:$PATH \ + LOG_DIR=/logs \ + WLP_OUTPUT_DIR=/opt/ol/wlp/output \ + WLP_SKIP_MAXPERMSIZE=true + +COPY --from=liberty $LIBERTY $LIBERTY +RUN ln -s /opt/ol/wlp/usr/servers/defaultServer /config + +COPY --chown=1001:0 server.xml /config/server.xml +COPY --chown=1001:0 app.war /config/apps/ +RUN configure.sh + +EXPOSE 8080 + +ENTRYPOINT ["/opt/ol/helpers/runtime/docker-server.sh"] +CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/liberty.windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/liberty.windows.dockerfile new file mode 100644 index 000000000..948db5fc7 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/liberty.windows.dockerfile @@ -0,0 +1,21 @@ +ARG jdk +ARG vm +ARG version +ARG release + +# Unzip in a separate container so that zip file layer is not part of final image +FROM mcr.microsoft.com/windows/servercore:1809 as builder +ARG version +ARG release +ADD https://public.dhe.ibm.com/ibmdl/export/pub/software/openliberty/runtime/release/${release}/openliberty-${version}.zip /server.zip +RUN ["powershell", "-Command", "expand-archive -Path /server.zip -DestinationPath /server"] + +FROM adoptopenjdk:${jdk}-jdk-${vm}-windowsservercore-1809 +ARG version +# Make /server the base directory to simplify all further paths +COPY --from=builder /server/wlp /server +COPY server.xml /server/usr/servers/defaultServer/ +COPY app.war /server/usr/servers/defaultServer/apps/ + +WORKDIR /server/bin +CMD /server/bin/server.bat run defaultServer diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/main/docker/liberty/server.xml b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/main/docker/liberty/server.xml new file mode 100644 index 000000000..88fd6adcd --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/main/docker/liberty/server.xml @@ -0,0 +1,12 @@ + + + + + javaee-8.0 + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/main/docker/payara/launch.bat b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/main/docker/payara/launch.bat new file mode 100644 index 000000000..c33d52396 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/main/docker/payara/launch.bat @@ -0,0 +1,8 @@ +:: server is run through a launcher process, to add jvm arguments we need to add them to configuration file +:: firstly split JVM_ARGS environment variable by space character and put each argument into tag +:: after that place options into configuration xml after -server +SET IN_CONF_FILE=/server/glassfish/domains/domain1/config/domain.xml +SET OUT_CONF_FILE=/server/glassfish/domains/domain1/config/domain.xml +powershell -command "$opts='' + $env:JVM_ARGS + ''; $opts=$opts -replace ' ', ''; (gc $env:IN_CONF_FILE) -replace '-server', ('-server' + $opts) | sc $env:OUT_CONF_FILE" +:: --verbose starts server in foreground mode where output is printed to console +java -jar glassfish/lib/client/appserver-cli.jar start-domain --verbose domain1 diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara-custom-5.2020.6.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara-custom-5.2020.6.dockerfile new file mode 100644 index 000000000..ae983f57f --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara-custom-5.2020.6.dockerfile @@ -0,0 +1,33 @@ +ARG version +ARG jdk +ARG vm + +FROM payara/server-full:${version} as default +ENV HOME_DIR=$HOME_DIR + +FROM adoptopenjdk:${jdk}-jdk-${vm} + +# These environment variables have been confirmed to work with 5.2020.6 only +ENV HOME_DIR=/opt/payara +ENV PAYARA_DIR="${HOME_DIR}/appserver" \ + SCRIPT_DIR="${HOME_DIR}/scripts" \ + CONFIG_DIR="${HOME_DIR}/config" \ + DEPLOY_DIR="${HOME_DIR}/deployments" \ + PASSWORD_FILE="${HOME_DIR}/passwordFile" \ + ADMIN_USER="admin" \ + ADMIN_PASSWORD="admin" \ + MEM_MAX_RAM_PERCENTAGE=70.0 \ + MEM_XSS=512k \ + DOMAIN_NAME="production" \ + PREBOOT_COMMANDS="${HOME_DIR}/config/pre-boot-commands.asadmin" \ + POSTBOOT_COMMANDS="${HOME_DIR}/config/post-boot-commands.asadmin" \ + PATH="${PATH}:${HOME_DIR}/scripts" + +COPY --from=default $HOME_DIR $HOME_DIR +RUN rm ${PAYARA_DIR}/glassfish/modules/phonehome-bootstrap.jar + +WORKDIR $HOME_DIR + +EXPOSE 8080 +CMD ["entrypoint.sh"] +COPY app.war $DEPLOY_DIR diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara.dockerfile new file mode 100644 index 000000000..33e3c752c --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara.dockerfile @@ -0,0 +1,8 @@ +ARG version +ARG jdk + +FROM payara/server-full:${version}${tagSuffix} + +RUN rm ${PAYARA_DIR}/glassfish/modules/phonehome-bootstrap.jar + +COPY app.war $DEPLOY_DIR \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara.windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara.windows.dockerfile new file mode 100644 index 000000000..acd0d55f3 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/payara.windows.dockerfile @@ -0,0 +1,19 @@ +ARG jdk +ARG vm +ARG version + +# Unzip in a separate container so that zip file layer is not part of final image +FROM mcr.microsoft.com/windows/servercore:1809 as builder +ARG version +ADD https://s3-eu-west-1.amazonaws.com/payara.fish/Payara+Downloads/${version}/payara-${version}.zip /server.zip +RUN ["powershell", "-Command", "expand-archive -Path /server.zip -DestinationPath /server"] +RUN ["powershell", "-Command", "remove-item -Path /server/payara5/glassfish/modules/phonehome-bootstrap.jar"] + +FROM adoptopenjdk:${jdk}-jdk-${vm}-windowsservercore-1809 +ARG version +# Make /server the base directory to simplify all further paths +COPY --from=builder /server/payara5 /server +COPY app.war /server/glassfish/domains/domain1/autodeploy/ +COPY launch.bat /server/ +WORKDIR /server +CMD /server/launch.bat diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomcat.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomcat.dockerfile new file mode 100644 index 000000000..3f159df47 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomcat.dockerfile @@ -0,0 +1,7 @@ +ARG version +ARG jdk +ARG vm + +FROM tomcat:${version}-jdk${jdk}-adoptopenjdk-${vm} + +COPY app.war /usr/local/tomcat/webapps/ \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomcat.windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomcat.windows.dockerfile new file mode 100644 index 000000000..7f39b8547 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomcat.windows.dockerfile @@ -0,0 +1,22 @@ +ARG jdk +ARG vm +ARG majorVersion +ARG version + +# Unzip in a separate container so that zip file layer is not part of final image +FROM mcr.microsoft.com/windows/servercore:1809 as builder +ARG majorVersion +ARG version +ADD https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${version}/bin/apache-tomcat-${version}-windows-x64.zip /server.zip +RUN ["powershell", "-Command", "expand-archive -Path /server.zip -DestinationPath /server"] + +FROM adoptopenjdk:${jdk}-jdk-${vm}-windowsservercore-1809 +ARG version +# Make /server the base directory to simplify all further paths +COPY --from=builder /server/apache-tomcat-${version} /server +# Delete default webapps to match the behavior of the official Linux Tomcat image +RUN ["powershell", "-Command", "Remove-Item -Recurse -Path /server/webapps"] +RUN ["powershell", "-Command", "New-Item -ItemType directory -Path /server/webapps"] +COPY app.war /server/webapps/ +WORKDIR /server/bin +CMD /server/bin/catalina.bat run diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee-custom.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee-custom.dockerfile new file mode 100644 index 000000000..e86d41679 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee-custom.dockerfile @@ -0,0 +1,15 @@ +ARG version +ARG jdk +ARG vm + +FROM tomee:${jdk}-jre-${version}-webprofile as default + +FROM adoptopenjdk:${jdk}-jdk-${vm} + +ENV SERVER_BASE=/usr/local/tomee +COPY --from=default $SERVER_BASE $SERVER_BASE +WORKDIR $SERVER_BASE + +EXPOSE 8080 +CMD ["bin/catalina.sh", "run"] +COPY app.war $SERVER_BASE/webapps/ diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee.dockerfile new file mode 100644 index 000000000..8fb354bc2 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee.dockerfile @@ -0,0 +1,6 @@ +ARG version +ARG jdk + +FROM tomee:${jdk}-jre-${version}-webprofile + +COPY app.war /usr/local/tomee/webapps/ \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee.windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee.windows.dockerfile new file mode 100644 index 000000000..47964a56f --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/tomee.windows.dockerfile @@ -0,0 +1,18 @@ +ARG jdk +ARG vm +ARG version + +# Unzip in a separate container so that zip file layer is not part of final image +FROM mcr.microsoft.com/windows/servercore:1809 as builder +ARG majorVersion +ARG version +ADD https://archive.apache.org/dist/tomee/tomee-${version}/apache-tomee-${version}-webprofile.zip /server.zip +RUN ["powershell", "-Command", "expand-archive -Path /server.zip -DestinationPath /server"] + +FROM adoptopenjdk:${jdk}-jdk-${vm}-windowsservercore-1809 +ARG version +# Make /server the base directory to simplify all further paths +COPY --from=builder /server/apache-tomee-webprofile-${version} /server +COPY app.war /server/webapps/ +WORKDIR /server/bin +CMD /server/bin/catalina.bat run diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/wildfly.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/wildfly.dockerfile new file mode 100644 index 000000000..9a929ea18 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/wildfly.dockerfile @@ -0,0 +1,47 @@ +ARG jdk +ARG vm +FROM adoptopenjdk:${jdk}-jdk-${vm} + +# Create a user and group used to launch processes +# The user ID 1000 is the default for the first "regular" user on Fedora/RHEL, +# so there is a high chance that this ID will be equal to the current user +# making it easier to use volumes (no permission issues) +RUN groupadd -r jboss -g 1000 && useradd -u 1000 -r -g jboss -m -d /opt/jboss -s /sbin/nologin -c "JBoss user" jboss && \ + chmod 755 /opt/jboss + +# Set the working directory to jboss' user home directory +WORKDIR /opt/jboss + +# Specify the user which should be used to execute all commands below +USER jboss + +# Set the WILDFLY_VERSION env variable +ARG version +ENV WILDFLY_VERSION=${version} +ENV JBOSS_HOME /opt/jboss/wildfly + +USER root +RUN echo curl -O https://download.jboss.org/wildfly/$WILDFLY_VERSION/wildfly-$WILDFLY_VERSION.tar.gz +# Add the WildFly distribution to /opt, and make wildfly the owner of the extracted tar content +# Make sure the distribution is available from a well-known place +RUN cd $HOME \ + && curl -O https://download.jboss.org/wildfly/$WILDFLY_VERSION/wildfly-$WILDFLY_VERSION.tar.gz \ + && tar xf wildfly-$WILDFLY_VERSION.tar.gz \ + && mv $HOME/wildfly-$WILDFLY_VERSION $JBOSS_HOME \ + && rm wildfly-$WILDFLY_VERSION.tar.gz \ + && chown -R jboss:0 ${JBOSS_HOME} \ + && chmod -R g+rw ${JBOSS_HOME} + +# Ensure signals are forwarded to the JVM process correctly for graceful shutdown +ENV LAUNCH_JBOSS_IN_BACKGROUND true + +USER jboss + +# Expose the ports we're interested in +EXPOSE 8080 + +# Set the default command to run on boot +# This will boot WildFly in the standalone mode and bind to all interface +CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0"] + +COPY app.war /opt/jboss/wildfly/standalone/deployments/ \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/matrix/src/wildfly.windows.dockerfile b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/wildfly.windows.dockerfile new file mode 100644 index 000000000..6fd7c11ff --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/matrix/src/wildfly.windows.dockerfile @@ -0,0 +1,17 @@ +ARG jdk +ARG vm +ARG version + +# Unzip in a separate container so that zip file layer is not part of final image +FROM mcr.microsoft.com/windows/servercore:1809 as builder +ARG version +ADD http://download.jboss.org/wildfly/${version}/wildfly-${version}.zip /server.zip +RUN ["powershell", "-Command", "expand-archive -Path /server.zip -DestinationPath /server"] + +FROM adoptopenjdk:${jdk}-jdk-${vm}-windowsservercore-1809 +ARG version +# Make /server the base directory to simplify all further paths +COPY --from=builder /server/wildfly-${version} /server +COPY app.war /server/standalone/deployments/ +WORKDIR /server/bin +CMD /server/bin/standalone.bat -b 0.0.0.0 diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/README.md b/opentelemetry-java-instrumentation/smoke-tests/play/README.md new file mode 100644 index 000000000..3e3ce3e46 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/README.md @@ -0,0 +1,6 @@ +# Play framework smoke test +Play application used by smoke tests of OpenTelemetry java agent. +Builds and publishes Docker image containing a trivial Play application. + +This is a separate gradle project, independent from the rest. This was done to allow +to build and publish it only when actually needed and not on every project build. \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/app/controllers/HomeController.scala b/opentelemetry-java-instrumentation/smoke-tests/play/app/controllers/HomeController.scala new file mode 100644 index 000000000..bd2f9859b --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/app/controllers/HomeController.scala @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package controllers + +import javax.inject.Inject + +import play.api.mvc._ + +/** Creates an `Action` to handle HTTP requests to the application's welcome greeting */ +class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) { + + /** Create an Action to return a greeting */ + def doGet(id: Option[Int]) = Action { implicit request: Request[AnyContent] => + val idVal = id.getOrElse(-1) + if (idVal > 0) { + Ok("Welcome %d.".format(idVal)) + } else { + Ok("No ID.") + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/build.gradle b/opentelemetry-java-instrumentation/smoke-tests/play/build.gradle new file mode 100644 index 000000000..2344ea1e5 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/build.gradle @@ -0,0 +1,45 @@ +plugins { + id "org.gradle.playframework" version "0.9" + id 'com.google.cloud.tools.jib' version '2.5.0' +} + +ext { + playVersion = "2.6.20" + scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") +} + +play { + platform { + playVersion = project.ext.playVersion + scalaVersion = project.ext.scalaVersion + javaVersion = JavaVersion.VERSION_1_8 + } + injectedRoutesGenerator = true +} + +repositories { + mavenLocal() + mavenCentral() + maven { + name "lightbend-maven-releases" + url "https://repo.lightbend.com/lightbend/maven-release" + } +} + +description = 'Play Integration Tests.' + +dependencies { + implementation "com.typesafe.play:play-guice_$scalaVersion:$playVersion" + implementation "com.typesafe.play:play-logback_$scalaVersion:$playVersion" + implementation "com.typesafe.play:filters-helpers_$scalaVersion:$playVersion" +} + +def targetJDK = project.hasProperty("targetJDK") ? project.targetJDK : 11 + +def tag = findProperty("tag") ?: new Date().format("yyyyMMdd.HHmmSS") + +jib { + from.image = "bellsoft/liberica-openjdk-alpine:$targetJDK" + to.image = "ghcr.io/open-telemetry/java-test-containers:smoke-play-jdk$targetJDK-$tag" + container.mainClass = "play.core.server.ProdServerStart" +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/conf/application.conf b/opentelemetry-java-instrumentation/smoke-tests/play/conf/application.conf new file mode 100644 index 000000000..0ab7303ce --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/conf/application.conf @@ -0,0 +1,3 @@ +# https://www.playframework.com/documentation/latest/Configuration +play.http.secret.key=agentbenchmarktest0xCAFEDEAD +http.port=8080 diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/conf/logback.xml b/opentelemetry-java-instrumentation/smoke-tests/play/conf/logback.xml new file mode 100644 index 000000000..72fc3ca1f --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/conf/logback.xml @@ -0,0 +1,29 @@ + + + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/conf/routes b/opentelemetry-java-instrumentation/smoke-tests/play/conf/routes new file mode 100644 index 000000000..2c2d095eb --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/conf/routes @@ -0,0 +1,7 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# https://www.playframework.com/documentation/latest/ScalaRouting +# ~~~~ + +# An example controller showing a sample home page +GET /welcome controllers.HomeController.doGet(id: Option[Int]) diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java-instrumentation/smoke-tests/play/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..912744eeb --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857 +size 58910 diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java-instrumentation/smoke-tests/play/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..c1255a265 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionSha256Sum=e6f83508f0970452f56197f610d13c5f593baaf43c0e3c6a571e5967be754025 diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/gradlew b/opentelemetry-java-instrumentation/smoke-tests/play/gradlew new file mode 100755 index 000000000..fbd7c5158 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java-instrumentation/smoke-tests/play/gradlew.bat b/opentelemetry-java-instrumentation/smoke-tests/play/gradlew.bat new file mode 100644 index 000000000..5093609d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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/opentelemetry-java-instrumentation/smoke-tests/play/settings.gradle b/opentelemetry-java-instrumentation/smoke-tests/play/settings.gradle new file mode 100644 index 000000000..2aaf6c3cc --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/play/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'play' diff --git a/opentelemetry-java-instrumentation/smoke-tests/smoke-tests.gradle b/opentelemetry-java-instrumentation/smoke-tests/smoke-tests.gradle new file mode 100644 index 000000000..ec777d13e --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/smoke-tests.gradle @@ -0,0 +1,78 @@ +import java.time.Duration + +apply plugin: "otel.java-conventions" + +description = 'smoke-tests' + +otelJava { + // we only need to run the Spock test itself under a single Java version, and the Spock test in + // turn is parameterized and runs the test using different docker containers that run different + // Java versions + minJavaVersionSupported = JavaVersion.VERSION_11 + maxJavaVersionForTests = JavaVersion.VERSION_11 +} + +def dockerJavaVersion = "3.2.5" +dependencies { + api "org.spockframework:spock-core" + api project(':testing-common') + + implementation platform("io.grpc:grpc-bom:1.33.1") + implementation "org.slf4j:slf4j-api" + implementation "run.mone:opentelemetry-api" + implementation "run.mone:opentelemetry-proto" + implementation "org.testcontainers:testcontainers" + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.google.protobuf:protobuf-java-util:3.12.4' + implementation 'io.grpc:grpc-netty-shaded' + implementation 'io.grpc:grpc-protobuf' + implementation 'io.grpc:grpc-stub' + + testImplementation("com.github.docker-java:docker-java-core:$dockerJavaVersion") + testImplementation("com.github.docker-java:docker-java-transport-httpclient5:$dockerJavaVersion") + +} + +test { + inputs.files(tasks.findByPath(':javaagent:shadowJar').outputs.files) + maxParallelForks = 2 + + testLogging.showStandardStreams = true + + // TODO investigate why smoke tests occasionally hang forever + // this needs to be long enough so that smoke tests that are just running slow don't time out + timeout.set(Duration.ofMinutes(45)) + + //We enable/disable smoke tests based on the java version requests + //In addition to that we disable them by default on local machines + enabled = enabled && (System.getenv("CI") != null || findProperty('runSmokeTests')) + + def suites = [ + "glassfish": ["**/GlassFishSmokeTest.*"], + "jetty" : ["**/JettySmokeTest.*"], + "liberty" : ["**/LibertySmokeTest.*", "**/LibertyServletOnlySmokeTest.*"], + "tomcat" : ["**/TomcatSmokeTest.*"], + "tomee" : ["**/TomeeSmokeTest.*"], + "wildfly" : ["**/WildflySmokeTest.*"] + ] + + def suite = findProperty('smokeTestSuite') + if (suite != null) { + if ('other' == suite) { + suites.values().each { + exclude it + } + } else if (suites.containsKey(suite)) { + include suites.get(suite) + } else { + throw new GradleException('Unknown smoke test suite: ' + suite) + } + } + + def shadowTask = project(":javaagent").tasks.named("shadowJar").get() + inputs.files(layout.files(shadowTask)) + + doFirst { + jvmArgs "-Dio.opentelemetry.smoketest.agent.shadowJar.path=${shadowTask.archiveFile.get()}" + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/README.md b/opentelemetry-java-instrumentation/smoke-tests/springboot/README.md new file mode 100644 index 000000000..2e054235d --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/README.md @@ -0,0 +1,6 @@ +# Spring Boot smoke test +Spring Boot application used by smoke tests of OpenTelemetry java agent. +Builds and publishes Docker image containing a trivial Spring MVC application. + +This is a separate gradle project, independent from the rest. This was done to allow +to build and publish it only when actually needed and not on every project build. \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/build.gradle b/opentelemetry-java-instrumentation/smoke-tests/springboot/build.gradle new file mode 100644 index 000000000..99b9fcb21 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'org.springframework.boot' version '2.3.2.RELEASE' + id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id 'java' + id 'com.google.cloud.tools.jib' version '2.5.0' +} + +group = 'io.opentelemetry' +version = '0.0.1-SNAPSHOT' + +repositories { + mavenCentral() + // this is only needed for the working against unreleased otel-java snapshots + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + content { + includeGroup "io.opentelemetry" + } + } +} + +dependencies { + implementation platform("io.opentelemetry:opentelemetry-bom:1.0.0") + + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'io.opentelemetry:opentelemetry-extension-annotations' + implementation 'io.opentelemetry:opentelemetry-api' +} + +compileJava { + options.release.set(8) +} + +def targetJDK = project.hasProperty("targetJDK") ? project.targetJDK : 11 + +def tag = findProperty("tag") ?: new Date().format("yyyyMMdd.HHmmSS") + +jib { + from.image = "bellsoft/liberica-openjdk-alpine:$targetJDK" + to.image = "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$targetJDK-$tag" + container.ports = ["8080"] +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java-instrumentation/smoke-tests/springboot/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..912744eeb --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857 +size 58910 diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java-instrumentation/smoke-tests/springboot/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..c1255a265 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionSha256Sum=e6f83508f0970452f56197f610d13c5f593baaf43c0e3c6a571e5967be754025 diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/gradlew b/opentelemetry-java-instrumentation/smoke-tests/springboot/gradlew new file mode 100755 index 000000000..fbd7c5158 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/gradlew.bat b/opentelemetry-java-instrumentation/smoke-tests/springboot/gradlew.bat new file mode 100644 index 000000000..5093609d5 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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/opentelemetry-java-instrumentation/smoke-tests/springboot/settings.gradle b/opentelemetry-java-instrumentation/smoke-tests/springboot/settings.gradle new file mode 100644 index 000000000..9c0240332 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'springboot' diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/SpringbootApplication.java b/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/SpringbootApplication.java new file mode 100644 index 000000000..ce4431f6c --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/SpringbootApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.smoketest.springboot; + +import java.lang.management.ManagementFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringbootApplication { + + public static void main(final String[] args) { + SpringApplication.run(SpringbootApplication.class, args); + System.out.println("Started in " + ManagementFactory.getRuntimeMXBean().getUptime() + "ms"); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/controller/PropagatingController.java b/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/controller/PropagatingController.java new file mode 100644 index 000000000..b5c520ef3 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/controller/PropagatingController.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.smoketest.springboot.controller; + +import io.opentelemetry.api.trace.Span; +import java.net.URI; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +/** + * This controller demonstrates that context propagation works across http calls. + * Calling /front should return a string which contains two traceId separated by ";". + * First traceId was reported by /front handler, the second one was returned by + * /back handler which was called by /front. If context propagation + * works correctly, then both values should be the same. + */ +@RestController +public class PropagatingController { + private final RestTemplate restTemplate; + private final Environment environment; + + public PropagatingController(RestTemplateBuilder restTemplateBuilder, Environment environment) { + this.restTemplate = restTemplateBuilder.build(); + this.environment = environment; + } + + @RequestMapping("/front") + public String front() { + URI backend = ServletUriComponentsBuilder + .fromCurrentContextPath() + .port(environment.getProperty("local.server.port")) + .path("/back") + .build() + .toUri(); + String backendTraceId = restTemplate.getForObject(backend, String.class); + String frontendTraceId = Span.current().getSpanContext().getTraceId(); + return String.format("%s;%s", frontendTraceId, backendTraceId); + } + + @RequestMapping("/back") + public String back() { + return Span.current().getSpanContext().getTraceId(); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/controller/WebController.java b/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/controller/WebController.java new file mode 100644 index 000000000..49aa0220f --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/java/io/opentelemetry/smoketest/springboot/controller/WebController.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.smoketest.springboot.controller; + +import io.opentelemetry.extension.annotations.WithSpan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WebController { + private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); + + @RequestMapping("/greeting") + public String greeting() { + LOGGER.info("HTTP request received"); + return withSpan(); + } + + @WithSpan + public String withSpan() { + return "Hi!"; + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/resources/application.properties b/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/resources/application.properties new file mode 100644 index 000000000..c05ec66fe --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/springboot/src/main/resources/application.properties @@ -0,0 +1,3 @@ +logging.level.root=WARN +logging.level.io.opentelemetry=INFO +logging.pattern.console=%-5level [%t] %C{1.}: %msg trace_id=%X{trace_id}%n \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/AppServerTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/AppServerTest.groovy new file mode 100644 index 000000000..10ccfd6ec --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/AppServerTest.groovy @@ -0,0 +1,366 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.OS_TYPE +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.OsTypeValues.LINUX +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.OsTypeValues.WINDOWS +import static org.junit.Assume.assumeTrue + +import io.opentelemetry.proto.trace.v1.Span +import java.util.jar.Attributes +import java.util.jar.JarFile +import org.junit.runner.RunWith +import spock.lang.Shared +import spock.lang.Unroll + +@RunWith(AppServerTestRunner) +abstract class AppServerTest extends SmokeTest { + @Shared + String jdk + @Shared + String serverVersion + @Shared + boolean isWindows + + def setupSpec() { + def appServer = AppServerTestRunner.currentAppServer(this.getClass()) + serverVersion = appServer.version() + jdk = appServer.jdk() + + isWindows = System.getProperty("os.name").toLowerCase().contains("windows") && + "1" != System.getenv("USE_LINUX_CONTAINERS") + startTarget(jdk, serverVersion, isWindows) + } + + @Override + protected String getTargetImage(String jdk) { + throw new UnsupportedOperationException("App servers tests should use getTargetImagePrefix") + } + + @Override + protected String getTargetImage(String jdk, String serverVersion, boolean windows) { + String platformSuffix = windows ? "-windows" : "" + String extraTag = "20210428.792292726" + String fullSuffix = "-${serverVersion}-jdk$jdk$platformSuffix-$extraTag" + return getTargetImagePrefix() + fullSuffix + } + + protected abstract String getTargetImagePrefix() + + def cleanupSpec() { + stopTarget() + } + + boolean testSmoke() { + true + } + + boolean testAsyncSmoke() { + true + } + + boolean testException() { + true + } + + boolean testRequestWebInfWebXml() { + true + } + + //TODO add assert that server spans were created by servers, not by servlets + @Unroll + def "#appServer smoke test on JDK #jdk"(String appServer, String jdk, boolean isWindows) { + assumeTrue(testSmoke()) + + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/app/greeting").aggregate().join() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + String responseBody = response.contentUtf8() + + then: "There is one trace" + traceIds.size() == 1 + + and: "trace id is present in the HTTP headers as reported by the called endpoint" + responseBody.contains(traceIds.find()) + + and: "Server spans in the distributed trace" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 2 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/greeting')) == 1 + traces.countSpansByName(getSpanName('/app/headers')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", "http://localhost:${containerManager.getTargetMappedPort(8080)}/app/greeting") == 1 + + and: "Client and server spans for the remote call" + traces.countFilteredAttributes("http.url", "http://localhost:8080/app/headers") == 2 + + and: "Number of spans with http protocol version" + traces.countFilteredAttributes("http.flavor", "1.1") == 3 + + and: "Number of spans tagged with current otel library version" + traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == 3 + + and: "Number of spans tagged with expected OS type" + traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == 3 + + where: + [appServer, jdk, isWindows] << getTestParams() + } + + @Unroll + def "#appServer test static file found on JDK #jdk"(String appServer, String jdk, boolean isWindows) { + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/app/hello.txt").aggregate().join() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + String responseBody = response.contentUtf8() + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response contains Hello" + responseBody.contains("Hello") + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/hello.txt')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", "http://localhost:${containerManager.getTargetMappedPort(8080)}/app/hello.txt") == 1 + + and: "Number of spans tagged with current otel library version" + traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == 1 + + and: "Number of spans tagged with expected OS type" + traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == 1 + + where: + [appServer, jdk, isWindows] << getTestParams() + } + + @Unroll + def "#appServer test static file not found on JDK #jdk"(String appServer, String jdk, boolean isWindows) { + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/app/file-that-does-not-exist").aggregate().join() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response code is 404" + response.status().code() == 404 + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/file-that-does-not-exist')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", "http://localhost:${containerManager.getTargetMappedPort(8080)}/app/file-that-does-not-exist") == 1 + + and: "Number of spans tagged with current otel library version" + traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == traces.countSpans() + + and: "Number of spans tagged with expected OS type" + traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == traces.countSpans() + + where: + [appServer, jdk, isWindows] << getTestParams() + } + + @Unroll + def "#appServer test request for WEB-INF/web.xml on JDK #jdk"(String appServer, String jdk, boolean isWindows) { + assumeTrue(testRequestWebInfWebXml()) + + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/app/WEB-INF/web.xml").aggregate().join() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response code is 404" + response.status().code() == 404 + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/WEB-INF/web.xml')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", "http://localhost:${containerManager.getTargetMappedPort(8080)}/app/WEB-INF/web.xml") == 1 + + and: "Number of spans with http protocol version" + traces.countFilteredAttributes("http.flavor", "1.1") == 1 + + and: "Number of spans tagged with current otel library version" + traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == traces.countSpans() + + and: "Number of spans tagged with expected OS type" + traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == traces.countSpans() + + where: + [appServer, jdk, isWindows] << getTestParams() + } + + @Unroll + def "#appServer test request with error JDK #jdk"(String appServer, String jdk, boolean isWindows) { + assumeTrue(testException()) + + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/app/exception").aggregate().join() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response code is 500" + response.status().code() == 500 + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/exception')) == 1 + + and: "There is one exception" + traces.countFilteredEventAttributes('exception.message', 'This is expected') == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", "http://localhost:${containerManager.getTargetMappedPort(8080)}/app/exception") == 1 + + and: "Number of spans tagged with current otel library version" + traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == 1 + + and: "Number of spans tagged with expected OS type" + traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == 1 + + where: + [appServer, jdk, isWindows] << getTestParams() + } + + @Unroll + def "#appServer test request outside deployed application JDK #jdk"(String appServer, String jdk, boolean isWindows) { + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless").aggregate().join() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response code is 404" + response.status().code() == 404 + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", "http://localhost:${containerManager.getTargetMappedPort(8080)}/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless") == 1 + + and: "Number of spans with http protocol version" + traces.countFilteredAttributes("http.flavor", "1.1") == 1 + + and: "Number of spans tagged with current otel library version" + traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == traces.countSpans() + + and: "Number of spans tagged with expected OS type" + traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == traces.countSpans() + + where: + [appServer, jdk, isWindows] << getTestParams() + } + + @Unroll + def "#appServer async smoke test on JDK #jdk"(String appServer, String jdk, boolean isWindows) { + assumeTrue(testAsyncSmoke()) + + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/app/asyncgreeting").aggregate().join() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + String responseBody = response.contentUtf8() + + then: "There is one trace" + traceIds.size() == 1 + + and: "trace id is present in the HTTP headers as reported by the called endpoint" + responseBody.contains(traceIds.find()) + + and: "Server spans in the distributed trace" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 2 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/asyncgreeting')) == 1 + traces.countSpansByName(getSpanName('/app/headers')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", "http://localhost:${containerManager.getTargetMappedPort(8080)}/app/asyncgreeting") == 1 + + and: "Client and server spans for the remote call" + traces.countFilteredAttributes("http.url", "http://localhost:8080/app/headers") == 2 + + and: "Number of spans with http protocol version" + traces.countFilteredAttributes("http.flavor", "1.1") == 3 + + and: "Number of spans tagged with current otel library version" + traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == 3 + + and: "Number of spans tagged with expected OS type" + traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == 3 + + where: + [appServer, jdk, isWindows] << getTestParams() + } + + protected String getSpanName(String path) { + switch (path) { + case "/app/greeting": + case "/app/headers": + case "/app/exception": + case "/app/asyncgreeting": + return path + case "/app/hello.txt": + case "/app/file-that-does-not-exist": + return "/app/*" + } + return "HTTP GET" + } + + protected List> getTestParams() { + return [ + [serverVersion, jdk, isWindows] + ] + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GlassFishSmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GlassFishSmokeTest.groovy new file mode 100644 index 000000000..8255c8b21 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GlassFishSmokeTest.groovy @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import java.time.Duration + +@AppServer(version = "5.2020.6", jdk = "8") +@AppServer(version = "5.2020.6", jdk = "8-openj9") +@AppServer(version = "5.2020.6", jdk = "11") +@AppServer(version = "5.2020.6", jdk = "11-openj9") +class GlassFishSmokeTest extends AppServerTest { + + protected String getTargetImagePrefix() { + "ghcr.io/open-telemetry/java-test-containers:payara" + } + + @Override + protected Map getExtraEnv() { + return ["HZ_PHONE_HOME_ENABLED": "false"] + } + + @Override + protected String getJvmArgsEnvVarName() { + return "JVM_ARGS" + } + + @Override + protected TargetWaitStrategy getWaitStrategy() { + return new TargetWaitStrategy.Log(Duration.ofMinutes(3), ".*(app was successfully deployed|deployed with name app).*") + } + + @Override + protected String getSpanName(String path) { + switch (path) { + case "/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless": + return "/*" + } + return super.getSpanName(path) + } + + @Override + boolean testRequestWebInfWebXml() { + false + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GrpcSmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GrpcSmokeTest.groovy new file mode 100644 index 000000000..5255128bc --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GrpcSmokeTest.groovy @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import static java.util.stream.Collectors.toSet + +import io.grpc.ManagedChannelBuilder +import io.opentelemetry.api.trace.TraceId +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc +import java.util.jar.Attributes +import java.util.jar.JarFile +import spock.lang.IgnoreIf +import spock.lang.Unroll + +@IgnoreIf({ os.windows }) +class GrpcSmokeTest extends SmokeTest { + + protected String getTargetImage(String jdk) { + "ghcr.io/open-telemetry/java-test-containers:smoke-grpc-jdk$jdk-20210225.598590600" + } + + @Unroll + def "grpc smoke test on JDK #jdk"(int jdk) { + setup: + def output = startTarget(jdk) + + def channel = ManagedChannelBuilder.forAddress("localhost", containerManager.getTargetMappedPort(8080)) + .usePlaintext() + .build() + def stub = TraceServiceGrpc.newBlockingStub(channel) + + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + stub.export(ExportTraceServiceRequest.getDefaultInstance()) + Collection traces = waitForTraces() + + then: + countSpansByName(traces, 'opentelemetry.proto.collector.trace.v1.TraceService/Export') == 1 + countSpansByName(traces, 'TestService.withSpan') == 1 + + [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.auto.version") + .map { it.stringValue } + .collect(toSet()) + + then: "correct traceIds are logged via MDC instrumentation" + def loggedTraceIds = getLoggedTraceIds(output) + def spanTraceIds = getSpanStream(traces) + .map({ TraceId.fromBytes(it.getTraceId().toByteArray()) }) + .collect(toSet()) + loggedTraceIds == spanTraceIds + + cleanup: + stopTarget() + channel.shutdown() + + where: + jdk << [8, 11, 15] + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JaegerExporterSmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JaegerExporterSmokeTest.groovy new file mode 100644 index 000000000..af51cac95 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JaegerExporterSmokeTest.groovy @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import static java.util.stream.Collectors.toSet + +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import java.util.jar.Attributes +import java.util.jar.JarFile +import spock.lang.IgnoreIf + +@IgnoreIf({ os.windows }) +class JaegerExporterSmokeTest extends SmokeTest { + + protected String getTargetImage(String jdk) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210218.577304949" + } + + @Override + protected Map getExtraEnv() { + return [ + "OTEL_TRACES_EXPORTER" : "jaeger", + "OTEL_EXPORTER_JAEGER_ENDPOINT" : "http://collector:14250" + ] + } + + def "spring boot smoke test with jaeger grpc"() { + setup: + startTarget(11) + + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/greeting").aggregate().join() + Collection traces = waitForTraces() + + then: + response.contentUtf8() == "Hi!" + countSpansByName(traces, '/greeting') == 1 + countSpansByName(traces, 'WebController.greeting') == 1 + countSpansByName(traces, 'WebController.withSpan') == 1 + + [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.auto.version") + .map { it.stringValue } + .collect(toSet()) + + cleanup: + stopTarget() + + } + +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettySmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettySmokeTest.groovy new file mode 100644 index 000000000..ead42a78a --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettySmokeTest.groovy @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +@AppServer(version = "9.4.35", jdk = "8") +@AppServer(version = "9.4.35", jdk = "8-openj9") +@AppServer(version = "9.4.35", jdk = "11") +@AppServer(version = "9.4.35", jdk = "11-openj9") +@AppServer(version = "10.0.0", jdk = "11") +@AppServer(version = "10.0.0", jdk = "11-openj9") +@AppServer(version = "10.0.0", jdk = "15") +@AppServer(version = "10.0.0", jdk = "15-openj9") +@AppServer(version = "11.0.1", jdk = "11") +@AppServer(version = "11.0.1", jdk = "11-openj9") +@AppServer(version = "11.0.1", jdk = "15") +@AppServer(version = "11.0.1", jdk = "15-openj9") +class JettySmokeTest extends AppServerTest { + + protected String getTargetImagePrefix() { + "ghcr.io/open-telemetry/java-test-containers:jetty" + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertyServletOnlySmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertyServletOnlySmokeTest.groovy new file mode 100644 index 000000000..402811a34 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertyServletOnlySmokeTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import spock.lang.IgnoreIf + +@AppServer(version = "20.0.0.12", jdk = "8") +@IgnoreIf({ os.windows }) //WindowsTestContainerManager does not support extra resources +class LibertyServletOnlySmokeTest extends LibertySmokeTest { + + @Override + protected Map getExtraResources() { + return ["liberty-servlet.xml": "/config/server.xml"] + } + + @Override + protected String getSpanName(String path) { + switch (path) { + case "/app/hello.txt": + case "/app/file-that-does-not-exist": + return "HTTP GET" + } + return super.getSpanName(path) + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertySmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertySmokeTest.groovy new file mode 100644 index 000000000..1b27d1763 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertySmokeTest.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import java.time.Duration + +@AppServer(version = "20.0.0.12", jdk = "8") +@AppServer(version = "20.0.0.12", jdk = "8-openj9") +@AppServer(version = "20.0.0.12", jdk = "11") +@AppServer(version = "20.0.0.12", jdk = "11-openj9") +class LibertySmokeTest extends AppServerTest { + + protected String getTargetImagePrefix() { + "ghcr.io/open-telemetry/java-test-containers:liberty" + } + + @Override + protected TargetWaitStrategy getWaitStrategy() { + return new TargetWaitStrategy.Log(Duration.ofMinutes(3), ".*server is ready to run a smarter planet.*") + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/MetricsInspector.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/MetricsInspector.groovy new file mode 100644 index 000000000..eb74a61e7 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/MetricsInspector.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest + +class MetricsInspector { + final Collection requests + + MetricsInspector(Collection requests) { + this.requests = requests + } + + boolean hasMetricsNamed(String metricName) { + requests.stream() + .flatMap({ it.resourceMetricsList.stream() }) + .flatMap({ it.instrumentationLibraryMetricsList.stream() }) + .flatMap({ it.metricsList.stream() }) + .anyMatch({ it.name == metricName }) + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PlaySmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PlaySmokeTest.groovy new file mode 100644 index 000000000..508132790 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PlaySmokeTest.groovy @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import spock.lang.IgnoreIf + +@IgnoreIf({ os.windows }) +class PlaySmokeTest extends SmokeTest { + + protected String getTargetImage(String jdk) { + "ghcr.io/open-telemetry/java-test-containers:smoke-play-jdk$jdk-20201128.1734635" + } + + def "play smoke test on JDK #jdk"(int jdk) { + setup: + startTarget(jdk) + when: + def response = client().get("/welcome?id=1").aggregate().join() + Collection traces = waitForTraces() + + then: + response.contentUtf8() == "Welcome 1." + //Both play and akka-http support produce spans with the same name. + //One internal, one SERVER + countSpansByName(traces, '/welcome') == 2 + + cleanup: + stopTarget() + + where: + jdk << [8, 11, 15] + } + +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PropagationTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PropagationTest.groovy new file mode 100644 index 000000000..b4c5e1bff --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PropagationTest.groovy @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import static java.util.stream.Collectors.toSet + +import io.opentelemetry.api.trace.TraceId +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import spock.lang.IgnoreIf + +abstract class PropagationTest extends SmokeTest { + + @Override + protected String getTargetImage(String jdk) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210218.577304949" + } + + def "Should propagate test"() { + setup: + startTarget(11) + when: + def response = client().get("/front").aggregate().join() + Collection traces = waitForTraces() + def traceIds = getSpanStream(traces) + .map({ TraceId.fromBytes(it.getTraceId().toByteArray()) }) + .collect(toSet()) + + then: + traceIds.size() == 1 + + def traceId = traceIds.first() + + response.contentUtf8() == "${traceId};${traceId}" + + cleanup: + stopTarget() + + } + +} + +@IgnoreIf({ os.windows }) +class DefaultPropagationTest extends PropagationTest { +} + +@IgnoreIf({ os.windows }) +class W3CPropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "tracecontext"] + } +} + +@IgnoreIf({ os.windows }) +class B3PropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "b3"] + } +} + +@IgnoreIf({ os.windows }) +class B3MultiPropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "b3multi"] + } +} + +@IgnoreIf({ os.windows }) +class JaegerPropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "jaeger"] + } +} + +@IgnoreIf({ os.windows }) +class OtTracePropagationTest extends SmokeTest { + @Override + protected String getTargetImage(String jdk) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210218.577304949" + } + + // OtTracer only propagates lower half of trace ID so we have to mangle the trace IDs similar to + // the Lightstep backend. + def "Should propagate test"() { + setup: + startTarget(11) + when: + def response = client().get("/front").aggregate().join() + Collection traces = waitForTraces() + def traceIds = getSpanStream(traces) + .map({ TraceId.fromBytes(it.getTraceId().toByteArray()).substring(16) }) + .collect(toSet()) + + then: + traceIds.size() == 1 + + def traceId = traceIds.first() + + response.contentUtf8().matches(/[0-9a-f]{16}${traceId};[0]{16}${traceId}/) + + cleanup: + stopTarget() + } + + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "ottrace"] + } +} + +@IgnoreIf({ os.windows }) +class XRayPropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "xray"] + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SmokeTest.groovy new file mode 100644 index 000000000..05e10e20f --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SmokeTest.groovy @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import static java.util.stream.Collectors.toSet + +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import io.opentelemetry.proto.common.v1.AnyValue +import io.opentelemetry.proto.trace.v1.Span +import io.opentelemetry.smoketest.windows.WindowsTestContainerManager +import io.opentelemetry.testing.internal.armeria.client.WebClient +import java.util.regex.Pattern +import java.util.stream.Stream +import org.testcontainers.containers.output.ToStringConsumer +import spock.lang.Shared +import spock.lang.Specification + +abstract class SmokeTest extends Specification { + private static final Pattern TRACE_ID_PATTERN = Pattern.compile(".*trace_id=(?[a-zA-Z0-9]+).*") + + protected static final TestContainerManager containerManager = createContainerManager() + + @Shared + private TelemetryRetriever telemetryRetriever + + @Shared + protected String agentPath = System.getProperty("io.opentelemetry.smoketest.agent.shadowJar.path") + + protected WebClient client() { + return WebClient.of("h1c://localhost:${containerManager.getTargetMappedPort(8080)}") + } + + /** + * Subclasses can override this method to pass jvm arguments in another environment variable + */ + protected String getJvmArgsEnvVarName() { + return "JAVA_TOOL_OPTIONS" + } + + /** + * Subclasses can override this method to customise target application's environment + */ + protected Map getExtraEnv() { + return Collections.emptyMap() + } + + /** + * Subclasses can override this method to provide additional files to copy to target container + */ + protected Map getExtraResources() { + return [:] + } + + def setupSpec() { + containerManager.startEnvironmentOnce() + telemetryRetriever = new TelemetryRetriever(containerManager.getBackendMappedPort()) + } + + def startTarget(int jdk) { + startTarget(String.valueOf(jdk), null, false) + } + + def startTarget(String jdk, String serverVersion, boolean windows) { + def targetImage = getTargetImage(jdk, serverVersion, windows) + return containerManager.startTarget(targetImage, agentPath, jvmArgsEnvVarName, extraEnv, extraResources, getWaitStrategy()) + } + + protected abstract String getTargetImage(String jdk) + + protected String getTargetImage(String jdk, String serverVersion, boolean windows) { + return getTargetImage(jdk) + } + + protected TargetWaitStrategy getWaitStrategy() { + return null + } + + def cleanup() { + telemetryRetriever.clearTelemetry() + } + + def stopTarget() { + containerManager.stopTarget() + } + + protected static Stream findResourceAttribute(Collection traces, + String attributeKey) { + return traces.stream() + .flatMap { it.getResourceSpansList().stream() } + .flatMap { it.getResource().getAttributesList().stream() } + .filter { it.key == attributeKey } + .map { it.value } + } + + protected static int countSpansByName(Collection traces, String spanName) { + return getSpanStream(traces).filter { it.name == spanName }.count() + } + + protected static Stream getSpanStream(Collection traces) { + return traces.stream() + .flatMap { it.getResourceSpansList().stream() } + .flatMap { it.getInstrumentationLibrarySpansList().stream() } + .flatMap { it.getSpansList().stream() } + } + + protected Collection waitForTraces() { + return telemetryRetriever.waitForTraces() + } + + protected Collection waitForMetrics() { + return telemetryRetriever.waitForMetrics() + } + + protected static Set getLoggedTraceIds(ToStringConsumer output) { + output.toUtf8String().lines() + .flatMap(SmokeTest.&findTraceId) + .collect(toSet()) + } + + private static Stream findTraceId(String log) { + def m = TRACE_ID_PATTERN.matcher(log) + m.matches() ? Stream.of(m.group("traceId")) : Stream.empty() as Stream + } + + protected static boolean isVersionLogged(ToStringConsumer output, String version) { + output.toUtf8String().lines() + .filter({ it.contains("opentelemetry-javaagent - version: " + version) }) + .findFirst() + .isPresent() + } + + private static TestContainerManager createContainerManager() { + boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows") + + if (isWindows && "1" != System.getenv("USE_LINUX_CONTAINERS")) { + return new WindowsTestContainerManager() + } + + return new LinuxTestContainerManager() + } + +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootSmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootSmokeTest.groovy new file mode 100644 index 000000000..413ba8efa --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootSmokeTest.groovy @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import static java.util.stream.Collectors.toSet + +import io.opentelemetry.api.trace.TraceId +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import java.util.jar.Attributes +import java.util.jar.JarFile +import spock.lang.IgnoreIf +import spock.lang.Unroll + +@IgnoreIf({ os.windows }) +class SpringBootSmokeTest extends SmokeTest { + + protected String getTargetImage(String jdk) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210218.577304949" + } + + @Unroll + def "spring boot smoke test on JDK #jdk"(int jdk) { + setup: + def output = startTarget(jdk) + def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION).toString() + + when: + def response = client().get("/greeting").aggregate().join() + Collection traces = waitForTraces() + + then: "spans are exported" + response.contentUtf8() == "Hi!" + countSpansByName(traces, '/greeting') == 1 + countSpansByName(traces, 'WebController.greeting') == 1 + countSpansByName(traces, 'WebController.withSpan') == 1 + + then: "correct agent version is captured in the resource" + [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.auto.version") + .map { it.stringValue } + .collect(toSet()) + + then: "OS is captured in the resource" + findResourceAttribute(traces, "os.type") + .map { it.stringValue } + .findAny() + .isPresent() + + then: "javaagent logs its version on startup" + isVersionLogged(output, currentAgentVersion) + + then: "correct traceIds are logged via MDC instrumentation" + def loggedTraceIds = getLoggedTraceIds(output) + def spanTraceIds = getSpanStream(traces) + .map({ TraceId.fromBytes(it.getTraceId().toByteArray()) }) + .collect(toSet()) + loggedTraceIds == spanTraceIds + + then: "JVM metrics are exported" + def metrics = new MetricsInspector(waitForMetrics()) + metrics.hasMetricsNamed("runtime.jvm.gc.time") + metrics.hasMetricsNamed("runtime.jvm.gc.count") + metrics.hasMetricsNamed("runtime.jvm.memory.area") + metrics.hasMetricsNamed("runtime.jvm.memory.pool") + + cleanup: + stopTarget() + + where: + jdk << [8, 11, 15] + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootWithSamplingSmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootWithSamplingSmokeTest.groovy new file mode 100644 index 000000000..d5fe55d61 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootWithSamplingSmokeTest.groovy @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import spock.lang.IgnoreIf + +@IgnoreIf({ os.windows }) +class SpringBootWithSamplingSmokeTest extends SmokeTest { + + static final double SAMPLER_PROBABILITY = 0.2 + static final int NUM_TRIES = 1000 + static final int ALLOWED_DEVIATION = 0.1 * NUM_TRIES + + protected String getTargetImage(String jdk) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210218.577304949" + } + + @Override + protected Map getExtraEnv() { + return [ + "OTEL_TRACES_SAMPLER": "parentbased_traceidratio", + "OTEL_TRACES_SAMPLER_ARG": String.valueOf(SAMPLER_PROBABILITY), + ] + } + + def "spring boot with probability sampling enabled on JDK #jdk"(int jdk) { + setup: + startTarget(jdk) + when: + for (int i = 1; i <= NUM_TRIES; i++) { + client().get("/greeting").aggregate().join() + } + Collection traces = waitForTraces() + + then: + // since sampling is enabled, not really expecting to receive NUM_TRIES spans + Math.abs(countSpansByName(traces, 'WebController.greeting') - (SAMPLER_PROBABILITY * NUM_TRIES)) <= ALLOWED_DEVIATION + Math.abs(countSpansByName(traces, '/greeting') - (SAMPLER_PROBABILITY * NUM_TRIES)) <= ALLOWED_DEVIATION + + cleanup: + stopTarget() + + where: + jdk << [8, 11, 15] + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TelemetryRetriever.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TelemetryRetriever.groovy new file mode 100644 index 000000000..c06855752 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TelemetryRetriever.groovy @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.protobuf.GeneratedMessageV3 +import com.google.protobuf.util.JsonFormat +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import io.opentelemetry.testing.internal.armeria.client.WebClient +import java.util.concurrent.TimeUnit +import java.util.function.Supplier + +class TelemetryRetriever { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + + + final WebClient client + + TelemetryRetriever(int backendPort) { + client = WebClient.of("http://localhost:${backendPort}") + } + + void clearTelemetry() { + client.get("/clear").aggregate().join() + } + + Collection waitForTraces() { + return waitForTelemetry("get-traces", { ExportTraceServiceRequest.newBuilder() }) + } + + Collection waitForMetrics() { + return waitForTelemetry("get-metrics", { ExportMetricsServiceRequest.newBuilder() }) + } + + private Collection waitForTelemetry(String path, Supplier builderConstructor) { + def content = waitForContent(path) + + return OBJECT_MAPPER.readTree(content).collect { + def builder = builderConstructor.get() + // TODO(anuraaga): Register parser into object mapper to avoid de -> re -> deserialize. + JsonFormat.parser().merge(OBJECT_MAPPER.writeValueAsString(it), builder) + return (T) builder.build() + } + } + + private String waitForContent(String path) { + long previousSize = 0 + long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30) + String content = "[]" + while (System.currentTimeMillis() < deadline) { + content = client.get(path).aggregate().join().contentUtf8() + if (content.length() > 2 && content.length() == previousSize) { + break + } + previousSize = content.length() + println "Curent content size $previousSize" + TimeUnit.MILLISECONDS.sleep(500) + } + + return content + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomcatSmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomcatSmokeTest.groovy new file mode 100644 index 000000000..b259cd75a --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomcatSmokeTest.groovy @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +@AppServer(version = "7.0.107", jdk = "8") +@AppServer(version = "7.0.107", jdk = "8-openj9") +@AppServer(version = "8.5.60", jdk = "8") +@AppServer(version = "8.5.60", jdk = "8-openj9") +@AppServer(version = "8.5.60", jdk = "11") +@AppServer(version = "8.5.60", jdk = "11-openj9") +@AppServer(version = "9.0.40", jdk = "8") +@AppServer(version = "9.0.40", jdk = "8-openj9") +@AppServer(version = "9.0.40", jdk = "11") +@AppServer(version = "9.0.40", jdk = "11-openj9") +@AppServer(version = "10.0.4", jdk = "11") +@AppServer(version = "10.0.4", jdk = "11-openj9") +@AppServer(version = "10.0.4", jdk = "15") +@AppServer(version = "10.0.4", jdk = "15-openj9") +class TomcatSmokeTest extends AppServerTest { + + protected String getTargetImagePrefix() { + "ghcr.io/open-telemetry/java-test-containers:tomcat" + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomeeSmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomeeSmokeTest.groovy new file mode 100644 index 000000000..bb8661577 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomeeSmokeTest.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import java.time.Duration + +@AppServer(version = "7.0.0", jdk = "8") +@AppServer(version = "7.0.0", jdk = "8-openj9") +@AppServer(version = "8.0.6", jdk = "8") +@AppServer(version = "8.0.6", jdk = "8-openj9") +@AppServer(version = "8.0.6", jdk = "11") +@AppServer(version = "8.0.6", jdk = "11-openj9") +class TomeeSmokeTest extends AppServerTest { + + protected String getTargetImagePrefix() { + "ghcr.io/open-telemetry/java-test-containers:tomee" + } + + @Override + protected TargetWaitStrategy getWaitStrategy() { + return new TargetWaitStrategy.Log(Duration.ofMinutes(3), ".*Server startup in.*") + } + + @Override + protected String getSpanName(String path) { + switch (path) { + case "/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless": + return "/*" + } + return super.getSpanName(path) + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TraceInspector.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TraceInspector.java new file mode 100644 index 000000000..664a20940 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TraceInspector.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +import com.google.protobuf.ByteString; +import groovy.lang.GString; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.Span; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TraceInspector { + final Collection traces; + + public TraceInspector(Collection traces) { + this.traces = traces; + } + + public Stream getSpanStream() { + return traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .flatMap(it -> it.getInstrumentationLibrarySpansList().stream()) + .flatMap(it -> it.getSpansList().stream()); + } + + public Stream findResourceAttribute(String attributeKey) { + return traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .flatMap(it -> it.getResource().getAttributesList().stream()) + .filter(it -> it.getKey().equals(attributeKey)) + .map(KeyValue::getValue); + } + + public long countFilteredResourceAttributes(String attributeName, Object attributeValue) { + return traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .map(ResourceSpans::getResource) + .flatMap(it -> it.getAttributesList().stream()) + .filter(a -> a.getKey().equals(attributeName)) + .map(a -> a.getValue().getStringValue()) + .filter(s -> s.equals(attributeValue)) + .count(); + } + + public long countFilteredAttributes(String attributeName, Object attributeValue) { + final Object value; + if (attributeValue instanceof GString) { + value = attributeValue.toString(); + } else { + value = attributeValue; + } + return getSpanStream() + .flatMap(s -> s.getAttributesList().stream()) + .filter(a -> a.getKey().equals(attributeName)) + .map(a -> a.getValue().getStringValue()) + .filter(s -> s.equals(value)) + .count(); + } + + public long countFilteredEventAttributes(String attributeName, Object attributeValue) { + return getSpanStream() + .flatMap(s -> s.getEventsList().stream()) + .flatMap(e -> e.getAttributesList().stream()) + .filter(a -> a.getKey().equals(attributeName)) + .map(a -> a.getValue().getStringValue()) + .filter(s -> s.equals(attributeValue)) + .count(); + } + + protected int countSpansByName(String spanName) { + return (int) getSpanStream().filter(it -> it.getName().equals(spanName)).count(); + } + + protected int countSpansByKind(Span.SpanKind spanKind) { + return (int) getSpanStream().filter(it -> it.getKind().equals(spanKind)).count(); + } + + protected int countSpans() { + return (int) getSpanStream().count(); + } + + public int size() { + return traces.size(); + } + + public Set getTraceIds() { + return getSpanStream() + .map(Span::getTraceId) + .map(ByteString::toByteArray) + .map(TraceId::fromBytes) + .collect(Collectors.toSet()); + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/WildflySmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/WildflySmokeTest.groovy new file mode 100644 index 000000000..d12cb2957 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/WildflySmokeTest.groovy @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import io.opentelemetry.proto.trace.v1.Span +import spock.lang.Unroll + +@AppServer(version = "13.0.0.Final", jdk = "8") +@AppServer(version = "13.0.0.Final", jdk = "8-openj9") +@AppServer(version = "17.0.1.Final", jdk = "11") +@AppServer(version = "17.0.1.Final", jdk = "11-openj9") +@AppServer(version = "21.0.0.Final", jdk = "11") +@AppServer(version = "21.0.0.Final", jdk = "11-openj9") +class WildflySmokeTest extends AppServerTest { + + protected String getTargetImagePrefix() { + "ghcr.io/open-telemetry/java-test-containers:wildfly" + } + + @Unroll + def "JSP smoke test on WildFly"() { + when: + def response = client().get("/app/jsp").aggregate().join() + TraceInspector traces = new TraceInspector(waitForTraces()) + String responseBody = response.contentUtf8() + + then: + response.status().isSuccess() + responseBody.contains("Successful JSP test") + + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + traces.countSpansByName('/app/jsp') == 1 + + where: + [appServer, jdk] << getTestParams() + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/ZipkinExporterSmokeTest.groovy b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/ZipkinExporterSmokeTest.groovy new file mode 100644 index 000000000..ba1909ae1 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/ZipkinExporterSmokeTest.groovy @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest + +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import spock.lang.IgnoreIf + +@IgnoreIf({ os.windows }) +class ZipkinExporterSmokeTest extends SmokeTest { + + protected String getTargetImage(String jdk) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210218.577304949" + } + + @Override + protected Map getExtraEnv() { + return [ + "OTEL_TRACES_EXPORTER" : "zipkin", + "OTEL_EXPORTER_ZIPKIN_ENDPOINT" : "http://collector:9411/api/v2/spans" + ] + } + + def "spring boot smoke test with Zipkin"() { + setup: + startTarget(11) + + // def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name.IMPLEMENTATION_VERSION) + + when: + def response = client().get("/greeting").aggregate().join() + Collection traces = waitForTraces() + + then: + response.contentUtf8() == "Hi!" + countSpansByName(traces, '/greeting') == 1 + countSpansByName(traces, 'webcontroller.greeting') == 1 + countSpansByName(traces, 'webcontroller.withspan') == 1 + + //This is currently broken, see https://github.com/open-telemetry/opentelemetry-java/issues/1970 +// [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.auto.version") +// .map { it.stringValue } +// .collect(toSet()) + + cleanup: + stopTarget() + + } + +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java new file mode 100644 index 000000000..72d767236 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractTestContainerManager implements TestContainerManager { + protected static final int TARGET_PORT = 8080; + protected static final int BACKEND_PORT = 8080; + + protected static final String BACKEND_ALIAS = "backend"; + protected static final String COLLECTOR_ALIAS = "collector"; + protected static final String TARGET_AGENT_FILENAME = "opentelemetry-javaagent.jar"; + protected static final String COLLECTOR_CONFIG_RESOURCE = "/otel.yaml"; + + private boolean started = false; + + protected Map getAgentEnvironment(String jvmArgsEnvVarName) { + Map environment = new HashMap<>(); + environment.put(jvmArgsEnvVarName, "-javaagent:/" + TARGET_AGENT_FILENAME); + environment.put("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "1"); + environment.put("OTEL_BSP_SCHEDULE_DELAY", "10ms"); + environment.put("OTEL_IMR_EXPORT_INTERVAL", "1000"); + environment.put("OTEL_EXPORTER_OTLP_ENDPOINT", "http://" + COLLECTOR_ALIAS + ":55680"); + environment.put("OTEL_RESOURCE_ATTRIBUTES", "service.name=smoke-test"); + environment.put("OTEL_JAVAAGENT_DEBUG", "true"); + return environment; + } + + protected abstract void startEnvironment(); + + protected abstract void stopEnvironment(); + + @Override + public void startEnvironmentOnce() { + if (!started) { + started = true; + startEnvironment(); + Runtime.getRuntime().addShutdownHook(new Thread(this::stopEnvironment)); + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServer.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServer.java new file mode 100644 index 000000000..c43dd9b8a --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServer.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(AppServers.class) +@Inherited +public @interface AppServer { + String version(); + + String jdk(); +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServerTestRunner.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServerTestRunner.java new file mode 100644 index 000000000..d72c08f5e --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServerTestRunner.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.model.InitializationError; +import org.spockframework.runtime.Sputnik; + +/** + * Customized spock test runner that runs tests on multiple app server versions based on {@link + * AppServer} annotations. This runner selects first server based on {@link AppServer} annotation + * calls setupSpec, all test method and cleanupSpec, selects next {@link AppServer} and calls the + * same methods. This process is repeated until tests have run for all {@link AppServer} + * annotations. Tests should start server in setupSpec and stop it in cleanupSpec. + */ +public class AppServerTestRunner extends Sputnik { + private static final Map, AppServer> runningAppServer = + Collections.synchronizedMap(new HashMap<>()); + private final Class testClass; + private final AppServer[] appServers; + + public AppServerTestRunner(Class clazz) throws InitializationError { + super(clazz); + testClass = clazz; + appServers = clazz.getAnnotationsByType(AppServer.class); + if (appServers.length == 0) { + throw new IllegalStateException("Add AppServer or AppServers annotation to test class"); + } + } + + @Override + public void run(RunNotifier notifier) { + // run tests for all app servers + try { + for (AppServer appServer : appServers) { + runningAppServer.put(testClass, appServer); + super.run(notifier); + } + } finally { + runningAppServer.remove(testClass); + } + } + + // expose currently running app server + // used to get current server and jvm version inside the test class + public static AppServer currentAppServer(Class testClass) { + AppServer appServer = runningAppServer.get(testClass); + if (appServer == null) { + throw new IllegalStateException("Test not running for " + testClass); + } + return appServer; + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServers.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServers.java new file mode 100644 index 000000000..9bd11be4f --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/AppServers.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface AppServers { + AppServer[] value(); +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/LinuxTestContainerManager.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/LinuxTestContainerManager.java new file mode 100644 index 000000000..4b5913700 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/LinuxTestContainerManager.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +import java.time.Duration; +import java.util.Map; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class LinuxTestContainerManager extends AbstractTestContainerManager { + private static final Logger logger = LoggerFactory.getLogger(LinuxTestContainerManager.class); + private static final Logger collectorLogger = LoggerFactory.getLogger("Collector"); + private static final Logger backendLogger = LoggerFactory.getLogger("Backend"); + + private final Network network = Network.newNetwork(); + private GenericContainer backend = null; + private GenericContainer collector = null; + private GenericContainer target = null; + + @Override + protected void startEnvironment() { + backend = + new GenericContainer<>( + DockerImageName.parse( + "ghcr.io/open-telemetry/java-test-containers:smoke-fake-backend-20210611.927888723")) + .withExposedPorts(BACKEND_PORT) + .waitingFor(Wait.forHttp("/health").forPort(BACKEND_PORT)) + .withNetwork(network) + .withNetworkAliases(BACKEND_ALIAS) + .withLogConsumer(new Slf4jLogConsumer(backendLogger)); + backend.start(); + + collector = + new GenericContainer<>(DockerImageName.parse("otel/opentelemetry-collector-dev:latest")) + .dependsOn(backend) + .withNetwork(network) + .withNetworkAliases(COLLECTOR_ALIAS) + .withLogConsumer(new Slf4jLogConsumer(collectorLogger)) + .withCopyFileToContainer( + MountableFile.forClasspathResource(COLLECTOR_CONFIG_RESOURCE), "/etc/otel.yaml") + .withCommand("--config /etc/otel.yaml"); + collector.start(); + } + + @Override + protected void stopEnvironment() { + if (backend != null) { + backend.stop(); + backend = null; + } + + if (collector != null) { + collector.stop(); + collector = null; + } + + network.close(); + } + + @Override + public int getBackendMappedPort() { + return backend.getMappedPort(BACKEND_PORT); + } + + @Override + public int getTargetMappedPort(int originalPort) { + return target.getMappedPort(originalPort); + } + + @Override + public Consumer startTarget( + String targetImageName, + String agentPath, + String jvmArgsEnvVarName, + Map extraEnv, + Map extraResources, + TargetWaitStrategy waitStrategy) { + + Consumer output = new ToStringConsumer(); + target = + new GenericContainer<>(DockerImageName.parse(targetImageName)) + .withStartupTimeout(Duration.ofMinutes(5)) + .withExposedPorts(TARGET_PORT) + .withNetwork(network) + .withLogConsumer(output) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withCopyFileToContainer( + MountableFile.forHostPath(agentPath), "/" + TARGET_AGENT_FILENAME) + .withEnv(getAgentEnvironment(jvmArgsEnvVarName)) + .withEnv(extraEnv); + + extraResources.forEach( + (file, path) -> + target.withCopyFileToContainer(MountableFile.forClasspathResource(file), path)); + + if (waitStrategy != null) { + if (waitStrategy instanceof TargetWaitStrategy.Log) { + target = + target.waitingFor( + Wait.forLogMessage(((TargetWaitStrategy.Log) waitStrategy).regex, 1) + .withStartupTimeout(waitStrategy.timeout)); + } + } + target.start(); + return output; + } + + @Override + public void stopTarget() { + if (target != null) { + target.stop(); + target = null; + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TargetWaitStrategy.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TargetWaitStrategy.java new file mode 100644 index 000000000..73dd6cf34 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TargetWaitStrategy.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +import java.time.Duration; + +public abstract class TargetWaitStrategy { + public final Duration timeout; + + protected TargetWaitStrategy(Duration timeout) { + this.timeout = timeout; + } + + public static class Log extends TargetWaitStrategy { + public final String regex; + + public Log(Duration timeout, String regex) { + super(timeout); + this.regex = regex; + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TestContainerManager.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TestContainerManager.java new file mode 100644 index 000000000..a3e7d6c3a --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TestContainerManager.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +import java.util.Map; +import java.util.function.Consumer; +import org.testcontainers.containers.output.OutputFrame; + +public interface TestContainerManager { + + void startEnvironmentOnce(); + + int getBackendMappedPort(); + + int getTargetMappedPort(int originalPort); + + Consumer startTarget( + String targetImageName, + String agentPath, + String jvmArgsEnvVarName, + Map extraEnv, + Map extraResources, + TargetWaitStrategy waitStrategy); + + void stopTarget(); +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TestImage.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TestImage.java new file mode 100644 index 000000000..65d3e5178 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/TestImage.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest; + +public class TestImage { + public final Platform platform; + public final String imageName; + + public TestImage(Platform platform, String imageName) { + this.platform = platform; + this.imageName = imageName; + } + + @Override + public String toString() { + return imageName + "(" + platform + ")"; + } + + public static TestImage linuxImage(String imageName) { + return new TestImage(Platform.LINUX_X86_64, imageName); + } + + public static TestImage windowsImage(String imageName) { + return new TestImage(Platform.WINDOWS_X86_64, imageName); + } + + public enum Platform { + WINDOWS_X86_64, + LINUX_X86_64, + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/ContainerLogFrameConsumer.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/ContainerLogFrameConsumer.java new file mode 100644 index 000000000..63c68e329 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/ContainerLogFrameConsumer.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.windows; + +import com.github.dockerjava.api.async.ResultCallbackTemplate; +import com.github.dockerjava.api.model.Frame; +import com.google.common.base.Charsets; +import java.util.ArrayList; +import java.util.List; + +public class ContainerLogFrameConsumer + extends ResultCallbackTemplate + implements ContainerLogHandler { + private final List listeners; + + public ContainerLogFrameConsumer() { + this.listeners = new ArrayList<>(); + } + + @Override + public void addListener(Listener listener) { + listeners.add(listener); + } + + @Override + public void onNext(Frame frame) { + LineType lineType = getLineType(frame); + + if (lineType != null) { + byte[] bytes = frame.getPayload(); + String text = bytes == null ? "" : new String(bytes, Charsets.UTF_8); + + for (Listener listener : listeners) { + listener.accept(lineType, text); + } + } + } + + private static LineType getLineType(Frame frame) { + switch (frame.getStreamType()) { + case STDOUT: + return LineType.STDOUT; + case STDERR: + return LineType.STDERR; + default: + return null; + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/ContainerLogHandler.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/ContainerLogHandler.java new file mode 100644 index 000000000..d1bfaef88 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/ContainerLogHandler.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.windows; + +public interface ContainerLogHandler { + void addListener(Listener listener); + + interface Listener { + void accept(LineType type, String text); + } + + enum LineType { + STDOUT, + STDERR + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/Slf4jDockerLogLineListener.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/Slf4jDockerLogLineListener.java new file mode 100644 index 000000000..fca313b41 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/Slf4jDockerLogLineListener.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.windows; + +import org.slf4j.Logger; + +public class Slf4jDockerLogLineListener implements ContainerLogHandler.Listener { + private final Logger logger; + + public Slf4jDockerLogLineListener(Logger logger) { + this.logger = logger; + } + + @Override + public void accept(ContainerLogHandler.LineType type, String text) { + String normalizedText = text.replaceAll("((\\r?\\n)|(\\r))$", ""); + + switch (type) { + case STDERR: + this.logger.error("STDERR: {}", normalizedText); + break; + case STDOUT: + this.logger.info("STDOUT: {}", normalizedText); + break; + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/WindowsTestContainerManager.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/WindowsTestContainerManager.java new file mode 100644 index 000000000..63c267ade --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/WindowsTestContainerManager.java @@ -0,0 +1,511 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.smoketest.windows; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.command.InspectContainerResponse; +import com.github.dockerjava.api.command.PullImageResultCallback; +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.PortBinding; +import com.github.dockerjava.api.model.Ports; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientImpl; +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; +import io.opentelemetry.smoketest.AbstractTestContainerManager; +import io.opentelemetry.smoketest.TargetWaitStrategy; +import io.opentelemetry.testing.internal.armeria.client.WebClient; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.net.URI; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.io.IOUtils; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.rnorth.ducttape.TimeoutException; +import org.rnorth.ducttape.ratelimits.RateLimiter; +import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; +import org.rnorth.ducttape.unreliables.Unreliables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.OutputFrame; + +public class WindowsTestContainerManager extends AbstractTestContainerManager { + private static final Logger logger = LoggerFactory.getLogger(WindowsTestContainerManager.class); + private static final Logger collectorLogger = LoggerFactory.getLogger("Collector"); + private static final Logger backendLogger = LoggerFactory.getLogger("Backend"); + + private static final String NPIPE_URI = "npipe:////./pipe/docker_engine"; + private static final String COLLECTOR_CONFIG_FILE_PATH = "/collector-config.yml"; + + private final DockerClient client = + DockerClientImpl.getInstance( + new DefaultDockerClientConfig.Builder().withDockerHost(NPIPE_URI).build(), + new ApacheDockerHttpClient.Builder().dockerHost(URI.create(NPIPE_URI)).build()); + + @Nullable private String natNetworkId = null; + @Nullable private Container backend; + @Nullable private Container collector; + @Nullable private Container target; + + @Override + protected void startEnvironment() { + natNetworkId = + client + .createNetworkCmd() + .withDriver("nat") + .withName(UUID.randomUUID().toString()) + .exec() + .getId(); + + String backendSuffix = "-windows-20210611.927888723"; + + String backendImageName = + "ghcr.io/open-telemetry/java-test-containers:smoke-fake-backend" + backendSuffix; + if (!imageExists(backendImageName)) { + pullImage(backendImageName); + } + + backend = + startContainer( + backendImageName, + command -> + command + .withAliases(BACKEND_ALIAS) + .withExposedPorts(ExposedPort.tcp(BACKEND_PORT)) + .withHostConfig( + HostConfig.newHostConfig() + .withAutoRemove(true) + .withNetworkMode(natNetworkId) + .withPortBindings( + new PortBinding( + new Ports.Binding(null, null), ExposedPort.tcp(BACKEND_PORT)))), + containerId -> {}, + new HttpWaiter(BACKEND_PORT, "/health", Duration.ofSeconds(60)), + /* inspect= */ true, + backendLogger); + + String collectorImageName = + "ghcr.io/open-telemetry/java-test-containers:collector" + backendSuffix; + if (!imageExists(collectorImageName)) { + pullImage(collectorImageName); + } + collector = + startContainer( + collectorImageName, + command -> + command + .withAliases(COLLECTOR_ALIAS) + .withHostConfig( + HostConfig.newHostConfig() + .withAutoRemove(true) + .withNetworkMode(natNetworkId)) + .withCmd("--config", COLLECTOR_CONFIG_FILE_PATH), + containerId -> { + try (InputStream configFileStream = + this.getClass().getResourceAsStream(COLLECTOR_CONFIG_RESOURCE)) { + copyFileToContainer( + containerId, IOUtils.toByteArray(configFileStream), COLLECTOR_CONFIG_FILE_PATH); + } catch (IOException e) { + throw new IllegalStateException(e); + } + }, + new NoOpWaiter(), + /* inspect= */ false, + collectorLogger); + } + + @Override + protected void stopEnvironment() { + stopTarget(); + + killContainer(collector); + collector = null; + + killContainer(backend); + backend = null; + + if (natNetworkId != null) { + client.removeNetworkCmd(natNetworkId); + natNetworkId = null; + } + } + + @Override + public int getBackendMappedPort() { + return extractMappedPort(backend, BACKEND_PORT); + } + + @Override + public int getTargetMappedPort(int originalPort) { + return extractMappedPort(target, originalPort); + } + + // TODO add support for extraResources + @Override + public Consumer startTarget( + String targetImageName, + String agentPath, + String jvmArgsEnvVarName, + Map extraEnv, + Map extraResources, + TargetWaitStrategy waitStrategy) { + stopTarget(); + + if (!imageExists(targetImageName)) { + pullImage(targetImageName); + } + + List environment = new ArrayList<>(); + getAgentEnvironment(jvmArgsEnvVarName) + .forEach((key, value) -> environment.add(key + "=" + value)); + extraEnv.forEach((key, value) -> environment.add(key + "=" + value)); + + target = + startContainer( + targetImageName, + command -> + command + .withExposedPorts(ExposedPort.tcp(TARGET_PORT)) + .withHostConfig( + HostConfig.newHostConfig() + .withAutoRemove(true) + .withNetworkMode(natNetworkId) + .withPortBindings( + new PortBinding( + new Ports.Binding(null, null), ExposedPort.tcp(TARGET_PORT)))) + .withEnv(environment), + containerId -> { + try (InputStream agentFileStream = new FileInputStream(agentPath)) { + copyFileToContainer( + containerId, IOUtils.toByteArray(agentFileStream), "/" + TARGET_AGENT_FILENAME); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }, + createTargetWaiter(waitStrategy), + /* inspect= */ true, + logger); + return null; + } + + @Override + public void stopTarget() { + killContainer(target); + target = null; + } + + private void pullImage(String imageName) { + logger.info("Pulling {}", imageName); + + try { + client.pullImageCmd(imageName).exec(new PullImageResultCallback()).awaitCompletion(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private boolean imageExists(String imageName) { + try { + client.inspectImageCmd(imageName).exec(); + return true; + } catch (RuntimeException e) { + return false; + } + } + + private void copyFileToContainer(String containerId, byte[] content, String containerPath) + throws IOException { + try (ByteArrayOutputStream output = new ByteArrayOutputStream(); + TarArchiveOutputStream archiveStream = new TarArchiveOutputStream(output)) { + archiveStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + + TarArchiveEntry entry = new TarArchiveEntry(containerPath); + entry.setSize(content.length); + entry.setMode(0100644); + + archiveStream.putArchiveEntry(entry); + IOUtils.write(content, archiveStream); + archiveStream.closeArchiveEntry(); + archiveStream.finish(); + + client + .copyArchiveToContainerCmd(containerId) + .withTarInputStream(new ByteArrayInputStream(output.toByteArray())) + .withRemotePath("/") + .exec(); + } + } + + private ContainerLogHandler consumeLogs(String containerId, Waiter waiter, Logger logger) { + ContainerLogFrameConsumer consumer = new ContainerLogFrameConsumer(); + waiter.configureLogger(consumer); + + client + .logContainerCmd(containerId) + .withFollowStream(true) + .withSince(0) + .withStdOut(true) + .withStdErr(true) + .exec(consumer); + + consumer.addListener(new Slf4jDockerLogLineListener(logger)); + return consumer; + } + + private static int extractMappedPort(Container container, int internalPort) { + Ports.Binding[] binding = + container + .inspectResponse + .getNetworkSettings() + .getPorts() + .getBindings() + .get(ExposedPort.tcp(internalPort)); + if (binding != null && binding.length > 0 && binding[0] != null) { + return Integer.parseInt(binding[0].getHostPortSpec()); + } else { + throw new IllegalStateException("Port " + internalPort + " not mapped to host."); + } + } + + private Container startContainer( + String imageName, + Consumer createAction, + Consumer prepareAction, + Waiter waiter, + boolean inspect, + Logger logger) { + + if (waiter == null) { + waiter = new NoOpWaiter(); + } + + CreateContainerCmd createCommand = client.createContainerCmd(imageName); + createAction.accept(createCommand); + + String containerId = createCommand.exec().getId(); + + prepareAction.accept(containerId); + + client.startContainerCmd(containerId).exec(); + ContainerLogHandler logHandler = consumeLogs(containerId, waiter, logger); + + InspectContainerResponse inspectResponse = + inspect ? client.inspectContainerCmd(containerId).exec() : null; + Container container = new Container(imageName, containerId, logHandler, inspectResponse); + + waiter.waitFor(container); + return container; + } + + private void killContainer(Container container) { + if (container != null) { + try { + client.killContainerCmd(container.containerId).exec(); + } catch (NotFoundException e) { + // The containers are flagged as remove-on-exit, so not finding them can be expected + } + } + } + + private static class Container { + public final String imageName; + public final String containerId; + public final ContainerLogHandler logConsumer; + public final InspectContainerResponse inspectResponse; + + private Container( + String imageName, + String containerId, + ContainerLogHandler logConsumer, + InspectContainerResponse inspectResponse) { + this.imageName = imageName; + this.containerId = containerId; + this.logConsumer = logConsumer; + this.inspectResponse = inspectResponse; + } + } + + private static Waiter createTargetWaiter(TargetWaitStrategy strategy) { + if (strategy instanceof TargetWaitStrategy.Log) { + TargetWaitStrategy.Log details = (TargetWaitStrategy.Log) strategy; + return new LogWaiter(Pattern.compile(details.regex), details.timeout); + } else { + return new PortWaiter(TARGET_PORT, Duration.ofSeconds(60)); + } + } + + private interface Waiter { + default void configureLogger(ContainerLogHandler logHandler) {} + + void waitFor(Container container); + } + + private static class NoOpWaiter implements Waiter { + @Override + public void waitFor(Container container) { + // No waiting + } + } + + private static class LogWaiter implements Waiter { + private final Pattern regex; + private final Duration timeout; + private final CountDownLatch lineHit = new CountDownLatch(1); + + private LogWaiter(Pattern regex, Duration timeout) { + this.regex = regex; + this.timeout = timeout; + } + + @Override + public void configureLogger(ContainerLogHandler logHandler) { + logHandler.addListener( + (type, text) -> { + if (lineHit.getCount() > 0) { + if (regex.matcher(text).find()) { + lineHit.countDown(); + } + } + }); + } + + @Override + public void waitFor(Container container) { + logger.info( + "Waiting for container {}/{} to hit log line {}", + container.imageName, + container.containerId, + regex.toString()); + + try { + lineHit.await(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + + logger.info("Done waiting for container {}/{}", container.imageName, container.containerId); + } + } + + private static class HttpWaiter implements Waiter { + private static final WebClient CLIENT = + WebClient.builder().responseTimeout(Duration.ofSeconds(1)).build(); + + private final int internalPort; + private final String path; + private final Duration timeout; + private final RateLimiter rateLimiter = + RateLimiterBuilder.newBuilder() + .withRate(1, TimeUnit.SECONDS) + .withConstantThroughput() + .build(); + + private HttpWaiter(int internalPort, String path, Duration timeout) { + this.internalPort = internalPort; + this.path = path; + this.timeout = timeout; + } + + @Override + public void waitFor(Container container) { + String url = "http://localhost:" + extractMappedPort(container, internalPort) + path; + + logger.info( + "Waiting for container {}/{} on url {}", container.imageName, container.containerId, url); + + try { + Unreliables.retryUntilSuccess( + (int) timeout.toMillis(), + TimeUnit.MILLISECONDS, + () -> { + rateLimiter.doWhenReady( + () -> { + AggregatedHttpResponse response = CLIENT.get(url).aggregate().join(); + + if (response.status().code() != 200) { + throw new IllegalStateException( + "Received status code " + response.status().code() + " from " + url); + } + }); + + return true; + }); + } catch (TimeoutException e) { + throw new IllegalStateException( + "Timed out waiting for container " + container.imageName, e); + } + + logger.info("Done waiting for container {}/{}", container.imageName, container.containerId); + } + } + + private static class PortWaiter implements Waiter { + private final int internalPort; + private final Duration timeout; + private final RateLimiter rateLimiter = + RateLimiterBuilder.newBuilder() + .withRate(1, TimeUnit.SECONDS) + .withConstantThroughput() + .build(); + + private PortWaiter(int internalPort, Duration timeout) { + this.internalPort = internalPort; + this.timeout = timeout; + } + + @Override + public void waitFor(Container container) { + logger.info( + "Waiting for container {}/{} on port {}", + container.imageName, + container.containerId, + internalPort); + + try { + Unreliables.retryUntilSuccess( + (int) timeout.toMillis(), + TimeUnit.MILLISECONDS, + () -> { + rateLimiter.doWhenReady( + () -> { + int externalPort = extractMappedPort(container, internalPort); + + try { + new Socket("localhost", externalPort).close(); + } catch (IOException e) { + throw new IllegalStateException( + "Socket not listening yet: " + externalPort, e); + } + }); + + return true; + }); + } catch (TimeoutException e) { + throw new IllegalStateException( + "Timed out waiting for container " + container.imageName, e); + } + + logger.info("Done waiting for container {}/{}", container.imageName, container.containerId); + } + } +} diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/package-info.java b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/package-info.java new file mode 100644 index 000000000..a302cdad4 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/package-info.java @@ -0,0 +1,7 @@ +/** + * This package implements the container management for Windows containers, which Testcontainers + * does not currently support (it only supports running Linux containers on Windows, but not Windows + * containers). As soon as Testcontainers add full support for Windows containers, the need for this + * package should be reevaluated. + */ +package io.opentelemetry.smoketest.windows; diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/liberty-servlet.xml b/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/liberty-servlet.xml new file mode 100644 index 000000000..8157a50ba --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/liberty-servlet.xml @@ -0,0 +1,12 @@ + + + + + servlet-4.0 + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/logback.xml b/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/logback.xml new file mode 100644 index 000000000..4dff78d12 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/logback.xml @@ -0,0 +1,23 @@ + + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/otel.yaml b/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/otel.yaml new file mode 100644 index 000000000..d6a6b2b53 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/otel.yaml @@ -0,0 +1,40 @@ +extensions: + health_check: + pprof: + endpoint: 0.0.0.0:1777 + zpages: + endpoint: 0.0.0.0:55679 + +receivers: + otlp: + protocols: + grpc: + zipkin: + jaeger: + protocols: + grpc: + +processors: + batch: + +exporters: + logging/logging_debug: + loglevel: debug + logging/logging_info: + loglevel: info + otlp: + endpoint: backend:8080 + insecure: true + +service: + pipelines: + traces: + receivers: [ otlp, zipkin, jaeger ] + processors: [ batch ] + exporters: [ logging/logging_debug, otlp ] + metrics: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ logging/logging_info, otlp ] + + extensions: [ health_check, pprof, zpages ] diff --git a/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/testcontainers.properties b/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/testcontainers.properties new file mode 100644 index 000000000..b5226d3e4 --- /dev/null +++ b/opentelemetry-java-instrumentation/smoke-tests/src/test/resources/testcontainers.properties @@ -0,0 +1,2 @@ +# https://github.com/testcontainers/testcontainers-java/issues/3531 +transport.type=httpclient5 diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/integration-tests.gradle b/opentelemetry-java-instrumentation/testing-common/integration-tests/integration-tests.gradle new file mode 100644 index 000000000..ee17436d2 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/integration-tests.gradle @@ -0,0 +1,38 @@ +ext.skipPublish = true + +apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + implementation project(':testing-common:library-for-integration-tests') + + testCompileOnly project(':instrumentation-api') + testCompileOnly project(':javaagent-api') + testCompileOnly project(':javaagent-tooling') + testCompileOnly project(':javaagent-extension-api') + + testImplementation "net.bytebuddy:byte-buddy" + testImplementation "net.bytebuddy:byte-buddy-agent" + + testImplementation "com.google.guava:guava" + testImplementation "run.mone:opentelemetry-extension-annotations" + + testImplementation "cglib:cglib:3.2.5" + + // test instrumenting java 1.1 bytecode + // TODO do we want this? + testImplementation "net.sf.jt400:jt400:6.1" +} + +test { + filter { + excludeTestsMatching 'context.FieldInjectionDisabledTest' + } + // this is needed for AgentInstrumentationSpecificationTest + jvmArgs '-Dotel.javaagent.exclude-classes=config.exclude.packagename.*,config.exclude.SomeClass,config.exclude.SomeClass$NestedClass' +} +test.finalizedBy(tasks.register("testFieldInjectionDisabled", Test) { + filter { + includeTestsMatching 'context.FieldInjectionDisabledTest' + } + jvmArgs '-Dotel.javaagent.experimental.field-injection.enabled=false' +}) diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java new file mode 100644 index 000000000..518ec9c58 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class IbmResourceLevelInstrumentationModule extends InstrumentationModule { + public IbmResourceLevelInstrumentationModule() { + super(IbmResourceLevelInstrumentationModule.class.getName()); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ResourceLevelInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java new file mode 100644 index 000000000..eebc6e56f --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ResourceLevelInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.ibm.as400.resource.ResourceLevel"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("toString"), this.getClass().getName() + "$ToStringAdvice"); + } + + public static class ToStringAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void toStringReplace(@Advice.Return(readOnly = false) String ret) { + ret = "instrumented"; + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/Context.java b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/Context.java new file mode 100644 index 000000000..23d190df6 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/Context.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package context; + +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; + +public class Context { + public static final ContextStore.Factory FACTORY = Context::new; + + public int count = 0; +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/ContextTestInstrumentation.java b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/ContextTestInstrumentation.java new file mode 100644 index 000000000..0616df9df --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/ContextTestInstrumentation.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package context; + +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.api.ContextStore; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import library.KeyClass; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ContextTestInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return nameStartsWith("library."); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("isInstrumented"), this.getClass().getName() + "$MarkInstrumentedAdvice"); + transformer.applyAdviceToMethod( + named("incrementContextCount"), + this.getClass().getName() + "$StoreAndIncrementApiUsageAdvice"); + transformer.applyAdviceToMethod( + named("getContextCount"), this.getClass().getName() + "$GetApiUsageAdvice"); + transformer.applyAdviceToMethod( + named("putContextCount"), this.getClass().getName() + "$PutApiUsageAdvice"); + transformer.applyAdviceToMethod( + named("removeContextCount"), this.getClass().getName() + "$RemoveApiUsageAdvice"); + } + + @SuppressWarnings("unused") + public static class MarkInstrumentedAdvice { + @Advice.OnMethodExit + public static void methodExit(@Advice.Return(readOnly = false) boolean isInstrumented) { + isInstrumented = true; + } + } + + @SuppressWarnings("unused") + public static class StoreAndIncrementApiUsageAdvice { + @Advice.OnMethodExit + public static void methodExit( + @Advice.This KeyClass thiz, @Advice.Return(readOnly = false) int contextCount) { + ContextStore contextStore = + InstrumentationContext.get(KeyClass.class, Context.class); + Context context = contextStore.putIfAbsent(thiz, new Context()); + contextCount = ++context.count; + } + } + + @SuppressWarnings("unused") + public static class StoreAndIncrementWithFactoryApiUsageAdvice { + @Advice.OnMethodExit + public static void methodExit( + @Advice.This KeyClass thiz, @Advice.Return(readOnly = false) int contextCount) { + ContextStore contextStore = + InstrumentationContext.get(KeyClass.class, Context.class); + Context context = contextStore.putIfAbsent(thiz, Context.FACTORY); + contextCount = ++context.count; + } + } + + @SuppressWarnings("unused") + public static class GetApiUsageAdvice { + @Advice.OnMethodExit + public static void methodExit( + @Advice.This KeyClass thiz, @Advice.Return(readOnly = false) int contextCount) { + ContextStore contextStore = + InstrumentationContext.get(KeyClass.class, Context.class); + Context context = contextStore.get(thiz); + contextCount = context == null ? 0 : context.count; + } + } + + @SuppressWarnings("unused") + public static class PutApiUsageAdvice { + @Advice.OnMethodExit + public static void methodExit(@Advice.This KeyClass thiz, @Advice.Argument(0) int value) { + ContextStore contextStore = + InstrumentationContext.get(KeyClass.class, Context.class); + Context context = new Context(); + context.count = value; + contextStore.put(thiz, context); + } + } + + @SuppressWarnings("unused") + public static class RemoveApiUsageAdvice { + @Advice.OnMethodExit + public static void methodExit(@Advice.This KeyClass thiz) { + ContextStore contextStore = + InstrumentationContext.get(KeyClass.class, Context.class); + contextStore.put(thiz, null); + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/ContextTestInstrumentationModule.java b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/ContextTestInstrumentationModule.java new file mode 100644 index 000000000..a1be8d62c --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/main/java/context/ContextTestInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package context; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ContextTestInstrumentationModule extends InstrumentationModule { + public ContextTestInstrumentationModule() { + super("context-test-instrumentation"); + } + + @Override + public boolean isHelperClass(String className) { + return className.equals(getClass().getPackage().getName() + ".Context"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ContextTestInstrumentation()); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy new file mode 100644 index 000000000..66f985399 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import com.google.common.reflect.ClassPath +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.ClasspathUtils +import io.opentelemetry.javaagent.tooling.Constants +import java.util.concurrent.TimeoutException +import org.slf4j.LoggerFactory + +// this test is run using +// -Dotel.javaagent.exclude-classes=config.exclude.packagename.*,config.exclude.SomeClass,config.exclude.SomeClass$NestedClass +// (see integration-tests.gradle) +class AgentInstrumentationSpecificationTest extends AgentInstrumentationSpecification { + private static final ClassLoader BOOTSTRAP_CLASSLOADER = null + + def "classpath setup"() { + setup: + final List bootstrapClassesIncorrectlyLoaded = [] + for (ClassPath.ClassInfo info : getTestClasspath().getAllClasses()) { + for (int i = 0; i < Constants.BOOTSTRAP_PACKAGE_PREFIXES.size(); ++i) { + if (info.getName().startsWith(Constants.BOOTSTRAP_PACKAGE_PREFIXES[i])) { + Class bootstrapClass = Class.forName(info.getName()) + def loader + try { + loader = bootstrapClass.getClassLoader() + } catch (NoClassDefFoundError e) { + // some classes in com.google.errorprone.annotations cause groovy to throw + // java.lang.NoClassDefFoundError: [Ljavax/lang/model/element/Modifier; + break + } + if (loader != BOOTSTRAP_CLASSLOADER) { + bootstrapClassesIncorrectlyLoaded.add(bootstrapClass) + } + break + } + } + } + + expect: + bootstrapClassesIncorrectlyLoaded == [] + } + + def "waiting for child spans times out"() { + when: + runUnderTrace("parent") { + waitForTraces(1) + } + + then: + thrown(TimeoutException) + } + + def "logging works"() { + when: + LoggerFactory.getLogger(AgentInstrumentationSpecificationTest).debug("hello") + then: + noExceptionThrown() + } + + def "excluded classes are not instrumented"() { + when: + runUnderTrace("parent") { + subject.run() + } + + then: + assertTraces(1) { + trace(0, spanName ? 2 : 1) { + span(0) { + name "parent" + } + if (spanName) { + span(1) { + name spanName + childOf span(0) + } + } + } + } + + where: + subject | spanName + new config.SomeClass() | "SomeClass.run" + new config.SomeClass.NestedClass() | "NestedClass.run" + new config.exclude.SomeClass() | null + new config.exclude.SomeClass.NestedClass() | null + new config.exclude.packagename.SomeClass() | null + new config.exclude.packagename.SomeClass.NestedClass() | null + } + + def "test unblocked by completed span"() { + setup: + runUnderTrace("parent") { + runUnderTrace("child") {} + } + + expect: + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + hasNoParent() + } + span(1) { + name "child" + childOf span(0) + } + } + } + } + + private static ClassPath getTestClasspath() { + ClassLoader testClassLoader = ClasspathUtils.getClassLoader() + if (!(testClassLoader instanceof URLClassLoader)) { + // java9's system loader does not extend URLClassLoader + // which breaks Guava ClassPath lookup + testClassLoader = buildJavaClassPathClassLoader() + } + try { + return ClassPath.from(testClassLoader) + } catch (IOException e) { + throw new IllegalStateException(e) + } + } + + /** + * Parse JVM classpath and return ClassLoader containing all classpath entries. Inspired by Guava. + */ + private static ClassLoader buildJavaClassPathClassLoader() { + List urls = new ArrayList<>() + for (String entry : getClasspath()) { + try { + try { + urls.add(new File(entry).toURI().toURL()) + } catch (SecurityException e) { // File.toURI checks to see if the file is a directory + urls.add(new URL("file", null, new File(entry).getAbsolutePath())) + } + } catch (MalformedURLException e) { + throw new IllegalStateException(e) + } + } + return new URLClassLoader(urls.toArray(new URL[0]), (ClassLoader) null) + } + + private static String[] getClasspath() { + return System.getProperty("java.class.path").split(System.getProperty("path.separator")) + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/InstrumentOldBytecode.groovy b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/InstrumentOldBytecode.groovy new file mode 100644 index 000000000..629be499d --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/InstrumentOldBytecode.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.ibm.as400.resource.ResourceLevel +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +class InstrumentOldBytecode extends AgentInstrumentationSpecification { + def "can instrument old bytecode"() { + expect: + new ResourceLevel().toString() == "instrumented" + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/context/FieldBackedProviderTest.groovy b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/context/FieldBackedProviderTest.groovy new file mode 100644 index 000000000..a8b914213 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/context/FieldBackedProviderTest.groovy @@ -0,0 +1,189 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package context + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.utils.ClasspathUtils +import io.opentelemetry.instrumentation.test.utils.GcUtils +import io.opentelemetry.javaagent.testing.common.TestAgentListenerAccess +import java.lang.instrument.ClassDefinition +import java.lang.ref.WeakReference +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.concurrent.atomic.AtomicReference +import library.KeyClass +import library.UntransformableKeyClass +import net.bytebuddy.agent.ByteBuddyAgent +import net.sf.cglib.proxy.Enhancer +import net.sf.cglib.proxy.MethodInterceptor +import net.sf.cglib.proxy.MethodProxy + +// this test is run using +// -Dotel.instrumentation.context-test-instrumentation.enabled=true +// (see integration-tests.gradle) +class FieldBackedProviderTest extends AgentInstrumentationSpecification { + + def setupSpec() { + TestAgentListenerAccess.addSkipErrorCondition({ typeName, throwable -> + return typeName.startsWith('library.Incorrect') && + throwable.getMessage().startsWith("Incorrect Context Api Usage detected.") + }) + TestAgentListenerAccess.addSkipTransformationCondition({ typeName -> + return typeName != null && typeName.endsWith("UntransformableKeyClass") + }) + } + + def "#keyClassName structure modified = #shouldModifyStructure"() { + setup: + boolean hasField = false + boolean isPrivate = false + boolean isTransient = false + for (Field field : keyClass.getDeclaredFields()) { + if (field.getName().startsWith("__opentelemetry")) { + isPrivate = Modifier.isPrivate(field.getModifiers()) + isTransient = Modifier.isTransient(field.getModifiers()) + hasField = true + break + } + } + + boolean hasMarkerInterface = false + boolean hasAccessorInterface = false + for (Class inter : keyClass.getInterfaces()) { + if (inter.getName() == 'io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker') { + hasMarkerInterface = true + } + if (inter.getName().startsWith('io.opentelemetry.javaagent.bootstrap.instrumentation.context.FieldBackedProvider$ContextAccessor')) { + hasAccessorInterface = true + } + } + + expect: + hasField == shouldModifyStructure + isPrivate == shouldModifyStructure + isTransient == shouldModifyStructure + hasMarkerInterface == shouldModifyStructure + hasAccessorInterface == shouldModifyStructure + keyClass.newInstance().isInstrumented() == shouldModifyStructure + + where: + keyClass | keyClassName | shouldModifyStructure + KeyClass | keyClass.getSimpleName() | true + UntransformableKeyClass | keyClass.getSimpleName() | false + } + + def "correct api usage stores state in map"() { + when: + instance1.incrementContextCount() + + then: + instance1.incrementContextCount() == 2 + instance2.incrementContextCount() == 1 + + where: + instance1 | instance2 + new KeyClass() | new KeyClass() + new UntransformableKeyClass() | new UntransformableKeyClass() + } + + def "get/put test"() { + when: + instance1.putContextCount(10) + + then: + instance1.getContextCount() == 10 + + where: + instance1 | _ + new KeyClass() | _ + new UntransformableKeyClass() | _ + } + + def "remove test"() { + given: + instance1.putContextCount(10) + + when: + instance1.removeContextCount() + + then: + instance1.getContextCount() == 0 + + where: + instance1 | _ + new KeyClass() | _ + new UntransformableKeyClass() | _ + } + + def "works with cglib enhanced instances which duplicates context getter and setter methods"() { + setup: + Enhancer enhancer = new Enhancer() + enhancer.setSuperclass(KeyClass) + enhancer.setCallback(new MethodInterceptor() { + @Override + Object intercept(Object arg0, Method arg1, Object[] arg2, + MethodProxy arg3) throws Throwable { + return arg3.invokeSuper(arg0, arg2) + } + }) + + when: + (KeyClass) enhancer.create() + + then: + noExceptionThrown() + } + + def "backing map should not create strong refs to key class instances #keyValue.get().getClass().getName()"() { + when: + int count = keyValue.get().incrementContextCount() + WeakReference instanceRef = new WeakReference(keyValue.get()) + keyValue.set(null) + GcUtils.awaitGc(instanceRef) + + then: + instanceRef.get() == null + count == 1 + + where: + keyValue | _ + new AtomicReference(new KeyClass()) | _ + new AtomicReference(new UntransformableKeyClass()) | _ + } + + def "context classes are retransform safe"() { + when: + ByteBuddyAgent.install() + ByteBuddyAgent.getInstrumentation().retransformClasses(KeyClass) + ByteBuddyAgent.getInstrumentation().retransformClasses(UntransformableKeyClass) + + then: + new KeyClass().isInstrumented() + !new UntransformableKeyClass().isInstrumented() + new KeyClass().incrementContextCount() == 1 + new UntransformableKeyClass().incrementContextCount() == 1 + } + + // NB: This test will fail if some other agent is also running that modifies the class structure + // in a way that is incompatible with redefining the class back to its original bytecode. + // A likely culprit is jacoco if you start seeing failure here due to a change make sure jacoco + // exclusion is working. + def "context classes are redefine safe"() { + when: + ByteBuddyAgent.install() + ByteBuddyAgent.getInstrumentation().redefineClasses(new ClassDefinition(KeyClass, ClasspathUtils.convertToByteArray(KeyClass))) + ByteBuddyAgent.getInstrumentation().redefineClasses(new ClassDefinition(UntransformableKeyClass, ClasspathUtils.convertToByteArray(UntransformableKeyClass))) + + then: + new KeyClass().isInstrumented() + !new UntransformableKeyClass().isInstrumented() + new KeyClass().incrementContextCount() == 1 + new UntransformableKeyClass().incrementContextCount() == 1 + } +} + + diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/context/FieldInjectionDisabledTest.groovy b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/context/FieldInjectionDisabledTest.groovy new file mode 100644 index 000000000..ba9cfe296 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/groovy/context/FieldInjectionDisabledTest.groovy @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package context + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.javaagent.testing.common.TestAgentListenerAccess +import java.lang.reflect.Field +import library.DisabledKeyClass + +// this test is run using: +// -Dotel.javaagent.experimental.field-injection.enabled=false +// -Dotel.instrumentation.context-test-instrumentation.enabled=true +// (see integration-tests.gradle) +class FieldInjectionDisabledTest extends AgentInstrumentationSpecification { + + def setupSpec() { + TestAgentListenerAccess.addSkipErrorCondition({ typeName, throwable -> + return typeName.startsWith(ContextTestInstrumentationModule.getName() + '$Incorrect') && throwable.getMessage().startsWith("Incorrect Context Api Usage detected.") + }) + } + + def "Check that structure is not modified when structure modification is disabled"() { + setup: + def keyClass = DisabledKeyClass + boolean hasField = false + for (Field field : keyClass.getDeclaredFields()) { + if (field.getName().startsWith("__opentelemetry")) { + hasField = true + break + } + } + + boolean hasMarkerInterface = false + boolean hasAccessorInterface = false + for (Class inter : keyClass.getInterfaces()) { + if (inter.getName() == 'io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker') { + hasMarkerInterface = true + } + if (inter.getName().startsWith('io.opentelemetry.javaagent.bootstrap.instrumentation.context.FieldBackedProvider$ContextAccessor')) { + hasAccessorInterface = true + } + } + + expect: + hasField == false + hasMarkerInterface == false + hasAccessorInterface == false + keyClass.newInstance().isInstrumented() == true + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/SomeClass.java b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/SomeClass.java new file mode 100644 index 000000000..0b88f99f5 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/SomeClass.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package config; + +import io.opentelemetry.extension.annotations.WithSpan; + +public class SomeClass implements Runnable { + + @WithSpan + @Override + public void run() {} + + public static class NestedClass implements Runnable { + + @WithSpan + @Override + public void run() {} + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/exclude/SomeClass.java b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/exclude/SomeClass.java new file mode 100644 index 000000000..7db69d134 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/exclude/SomeClass.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package config.exclude; + +import io.opentelemetry.extension.annotations.WithSpan; + +public class SomeClass implements Runnable { + + @WithSpan + @Override + public void run() {} + + public static class NestedClass implements Runnable { + + @WithSpan + @Override + public void run() {} + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/exclude/packagename/SomeClass.java b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/exclude/packagename/SomeClass.java new file mode 100644 index 000000000..7f9c2ac16 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/integration-tests/src/test/java/config/exclude/packagename/SomeClass.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package config.exclude.packagename; + +import io.opentelemetry.extension.annotations.WithSpan; + +public class SomeClass implements Runnable { + + @WithSpan + @Override + public void run() {} + + public static class NestedClass implements Runnable { + + @WithSpan + @Override + public void run() {} + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/library-for-integration-tests.gradle b/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/library-for-integration-tests.gradle new file mode 100644 index 000000000..8b761a00d --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/library-for-integration-tests.gradle @@ -0,0 +1 @@ +apply plugin: "otel.java-conventions" diff --git a/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/DisabledKeyClass.java b/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/DisabledKeyClass.java new file mode 100644 index 000000000..6289053dd --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/DisabledKeyClass.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package library; + +/** A class that is used that field injection can be disabled. */ +public class DisabledKeyClass extends KeyClass { + @Override + public boolean isInstrumented() { + return false; + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/KeyClass.java b/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/KeyClass.java new file mode 100644 index 000000000..74250dc52 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/KeyClass.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package library; + +public class KeyClass { + public boolean isInstrumented() { + // implementation replaced with test instrumentation + return false; + } + + public int incrementContextCount() { + // implementation replaced with test instrumentation + return -1; + } + + public int incrementContextCountWithFactory() { + // implementation replaced with test instrumentation + return -1; + } + + public int getContextCount() { + // implementation replaced with test instrumentation + return -1; + } + + public void putContextCount(int value) { + // implementation replaced with test instrumentation + } + + public void removeContextCount() { + // implementation replaced with test instrumentation + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/UntransformableKeyClass.java b/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/UntransformableKeyClass.java new file mode 100644 index 000000000..11c9a5dfe --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/library-for-integration-tests/src/main/java/library/UntransformableKeyClass.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package library; + +/** + * A class which will not be transformed by our instrumentation due to, see + * FieldBackedProviderTest's skipTransformationConditions() method. + */ +public class UntransformableKeyClass extends KeyClass { + @Override + public boolean isInstrumented() { + return false; + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/AgentInstrumentationSpecification.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/AgentInstrumentationSpecification.groovy new file mode 100644 index 000000000..20a4b20db --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/AgentInstrumentationSpecification.groovy @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test + +/** + * Base class for spock specifications that test javaagent instrumentations. + */ +abstract class AgentInstrumentationSpecification extends InstrumentationSpecification implements AgentTestTrait { +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/AgentTestTrait.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/AgentTestTrait.groovy new file mode 100644 index 000000000..4a64b9257 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/AgentTestTrait.groovy @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test + +import io.opentelemetry.instrumentation.testing.AgentTestRunner +import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner + +/** + * A trait which initializes agent tests, including bytecode manipulation and a test span exporter. + * All agent tests should implement this trait. + */ +trait AgentTestTrait { + + InstrumentationTestRunner testRunner() { + AgentTestRunner.instance() + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/InstrumentationSpecification.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/InstrumentationSpecification.groovy new file mode 100644 index 000000000..4d0fb8706 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/InstrumentationSpecification.groovy @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.ContextStorage +import io.opentelemetry.instrumentation.test.asserts.InMemoryExporterAssert +import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner +import io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil +import io.opentelemetry.sdk.metrics.data.MetricData +import io.opentelemetry.sdk.trace.data.SpanData +import spock.lang.Specification + +/** + * Base class for test specifications that are shared between instrumentation libraries and agent. + * The methods in this class are implemented by {@link AgentTestTrait} and + * {@link LibraryTestTrait}. + */ +abstract class InstrumentationSpecification extends Specification { + abstract InstrumentationTestRunner testRunner() + + def setupSpec() { + testRunner().beforeTestClass() + } + + /** + * Clears all data exported during a test. + */ + def setup() { + assert !Span.current().getSpanContext().isValid(): "Span is active before test has started: " + Span.current() + testRunner().clearAllExportedData() + } + + def cleanup() { + ContextStorage storage = ContextStorage.get() + if (storage instanceof AutoCloseable) { + ((AutoCloseable) storage).close() + } + } + + def cleanupSpec() { + testRunner().afterTestClass() + } + + /** Return the {@link OpenTelemetry} instance used to produce telemetry data. */ + OpenTelemetry getOpenTelemetry() { + testRunner().openTelemetry + } + + /** Return a list of all captured traces, where each trace is a sorted list of spans. */ + List> getTraces() { + TelemetryDataUtil.groupTraces(testRunner().getExportedSpans()) + } + + /** Return a list of all captured metrics. */ + List getMetrics() { + testRunner().getExportedMetrics() + } + + /** + * Removes all captured telemetry data. After calling this method {@link #getTraces()} and + * {@link #getMetrics()} will return empty lists until more telemetry data is captured. + */ + void clearExportedData() { + testRunner().clearAllExportedData() + } + + boolean forceFlushCalled() { + return testRunner().forceFlushCalled() + } + + /** + * Wait until at least {@code numberOfTraces} traces are completed and return all captured traces. + * Note that there may be more than {@code numberOfTraces} collected. By default this waits up to + * 20 seconds, then times out. + */ + List> waitForTraces(int numberOfTraces) { + TelemetryDataUtil.waitForTraces({ testRunner().getExportedSpans() }, numberOfTraces) + } + + void ignoreTracesAndClear(int numberOfTraces) { + waitForTraces(numberOfTraces) + clearExportedData() + } + + void assertTraces( + final int size, + @ClosureParams( + value = SimpleType, + options = "io.opentelemetry.instrumentation.test.asserts.ListWriterAssert") + @DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST) + final Closure spec) { + InMemoryExporterAssert.assertTraces({ testRunner().getExportedSpans() }, size, spec) + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/LibraryInstrumentationSpecification.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/LibraryInstrumentationSpecification.groovy new file mode 100644 index 000000000..a423f64bb --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/LibraryInstrumentationSpecification.groovy @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test + +/** + * Base class for spock specifications that test library instrumentations. + */ +abstract class LibraryInstrumentationSpecification extends InstrumentationSpecification implements LibraryTestTrait { +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/LibraryTestTrait.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/LibraryTestTrait.groovy new file mode 100644 index 000000000..d24e2937e --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/LibraryTestTrait.groovy @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test + + +import io.opentelemetry.instrumentation.testing.LibraryTestRunner + +/** + * A trait which initializes instrumentation library tests, including a test span exporter. All + * library tests should implement this trait. + */ +trait LibraryTestTrait { + // library test runner has to be initialized statically so that GlobalOpenTelemetry is set as soon as possible + private static final LibraryTestRunner RUNNER = LibraryTestRunner.instance() + + LibraryTestRunner testRunner() { + RUNNER + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/RetryOnAddressAlreadyInUseTrait.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/RetryOnAddressAlreadyInUseTrait.groovy new file mode 100644 index 000000000..14def8fc7 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/RetryOnAddressAlreadyInUseTrait.groovy @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * A trait for retrying operation when it fails with "java.net.BindException: Address already in use" + */ +trait RetryOnAddressAlreadyInUseTrait { + private static final Logger log = LoggerFactory.getLogger(RetryOnAddressAlreadyInUseTrait) + + /** + * This is used by setupSpec() methods to auto-retry setup that depends on finding and then using + * an available free port, because that kind of setup can fail sporadically if the available port + * gets re-used between when we find the available port and when we use it. + * + * @param closure the groovy closure to run with retry + */ + static void withRetryOnAddressAlreadyInUse(Closure closure) { + withRetryOnAddressAlreadyInUse(closure, 3) + } + + static void withRetryOnAddressAlreadyInUse(Closure closure, int numRetries) { + try { + closure.call() + } catch (Throwable t) { + // typically this is "java.net.BindException: Address already in use", but also can be + // "io.netty.channel.unix.Errors$NativeIoException: bind() failed: Address already in use" + if (numRetries == 0 || !t.getMessage().contains("Address already in use")) { + throw t + } + log.debug("retrying due to bind exception: {}", t.getMessage(), t) + withRetryOnAddressAlreadyInUse(closure, numRetries - 1) + } + } +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/AttributesAssert.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/AttributesAssert.groovy new file mode 100644 index 000000000..c0163284b --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/AttributesAssert.groovy @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.asserts + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import java.util.regex.Pattern + +class AttributesAssert { + private final Map attributes + private final Set assertedAttributes = new TreeSet<>() + + private AttributesAssert(Map attributes) { + this.attributes = attributes + } + + static void assertAttributes(Map attributes, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) + @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + def asserter = new AttributesAssert(attributes) + def clone = (Closure) spec.clone() + clone.delegate = asserter + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone(asserter) + asserter.assertAttributesAllVerified() + } + + def attribute(String name, expected) { + if (expected == null) { + return + } + assertedAttributes.add(name) + def value = attributes.get(name) + if (expected instanceof Pattern) { + assert value =~ expected + } else if (expected instanceof Class) { + assert ((Class) expected).isInstance(value) + } else if (expected instanceof Closure) { + assert ((Closure) expected).call(value) + } else { + assert value == expected + } + } + + def methodMissing(String name, args) { + if (args.length == 0) { + throw new IllegalArgumentException(args.toString()) + } + attribute(name, args[0]) + } + + // this could be private, but then codenarc fails, thinking (incorrectly) that it's unused + void assertAttributesAllVerified() { + Set allAttributes = new TreeSet<>(attributes.keySet()) + Set unverifiedAttributes = new TreeSet(allAttributes) + unverifiedAttributes.removeAll(assertedAttributes) + + // Don't need to verify thread details. + assertedAttributes.add("thread.id") + unverifiedAttributes.remove("thread.id") + assertedAttributes.add("thread.name") + unverifiedAttributes.remove("thread.name") + + // The first and second condition in the assert are exactly the same + // but both are included in order to provide better context in the error message. + // containsAll because tests may assert more attributes than span actually has + assert unverifiedAttributes.isEmpty() && assertedAttributes.containsAll(allAttributes) + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/EventAssert.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/EventAssert.groovy new file mode 100644 index 000000000..2e000686a --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/EventAssert.groovy @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.asserts + +import static AttributesAssert.assertAttributes + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.sdk.trace.data.EventData + +class EventAssert { + private final EventData event + private final checked = [:] + + private EventAssert(event) { + this.event = event + } + + static void assertEvent(EventData event, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.EventAssert']) + @DelegatesTo(value = EventAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + def asserter = new EventAssert(event) + asserter.assertEvent spec + } + + void assertEvent( + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.EventAssert']) + @DelegatesTo(value = EventAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + def clone = (Closure) spec.clone() + clone.delegate = this + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone(this) + } + + def eventName(String name) { + assert event.name == name + checked.name = true + } + + void attributes(@ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) + @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + assertAttributes(toMap(event.attributes), spec) + } + + + private Map toMap(Attributes attributes) { + def map = new HashMap() + attributes.forEach {key, value -> + map.put(key.key, value) + } + return map + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/InMemoryExporterAssert.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/InMemoryExporterAssert.groovy new file mode 100644 index 000000000..56369f7eb --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/InMemoryExporterAssert.groovy @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.asserts + +import static TraceAssert.assertTrace + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.function.Supplier +import org.codehaus.groovy.runtime.powerassert.PowerAssertionError +import org.spockframework.runtime.Condition +import org.spockframework.runtime.ConditionNotSatisfiedError +import org.spockframework.runtime.model.TextPosition + +class InMemoryExporterAssert { + private final List> traces + private final Supplier> spanSupplier + + private final Set assertedIndexes = new HashSet<>() + + private InMemoryExporterAssert(List> traces, Supplier> spanSupplier) { + this.traces = traces + this.spanSupplier = spanSupplier + } + + static void assertTraces(Supplier> spanSupplier, int expectedSize, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.ListWriterAssert']) + @DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + try { + def traces = TelemetryDataUtil.waitForTraces(spanSupplier, expectedSize) + assert traces.size() == expectedSize + def asserter = new InMemoryExporterAssert(traces, spanSupplier) + def clone = (Closure) spec.clone() + clone.delegate = asserter + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone(asserter) + asserter.assertTracesAllVerified() + } catch (PowerAssertionError e) { + def stackLine = null + for (int i = 0; i < e.stackTrace.length; i++) { + def className = e.stackTrace[i].className + def skip = className.startsWith("org.codehaus.groovy.") || + className.startsWith("io.opentelemetry.instrumentation.test.") || + className.startsWith("sun.reflect.") || + className.startsWith("groovy.lang.") || + className.startsWith("java.lang.") + if (skip) { + continue + } + stackLine = e.stackTrace[i] + break + } + def condition = new Condition(null, "$stackLine", TextPosition.create(stackLine == null ? 0 : stackLine.lineNumber, 0), e.message, null, e) + throw new ConditionNotSatisfiedError(condition, e) + } + } + + List> getTraces() { + return traces + } + + void trace(int index, int expectedSize, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.TraceAssert']) + @DelegatesTo(value = TraceAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + if (index >= traces.size()) { + throw new ArrayIndexOutOfBoundsException(index) + } + assertedIndexes.add(index) + assertTrace(spanSupplier, traces[index][0].traceId, expectedSize, spec) + } + + static Comparator> orderByRootSpanName(String... names) { + def list = Arrays.asList(names) + return Comparator.comparing { item -> list.indexOf(item[0].name) } + } + + static Comparator> orderByRootSpanKind(SpanKind... spanKinds) { + def list = Arrays.asList(spanKinds) + return Comparator.comparing { item -> list.indexOf(item[0].kind) } + } + + void assertTracesAllVerified() { + assert assertedIndexes.size() == traces.size() + } + + void sortSpansByStartTime() { + traces.each { + it.sort { a, b -> + return a.startEpochNanos <=> b.startEpochNanos + } + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/SpanAssert.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/SpanAssert.groovy new file mode 100644 index 000000000..22cedc3ad --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/SpanAssert.groovy @@ -0,0 +1,174 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.asserts + +import static AttributesAssert.assertAttributes +import static io.opentelemetry.instrumentation.test.asserts.EventAssert.assertEvent + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.SpanId +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.regex.Pattern + +class SpanAssert { + private final SpanData span + private final checked = [:] + + private final Set assertedEventIndexes = new HashSet<>() + + private SpanAssert(span) { + this.span = span + } + + static void assertSpan(SpanData span, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.SpanAssert']) + @DelegatesTo(value = SpanAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + def asserter = new SpanAssert(span) + asserter.assertSpan spec + asserter.assertEventsAllVerified() + } + + void assertSpan( + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.SpanAssert']) + @DelegatesTo(value = SpanAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + def clone = (Closure) spec.clone() + clone.delegate = this + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone(this) + assertDefaults() + } + + void event(int index, @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.EventAssert']) @DelegatesTo(value = EventAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + if (index >= span.events.size()) { + throw new ArrayIndexOutOfBoundsException(index) + } + assertedEventIndexes.add(index) + assertEvent(span.events.get(index), spec) + } + + def name(String expected) { + assert span.name == expected + checked.name = true + } + + def name(Pattern expected) { + assert span.name =~ expected + checked.name = true + } + + def name(Closure expected) { + assert ((Closure) expected).call(span.name) + checked.name = true + } + + def nameContains(String... expectedParts) { + for (String expectedPart : expectedParts) { + assert span.name.contains(expectedPart) + } + checked.name = true + } + + def kind(SpanKind expected) { + assert span.kind == expected + checked.kind = true + } + + def hasNoParent() { + assert !SpanId.isValid(span.parentSpanId) + checked.parentSpanId = true + } + + def parentSpanId(String expected) { + assert span.parentSpanId == expected + checked.parentId = true + } + + def traceId(String expected) { + assert span.traceId == expected + checked.traceId = true + } + + def childOf(SpanData expectedParent) { + parentSpanId(expectedParent.spanId) + traceId(expectedParent.traceId) + } + + def hasLink(SpanData expectedLink) { + hasLink(expectedLink.traceId, expectedLink.spanId) + } + + def hasLink(String expectedTraceId, String expectedSpanId) { + def found = false + for (def link : span.links) { + if (link.spanContext.traceId == expectedTraceId && link.spanContext.spanId == expectedSpanId) { + found = true + break + } + } + assert found + } + + def hasNoLinks() { + assert span.links.empty + } + + def status(StatusCode expected) { + assert span.status.statusCode == expected + checked.status = true + } + + def errorEvent(Class expectedClass) { + errorEvent(expectedClass, null) + } + + def errorEvent(Class expectedClass, expectedMessage) { + errorEvent(expectedClass, expectedMessage, 0) + } + + def errorEvent(Class errorClass, expectedMessage, int index) { + event(index) { + eventName(SemanticAttributes.EXCEPTION_EVENT_NAME) + attributes { + "${SemanticAttributes.EXCEPTION_TYPE.key}" errorClass.canonicalName + "${SemanticAttributes.EXCEPTION_STACKTRACE.key}" String + if (expectedMessage != null) { + "${SemanticAttributes.EXCEPTION_MESSAGE.key}" expectedMessage + } + } + } + } + + void assertDefaults() { + if (!checked.status) { + status(StatusCode.UNSET) + } + if (!checked.kind) { + kind(SpanKind.INTERNAL) + } + } + + void attributes(@ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) + @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + assertAttributes(toMap(span.attributes), spec) + } + + void assertEventsAllVerified() { + assert assertedEventIndexes.size() == span.events.size() + } + + private Map toMap(Attributes attributes) { + def map = new HashMap() + attributes.forEach { key, value -> + map.put(key.key, value) + } + return map + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/TraceAssert.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/TraceAssert.groovy new file mode 100644 index 000000000..0a032977c --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/TraceAssert.groovy @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.asserts + +import static SpanAssert.assertSpan + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.TimeUnit +import java.util.function.Supplier + +class TraceAssert { + private final List spans + + private final Set assertedIndexes = new HashSet<>() + + private TraceAssert(spans) { + this.spans = spans + } + + static void assertTrace(Supplier> spanSupplier, String traceId, int expectedSize, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.TraceAssert']) + @DelegatesTo(value = TraceAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + def spans = getTrace(spanSupplier, traceId) + def startTime = System.nanoTime() + while (spans.size() < expectedSize && elapsedSeconds(startTime) < 10) { + Thread.sleep(10) + spans = getTrace(spanSupplier, traceId) + } + assert spans.size() == expectedSize + def asserter = new TraceAssert(spans) + def clone = (Closure) spec.clone() + clone.delegate = asserter + clone.resolveStrategy = Closure.DELEGATE_FIRST + clone(asserter) + asserter.assertSpansAllVerified() + } + + private static long elapsedSeconds(long startTime) { + TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime) + } + + List getSpans() { + return spans + } + + SpanData span(int index) { + spans.get(index) + } + + void span(int index, @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.SpanAssert']) @DelegatesTo(value = SpanAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + if (index >= spans.size()) { + throw new ArrayIndexOutOfBoundsException(index) + } + assertedIndexes.add(index) + assertSpan(spans.get(index), spec) + } + + void span(String name, @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.SpanAssert']) @DelegatesTo(value = SpanAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) { + int index = -1 + for (int i = 0; i < spans.size(); i++) { + if (spans[i].name == name) { + index = i + break + } + } + span(index, spec) + } + + void assertSpansAllVerified() { + assert assertedIndexes.size() == spans.size() + } + + private static List getTrace(Supplier> spanSupplier, String traceId) { + List> traces = TelemetryDataUtil.groupTraces(spanSupplier.get()) + for (List trace : traces) { + if (trace[0].traceId == traceId) { + return trace + } + } + throw new AssertionError("Trace not found: " + traceId) + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/AbstractPromiseTest.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/AbstractPromiseTest.groovy new file mode 100644 index 000000000..816513f61 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/AbstractPromiseTest.groovy @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.base + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification + +// TODO: add a test for a longer chain of promises +abstract class AbstractPromiseTest extends AgentInstrumentationSpecification { + + abstract P newPromise() + + abstract M map(P promise, Closure callback) + + abstract void onComplete(M promise, Closure callback) + + abstract void complete(P promise, boolean value) + + abstract Boolean get(P promise) + + def "test call with parent"() { + setup: + def promise = newPromise() + + when: + runUnderTrace("parent") { + def mapped = map(promise) { "$it" } + onComplete(mapped) { + assert it == "$value" + runUnderTrace("callback") {} + } + runUnderTrace("other") { + complete(promise, value) + } + } + + then: + get(promise) == value + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "other", it.span(0)) + basicSpan(it, 2, "callback", it.span(0)) + } + } + + where: + value << [true, false] + } + + def "test call with parent delayed complete"() { + setup: + def promise = newPromise() + + when: + runUnderTrace("parent") { + def mapped = map(promise) { "$it" } + onComplete(mapped) { + assert it == "$value" + runUnderTrace("callback") {} + } + } + + runUnderTrace("other") { + complete(promise, value) + } + + then: + get(promise) == value + assertTraces(2) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "callback", span(0)) + } + trace(1, 1) { + basicSpan(it, 0, "other") + } + } + + where: + value << [true, false] + } + + def "test call with parent complete separate thread"() { + setup: + final promise = newPromise() + + when: + runUnderTrace("parent") { + def mapped = map(promise) { "$it" } + onComplete(mapped) { + assert it == "$value" + runUnderTrace("callback") {} + } + Thread.start { + complete(promise, value) + }.join() + } + + then: + get(promise) == value + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + basicSpan(it, 1, "callback", it.span(0)) + } + } + + where: + value << [true, false] + } + + def "test call with no parent"() { + setup: + def promise = newPromise() + + when: + def mapped = map(promise) { "$it" } + onComplete(mapped) { + assert it == "$value" + runUnderTrace("callback") {} + } + + runUnderTrace("other") { + complete(promise, value) + } + + then: + get(promise) == value + assertTraces(1) { + trace(0, 2) { + // TODO: is this really the behavior we want? + basicSpan(it, 0, "other") + basicSpan(it, 1, "callback", it.span(0)) + } + } + + where: + value << [true, false] + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy new file mode 100644 index 000000000..dd776d05d --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy @@ -0,0 +1,1042 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.base + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicClientSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderParentClientSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP +import static io.opentelemetry.testing.internal.armeria.common.MediaType.PLAIN_TEXT_UTF_8 +import static org.junit.Assume.assumeTrue + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanBuilder +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.AttributesAssert +import io.opentelemetry.instrumentation.test.asserts.SpanAssert +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.server.http.RequestContextGetter +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.common.HttpData +import io.opentelemetry.testing.internal.armeria.common.HttpRequest +import io.opentelemetry.testing.internal.armeria.common.HttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpStatus +import io.opentelemetry.testing.internal.armeria.common.ResponseHeaders +import io.opentelemetry.testing.internal.armeria.common.ResponseHeadersBuilder +import io.opentelemetry.testing.internal.armeria.server.DecoratingHttpServiceFunction +import io.opentelemetry.testing.internal.armeria.server.HttpService +import io.opentelemetry.testing.internal.armeria.server.ServerBuilder +import io.opentelemetry.testing.internal.armeria.server.ServiceRequestContext +import io.opentelemetry.testing.internal.armeria.server.logging.LoggingService +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.ServerExtension +import java.security.KeyStore +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.function.Supplier +import javax.net.ssl.KeyManagerFactory +import spock.lang.Requires +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class HttpClientTest extends InstrumentationSpecification { + protected static final BODY_METHODS = ["POST", "PUT"] + protected static final CONNECT_TIMEOUT_MS = 5000 + protected static final BASIC_AUTH_KEY = "custom-authorization-header" + protected static final BASIC_AUTH_VAL = "plain text auth token" + + @Shared + Tracer tracer = openTelemetry.getTracer("test") + + @Shared + def server= new ServerExtension(false) { + @Override + protected void configure(ServerBuilder sb) throws Exception { + KeyStore keystore = KeyStore.getInstance("PKCS12") + keystore.load(new FileInputStream(new File(System.getProperty("javax.net.ssl.trustStore"))), "testing".toCharArray()) + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + kmf.init(keystore, "testing".toCharArray()) + + sb.http(0) + .https(0) + .tls(kmf) + .service("/success") {ctx, req -> + ResponseHeadersBuilder headers = ResponseHeaders.builder(HttpStatus.OK) + def testRequestId = req.headers().get("test-request-id") + if (testRequestId != null) { + headers.set("test-request-id", testRequestId) + } + HttpResponse.of(headers.build(), HttpData.ofAscii("Hello.")) + } + .service("/client-error") {ctx, req -> + HttpResponse.of(HttpStatus.BAD_REQUEST, PLAIN_TEXT_UTF_8, "Invalid RQ") + } + .service("/error") {ctx, req -> + HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, PLAIN_TEXT_UTF_8, "Sorry.") + } + .service("/redirect") {ctx, req -> + HttpResponse.ofRedirect(HttpStatus.FOUND, "/success") + } + .service("/another-redirect") {ctx, req -> + HttpResponse.ofRedirect(HttpStatus.FOUND, "/redirect") + } + .service("/circular-redirect") {ctx, req -> + HttpResponse.ofRedirect(HttpStatus.FOUND, "/circular-redirect") + } + .service("/secured") {ctx, req -> + if (req.headers().get(BASIC_AUTH_KEY) == BASIC_AUTH_VAL) { + return HttpResponse.of(HttpStatus.OK, PLAIN_TEXT_UTF_8, "secured string under basic auth") + } + return HttpResponse.of(HttpStatus.UNAUTHORIZED, PLAIN_TEXT_UTF_8, "Unauthorized") + } + .service("/to-secured") {ctx, req -> + HttpResponse.ofRedirect(HttpStatus.FOUND, "/secured") + } + .decorator(new DecoratingHttpServiceFunction() { + @Override + HttpResponse serve(HttpService delegate, ServiceRequestContext ctx, HttpRequest req) { + for (String field : openTelemetry.propagators.textMapPropagator.fields()) { + if (req.headers().getAll(field).size() > 1) { + throw new AssertionError((Object) ("more than one " + field + " header present")) + } + } + SpanBuilder span = tracer.spanBuilder("test-http-server") + .setSpanKind(SERVER) + .setParent(openTelemetry.propagators.textMapPropagator.extract(Context.current(), ctx, RequestContextGetter.INSTANCE)) + + def traceRequestId = req.headers().get("test-request-id") + if (traceRequestId != null) { + span.setAttribute("test.request.id", Integer.parseInt(traceRequestId)) + } + span.startSpan().end() + + return delegate.serve(ctx, req) + } + }) + .decorator(LoggingService.newDecorator()) + } + } + + def setupSpec() { + server.start() + } + + def cleanupSpec() { + server.stop() + } + + // ideally private, but then groovy closures in this class cannot find them + final int doRequest(String method, URI uri, Map headers = [:]) { + def request = buildRequest(method, uri, headers) + return sendRequest(request, method, uri, headers) + } + + private int doReusedRequest(String method, URI uri) { + def request = buildRequest(method, uri, [:]) + sendRequest(request, method, uri, [:]) + return sendRequest(request, method, uri, [:]) + } + + private int doRequestWithExistingTracingHeaders(String method, URI uri) { + def headers = new HashMap() + for (String field : GlobalOpenTelemetry.getPropagators().getTextMapPropagator().fields()) { + headers.put(field, "12345789") + } + def request = buildRequest(method, uri, headers) + return sendRequest(request, method, uri, headers) + } + + // ideally private, but then groovy closures in this class cannot find them + final RequestResult doRequestWithCallback(String method, URI uri, Map headers = [:], + Runnable callback) { + def request = buildRequest(method, uri, headers) + def requestResult = new RequestResult(callback) + sendRequestWithCallback(request, method, uri, headers, requestResult) + return requestResult + } + + /** + * Helper class for capturing result of asynchronous request and running a callback when result + * is received. + */ + static class RequestResult { + private static final long timeout = 10_000 + private final CountDownLatch valueReady = new CountDownLatch(1) + private final Runnable callback + private int status + private Throwable throwable + + RequestResult(Runnable callback) { + this.callback = callback + } + + void complete(int status) { + complete({ status }, null) + } + + void complete(Throwable throwable) { + complete(null, throwable) + } + + void complete(Supplier status, Throwable throwable) { + if (throwable != null) { + this.throwable = throwable + } else { + this.status = status.get() + } + callback.run() + valueReady.countDown() + } + + int get() { + if (!valueReady.await(timeout, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Timed out waiting for response in " + timeout + "ms") + } + if (throwable != null) { + throw throwable + } + return status + } + } + + /** + * Build the request to be passed to + * {@link #sendRequest(java.lang.Object, java.lang.String, java.net.URI, java.util.Map)}. + * + * By splitting this step out separate from {@code sendRequest}, tests and re-execute the same + * request a second time to verify that the traceparent header is not added multiple times to the + * request, and that the last one wins. Tests will fail if the header shows multiple times. + */ + abstract REQUEST buildRequest(String method, URI uri, Map headers) + + /** + * Make the request and return the status code of the response synchronously. Some clients, e.g., + * HTTPUrlConnection only support synchronous execution without callbacks, and many offer a + * dedicated API for invoking synchronously, such as OkHttp's execute method. When implementing + * this method, such an API should be used and the HTTP status code of the response returned, + * for example: + * + * @Override + * int sendRequest(Request request, String method, URI uri, Map headers) { + * CompletableFuture future = new CompletableFuture<>( + * sendRequestWithCallback(request, method, uri, headers) { + * future.complete(it.statusCode()) + * } + * return future.get() + * } + */ + abstract int sendRequest(REQUEST request, String method, URI uri, Map headers) + + /** + * Make the request and return the status code of the response through the callback. This method + * should be implemented if the client offers any request execution methods that accept a callback + * which receives the response. This will generally be an API for asynchronous execution of a + * request, such as OkHttp's enqueue method, but may also be a callback executed synchronously, + * such as ApacheHttpClient's response handler callbacks. This method is used in tests to verify + * the context is propagated correctly to such callbacks. + * + * @Override + * void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + * // Hypothetical client accepting a callback + * client.executeAsync(request) { + * void success(Response response) { + * requestResult.complete(response.statusCode()) + * } + * void failure(Throwable throwable) { + * requestResult.complete(throwable) + * } + * } + * + * // Hypothetical client returning a CompletableFuture + * client.executeAsync(request).whenComplete { response, throwable -> + * requestResult.complete({ response.statusCode() }, throwable) + * } + * } + * + * If the client offers no APIs that accept callbacks, then this method should not be implemented + * and instead, {@link #testCallback} should be implemented to return false. + */ + void sendRequestWithCallback(REQUEST request, String method, URI uri, Map headers, + RequestResult requestResult) { + // Must be implemented if testAsync is true + throw new UnsupportedOperationException() + } + + static int getPort(URI uri) { + if (uri.port != -1) { + return uri.port + } else if (uri.scheme == "http") { + return 80 + } else if (uri.scheme == "https") { + 443 + } else { + throw new IllegalArgumentException("Unexpected uri: $uri") + } + } + + Integer responseCodeOnRedirectError() { + return null + } + + String userAgent() { + return null + } + + /** A list of additional HTTP client span attributes extracted by the instrumentation per URI. */ + Set> httpAttributes(URI uri) { + [ + SemanticAttributes.HTTP_URL, + SemanticAttributes.HTTP_METHOD, + SemanticAttributes.HTTP_FLAVOR, + SemanticAttributes.HTTP_USER_AGENT + ] + } + + def "basic #method request #url"() { + when: + def responseCode = doRequest(method, url) + + then: + responseCode == 200 + assertTraces(1) { + trace(0, 2) { + clientSpan(it, 0, null, method, url) + serverSpan(it, 1, span(0)) + } + } + + where: + path << ["/success", "/success?with=params"] + + method = "GET" + url = resolveAddress(path) + } + + def "basic #method request with parent"() { + when: + def uri = resolveAddress("/success") + def responseCode = runUnderTrace("parent") { + doRequest(method, uri) + } + + then: + responseCode == 200 + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + clientSpan(it, 1, span(0), method) + serverSpan(it, 2, span(1)) + } + } + + where: + method << BODY_METHODS + } + + def "should suppress nested CLIENT span if already under parent CLIENT span (#method)"() { + given: + assumeTrue(testWithClientParent()) + + when: + def uri = resolveAddress("/success") + def responseCode = runUnderParentClientSpan { + doRequest(method, uri) + } + + then: + responseCode == 200 + // there should be 2 separate traces since the nested CLIENT span is suppressed + // (and the span context propagation along with it) + assertTraces(2) { + traces.sort(orderByRootSpanKind(CLIENT, SERVER)) + + trace(0, 1) { + basicClientSpan(it, 0, "parent-client-span") + } + trace(1, 1) { + serverSpan(it, 0) + } + } + + where: + method << BODY_METHODS + } + + + //FIXME: add tests for POST with large/chunked data + + def "trace request with callback and parent"() { + given: + assumeTrue(testCallback()) + assumeTrue(testCallbackWithParent()) + + when: + def uri = resolveAddress("/success") + def requestResult = runUnderTrace("parent") { + doRequestWithCallback(method, uri) { + runUnderTrace("child") {} + } + } + + then: + requestResult.get() == 200 + // only one trace (client). + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "parent") + clientSpan(it, 1, span(0), method) + serverSpan(it, 2, span(1)) + basicSpan(it, 3, "child", span(0)) + } + } + + where: + method = "GET" + } + + def "trace request with callback and no parent"() { + given: + assumeTrue(testCallback()) + + when: + def uri = resolveAddress("/success") + def requestResult = doRequestWithCallback(method, uri) { + runUnderTrace("callback") { + } + } + + then: + requestResult.get() == 200 + // only one trace (client). + assertTraces(2) { + trace(0, 2) { + clientSpan(it, 0, null, method) + serverSpan(it, 1, span(0)) + } + trace(1, 1) { + basicSpan(it, 0, "callback") + } + } + + where: + method = "GET" + } + + def "basic #method request with 1 redirect"() { + // TODO quite a few clients create an extra span for the redirect + // This test should handle both types or we should unify how the clients work + + given: + assumeTrue(testRedirects()) + def uri = resolveAddress("/redirect") + + when: + def responseCode = doRequest(method, uri) + + then: + responseCode == 200 + assertTraces(1) { + trace(0, 3) { + clientSpan(it, 0, null, method, uri) + serverSpan(it, 1, span(0)) + serverSpan(it, 2, span(0)) + } + } + + where: + method = "GET" + } + + def "basic #method request with 2 redirects"() { + given: + assumeTrue(testRedirects()) + def uri = resolveAddress("/another-redirect") + + when: + def responseCode = doRequest(method, uri) + + then: + responseCode == 200 + assertTraces(1) { + trace(0, 4) { + clientSpan(it, 0, null, method, uri) + serverSpan(it, 1, span(0)) + serverSpan(it, 2, span(0)) + serverSpan(it, 3, span(0)) + } + } + + where: + method = "GET" + } + + def "basic #method request with circular redirects"() { + given: + assumeTrue(testRedirects() && testCircularRedirects()) + def uri = resolveAddress("/circular-redirect") + + when: + doRequest(method, uri) + + then: + def ex = thrown(Exception) + def thrownException = ex instanceof ExecutionException ? ex.cause : ex + + and: + assertTraces(1) { + trace(0, 1 + maxRedirects()) { + clientSpan(it, 0, null, method, uri, responseCodeOnRedirectError(), thrownException) + for (int i = 1; i < maxRedirects() + 1; i++) { + serverSpan(it, i, span(0)) + } + } + } + + where: + method = "GET" + } + + def "redirect #method to secured endpoint copies auth header"() { + given: + assumeTrue(testRedirects()) + def uri = resolveAddress("/to-secured") + + when: + + def responseCode = doRequest(method, uri, [(BASIC_AUTH_KEY): BASIC_AUTH_VAL]) + + then: + responseCode == 200 + assertTraces(1) { + trace(0, 3) { + clientSpan(it, 0, null, method, uri) + serverSpan(it, 1, span(0)) + serverSpan(it, 2, span(0)) + } + } + + where: + method = "GET" + } + + def "error span"() { + def uri = resolveAddress("/error") + when: + runUnderTrace("parent") { + try { + doRequest(method, uri) + } catch (Exception ignored) { + } + } + + then: + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent", null) + clientSpan(it, 1, span(0), method, uri, 500) + serverSpan(it, 2, span(1)) + } + } + + where: + method = "GET" + } + + def "reuse request"() { + given: + assumeTrue(testReusedRequest()) + + when: + def responseCode = doReusedRequest(method, url) + + then: + responseCode == 200 + assertTraces(2) { + trace(0, 2) { + clientSpan(it, 0, null, method, url) + serverSpan(it, 1, span(0)) + } + trace(1, 2) { + clientSpan(it, 0, null, method, url) + serverSpan(it, 1, span(0)) + } + } + + where: + path = "/success" + method = "GET" + url = resolveAddress(path) + } + + // this test verifies two things: + // * the javaagent doesn't cause multiples of tracing headers to be added + // (TestHttpServer throws exception if there are multiples) + // * the javaagent overwrites the existing tracing headers + // (so that it propagates the same trace id / span id that it reports to the backend + // and the trace is not broken) + def "request with existing tracing headers"() { + when: + def responseCode = doRequestWithExistingTracingHeaders(method, url) + + then: + responseCode == 200 + assertTraces(1) { + trace(0, 2) { + clientSpan(it, 0, null, method, url) + serverSpan(it, 1, span(0)) + } + } + + where: + path = "/success" + method = "GET" + url = resolveAddress(path) + } + + def "connection error (unopened port)"() { + given: + assumeTrue(testConnectionFailure()) + def uri = new URI("http://localhost:$UNUSABLE_PORT/") + + when: + runUnderTrace("parent") { + doRequest(method, uri) + } + + then: + def ex = thrown(Exception) + def thrownException = ex instanceof ExecutionException ? ex.cause : ex + + and: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, thrownException) + clientSpan(it, 1, span(0), method, uri, null, thrownException) + } + } + + where: + method = "GET" + } + + def "connection error (unopened port) with callback"() { + given: + assumeTrue(testConnectionFailure()) + assumeTrue(testCallback()) + assumeTrue(testErrorWithCallback()) + def uri = new URI("http://localhost:$UNUSABLE_PORT/") + + when: + def requestResult = runUnderTrace("parent") { + doRequestWithCallback(method, uri, [:]) { + runUnderTrace("callback") { + } + } + } + requestResult.get() + + then: + def ex = thrown(Exception) + def thrownException = ex instanceof ExecutionException ? ex.cause : ex + + and: + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + clientSpan(it, 1, span(0), method, uri, null, thrownException) + basicSpan(it, 2, "callback", span(0)) + } + } + + where: + method = "GET" + } + + def "connection error non routable address"() { + given: + assumeTrue(testRemoteConnection()) + def uri = new URI("https://192.0.2.1/") + + when: + runUnderTrace("parent") { + doRequest(method, uri) + } + + then: + def ex = thrown(Exception) + def thrownException = ex instanceof ExecutionException ? ex.cause : ex + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, thrownException) + clientSpan(it, 1, span(0), method, uri, null, thrownException) + } + } + + where: + method = "HEAD" + } + + // IBM JVM has different protocol support for TLS + @Requires({ !System.getProperty("java.vm.name").contains("IBM J9 VM") }) + def "test https request"() { + given: + assumeTrue(testRemoteConnection()) + assumeTrue(testHttps()) + def uri = new URI("https://localhost:${server.httpsPort()}/success") + + when: + def responseCode = doRequest(method, uri) + + then: + responseCode == 200 + assertTraces(1) { + trace(0, 2) { + clientSpan(it, 0, null, method, uri) + serverSpan(it, 1, span(0)) + } + } + + where: + method = "GET" + } + + /** + * This test fires a large number of concurrent requests. + * Each request first hits a HTTP server and then makes another client request. + * The goal of this test is to verify that in highly concurrent environment our instrumentations + * for http clients (especially inherently concurrent ones, such as Netty or Reactor) correctly + * propagate trace context. + */ + def "high concurrency test"() { + setup: + assumeTrue(testCausality()) + int count = 50 + def method = 'GET' + def url = resolveAddress("/success") + + def latch = new CountDownLatch(1) + + def pool = Executors.newFixedThreadPool(4) + + when: + count.times { index -> + def job = { + latch.await() + runUnderTrace("Parent span " + index) { + Span.current().setAttribute("test.request.id", index) + doRequest(method, url, ["test-request-id": index.toString()]) + } + } + pool.submit(job) + } + latch.countDown() + + then: + assertTraces(count) { + count.times { idx -> + trace(idx, 3) { + def rootSpan = it.span(0) + //Traces can be in arbitrary order, let us find out the request id of the current one + def requestId = Integer.parseInt(rootSpan.name.substring("Parent span ".length())) + + basicSpan(it, 0, "Parent span " + requestId, null, null) { + it."test.request.id" requestId + } + clientSpan(it, 1, span(0), method, url) + serverSpan(it, 2, span(1)) { + it."test.request.id" requestId + } + } + } + } + } + + def "high concurrency test with callback"() { + setup: + assumeTrue(testCausality()) + assumeTrue(testCallback()) + assumeTrue(testCallbackWithParent()) + + int count = 50 + def method = 'GET' + def url = resolveAddress("/success") + + def latch = new CountDownLatch(1) + + def pool = Executors.newFixedThreadPool(4) + + when: + count.times { index -> + def job = { + latch.await() + runUnderTrace("Parent span " + index) { + Span.current().setAttribute("test.request.id", index) + doRequestWithCallback(method, url, ["test-request-id": index.toString()]) { + runUnderTrace("child") {} + } + } + } + pool.submit(job) + } + latch.countDown() + + then: + assertTraces(count) { + count.times { idx -> + trace(idx, 4) { + def rootSpan = it.span(0) + //Traces can be in arbitrary order, let us find out the request id of the current one + def requestId = Integer.parseInt(rootSpan.name.substring("Parent span ".length())) + + basicSpan(it, 0, "Parent span " + requestId, null, null) { + it."test.request.id" requestId + } + clientSpan(it, 1, span(0), method, url) + serverSpan(it, 2, span(1)) { + it."test.request.id" requestId + } + basicSpan(it, 3, "child", span(0)) + } + } + } + } + + /** + * Almost similar to the "high concurrency test" test above, but all requests use the same single + * connection. + */ + def "high concurrency test on single connection"() { + setup: + def singleConnection = createSingleConnection("localhost", server.httpPort()) + assumeTrue(singleConnection != null) + int count = 50 + def method = 'GET' + def path = "/success" + def url = resolveAddress(path) + + def latch = new CountDownLatch(1) + def pool = Executors.newFixedThreadPool(4) + + when: + count.times { index -> + def job = { + latch.await() + runUnderTrace("Parent span " + index) { + Span.current().setAttribute("test.request.id", index) + singleConnection.doRequest(path, [(SingleConnection.REQUEST_ID_HEADER): index.toString()]) + } + } + pool.submit(job) + } + latch.countDown() + + then: + assertTraces(count) { + count.times { idx -> + trace(idx, 3) { + def rootSpan = it.span(0) + //Traces can be in arbitrary order, let us find out the request id of the current one + def requestId = Integer.parseInt(rootSpan.name.substring("Parent span ".length())) + + basicSpan(it, 0, "Parent span " + requestId, null, null) { + it."test.request.id" requestId + } + clientSpan(it, 1, span(0), method, url) + serverSpan(it, 2, span(1)) { + it."test.request.id" requestId + } + } + } + } + } + + //This method should create either a single connection to the target uri or a http client + //which is guaranteed to use the same connection for all requests + SingleConnection createSingleConnection(String host, int port) { + return null + } + + // parent span must be cast otherwise it breaks debugging classloading (junit loads it early) + void clientSpan(TraceAssert trace, int index, Object parentSpan, String method = "GET", URI uri = resolveAddress("/success"), Integer responseCode = 200, Throwable exception = null, String httpFlavor = "1.1") { + def userAgent = userAgent() + def httpClientAttributes = httpAttributes(uri) + trace.span(index) { + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + name expectedClientSpanName(uri, method) + kind CLIENT + if (exception) { + status ERROR + assertClientSpanErrorEvent(it, uri, exception) + } else if (responseCode >= 400) { + status ERROR + } + attributes { + "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP + if (uri.port == UNUSABLE_PORT || uri.host == "192.0.2.1") { + // TODO(anuraaga): For theses cases, there isn't actually a peer so we shouldn't be + // filling in peer information but some instrumentation does so based on the URL itself + // which is present in HTTP attributes. We should fix this. + "${SemanticAttributes.NET_PEER_NAME.key}" { it == null || it == uri.host } + "${SemanticAttributes.NET_PEER_PORT.key}" { it == null || it == uri.port || (uri.scheme == "https" && it == 443) } + } else { + "${SemanticAttributes.NET_PEER_NAME.key}" uri.host + "${SemanticAttributes.NET_PEER_PORT.key}" uri.port > 0 ? uri.port : { it == null || it == 443 } + } + "${SemanticAttributes.NET_PEER_IP.key}" { it == null || it == "127.0.0.1" || it == uri.host } // Optional + + if (httpClientAttributes.contains(SemanticAttributes.HTTP_URL)) { + "${SemanticAttributes.HTTP_URL.key}" { it == "${uri}" || it == "${removeFragment(uri)}" } + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_METHOD)) { + "${SemanticAttributes.HTTP_METHOD.key}" method + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_FLAVOR)) { + "${SemanticAttributes.HTTP_FLAVOR.key}" httpFlavor + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_USER_AGENT)) { + if (userAgent) { + "${SemanticAttributes.HTTP_USER_AGENT.key}" { it.startsWith(userAgent) } + } + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_HOST)) { + "${SemanticAttributes.HTTP_HOST}" { it == uri.host || it == "${uri.host}:${uri.port}" } + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH)) { + "${SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH}" Long + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH)) { + "${SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH}" Long + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_SCHEME)) { + "${SemanticAttributes.HTTP_SCHEME}" uri.scheme + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_TARGET)) { + "${SemanticAttributes.HTTP_TARGET}" uri.path + "${uri.query != null ? "?${uri.query}" : ""}" + } + + if (responseCode) { + "${SemanticAttributes.HTTP_STATUS_CODE.key}" responseCode + } + } + } + } + + void serverSpan(TraceAssert traces, int index, Object parentSpan = null, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) + @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure additionAttributesAssert = null) { + traces.span(index) { + name "test-http-server" + kind SERVER + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + if (additionAttributesAssert != null) { + attributes(additionAttributesAssert) + } + } + } + + String expectedClientSpanName(URI uri, String method) { + return method != null ? "HTTP $method" : "HTTP request" + } + + void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + assertClientSpanErrorEvent(spanAssert, uri, exception.class, exception.message) + } + + void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Class errorType, message) { + spanAssert.errorEvent(errorType, message) + } + + boolean testWithClientParent() { + true + } + + boolean testRedirects() { + true + } + + boolean testCircularRedirects() { + true + } + + // maximum number of redirects that http client follows before giving up + int maxRedirects() { + 2 + } + + boolean testReusedRequest() { + true + } + + boolean testConnectionFailure() { + true + } + + boolean testRemoteConnection() { + true + } + + boolean testHttps() { + true + } + + boolean testCausality() { + true + } + + boolean testCallback() { + return true + } + + boolean testCallbackWithParent() { + // FIXME: this hack is here because callback with parent is broken in play-ws when the stream() + // function is used. There is no way to stop a test from a derived class hence the flag + true + } + + boolean testErrorWithCallback() { + return true + } + + URI removeFragment(URI uri) { + return new URI(uri.scheme, null, uri.host, uri.port, uri.path, uri.query, null) + } + + protected URI resolveAddress(String path) { + return URI.create("http://localhost:${server.httpPort()}${path}") + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTest.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTest.groovy new file mode 100644 index 000000000..56dedf8ae --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTest.groovy @@ -0,0 +1,642 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.base + +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP +import static org.junit.Assume.assumeTrue + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import io.opentelemetry.testing.internal.armeria.common.HttpMethod +import io.opentelemetry.testing.internal.armeria.common.HttpRequest +import io.opentelemetry.testing.internal.armeria.common.HttpRequestBuilder +import java.util.concurrent.Callable +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import spock.lang.Unroll + +@Unroll +abstract class HttpServerTest extends InstrumentationSpecification implements HttpServerTestTrait { + + String expectedServerSpanName(ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return getContextPath() + "/path/:id/param" + case NOT_FOUND: + return getContextPath() + "/*" + default: + return endpoint.resolvePath(address).path + } + } + + String getContextPath() { + return "" + } + + boolean hasHandlerSpan(ServerEndpoint endpoint) { + false + } + + boolean hasHandlerAsControllerParentSpan(ServerEndpoint endpoint) { + true + } + + boolean hasExceptionOnServerSpan(ServerEndpoint endpoint) { + !hasHandlerSpan(endpoint) + } + + boolean hasRenderSpan(ServerEndpoint endpoint) { + false + } + + boolean hasResponseSpan(ServerEndpoint endpoint) { + false + } + + int getErrorPageSpansCount(ServerEndpoint endpoint) { + 1 + } + + boolean hasErrorPageSpans(ServerEndpoint endpoint) { + false + } + + boolean testNotFound() { + true + } + + boolean testPathParam() { + false + } + + boolean testErrorBody() { + true + } + + boolean testException() { + true + } + + Class expectedExceptionClass() { + Exception + } + + boolean testRedirect() { + true + } + + boolean testError() { + true + } + + boolean testConcurrency() { + false + } + + List> extraAttributes() { + [] + } + + enum ServerEndpoint { + SUCCESS("success", 200, "success"), + REDIRECT("redirect", 302, "/redirected"), + ERROR("error-status", 500, "controller error"), // "error" is a special path for some frameworks + EXCEPTION("exception", 500, "controller exception"), + NOT_FOUND("notFound", 404, "not found"), + + // TODO: add tests for the following cases: + QUERY_PARAM("query?some=query", 200, "some=query"), + // OkHttp never sends the fragment in the request, so these cases don't work. +// FRAGMENT_PARAM("fragment#some-fragment", 200, "some-fragment"), +// QUERY_FRAGMENT_PARAM("query/fragment?some=query#some-fragment", 200, "some=query#some-fragment"), + PATH_PARAM("path/123/param", 200, "123"), + AUTH_REQUIRED("authRequired", 200, null), + LOGIN("login", 302, null), + AUTH_ERROR("basicsecured/endpoint", 401, null), + INDEXED_CHILD("child", 200, null), + + public static final String ID_ATTRIBUTE_NAME = "test.request.id" + public static final String ID_PARAMETER_NAME = "id" + + private final URI uriObj + private final String path + final String query + final String fragment + final int status + final String body + final Boolean errored + + ServerEndpoint(String uri, int status, String body) { + this.uriObj = URI.create(uri) + this.path = uriObj.path + this.query = uriObj.query + this.fragment = uriObj.fragment + this.status = status + this.body = body + this.errored = status >= 400 + } + + String getPath() { + return "/$path" + } + + String rawPath() { + return path + } + + URI resolvePath(URI address) { + return address.resolve(path) + } + + URI resolve(URI address) { + return address.resolve(uriObj) + } + + URI resolveWithoutFragment(URI address) { + def uri = resolve(address) + return new URI(uri.scheme, null, uri.host, uri.port, uri.path, uri.query, null) + } + + /** + * Populates custom test attributes for the {@link HttpServerTest#controller} span (which must + * be the current span when this is called) based on URL parameters. Required for + * {@link #INDEXED_CHILD}. + */ + void collectSpanAttributes(UrlParameterProvider parameterProvider) { + if (this == INDEXED_CHILD) { + String value = parameterProvider.getParameter(ID_PARAMETER_NAME) + + if (value != null) { + Span.current().setAttribute(ID_ATTRIBUTE_NAME, value as long) + } + } + } + + private static final Map PATH_MAP = values().collectEntries { [it.path, it] } + + static ServerEndpoint forPath(String path) { + return PATH_MAP.get(path) + } + + // Static keyword required for Scala interop + static interface UrlParameterProvider { + String getParameter(String name) + } + } + + AggregatedHttpRequest request(ServerEndpoint uri, String method) { + def url = uri.resolvePath(address).toString() + // Force HTTP/1 via h1c so upgrade requests don't show up as traces + url = url.replace("http://", "h1c://") + if (uri.query != null) { + url += "?${uri.query}" + } + return AggregatedHttpRequest.of(HttpMethod.valueOf(method), url) + } + + static T controller(ServerEndpoint endpoint, Callable closure) { + assert Span.current().getSpanContext().isValid(): "Controller should have a parent span." + if (endpoint == NOT_FOUND) { + return closure.call() + } + return runUnderTrace("controller", closure) + } + + def "test success with #count requests"() { + setup: + def request = request(SUCCESS, method) + List responses = (1..count).collect { + return client.execute(request).aggregate().join() + } + + expect: + responses.each { response -> + assert response.status().code() == SUCCESS.status + assert response.contentUtf8() == SUCCESS.body + } + + and: + assertTheTraces(count, null, null, method, SUCCESS, null, responses[0]) + + where: + method = "GET" + count << [1, 4, 50] // make multiple requests. + } + + def "test success with parent"() { + setup: + def traceId = "00000000000000000000000000000123" + def parentId = "0000000000000456" + def request = AggregatedHttpRequest.of( + request(SUCCESS, method).headers().toBuilder() + .set("traceparent", "00-" + traceId.toString() + "-" + parentId.toString() + "-01") + .build()) + def response = client.execute(request).aggregate().join() + + expect: + response.status().code() == SUCCESS.status + response.contentUtf8() == SUCCESS.body + + and: + assertTheTraces(1, traceId, parentId, "GET", SUCCESS, null, response) + + where: + method = "GET" + } + + def "test tag query string for #endpoint"() { + setup: + def request = request(endpoint, method) + AggregatedHttpResponse response = client.execute(request).aggregate().join() + + expect: + response.status().code() == endpoint.status + response.contentUtf8() == endpoint.body + + and: + assertTheTraces(1, null, null, method, endpoint, null, response) + + where: + method = "GET" + endpoint << [SUCCESS, QUERY_PARAM] + } + + def "test redirect"() { + setup: + assumeTrue(testRedirect()) + def request = request(REDIRECT, method) + def response = client.execute(request).aggregate().join() + + expect: + response.status().code() == REDIRECT.status + response.headers().get("location") == REDIRECT.body || + new URI(response.headers().get("location")).normalize().toString() == "${address.resolve(REDIRECT.body)}" + + and: + assertTheTraces(1, null, null, method, REDIRECT, null, response) + + where: + method = "GET" + } + + def "test error"() { + setup: + assumeTrue(testError()) + def request = request(ERROR, method) + def response = client.execute(request).aggregate().join() + + expect: + response.status().code() == ERROR.status + if (testErrorBody()) { + response.contentUtf8() == ERROR.body + } + + and: + assertTheTraces(1, null, null, method, ERROR, null, response) + + where: + method = "GET" + } + + def "test exception"() { + setup: + assumeTrue(testException()) + def request = request(EXCEPTION, method) + def response = client.execute(request).aggregate().join() + + expect: + response.status().code() == EXCEPTION.status + + and: + assertTheTraces(1, null, null, method, EXCEPTION, EXCEPTION.body, response) + + where: + method = "GET" + } + + def "test notFound"() { + setup: + assumeTrue(testNotFound()) + def request = request(NOT_FOUND, method) + def response = client.execute(request).aggregate().join() + + expect: + response.status().code() == NOT_FOUND.status + + and: + assertTheTraces(1, null, null, method, NOT_FOUND, null, response) + + where: + method = "GET" + } + + def "test path param"() { + setup: + assumeTrue(testPathParam()) + def request = request(PATH_PARAM, method) + def response = client.execute(request).aggregate().join() + + expect: + response.status().code() == PATH_PARAM.status + response.contentUtf8() == PATH_PARAM.body + + and: + assertTheTraces(1, null, null, method, PATH_PARAM, null, response) + + where: + method = "GET" + } + + /* + This test fires a bunch of parallel request to the fixed backend endpoint. + That endpoint is supposed to create a new child span in the context of the SERVER span. + That child span is expected to have an attribute called "test.request.id". + The value of that attribute should be the value of request's parameter called "id". + + This test then asserts that there is the correct number of traces (one per request executed) + and that each trace has exactly three spans and both first and the last spans have "test.request.id" + attribute with equal value. Server span is not going to have that attribute because it is not + under the control of this test. + + This way we verify that child span created by the server actually corresponds to the client request. + */ + + def "high concurrency test"() { + setup: + assumeTrue(testConcurrency()) + int count = 100 + def endpoint = INDEXED_CHILD + + def latch = new CountDownLatch(1) + + def pool = Executors.newFixedThreadPool(4) + def propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator() + def setter = { HttpRequestBuilder carrier, String name, String value -> + carrier.header(name, value) + } + + when: + count.times { index -> + def job = { + latch.await() + HttpRequestBuilder request = HttpRequest.builder() + // Force HTTP/1 via h1c so upgrade requests don't show up as traces + .get(endpoint.resolvePath(address).toString().replace("http://", "h1c://")) + .queryParam(ServerEndpoint.ID_PARAMETER_NAME, "$index") + runUnderTrace("client " + index) { + Span.current().setAttribute(ServerEndpoint.ID_ATTRIBUTE_NAME, index) + propagator.inject(Context.current(), request, setter) + client.execute(request.build()).aggregate().join() + } + + } + pool.submit(job) + } + latch.countDown() + + then: + assertTraces(count) { + (0..count - 1).each { + trace(it, hasHandlerSpan(endpoint) ? 4 : 3) { + def rootSpan = it.span(0) + //Traces can be in arbitrary order, let us find out the request id of the current one + def requestId = Integer.parseInt(rootSpan.name.substring("client ".length())) + + basicSpan(it, 0, "client " + requestId, null, null) { + "${ServerEndpoint.ID_ATTRIBUTE_NAME}" requestId + } + indexedServerSpan(it, span(0), requestId) + + def controllerSpanIndex = 2 + + if (hasHandlerSpan(endpoint)) { + handlerSpan(it, 2, span(1), "GET", endpoint) + controllerSpanIndex++ + } + + def controllerParentSpanIndex = controllerSpanIndex - (hasHandlerAsControllerParentSpan(endpoint) ? 1 : 2) + indexedControllerSpan(it, controllerSpanIndex, span(controllerParentSpanIndex), requestId) + } + } + } + + cleanup: + pool.shutdownNow() + } + + //FIXME: add tests for POST with large/chunked data + + void assertTheTraces(int size, String traceID = null, String parentID = null, String method = "GET", ServerEndpoint endpoint = SUCCESS, String errorMessage = null, AggregatedHttpResponse response = null) { + def spanCount = 1 // server span + if (hasResponseSpan(endpoint)) { + spanCount++ + } + if (hasHandlerSpan(endpoint)) { + spanCount++ + } + if (endpoint != NOT_FOUND) { + spanCount++ // controller span + if (hasRenderSpan(endpoint)) { + spanCount++ + } + } + if (hasErrorPageSpans(endpoint)) { + spanCount += getErrorPageSpansCount(endpoint) + } + assertTraces(size) { + (0..size - 1).each { + trace(it, spanCount) { + def spanIndex = 0 + serverSpan(it, spanIndex++, traceID, parentID, method, response?.content()?.length(), endpoint) + if (hasHandlerSpan(endpoint)) { + handlerSpan(it, spanIndex++, span(0), method, endpoint) + } + if (endpoint != NOT_FOUND) { + def controllerSpanIndex = 0 + if (hasHandlerSpan(endpoint) && hasHandlerAsControllerParentSpan(endpoint)) { + controllerSpanIndex++ + } + controllerSpan(it, spanIndex++, span(controllerSpanIndex), errorMessage, expectedExceptionClass()) + if (hasRenderSpan(endpoint)) { + renderSpan(it, spanIndex++, span(0), method, endpoint) + } + } + if (hasResponseSpan(endpoint)) { + responseSpan(it, spanIndex, span(spanIndex - 1), span(0), method, endpoint) + spanIndex++ + } + if (hasErrorPageSpans(endpoint)) { + errorPageSpans(it, spanIndex, span(0), method, endpoint) + } + } + } + } + } + + void controllerSpan(TraceAssert trace, int index, Object parent, String errorMessage = null, Class exceptionClass = Exception) { + trace.span(index) { + name "controller" + if (errorMessage) { + status StatusCode.ERROR + errorEvent(exceptionClass, errorMessage) + } + childOf((SpanData) parent) + } + } + + void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + throw new UnsupportedOperationException("handlerSpan not implemented in " + getClass().name) + } + + void renderSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + throw new UnsupportedOperationException("renderSpan not implemented in " + getClass().name) + } + + void responseSpan(TraceAssert trace, int index, Object controllerSpan, Object handlerSpan, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + responseSpan(trace, index, controllerSpan, method, endpoint) + } + + void responseSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + throw new UnsupportedOperationException("responseSpan not implemented in " + getClass().name) + } + + void errorPageSpans(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { + throw new UnsupportedOperationException("errorPageSpans not implemented in " + getClass().name) + } + + void redirectSpan(TraceAssert trace, int index, Object parent) { + trace.span(index) { + name ~/\.sendRedirect$/ + kind SpanKind.INTERNAL + childOf((SpanData) parent) + } + } + + void sendErrorSpan(TraceAssert trace, int index, Object parent) { + trace.span(index) { + name ~/\.sendError$/ + kind SpanKind.INTERNAL + childOf((SpanData) parent) + } + } + + // parent span must be cast otherwise it breaks debugging classloading (junit loads it early) + void serverSpan(TraceAssert trace, int index, String traceID = null, String parentID = null, String method = "GET", Long responseContentLength = null, ServerEndpoint endpoint = SUCCESS) { + def extraAttributes = extraAttributes() + trace.span(index) { + name expectedServerSpanName(endpoint) + kind SpanKind.SERVER // can't use static import because of SERVER type parameter + if (endpoint.errored) { + status StatusCode.ERROR + } + if (parentID != null) { + traceId traceID + parentSpanId parentID + } else { + hasNoParent() + } + if (endpoint == EXCEPTION && hasExceptionOnServerSpan(endpoint)) { + event(0) { + eventName(SemanticAttributes.EXCEPTION_EVENT_NAME) + attributes { + "${SemanticAttributes.EXCEPTION_TYPE.key}" { it == null || it == expectedExceptionClass().name } + "${SemanticAttributes.EXCEPTION_MESSAGE.key}" { it == null || it == endpoint.body } + "${SemanticAttributes.EXCEPTION_STACKTRACE.key}" { it == null || it instanceof String } + } + } + } + attributes { + "${SemanticAttributes.NET_PEER_PORT.key}" { it == null || it instanceof Long } + "${SemanticAttributes.NET_PEER_IP.key}" { it == null || it == "127.0.0.1" } // Optional + "${SemanticAttributes.HTTP_CLIENT_IP.key}" { it == null || it == TEST_CLIENT_IP } + "${SemanticAttributes.HTTP_URL.key}" { it == "${endpoint.resolve(address)}" || it == "${endpoint.resolveWithoutFragment(address)}" } + "${SemanticAttributes.HTTP_METHOD.key}" method + "${SemanticAttributes.HTTP_STATUS_CODE.key}" endpoint.status + "${SemanticAttributes.HTTP_FLAVOR.key}" { it == "1.1" || it == "2.0" } + "${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT + + if (extraAttributes.contains(SemanticAttributes.HTTP_HOST)) { + "${SemanticAttributes.HTTP_HOST}" "localhost:${port}" + } + if (extraAttributes.contains(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH)) { + "${SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH}" Long + } + if (extraAttributes.contains(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH)) { + "${SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH}" Long + } + if (extraAttributes.contains(SemanticAttributes.HTTP_ROUTE)) { + // TODO(anuraaga): Revisit this when applying instrumenters to more libraries, Armeria + // currently reports '/*' which is a fallback route. + "${SemanticAttributes.HTTP_ROUTE}" String + } + if (extraAttributes.contains(SemanticAttributes.HTTP_SCHEME)) { + "${SemanticAttributes.HTTP_SCHEME}" "http" + } + if (extraAttributes.contains(SemanticAttributes.HTTP_SERVER_NAME)) { + "${SemanticAttributes.HTTP_SERVER_NAME}" String + } + if (extraAttributes.contains(SemanticAttributes.HTTP_TARGET)) { + "${SemanticAttributes.HTTP_TARGET}" endpoint.path + "${endpoint == QUERY_PARAM ? "?${endpoint.body}" : ""}" + } + if (extraAttributes.contains(SemanticAttributes.NET_PEER_NAME)) { + "${SemanticAttributes.NET_PEER_NAME}" "localhost" + } + if (extraAttributes.contains(SemanticAttributes.NET_TRANSPORT)) { + "${SemanticAttributes.NET_TRANSPORT}" IP_TCP + } + } + } + } + + void indexedServerSpan(TraceAssert trace, Object parent, int requestId) { + ServerEndpoint endpoint = INDEXED_CHILD + trace.span(1) { + name expectedServerSpanName(endpoint) + kind SpanKind.SERVER // can't use static import because of SERVER type parameter + childOf((SpanData) parent) + attributes { + "${SemanticAttributes.NET_PEER_PORT.key}" { it == null || it instanceof Long } + "${SemanticAttributes.NET_PEER_IP.key}" { it == null || it == "127.0.0.1" } // Optional + "${SemanticAttributes.HTTP_CLIENT_IP.key}" { it == null || it == TEST_CLIENT_IP } + "${SemanticAttributes.HTTP_URL.key}" endpoint.resolve(address).toString() + "?id=$requestId" + "${SemanticAttributes.HTTP_METHOD.key}" "GET" + "${SemanticAttributes.HTTP_STATUS_CODE.key}" 200 + "${SemanticAttributes.HTTP_FLAVOR.key}" "1.1" + "${SemanticAttributes.HTTP_USER_AGENT.key}" TEST_USER_AGENT + } + } + } + + void indexedControllerSpan(TraceAssert trace, int index, Object parent, int requestId) { + trace.span(index) { + name "controller" + childOf((SpanData) parent) + attributes { + "${ServerEndpoint.ID_ATTRIBUTE_NAME}" requestId + } + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTestTrait.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTestTrait.groovy new file mode 100644 index 000000000..7f4a22fe7 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTestTrait.groovy @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.base + +import ch.qos.logback.classic.Level +import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait +import io.opentelemetry.instrumentation.test.utils.LoggerUtils +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.testing.internal.armeria.client.ClientFactory +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.client.logging.LoggingClient +import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames +import java.time.Duration +import org.junit.AfterClass +import org.junit.BeforeClass +import org.slf4j.Logger +import org.slf4j.LoggerFactory +/** + * A trait for testing requests against http server. + */ +trait HttpServerTestTrait implements RetryOnAddressAlreadyInUseTrait { + static final Logger SERVER_LOGGER = LoggerFactory.getLogger("http-server") + static { + LoggerUtils.setLevel(SERVER_LOGGER, Level.DEBUG) + } + static final String TEST_CLIENT_IP = "1.1.1.1" + static final String TEST_USER_AGENT = "test-user-agent" + + // not using SERVER as type because it triggers a bug in groovy and java joint compilation + static Object server + static WebClient client = WebClient.builder() + .responseTimeout(Duration.ofMinutes(1)) + .writeTimeout(Duration.ofMinutes(1)) + .factory(ClientFactory.builder().connectTimeout(Duration.ofMinutes(1)).build()) + .setHeader(HttpHeaderNames.USER_AGENT, TEST_USER_AGENT) + .setHeader(HttpHeaderNames.X_FORWARDED_FOR, TEST_CLIENT_IP) + .decorator(LoggingClient.newDecorator()) + .build() + static int port + static URI address + + @BeforeClass + def setupServer() { + withRetryOnAddressAlreadyInUse({ + setupSpecUnderRetry() + }) + } + + def setupSpecUnderRetry() { + port = PortUtils.findOpenPort() + address = buildAddress() + server = startServer(port) + println getClass().name + " http server started at: http://localhost:$port" + getContextPath() + } + + URI buildAddress() { + return new URI("http://localhost:$port" + getContextPath() + "/") + } + + abstract SERVER startServer(int port) + + @AfterClass + def cleanupServer() { + if (server == null) { + println getClass().name + " can't stop null server" + return + } + stopServer(server) + server = null + println getClass().name + " http server stopped at: http://localhost:$port/" + } + + abstract void stopServer(SERVER server) + + String getContextPath() { + return "" + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java new file mode 100644 index 000000000..a074ced36 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.base; + +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +/** + * Helper class for http client tests which require a single connection. + * + *

    Tests for specific library should provide an implementation which satisfies the following + * conditions: + * + *

      + *
    • Has a constructor which accepts target host and port + *
    • For a given instance all invocations of {@link #doRequest(String, Map)} will reuse the same + * underlying connection to target host. + *
    + */ +public interface SingleConnection { + String REQUEST_ID_HEADER = "test-request-id"; + + int doRequest(String path, Map headers) + throws ExecutionException, InterruptedException, TimeoutException; +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/utils/TraceUtils.groovy b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/utils/TraceUtils.groovy new file mode 100644 index 000000000..d75fcbb69 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/utils/TraceUtils.groovy @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.extension.annotations.WithSpan +import io.opentelemetry.instrumentation.test.asserts.AttributesAssert +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.server.ServerTraceUtils +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.Callable +import java.util.concurrent.ExecutionException + +// TODO: convert all usages of this class to the Java TraceUtils one +class TraceUtils { + + private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test") + + static T runUnderServerTrace(String spanName, Callable r) { + return ServerTraceUtils.runUnderServerTrace(spanName, r) + } + + static T runUnderTrace(String spanName, Callable r) { + try { + Span span = tracer.spanBuilder(spanName).setSpanKind(SpanKind.INTERNAL).startSpan() + + try { + def result = span.makeCurrent().withCloseable { + r.call() + } + span.end() + return result + } catch (Exception e) { + span.setStatus(StatusCode.ERROR) + span.recordException(e instanceof ExecutionException ? e.getCause() : e) + span.end() + throw e + } + } catch (Throwable t) { + throw ExceptionUtils.sneakyThrow(t) + } + } + + static void runInternalSpan(String spanName) { + tracer.spanBuilder(spanName).startSpan().end() + } + + @WithSpan(value = "parent-client-span", kind = SpanKind.CLIENT) + static T runUnderParentClientSpan(Callable r) { + r.call() + } + + static basicClientSpan(TraceAssert trace, int index, String operation, Object parentSpan = null, Throwable exception = null, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) + @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure additionAttributesAssert = null) { + return basicSpanForKind(trace, index, SpanKind.CLIENT, operation, parentSpan, exception, additionAttributesAssert) + } + + static basicServerSpan(TraceAssert trace, int index, String operation, Object parentSpan = null, Throwable exception = null, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) + @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure additionAttributesAssert = null) { + return basicSpanForKind(trace, index, SpanKind.SERVER, operation, parentSpan, exception, additionAttributesAssert) + } + + // TODO rename to basicInternalSpan + static basicSpan(TraceAssert trace, int index, String operation, Object parentSpan = null, Throwable exception = null, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) + @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure additionAttributesAssert = null) { + return basicSpanForKind(trace, index, SpanKind.INTERNAL, operation, parentSpan, exception, additionAttributesAssert) + } + + private static basicSpanForKind(TraceAssert trace, int index, SpanKind spanKind, String operation, Object parentSpan = null, Throwable exception = null, + @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) + @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure additionAttributesAssert = null) { + trace.span(index) { + if (parentSpan == null) { + hasNoParent() + } else { + childOf((SpanData) parentSpan) + } + name operation + kind spanKind + if (exception) { + status StatusCode.ERROR + errorEvent(exception.class, exception.message) + } + + if (additionAttributesAssert != null) { + attributes(additionAttributesAssert) + } + } + } + + static T runUnderTraceWithoutExceptionCatch(String spanName, Callable r) { + Span span = tracer.spanBuilder(spanName).setSpanKind(SpanKind.INTERNAL).startSpan() + + try { + return span.makeCurrent().withCloseable { + r.call() + } + } finally { + span.end() + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/server/ServerTraceUtils.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/server/ServerTraceUtils.java new file mode 100644 index 000000000..462e4fe40 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/server/ServerTraceUtils.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.server; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.extension.annotations.WithSpan; +import java.util.concurrent.Callable; + +/** + * Some of our tests need to verify behavior when a span has been registered as the server span in + * context. These tests only happen with the agent right now, so we can use WithSpan to have the + * agent create a span under its management (where the context key is shaded). This testing approach + * will not work for library instrumentation that may use these keys in the future. This can be + * solved by https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1726 + */ +public final class ServerTraceUtils { + + @WithSpan(kind = SpanKind.SERVER) + public static T runUnderServerTrace(String spanName, Callable r) throws Exception { + Span.current().updateName(spanName); + return r.call(); + } + + private ServerTraceUtils() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/server/http/RequestContextGetter.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/server/http/RequestContextGetter.java new file mode 100644 index 000000000..263aeabb1 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/server/http/RequestContextGetter.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.server.http; + +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.testing.internal.armeria.server.ServiceRequestContext; +import io.opentelemetry.testing.internal.io.netty.util.AsciiString; +import java.util.Collections; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; + +public enum RequestContextGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(@Nullable ServiceRequestContext carrier) { + if (carrier == null) { + return Collections.emptyList(); + } + return carrier.request().headers().names().stream() + .map(AsciiString::toString) + .collect(Collectors.toList()); + } + + @Override + @Nullable + public String get(@Nullable ServiceRequestContext carrier, String key) { + if (carrier == null) { + return null; + } + return carrier.request().headers().get(key); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ClassUtils.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ClassUtils.java new file mode 100644 index 000000000..e0dcec7aa --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ClassUtils.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils; + +public final class ClassUtils { + + public static String getClassName(Class clazz) { + if (!clazz.isAnonymousClass()) { + return clazz.getSimpleName(); + } + String className = clazz.getName(); + if (clazz.getPackage() != null) { + String pkgName = clazz.getPackage().getName(); + if (!pkgName.isEmpty()) { + className = clazz.getName().replace(pkgName, "").substring(1); + } + } + return className; + } + + private ClassUtils() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ClasspathUtils.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ClasspathUtils.java new file mode 100644 index 000000000..0aef58e2b --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ClasspathUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.UUID; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +public final class ClasspathUtils { + + public static byte[] convertToByteArray(InputStream resource) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int bytesRead; + byte[] data = new byte[1024]; + while ((bytesRead = resource.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, bytesRead); + } + buffer.flush(); + return buffer.toByteArray(); + } + + public static byte[] convertToByteArray(Class clazz) throws IOException { + try (InputStream inputStream = + clazz.getClassLoader().getResourceAsStream(getResourceName(clazz.getName()))) { + return convertToByteArray(inputStream); + } + } + + /** + * Create a temporary jar on the filesystem with the bytes of the given classes. + * + *

    The jar file will be removed when the jvm exits. + * + * @param loader classloader used to load bytes + * @param resourceNames names of resources to copy into the new jar + * @return the location of the newly created jar. + */ + public static URL createJarWithClasses(ClassLoader loader, String... resourceNames) + throws IOException { + File tmpJar = File.createTempFile(UUID.randomUUID() + "", ".jar"); + tmpJar.deleteOnExit(); + + Manifest manifest = new Manifest(); + JarOutputStream target = new JarOutputStream(new FileOutputStream(tmpJar), manifest); + for (String resourceName : resourceNames) { + try (InputStream is = loader.getResourceAsStream(resourceName)) { + addToJar(resourceName, convertToByteArray(is), target); + } + } + target.close(); + + return tmpJar.toURI().toURL(); + } + + /** + * Create a temporary jar on the filesystem with the bytes of the given classes. + * + *

    The jar file will be removed when the jvm exits. + * + * @param classes classes to package into the jar. + * @return the location of the newly created jar. + */ + public static URL createJarWithClasses(Class... classes) throws IOException { + File tmpJar = File.createTempFile(UUID.randomUUID() + "", ".jar"); + tmpJar.deleteOnExit(); + + Manifest manifest = new Manifest(); + JarOutputStream target = new JarOutputStream(new FileOutputStream(tmpJar), manifest); + for (Class clazz : classes) { + addToJar(getResourceName(clazz.getName()), convertToByteArray(clazz), target); + } + target.close(); + + return tmpJar.toURI().toURL(); + } + + private static void addToJar(String resourceName, byte[] bytes, JarOutputStream jarOutputStream) + throws IOException { + JarEntry entry = new JarEntry(resourceName); + jarOutputStream.putNextEntry(entry); + jarOutputStream.write(bytes, 0, bytes.length); + jarOutputStream.closeEntry(); + } + + // Moved this to a java class because groovy was adding a hard ref to classLoader + public static boolean isClassLoaded(String className, ClassLoader classLoader) { + try { + Method findLoadedClassMethod = + ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + try { + findLoadedClassMethod.setAccessible(true); + Class loadedClass = (Class) findLoadedClassMethod.invoke(classLoader, className); + return null != loadedClass && loadedClass.getClassLoader() == classLoader; + } catch (Exception e) { + throw new IllegalStateException(e); + } finally { + findLoadedClassMethod.setAccessible(false); + } + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + /** com.foo.Bar to com/foo/Bar.class */ + private static String getResourceName(String className) { + if (!className.endsWith(".class")) { + return className.replace('.', '/') + ".class"; + } else { + return className; + } + } + + private ClasspathUtils() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ExceptionUtils.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ExceptionUtils.java new file mode 100644 index 000000000..62b0cf5d1 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/ExceptionUtils.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils; + +public final class ExceptionUtils { + + static RuntimeException sneakyThrow(Throwable t) { + if (t == null) { + throw new NullPointerException("t"); + } + return ExceptionUtils.sneakyThrow0(t); + } + + // Exactly what we want + @SuppressWarnings("TypeParameterUnusedInFormals") + private static T sneakyThrow0(Throwable t) throws T { + throw (T) t; + } + + private ExceptionUtils() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/GcUtils.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/GcUtils.java new file mode 100644 index 000000000..d8f6866c1 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/GcUtils.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils; + +import java.lang.ref.WeakReference; + +public final class GcUtils { + + public static void awaitGc() throws InterruptedException { + Object obj = new Object(); + WeakReference ref = new WeakReference<>(obj); + obj = null; + awaitGc(ref); + } + + public static void awaitGc(WeakReference ref) throws InterruptedException { + while (ref.get() != null) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + System.gc(); + System.runFinalization(); + } + } + + private GcUtils() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/LoggerUtils.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/LoggerUtils.java new file mode 100644 index 000000000..42b880411 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/LoggerUtils.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils; + +import ch.qos.logback.classic.Level; +import org.slf4j.Logger; + +public final class LoggerUtils { + public static void setLevel(Logger logger, Level level) { + // Some appserver tests (Jetty 11) somehow cause our logback logger not to be used, so we must + // check the type + if (logger instanceof ch.qos.logback.classic.Logger) { + ((ch.qos.logback.classic.Logger) logger).setLevel(level); + } + } + + private LoggerUtils() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortAllocator.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortAllocator.java new file mode 100644 index 000000000..985f260bd --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortAllocator.java @@ -0,0 +1,130 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils; + +import java.io.Closeable; +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for finding open ports that test servers can bind. Allocator splits allocation range + * to chunks and binds to the first port in chunk to claim it. If first port of chunk is already in + * use allocator assumes that some other process has already claimed that chunk and moves to next + * chunk. This should let us as run tests in parallel without them interfering with each other. + */ +class PortAllocator { + + static final int CHUNK_SIZE = 100; + static final int RANGE_MIN = 11000; + // end of allocator port range, should be below ephemeral port range + static final int RANGE_MAX = 32768; + + private final PortBinder portBinder; + private final List sockets = new ArrayList<>(); + // next candidate port + private int next = RANGE_MIN; + private int nextChunkStart = RANGE_MIN; + + PortAllocator() { + this(PortBinder.INSTANCE); + } + + PortAllocator(PortBinder portBinder) { + this.portBinder = portBinder; + + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + for (Closeable socket : sockets) { + try { + socket.close(); + } catch (IOException ignored) { + // Ignore + } + } + })); + } + + /** Find open port. */ + int getPort() { + return getPorts(1); + } + + /** Find consecutive range of open ports, returning the first one in the range. */ + synchronized int getPorts(int count) { + // as we bind to first port in each chunk the max amount of + // consecutive ports that we can find is CHUNK_SIZE - 1 + if (count < 1 || count >= CHUNK_SIZE) { + throw new IllegalStateException("Invalid count " + count); + } + while (next + count - 1 <= RANGE_MAX) { + // if current chunk doesn't have enough ports move to next chunk + if (next + count - 1 >= nextChunkStart) { + reserveNextChunk(); + } + // find requested amount of consecutive ports + while (next + count - 1 < nextChunkStart && next + count - 1 <= RANGE_MAX) { + // result is the lowest port in consecutive range + int result = next; + for (int i = 0; i < count; i++) { + int port = next; + next++; + if (!portBinder.canBind(port)) { + // someone has allocated a port in our port range, ignore it and try with + // the next port + break; + } else if (i == count - 1) { + return result; + } + } + } + } + // give up when port range is exhausted + throw new IllegalStateException("Failed to find suitable port"); + } + + private void reserveNextChunk() { + while (nextChunkStart < RANGE_MAX) { + // reserve a chunk, if binding to first port of chunk fails + // move to next chunk + Closeable serverSocket = portBinder.bind(nextChunkStart); + if (serverSocket != null) { + sockets.add(serverSocket); + next = nextChunkStart + 1; + nextChunkStart += CHUNK_SIZE; + return; + } + nextChunkStart += CHUNK_SIZE; + } + // give up when port range is exhausted + throw new IllegalStateException("Failed to reserve suitable port range"); + } + + static class PortBinder { + static final PortBinder INSTANCE = new PortBinder(); + + Closeable bind(int port) { + try { + return new ServerSocket(port); + } catch (IOException exception) { + return null; + } + } + + boolean canBind(int port) { + try { + ServerSocket socket = new ServerSocket(port); + socket.close(); + return true; + } catch (IOException exception) { + return false; + } + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortUtils.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortUtils.java new file mode 100644 index 000000000..acddb36b7 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.TimeUnit; + +public final class PortUtils { + + public static final int UNUSABLE_PORT = 61; + + private static final PortAllocator portAllocator = new PortAllocator(); + + /** Find consecutive open ports, returning the first one in the range. */ + public static int findOpenPorts(int count) { + return portAllocator.getPorts(count); + } + + /** Find open port. */ + public static int findOpenPort() { + return portAllocator.getPort(); + } + + private static boolean isPortOpen(int port) { + try (Socket socket = new Socket((String) null, port)) { + return true; + } catch (IOException e) { + return false; + } + } + + public static void waitForPortToOpen(int port, long timeout, TimeUnit unit) { + long waitUntil = System.currentTimeMillis() + unit.toMillis(timeout); + + while (System.currentTimeMillis() < waitUntil) { + if (isPortOpen(port)) { + return; + } + + try { + TimeUnit.MILLISECONDS.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for " + port + " to be opened"); + } + } + + throw new IllegalStateException("Timed out waiting for port " + port + " to be opened"); + } + + public static void waitForPortToOpen(int port, long timeout, TimeUnit unit, Process process) { + long waitUntil = System.currentTimeMillis() + unit.toMillis(timeout); + + while (System.currentTimeMillis() < waitUntil) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new IllegalStateException("Interrupted while waiting for " + port + " to be opened"); + } + + // Note: we should have used `process.isAlive()` here but it is java8 only + try { + process.exitValue(); + throw new IllegalStateException("Process died before port " + port + " was opened"); + } catch (IllegalThreadStateException e) { + // process is still alive, things are good. + } + + if (isPortOpen(port)) { + return; + } + } + + throw new IllegalStateException("Timed out waiting for port " + port + " to be opened"); + } + + private PortUtils() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java new file mode 100644 index 000000000..638472bd0 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.test.utils.LoggerUtils; +import io.opentelemetry.javaagent.testing.common.AgentTestingExporterAccess; +import io.opentelemetry.javaagent.testing.common.TestAgentListenerAccess; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import org.slf4j.LoggerFactory; + +/** + * An implementation of {@link InstrumentationTestRunner} that delegates most of its calls to the + * OpenTelemetry Javaagent that this process runs with. It uses the {@link + * AgentTestingExporterAccess} bridge class to retrieve exported traces and metrics data from the + * agent classloader. + */ +public final class AgentTestRunner implements InstrumentationTestRunner { + static { + LoggerUtils.setLevel(LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME), Level.WARN); + LoggerUtils.setLevel(LoggerFactory.getLogger("io.opentelemetry"), Level.DEBUG); + } + + private static final AgentTestRunner INSTANCE = new AgentTestRunner(); + + public static InstrumentationTestRunner instance() { + return INSTANCE; + } + + @Override + public void beforeTestClass() { + TestAgentListenerAccess.reset(); + } + + @Override + public void afterTestClass() { + // Cleanup before assertion. + assert TestAgentListenerAccess.getInstrumentationErrorCount() == 0 + : TestAgentListenerAccess.getInstrumentationErrorCount() + + " Instrumentation errors during test"; + // additional library ignores are ignored during tests, because they can make it really + // confusing for contributors wondering why their instrumentation is not applied + // + // but we then need to make sure that the additional library ignores won't then silently prevent + // the instrumentation from being applied in real life outside of these tests + assert TestAgentListenerAccess.getIgnoredButTransformedClassNames().isEmpty() + : "Transformed classes match global libraries ignore matcher: " + + TestAgentListenerAccess.getIgnoredButTransformedClassNames(); + } + + @Override + public void clearAllExportedData() { + AgentTestingExporterAccess.reset(); + } + + @Override + public OpenTelemetry getOpenTelemetry() { + return GlobalOpenTelemetry.get(); + } + + @Override + public List getExportedSpans() { + return AgentTestingExporterAccess.getExportedSpans(); + } + + @Override + public List getExportedMetrics() { + return AgentTestingExporterAccess.getExportedMetrics(); + } + + @Override + public boolean forceFlushCalled() { + return AgentTestingExporterAccess.forceFlushCalled(); + } + + private AgentTestRunner() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java new file mode 100644 index 000000000..7b624a4da --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; + +/** + * This interface defines a common set of operations for interaction with OpenTelemetry SDK and + * traces & metrics exporters. + * + * @see LibraryTestRunner + * @see AgentTestRunner + */ +public interface InstrumentationTestRunner { + void beforeTestClass(); + + void afterTestClass(); + + void clearAllExportedData(); + + OpenTelemetry getOpenTelemetry(); + + List getExportedSpans(); + + List getExportedMetrics(); + + boolean forceFlushCalled(); +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java new file mode 100644 index 000000000..ee9e3f0f7 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.Collections; +import java.util.List; + +/** + * An implementation of {@link InstrumentationTestRunner} that initializes OpenTelemetry SDK and + * uses in-memory exporter to collect traces and metrics. + */ +public final class LibraryTestRunner implements InstrumentationTestRunner { + + private static final OpenTelemetrySdk openTelemetry; + private static final InMemorySpanExporter testExporter; + private static boolean forceFlushCalled; + private static final LibraryTestRunner INSTANCE = new LibraryTestRunner(); + + static { + GlobalOpenTelemetry.resetForTest(); + + testExporter = InMemorySpanExporter.create(); + openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(new FlushTrackingSpanProcessor()) + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .addSpanProcessor(SimpleSpanProcessor.create(testExporter)) + .build()) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .buildAndRegisterGlobal(); + } + + public static LibraryTestRunner instance() { + return INSTANCE; + } + + @Override + public void beforeTestClass() { + // just in case: if there was any test that modified the global instance, reset it + if (GlobalOpenTelemetry.get() != openTelemetry) { + GlobalOpenTelemetry.resetForTest(); + GlobalOpenTelemetry.set(openTelemetry); + } + } + + @Override + public void afterTestClass() {} + + @Override + public void clearAllExportedData() { + testExporter.reset(); + forceFlushCalled = false; + } + + @Override + public OpenTelemetry getOpenTelemetry() { + return openTelemetry; + } + + public OpenTelemetrySdk getOpenTelemetrySdk() { + return openTelemetry; + } + + @Override + public List getExportedSpans() { + return testExporter.getFinishedSpanItems(); + } + + @Override + public List getExportedMetrics() { + // no metrics support yet + return Collections.emptyList(); + } + + @Override + public boolean forceFlushCalled() { + return forceFlushCalled; + } + + private LibraryTestRunner() {} + + private static class FlushTrackingSpanProcessor implements SpanProcessor { + @Override + public void onStart(Context parentContext, ReadWriteSpan span) {} + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan span) {} + + @Override + public boolean isEndRequired() { + return false; + } + + @Override + public CompletableResultCode forceFlush() { + forceFlushCalled = true; + return CompletableResultCode.ofSuccess(); + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/AgentInstrumentationExtension.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/AgentInstrumentationExtension.java new file mode 100644 index 000000000..079a01eaa --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/AgentInstrumentationExtension.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit; + +import io.opentelemetry.instrumentation.testing.AgentTestRunner; + +/** + * JUnit 5 extension for writing javaagent instrumentation tests. + * + *

    Example usage: + * + *

    + *   class MyAgentInstrumentationTest {
    + *     {@literal @}RegisterExtension
    + *     static final AgentInstrumentationExtension instrTesting = AgentInstrumentationExtension.create();
    + *
    + *     {@literal @}Test
    + *     void test() {
    + *       // test code ...
    + *
    + *       var spans = instrTesting.spans();
    + *       // assertions on collected spans ...
    + *     }
    + *   }
    + * 
    + * + *

    Note that {@link AgentInstrumentationExtension} will not work by itself, you have to run the + * tests process with the {@code agent-for-testing} javaagent. + */ +public final class AgentInstrumentationExtension extends InstrumentationExtension { + private AgentInstrumentationExtension() { + super(AgentTestRunner.instance()); + } + + public static AgentInstrumentationExtension create() { + return new AgentInstrumentationExtension(); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java new file mode 100644 index 000000000..2dd0af82c --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner; +import io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public abstract class InstrumentationExtension + implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback, AfterEachCallback { + private static final long DEFAULT_TRACE_WAIT_TIMEOUT_SECONDS = 20; + + private final InstrumentationTestRunner testRunner; + + protected InstrumentationExtension(InstrumentationTestRunner testRunner) { + this.testRunner = testRunner; + } + + @Override + public void beforeAll(ExtensionContext extensionContext) { + testRunner.beforeTestClass(); + } + + @Override + public void beforeEach(ExtensionContext extensionContext) { + testRunner.clearAllExportedData(); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + ContextStorage storage = ContextStorage.get(); + if (storage instanceof AutoCloseable) { + ((AutoCloseable) storage).close(); + } + } + + @Override + public void afterAll(ExtensionContext extensionContext) { + testRunner.afterTestClass(); + } + + /** Return the {@link OpenTelemetry} instance used to produce telemetry data. */ + public OpenTelemetry getOpenTelemetry() { + return testRunner.getOpenTelemetry(); + } + + /** Return a list of all captured spans. */ + public List spans() { + return testRunner.getExportedSpans(); + } + + /** Return a list of all captured traces, where each trace is a sorted list of spans. */ + public List> traces() { + return TelemetryDataUtil.groupTraces(spans()); + } + + /** Return a list of all captured metrics. */ + public List metrics() { + return testRunner.getExportedMetrics(); + } + + /** + * Removes all captured telemetry data. After calling this method {@link #spans()}, {@link + * #traces()} and {@link #metrics()} will return empty lists until more telemetry data is + * captured. + */ + public void clearData() { + testRunner.clearAllExportedData(); + } + + /** + * Wait until at least {@code numberOfTraces} traces are completed and return all captured traces. + * Note that there may be more than {@code numberOfTraces} collected. By default this waits up to + * 20 seconds, then times out. + * + * @throws TimeoutException when the operation times out + * @throws InterruptedException when the current thread is interrupted + */ + public List> waitForTraces(int numberOfTraces) + throws TimeoutException, InterruptedException { + return waitForTraces(numberOfTraces, DEFAULT_TRACE_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + /** + * Wait until at least {@code numberOfTraces} traces are completed and return all captured traces. + * Note that there may be more than {@code numberOfTraces} collected. + * + * @throws TimeoutException when the operation times out + * @throws InterruptedException when the current thread is interrupted + */ + public List> waitForTraces(int numberOfTraces, long timeout, TimeUnit unit) + throws TimeoutException, InterruptedException { + return TelemetryDataUtil.waitForTraces(this::spans, numberOfTraces, timeout, unit); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtension.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtension.java new file mode 100644 index 000000000..822eb7174 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtension.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit; + +import io.opentelemetry.instrumentation.testing.LibraryTestRunner; + +/** + * JUnit 5 extension for writing library instrumentation tests. + * + *

    Example usage: + * + *

    + *   class MyLibraryInstrumentationTest {
    + *     {@literal @}RegisterExtension
    + *     static final LibraryInstrumentationExtension instrTesting = LibraryInstrumentationExtension.create();
    + *
    + *     {@literal @}Test
    + *     void test() {
    + *       // test code ...
    + *
    + *       var spans = instrTesting.spans();
    + *       // assertions on collected spans ...
    + *     }
    + *   }
    + * 
    + */ +public final class LibraryInstrumentationExtension extends InstrumentationExtension { + private LibraryInstrumentationExtension() { + super(LibraryTestRunner.instance()); + } + + public static LibraryInstrumentationExtension create() { + return new LibraryInstrumentationExtension(); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TelemetryDataUtil.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TelemetryDataUtil.java new file mode 100644 index 000000000..a5b2d9b8d --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TelemetryDataUtil.java @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.util; + +import static java.util.stream.Collectors.toList; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public final class TelemetryDataUtil { + + public static List> groupTraces(List spans) { + List> traces = + new ArrayList<>( + spans.stream().collect(Collectors.groupingBy(SpanData::getTraceId)).values()); + sortTraces(traces); + for (int i = 0; i < traces.size(); i++) { + List trace = traces.get(i); + traces.set(i, sort(trace)); + } + return traces; + } + + public static List> waitForTraces(Supplier> supplier, int number) + throws InterruptedException, TimeoutException { + return waitForTraces(supplier, number, 20, TimeUnit.SECONDS); + } + + public static List> waitForTraces( + Supplier> supplier, int number, long timeout, TimeUnit unit) + throws InterruptedException, TimeoutException { + long startTime = System.nanoTime(); + List> allTraces = groupTraces(supplier.get()); + List> completeTraces = + allTraces.stream().filter(TelemetryDataUtil::isCompleted).collect(toList()); + while (completeTraces.size() < number && elapsedSeconds(startTime) < unit.toSeconds(timeout)) { + allTraces = groupTraces(supplier.get()); + completeTraces = allTraces.stream().filter(TelemetryDataUtil::isCompleted).collect(toList()); + Thread.sleep(10); + } + if (completeTraces.size() < number) { + throw new TimeoutException( + "Timeout waiting for " + + number + + " completed trace(s), found " + + completeTraces.size() + + " completed trace(s) and " + + allTraces.size() + + " total trace(s): " + + allTraces); + } + return completeTraces; + } + + private static long elapsedSeconds(long startTime) { + return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime); + } + + // must be called under tracesLock + private static void sortTraces(List> traces) { + traces.sort(Comparator.comparingLong(TelemetryDataUtil::getMinSpanOrder)); + } + + private static long getMinSpanOrder(List spans) { + return spans.stream().mapToLong(SpanData::getStartEpochNanos).min().orElse(0); + } + + @SuppressWarnings("UnstableApiUsage") + private static List sort(List trace) { + + Map lookup = new HashMap<>(); + for (SpanData span : trace) { + lookup.put(span.getSpanId(), new Node(span)); + } + + for (Node node : lookup.values()) { + String parentSpanId = node.span.getParentSpanId(); + if (SpanId.isValid(parentSpanId)) { + Node parentNode = lookup.get(parentSpanId); + if (parentNode != null) { + parentNode.childNodes.add(node); + node.root = false; + } + } + } + + List rootNodes = new ArrayList<>(); + for (Node node : lookup.values()) { + sortOneLevel(node.childNodes); + if (node.root) { + rootNodes.add(node); + } + } + sortOneLevel(rootNodes); + + List orderedNodes = new ArrayList<>(); + for (Node rootNode : rootNodes) { + traversePreOrder(rootNode, orderedNodes); + } + + List orderedSpans = new ArrayList<>(); + for (Node node : orderedNodes) { + orderedSpans.add(node.span); + } + return orderedSpans; + } + + private static void sortOneLevel(List nodes) { + nodes.sort(Comparator.comparingLong(node -> node.span.getStartEpochNanos())); + } + + private static void traversePreOrder(Node node, List accumulator) { + accumulator.add(node); + for (Node child : node.childNodes) { + traversePreOrder(child, accumulator); + } + } + + // trace is completed if root span is present + private static boolean isCompleted(List trace) { + for (SpanData span : trace) { + if (!SpanId.isValid(span.getParentSpanId())) { + return true; + } + if (span.getParentSpanId().equals("0000000000000456")) { + // this is a special parent id that some tests use + return true; + } + } + return false; + } + + private static class Node { + + private final SpanData span; + private final List childNodes = new ArrayList<>(); + private boolean root = true; + + private Node(SpanData span) { + this.span = span; + } + } + + private TelemetryDataUtil() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/ThrowingRunnable.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/ThrowingRunnable.java new file mode 100644 index 000000000..7fe2a5ae7 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/ThrowingRunnable.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.util; + +/** + * A utility interface representing a {@link Runnable} that may throw. + * + * @param Thrown exception type. + */ +@FunctionalInterface +public interface ThrowingRunnable { + void run() throws E; +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/ThrowingSupplier.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/ThrowingSupplier.java new file mode 100644 index 000000000..649ef6360 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/ThrowingSupplier.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.util; + +import java.util.function.Supplier; + +/** + * A utility interface representing a {@link Supplier} that may throw. + * + * @param Thrown exception type. + */ +@FunctionalInterface +public interface ThrowingSupplier { + T get() throws E; +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TraceUtils.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TraceUtils.java new file mode 100644 index 000000000..26067e22b --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TraceUtils.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.util; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; + +/** Utility class for creating spans in tests. */ +public final class TraceUtils { + + private static final TestTracer TRACER = new TestTracer(); + + public static void withSpan(String spanName, ThrowingRunnable callback) + throws E { + withSpan( + spanName, + () -> { + callback.run(); + return null; + }); + } + + public static T withSpan( + String spanName, ThrowingSupplier callback) throws E { + Context context = TRACER.startSpan(spanName); + try (Scope ignored = context.makeCurrent()) { + T result = callback.get(); + TRACER.end(context); + return result; + } catch (Throwable t) { + TRACER.endExceptionally(context, t); + throw t; + } + } + + public static void withClientSpan( + String spanName, ThrowingRunnable callback) throws E { + withClientSpan( + spanName, + () -> { + callback.run(); + return null; + }); + } + + public static T withClientSpan( + String spanName, ThrowingSupplier callback) throws E { + Context context = TRACER.startClientSpan(spanName); + try (Scope ignored = context.makeCurrent()) { + T result = callback.get(); + TRACER.end(context); + return result; + } catch (Throwable t) { + TRACER.endExceptionally(context, t); + throw t; + } + } + + public static void withServerSpan( + String spanName, ThrowingRunnable callback) throws E { + withServerSpan( + spanName, + () -> { + callback.run(); + return null; + }); + } + + public static T withServerSpan( + String spanName, ThrowingSupplier callback) throws E { + Context context = TRACER.startServerSpan(spanName); + try (Scope ignored = context.makeCurrent()) { + T result = callback.get(); + TRACER.end(context); + return result; + } catch (Throwable t) { + TRACER.endExceptionally(context, t); + throw t; + } + } + + private static final class TestTracer extends BaseTracer { + @Override + protected String getInstrumentationName() { + return "test"; + } + + Context startClientSpan(String name) { + Context parentContext = Context.current(); + Span span = spanBuilder(parentContext, name, SpanKind.CLIENT).startSpan(); + return withClientSpan(parentContext, span); + } + + Context startServerSpan(String name) { + Context parentContext = Context.current(); + Span span = spanBuilder(parentContext, name, SpanKind.SERVER).startSpan(); + return withServerSpan(parentContext, span); + } + } + + private TraceUtils() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/AgentClassLoaderAccess.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/AgentClassLoaderAccess.java new file mode 100644 index 000000000..0177e5ef4 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/AgentClassLoaderAccess.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.common; + +import java.lang.reflect.Field; + +public final class AgentClassLoaderAccess { + + private static final ClassLoader agentClassLoader; + + static { + try { + Class agentInitializerClass = + ClassLoader.getSystemClassLoader() + .loadClass("io.opentelemetry.javaagent.bootstrap.AgentInitializer"); + Field agentClassLoaderField = agentInitializerClass.getDeclaredField("agentClassLoader"); + agentClassLoaderField.setAccessible(true); + agentClassLoader = (ClassLoader) agentClassLoaderField.get(null); + } catch (Throwable t) { + throw new AssertionError("Could not access agent classLoader", t); + } + } + + public static ClassLoader getAgentClassLoader() { + return agentClassLoader; + } + + static Class loadClass(String name) { + try { + return agentClassLoader.loadClass(name); + } catch (ClassNotFoundException e) { + throw new AssertionError("Could not load class from agent classloader", e); + } + } + + private AgentClassLoaderAccess() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/AgentTestingExporterAccess.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/AgentTestingExporterAccess.java new file mode 100644 index 000000000..5737d48ec --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/AgentTestingExporterAccess.java @@ -0,0 +1,565 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.common; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static java.util.stream.Collectors.toList; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.common.LabelsBuilder; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.TraceStateBuilder; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.ArrayValue; +import io.opentelemetry.proto.common.v1.InstrumentationLibrary; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.common.v1.StringKeyValue; +import io.opentelemetry.proto.metrics.v1.HistogramDataPoint; +import io.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics; +import io.opentelemetry.proto.metrics.v1.IntDataPoint; +import io.opentelemetry.proto.metrics.v1.IntSum; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import io.opentelemetry.proto.metrics.v1.Sum; +import io.opentelemetry.proto.metrics.v1.SummaryDataPoint; +import io.opentelemetry.proto.resource.v1.Resource; +import io.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.Span; +import io.opentelemetry.proto.trace.v1.Status; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramData; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramPointData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public final class AgentTestingExporterAccess { + private static final char TRACESTATE_KEY_VALUE_DELIMITER = '='; + private static final char TRACESTATE_ENTRY_DELIMITER = ','; + private static final Pattern TRACESTATE_ENTRY_DELIMITER_SPLIT_PATTERN = + Pattern.compile("[ \t]*" + TRACESTATE_ENTRY_DELIMITER + "[ \t]*"); + + private static final MethodHandle getSpanExportRequests; + private static final MethodHandle getMetricExportRequests; + private static final MethodHandle reset; + private static final MethodHandle forceFlushCalled; + + static { + try { + Class agentTestingExporterFactoryClass = + AgentClassLoaderAccess.loadClass( + "io.opentelemetry.javaagent.testing.exporter.AgentTestingExporterFactory"); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + getSpanExportRequests = + lookup.findStatic( + agentTestingExporterFactoryClass, + "getSpanExportRequests", + MethodType.methodType(List.class)); + getMetricExportRequests = + lookup.findStatic( + agentTestingExporterFactoryClass, + "getMetricExportRequests", + MethodType.methodType(List.class)); + reset = + lookup.findStatic( + agentTestingExporterFactoryClass, "reset", MethodType.methodType(void.class)); + forceFlushCalled = + lookup.findStatic( + agentTestingExporterFactoryClass, + "forceFlushCalled", + MethodType.methodType(boolean.class)); + } catch (Exception e) { + throw new AssertionError("Error accessing fields with reflection.", e); + } + } + + public static void reset() { + try { + reset.invokeExact(); + } catch (Throwable t) { + throw new AssertionError("Could not invoke reset", t); + } + } + + public static boolean forceFlushCalled() { + try { + return (boolean) forceFlushCalled.invokeExact(); + } catch (Throwable t) { + throw new AssertionError("Could not invoke forceFlushCalled", t); + } + } + + @SuppressWarnings("unchecked") + public static List getExportedSpans() { + final List exportRequests; + try { + exportRequests = (List) getSpanExportRequests.invokeExact(); + } catch (Throwable t) { + throw new AssertionError("Could not invoke getSpanExportRequests", t); + } + + List allResourceSpans = + exportRequests.stream() + .map( + serialized -> { + try { + return ExportTraceServiceRequest.parseFrom(serialized); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError(e); + } + }) + .flatMap(request -> request.getResourceSpansList().stream()) + .collect(toList()); + List spans = new ArrayList<>(); + for (ResourceSpans resourceSpans : allResourceSpans) { + Resource resource = resourceSpans.getResource(); + for (InstrumentationLibrarySpans ilSpans : + resourceSpans.getInstrumentationLibrarySpansList()) { + InstrumentationLibrary instrumentationLibrary = ilSpans.getInstrumentationLibrary(); + for (Span span : ilSpans.getSpansList()) { + String traceId = bytesToHex(span.getTraceId().toByteArray()); + spans.add( + TestSpanData.builder() + .setSpanContext( + SpanContext.create( + traceId, + bytesToHex(span.getSpanId().toByteArray()), + TraceFlags.getDefault(), + extractTraceState(span.getTraceState()), HeraContext.getDefault())) + // TODO is it ok to use default trace flags and default trace state here? + .setParentSpanContext( + SpanContext.create( + traceId, + bytesToHex(span.getParentSpanId().toByteArray()), + TraceFlags.getDefault(), + TraceState.getDefault(), HeraContext.getDefault())) + .setResource( + io.opentelemetry.sdk.resources.Resource.create( + fromProto(resource.getAttributesList()))) + .setInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create( + instrumentationLibrary.getName(), instrumentationLibrary.getVersion())) + .setName(span.getName()) + .setStartEpochNanos(span.getStartTimeUnixNano()) + .setEndEpochNanos(span.getEndTimeUnixNano()) + .setAttributes(fromProto(span.getAttributesList())) + .setEvents( + span.getEventsList().stream() + .map( + event -> + EventData.create( + event.getTimeUnixNano(), + event.getName(), + fromProto(event.getAttributesList()), + event.getDroppedAttributesCount() + + event.getAttributesCount())) + .collect(toList())) + .setStatus(fromProto(span.getStatus())) + .setKind(fromProto(span.getKind())) + .setLinks( + span.getLinksList().stream() + .map( + link -> + LinkData.create( + SpanContext.create( + bytesToHex(link.getTraceId().toByteArray()), + bytesToHex(link.getSpanId().toByteArray()), + TraceFlags.getDefault(), + extractTraceState(link.getTraceState()), HeraContext.getDefault()), + fromProto(link.getAttributesList()), + link.getDroppedAttributesCount() + link.getAttributesCount())) + .collect(toList())) + // OTLP doesn't have hasRemoteParent + .setHasEnded(true) + .setTotalRecordedEvents(span.getEventsCount() + span.getDroppedEventsCount()) + .setTotalRecordedLinks(span.getLinksCount() + span.getDroppedLinksCount()) + .setTotalAttributeCount( + span.getAttributesCount() + span.getDroppedAttributesCount()) + .build()); + } + } + } + return spans; + } + + @SuppressWarnings("unchecked") + public static List getExportedMetrics() { + final List exportRequests; + try { + exportRequests = (List) getMetricExportRequests.invokeExact(); + } catch (Throwable t) { + throw new AssertionError("Could not invoke getMetricExportRequests", t); + } + + List allResourceMetrics = + exportRequests.stream() + .map( + serialized -> { + try { + return ExportMetricsServiceRequest.parseFrom(serialized); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError(e); + } + }) + .flatMap(request -> request.getResourceMetricsList().stream()) + .collect(toList()); + List metrics = new ArrayList<>(); + for (ResourceMetrics resourceMetrics : allResourceMetrics) { + Resource resource = resourceMetrics.getResource(); + for (InstrumentationLibraryMetrics ilMetrics : + resourceMetrics.getInstrumentationLibraryMetricsList()) { + InstrumentationLibrary instrumentationLibrary = ilMetrics.getInstrumentationLibrary(); + for (Metric metric : ilMetrics.getMetricsList()) { + metrics.add( + createMetricData( + metric, + io.opentelemetry.sdk.resources.Resource.create( + fromProto(resource.getAttributesList())), + InstrumentationLibraryInfo.create( + instrumentationLibrary.getName(), instrumentationLibrary.getVersion()))); + } + } + } + return metrics; + } + + private static MetricData createMetricData( + Metric metric, + io.opentelemetry.sdk.resources.Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo) { + switch (metric.getDataCase()) { + case INT_GAUGE: + return MetricData.createLongGauge( + resource, + instrumentationLibraryInfo, + metric.getName(), + metric.getDescription(), + metric.getUnit(), + LongGaugeData.create(getIntPoints(metric.getIntGauge().getDataPointsList()))); + case GAUGE: + return MetricData.createDoubleGauge( + resource, + instrumentationLibraryInfo, + metric.getName(), + metric.getDescription(), + metric.getUnit(), + DoubleGaugeData.create(getDoublePointDatas(metric.getGauge().getDataPointsList()))); + case INT_SUM: + IntSum intSum = metric.getIntSum(); + return MetricData.createLongSum( + resource, + instrumentationLibraryInfo, + metric.getName(), + metric.getDescription(), + metric.getUnit(), + LongSumData.create( + intSum.getIsMonotonic(), + getTemporality(intSum.getAggregationTemporality()), + getIntPoints(metric.getIntSum().getDataPointsList()))); + case SUM: + Sum doubleSum = metric.getSum(); + return MetricData.createDoubleSum( + resource, + instrumentationLibraryInfo, + metric.getName(), + metric.getDescription(), + metric.getUnit(), + DoubleSumData.create( + doubleSum.getIsMonotonic(), + getTemporality(doubleSum.getAggregationTemporality()), + getDoublePointDatas(metric.getSum().getDataPointsList()))); + case HISTOGRAM: + return MetricData.createDoubleHistogram( + resource, + instrumentationLibraryInfo, + metric.getName(), + metric.getDescription(), + metric.getUnit(), + DoubleHistogramData.create( + getTemporality(metric.getHistogram().getAggregationTemporality()), + getDoubleHistogramDataPoints(metric.getHistogram().getDataPointsList()))); + case SUMMARY: + return MetricData.createDoubleSummary( + resource, + instrumentationLibraryInfo, + metric.getName(), + metric.getDescription(), + metric.getUnit(), + DoubleSummaryData.create( + getDoubleSummaryDataPoints(metric.getSummary().getDataPointsList()))); + default: + throw new AssertionError("Unexpected metric data: " + metric.getDataCase()); + } + } + + private static Labels createLabels(List stringKeyValues) { + LabelsBuilder labelsBuilder = Labels.builder(); + for (StringKeyValue stringKeyValue : stringKeyValues) { + labelsBuilder.put(stringKeyValue.getKey(), stringKeyValue.getValue()); + } + return labelsBuilder.build(); + } + + private static List getIntPoints(List points) { + return points.stream() + .map( + point -> + LongPointData.create( + point.getStartTimeUnixNano(), + point.getTimeUnixNano(), + createLabels(point.getLabelsList()), + point.getValue())) + .collect(toList()); + } + + private static List getDoublePointDatas(List points) { + return points.stream() + .map( + point -> { + final double value; + switch (point.getValueCase()) { + case AS_INT: + value = point.getAsInt(); + break; + case AS_DOUBLE: + default: + value = point.getAsDouble(); + break; + } + return DoublePointData.create( + point.getStartTimeUnixNano(), + point.getTimeUnixNano(), + createLabels(point.getLabelsList()), + value); + }) + .collect(toList()); + } + + private static Collection getDoubleHistogramDataPoints( + List dataPointsList) { + return dataPointsList.stream() + .map( + point -> + DoubleHistogramPointData.create( + point.getStartTimeUnixNano(), + point.getTimeUnixNano(), + createLabels(point.getLabelsList()), + point.getSum(), + point.getExplicitBoundsList(), + point.getBucketCountsList())) + .collect(toList()); + } + + private static Collection getDoubleSummaryDataPoints( + List dataPointsList) { + return dataPointsList.stream() + .map( + point -> + DoubleSummaryPointData.create( + point.getStartTimeUnixNano(), + point.getTimeUnixNano(), + createLabels(point.getLabelsList()), + point.getCount(), + point.getSum(), + getValues(point))) + .collect(toList()); + } + + private static List getValues(SummaryDataPoint point) { + return point.getQuantileValuesList().stream() + .map(v -> ValueAtPercentile.create(v.getQuantile(), v.getValue())) + .collect(Collectors.toList()); + } + + private static AggregationTemporality getTemporality( + io.opentelemetry.proto.metrics.v1.AggregationTemporality aggregationTemporality) { + switch (aggregationTemporality) { + case AGGREGATION_TEMPORALITY_CUMULATIVE: + return AggregationTemporality.CUMULATIVE; + case AGGREGATION_TEMPORALITY_DELTA: + return AggregationTemporality.DELTA; + default: + throw new IllegalStateException( + "Unexpected aggregation temporality: " + aggregationTemporality); + } + } + + private static Attributes fromProto(List attributes) { + AttributesBuilder converted = Attributes.builder(); + for (KeyValue attribute : attributes) { + String key = attribute.getKey(); + AnyValue value = attribute.getValue(); + switch (value.getValueCase()) { + case STRING_VALUE: + converted.put(key, value.getStringValue()); + break; + case BOOL_VALUE: + converted.put(key, value.getBoolValue()); + break; + case INT_VALUE: + converted.put(key, value.getIntValue()); + break; + case DOUBLE_VALUE: + converted.put(key, value.getDoubleValue()); + break; + case ARRAY_VALUE: + ArrayValue array = value.getArrayValue(); + if (array.getValuesCount() != 0) { + switch (array.getValues(0).getValueCase()) { + case STRING_VALUE: + converted.put( + stringArrayKey(key), + array.getValuesList().stream().map(AnyValue::getStringValue).collect(toList())); + break; + case BOOL_VALUE: + converted.put( + booleanArrayKey(key), + array.getValuesList().stream().map(AnyValue::getBoolValue).collect(toList())); + break; + case INT_VALUE: + converted.put( + longArrayKey(key), + array.getValuesList().stream().map(AnyValue::getIntValue).collect(toList())); + break; + case DOUBLE_VALUE: + converted.put( + doubleArrayKey(key), + array.getValuesList().stream().map(AnyValue::getDoubleValue).collect(toList())); + break; + case VALUE_NOT_SET: + break; + default: + throw new IllegalStateException( + "Unexpected attribute: " + array.getValues(0).getValueCase()); + } + } + break; + case VALUE_NOT_SET: + break; + default: + throw new IllegalStateException("Unexpected attribute: " + value.getValueCase()); + } + } + return converted.build(); + } + + private static StatusData fromProto(Status status) { + final StatusCode code; + switch (status.getCode()) { + case STATUS_CODE_OK: + code = StatusCode.OK; + break; + case STATUS_CODE_ERROR: + code = StatusCode.ERROR; + break; + default: + code = StatusCode.UNSET; + break; + } + return StatusData.create(code, status.getMessage()); + } + + private static SpanKind fromProto(Span.SpanKind kind) { + switch (kind) { + case SPAN_KIND_INTERNAL: + return SpanKind.INTERNAL; + case SPAN_KIND_SERVER: + return SpanKind.SERVER; + case SPAN_KIND_CLIENT: + return SpanKind.CLIENT; + case SPAN_KIND_PRODUCER: + return SpanKind.PRODUCER; + case SPAN_KIND_CONSUMER: + return SpanKind.CONSUMER; + default: + throw new IllegalArgumentException("Unexpected span kind: " + kind); + } + } + + private static TraceState extractTraceState(String traceStateHeader) { + if (traceStateHeader.isEmpty()) { + return TraceState.getDefault(); + } + TraceStateBuilder traceStateBuilder = TraceState.builder(); + String[] listMembers = TRACESTATE_ENTRY_DELIMITER_SPLIT_PATTERN.split(traceStateHeader); + // Iterate in reverse order because when call builder set the elements is added in the + // front of the list. + for (int i = listMembers.length - 1; i >= 0; i--) { + String listMember = listMembers[i]; + int index = listMember.indexOf(TRACESTATE_KEY_VALUE_DELIMITER); + traceStateBuilder.put(listMember.substring(0, index), listMember.substring(index + 1)); + } + return traceStateBuilder.build(); + } + + private static String bytesToHex(byte[] bytes) { + char[] dest = new char[bytes.length * 2]; + bytesToBase16(bytes, dest); + return new String(dest); + } + + private static void bytesToBase16(byte[] bytes, char[] dest) { + for (int i = 0; i < bytes.length; i++) { + byteToBase16(bytes[i], dest, i * 2); + } + } + + private static void byteToBase16(byte value, char[] dest, int destOffset) { + int b = value & 0xFF; + dest[destOffset] = ENCODING[b]; + dest[destOffset + 1] = ENCODING[b | 0x100]; + } + + private static final String ALPHABET = "0123456789abcdef"; + private static final char[] ENCODING = buildEncodingArray(); + + 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 AgentTestingExporterAccess() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/Java8BytecodeBridge.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/Java8BytecodeBridge.java new file mode 100644 index 000000000..4df373dcd --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/Java8BytecodeBridge.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.common; + +import io.opentelemetry.context.Context; + +/** + * A helper for scala and kotlin test code, since those tests are compiled to Java 6 bytecode, and + * so they cannot access methods that rely on new Java 8 bytecode features such as calling a static + * interface methods. + */ +public final class Java8BytecodeBridge { + + /** Calls {@link Context#current()}. */ + public static Context currentContext() { + return Context.current(); + } + + private Java8BytecodeBridge() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/TestAgentListenerAccess.java b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/TestAgentListenerAccess.java new file mode 100644 index 000000000..81e37f8ce --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/TestAgentListenerAccess.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.common; + +import static java.lang.invoke.MethodType.methodType; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +public final class TestAgentListenerAccess { + + private static final MethodHandle reset; + private static final MethodHandle getInstrumentationErrorCount; + private static final MethodHandle getIgnoredButTransformedClassNames; + private static final MethodHandle addSkipTransformationCondition; + private static final MethodHandle addSkipErrorCondition; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + Class testAgentListenerClass = + AgentClassLoaderAccess.loadClass( + "io.opentelemetry.javaagent.testing.bytebuddy.TestAgentListener"); + reset = lookup.findStatic(testAgentListenerClass, "reset", methodType(void.class)); + getInstrumentationErrorCount = + lookup.findStatic( + testAgentListenerClass, "getInstrumentationErrorCount", methodType(int.class)); + getIgnoredButTransformedClassNames = + lookup.findStatic( + testAgentListenerClass, "getIgnoredButTransformedClassNames", methodType(List.class)); + addSkipTransformationCondition = + lookup.findStatic( + testAgentListenerClass, + "addSkipTransformationCondition", + methodType(void.class, Function.class)); + addSkipErrorCondition = + lookup.findStatic( + testAgentListenerClass, + "addSkipErrorCondition", + methodType(void.class, BiFunction.class)); + } catch (Throwable t) { + throw new AssertionError("Could not initialize accessors for TestAgentListener.", t); + } + } + + public static void reset() { + try { + reset.invokeExact(); + } catch (Throwable t) { + throw new AssertionError("Could not invoke TestAgentListener.reset", t); + } + } + + public static int getInstrumentationErrorCount() { + try { + return (int) getInstrumentationErrorCount.invokeExact(); + } catch (Throwable t) { + throw new AssertionError( + "Could not invoke TestAgentListener.getInstrumentationErrorCount", t); + } + } + + @SuppressWarnings("unchecked") + public static List getIgnoredButTransformedClassNames() { + try { + return (List) getIgnoredButTransformedClassNames.invokeExact(); + } catch (Throwable t) { + throw new AssertionError( + "Could not invoke TestAgentListener.getIgnoredButTransformedClassNames", t); + } + } + + public static void addSkipTransformationCondition(Function condition) { + try { + addSkipTransformationCondition.invokeExact(condition); + } catch (Throwable t) { + throw new AssertionError( + "Could not invoke TestAgentListener.addSkipTransformationCondition", t); + } + } + + public static void addSkipErrorCondition(BiFunction condition) { + try { + addSkipErrorCondition.invokeExact(condition); + } catch (Throwable t) { + throw new AssertionError("Could not invoke TestAgentListener.addSkipErrorCondition", t); + } + } + + private TestAgentListenerAccess() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/misc/README.md b/opentelemetry-java-instrumentation/testing-common/src/misc/README.md new file mode 100644 index 000000000..105a7b244 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/misc/README.md @@ -0,0 +1,5 @@ +testing-keystore.p12 generated with + +keytool -genkeypair -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore testing-keystore.p12 -validity 3650 -dname "CN=localhost" -ext "SAN=dns:localhost,ip:127.0.0.1" + +password = testing diff --git a/opentelemetry-java-instrumentation/testing-common/src/misc/testing-keystore.p12 b/opentelemetry-java-instrumentation/testing-common/src/misc/testing-keystore.p12 new file mode 100644 index 000000000..86d917932 Binary files /dev/null and b/opentelemetry-java-instrumentation/testing-common/src/misc/testing-keystore.p12 differ diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/HelperReferenceWrapperTest.groovy b/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/HelperReferenceWrapperTest.groovy new file mode 100644 index 000000000..be4207bed --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/HelperReferenceWrapperTest.groovy @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package muzzle + +import static io.opentelemetry.javaagent.extension.muzzle.Flag.ManifestationFlag +import static java.util.stream.Collectors.toList + +import io.opentelemetry.javaagent.extension.muzzle.ClassRef +import io.opentelemetry.javaagent.extension.muzzle.Flag +import io.opentelemetry.javaagent.extension.muzzle.Source +import io.opentelemetry.javaagent.tooling.muzzle.matcher.HelperReferenceWrapper +import net.bytebuddy.jar.asm.Type +import net.bytebuddy.pool.TypePool +import spock.lang.Shared +import spock.lang.Specification + +class HelperReferenceWrapperTest extends Specification { + + @Shared + def baseHelperClass = ClassRef.newBuilder(HelperReferenceWrapperTest.name + '$BaseHelper') + .setSuperClassName(HelperReferenceWrapperTestClasses.AbstractClasspathType.name) + .addFlag(ManifestationFlag.ABSTRACT) + .addMethod(new Source[0], new Flag[0], "foo", Type.VOID_TYPE) + .addMethod(new Source[0], [ManifestationFlag.ABSTRACT] as Flag[], "abstract", Type.INT_TYPE) + .build() + + @Shared + def helperClass = ClassRef.newBuilder(HelperReferenceWrapperTest.name + '$Helper') + .setSuperClassName(baseHelperClass.className) + .addInterfaceName(HelperReferenceWrapperTestClasses.Interface2.name) + .addMethod(new Source[0], new Flag[0], "bar", Type.VOID_TYPE) + .addField(new Source[0], new Flag[0], "field", Type.getType("Ljava/lang/Object;"), false) + .addField(new Source[0], new Flag[0], "declaredField", Type.getType("Ljava/lang/Object;"), true) + .addField(new Source[0], [Flag.VisibilityFlag.PRIVATE] as Flag[], "privateFieldsAreSkipped", Type.getType("Ljava/lang/Object;"), true) + .build() + + def "should wrap helper types"() { + given: + def typePool = TypePool.Default.of(HelperReferenceWrapperTest.classLoader) + def references = [ + (helperClass.className) : helperClass, + (baseHelperClass.className): baseHelperClass + ] + + when: + def helperWrapper = new HelperReferenceWrapper.Factory(typePool, references).create(helperClass) + + then: + with(helperWrapper) { helper -> + !helper.abstract + + with(helper.methods.collect(toList())) { + it.size() == 1 + with(it[0]) { + !it.abstract + it.name == "bar" + it.descriptor == "()V" + } + } + + with(helper.fields.collect(toList())) { + it.size() == 1 + with(it[0]) { + it.name == "declaredField" + it.descriptor == "Ljava/lang/Object;" + } + } + + helper.hasSuperTypes() + with(helper.superTypes.collect(toList())) { + it.size() == 2 + with(it[0]) { baseHelper -> + baseHelper.abstract + + with(baseHelper.methods.collect(toList())) { + it.size() == 2 + with(it[0]) { + !it.abstract + it.name == 'foo' + it.descriptor == '()V' + } + with(it[1]) { + it.abstract + it.name == 'abstract' + it.descriptor == '()I' + } + } + + baseHelper.hasSuperTypes() + with(baseHelper.superTypes.collect(toList())) { + it.size() == 1 + with(it[0]) { abstractClasspathType -> + abstractClasspathType.abstract + + abstractClasspathType.getMethods().collect(toList()).isEmpty() + + with(abstractClasspathType.fields.collect(toList())) { + it.size() == 1 + with(it[0]) { + it.name == "field" + it.descriptor == "Ljava/lang/Object;" + } + } + + abstractClasspathType.hasSuperTypes() + with(abstractClasspathType.superTypes.collect(toList())) { + it.size() == 2 + with(it[0]) { object -> + !object.hasSuperTypes() + } + with(it[1]) { interface1 -> + interface1.abstract + + with(interface1.methods.collect(toList())) { + it.size() == 1 + with(it[0]) { + it.abstract + it.name == "foo" + it.descriptor == "()V" + } + } + + !interface1.hasSuperTypes() + interface1.getSuperTypes().collect(toList()).isEmpty() + } + } + } + } + } + with(it[1]) { interface2 -> + interface2.abstract + + with(interface2.methods.collect(toList())) { + it.size() == 1 + with(it[0]) { + it.abstract + it.name == "bar" + it.descriptor == "()V" + } + } + + !interface2.hasSuperTypes() + interface2.getSuperTypes().collect(toList()).isEmpty() + } + } + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/ReferenceCollectorTest.groovy b/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/ReferenceCollectorTest.groovy new file mode 100644 index 000000000..106419b7e --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/ReferenceCollectorTest.groovy @@ -0,0 +1,373 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package muzzle + + +import static io.opentelemetry.javaagent.extension.muzzle.Flag.ManifestationFlag +import static io.opentelemetry.javaagent.extension.muzzle.Flag.MinimumVisibilityFlag +import static io.opentelemetry.javaagent.extension.muzzle.Flag.OwnershipFlag +import static io.opentelemetry.javaagent.extension.muzzle.Flag.VisibilityFlag +import static muzzle.TestClasses.HelperAdvice +import static muzzle.TestClasses.LdcAdvice +import static muzzle.TestClasses.MethodBodyAdvice + +import external.instrumentation.ExternalHelper +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.InstrumentationContextTestClasses +import io.opentelemetry.instrumentation.OtherTestHelperClasses +import io.opentelemetry.instrumentation.TestHelperClasses +import io.opentelemetry.javaagent.extension.muzzle.ClassRef +import io.opentelemetry.javaagent.extension.muzzle.FieldRef +import io.opentelemetry.javaagent.extension.muzzle.Flag +import io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCompilationException +import io.opentelemetry.javaagent.tooling.muzzle.collector.ReferenceCollector +import spock.lang.Specification +import spock.lang.Unroll + +class ReferenceCollectorTest extends Specification { + def "method body creates references"() { + setup: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(MethodBodyAdvice.name) + collector.prune() + def references = collector.getReferences() + + expect: + references.keySet() == [ + MethodBodyAdvice.A.name, + MethodBodyAdvice.B.name, + MethodBodyAdvice.SomeInterface.name, + MethodBodyAdvice.SomeImplementation.name + ] as Set + + def bRef = references[MethodBodyAdvice.B.name] + def aRef = references[MethodBodyAdvice.A.name] + + // interface flags + bRef.flags.contains(ManifestationFlag.NON_INTERFACE) + references[MethodBodyAdvice.SomeInterface.name].flags.contains(ManifestationFlag.INTERFACE) + + // class access flags + aRef.flags.contains(MinimumVisibilityFlag.PACKAGE_OR_HIGHER) + bRef.flags.contains(MinimumVisibilityFlag.PACKAGE_OR_HIGHER) + + // method refs + assertMethod bRef, 'method', '(Ljava/lang/String;)Ljava/lang/String;', + MinimumVisibilityFlag.PROTECTED_OR_HIGHER, + OwnershipFlag.NON_STATIC + assertMethod bRef, 'methodWithPrimitives', '(Z)V', + MinimumVisibilityFlag.PROTECTED_OR_HIGHER, + OwnershipFlag.NON_STATIC + assertMethod bRef, 'staticMethod', '()V', + MinimumVisibilityFlag.PROTECTED_OR_HIGHER, + OwnershipFlag.STATIC + assertMethod bRef, 'methodWithArrays', '([Ljava/lang/String;)[Ljava/lang/Object;', + MinimumVisibilityFlag.PROTECTED_OR_HIGHER, + OwnershipFlag.NON_STATIC + + // field refs + bRef.fields.isEmpty() + aRef.fields.size() == 2 + assertField aRef, 'publicB', MinimumVisibilityFlag.PACKAGE_OR_HIGHER, OwnershipFlag.NON_STATIC + assertField aRef, 'staticB', MinimumVisibilityFlag.PACKAGE_OR_HIGHER, OwnershipFlag.STATIC + } + + def "protected ref test"() { + setup: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(MethodBodyAdvice.B2.name) + collector.prune() + def references = collector.getReferences() + + expect: + assertMethod references[MethodBodyAdvice.B.name], 'protectedMethod', '()V', + MinimumVisibilityFlag.PROTECTED_OR_HIGHER, + OwnershipFlag.NON_STATIC + } + + def "ldc creates references"() { + setup: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(LdcAdvice.name) + collector.prune() + def references = collector.getReferences() + + expect: + references[MethodBodyAdvice.A.name] != null + } + + def "instanceof creates references"() { + setup: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(TestClasses.InstanceofAdvice.name) + collector.prune() + def references = collector.getReferences() + + expect: + references[MethodBodyAdvice.A.name] != null + } + + def "invokedynamic creates references"() { + setup: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(TestClasses.InvokeDynamicAdvice.name) + collector.prune() + def references = collector.getReferences() + + expect: + references['muzzle.TestClasses$MethodBodyAdvice$SomeImplementation'] != null + references['muzzle.TestClasses$MethodBodyAdvice$B'] != null + } + + def "should create references for helper classes"() { + when: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(HelperAdvice.name) + def references = collector.getReferences() + + then: + references.keySet() == [ + TestHelperClasses.Helper.name, + TestHelperClasses.HelperSuperClass.name, + TestHelperClasses.HelperInterface.name + ] as Set + + with(references[TestHelperClasses.HelperSuperClass.name]) { helperSuperClass -> + helperSuperClass.flags.contains(ManifestationFlag.ABSTRACT) + assertHelperSuperClassMethod(helperSuperClass, true) + assertMethod helperSuperClass, 'finalMethod', '()Ljava/lang/String;', + VisibilityFlag.PUBLIC, + OwnershipFlag.NON_STATIC, + ManifestationFlag.FINAL + } + + with(references[TestHelperClasses.HelperInterface.name]) { helperInterface -> + helperInterface.flags.contains(ManifestationFlag.ABSTRACT) + assertHelperInterfaceMethod helperInterface, true + } + + with(references[TestHelperClasses.Helper.name]) { helperClass -> + helperClass.flags.contains(ManifestationFlag.NON_FINAL) + assertHelperSuperClassMethod helperClass, false + assertHelperInterfaceMethod helperClass, false + } + } + + def "should collect field declaration references"() { + when: + def collector = new ReferenceCollector({ it == DeclaredFieldTestClass.Helper.name }) + collector.collectReferencesFromAdvice(DeclaredFieldTestClass.Advice.name) + collector.prune() + def references = collector.references + + then: + println references + + with(references[DeclaredFieldTestClass.Helper.name]) { helperClass -> + def superField = findField(helperClass, 'superField') + !superField.declared + + def field = findField(helperClass, 'helperField') + field.declared + } + + with(references[DeclaredFieldTestClass.LibraryBaseClass.name]) { libraryBaseClass -> + libraryBaseClass.fields.empty + } + } + + def "should find all helper classes"() { + when: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(HelperAdvice.name) + collector.prune() + def helperClasses = collector.getSortedHelperClasses() + + then: + assertThatContainsInOrder helperClasses, [ + TestHelperClasses.HelperInterface.name, + TestHelperClasses.Helper.name + ] + assertThatContainsInOrder helperClasses, [ + TestHelperClasses.HelperSuperClass.name, + TestHelperClasses.Helper.name + ] + } + + def "should correctly find helper classes from multiple advice classes"() { + when: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(TestClasses.HelperAdvice.name) + collector.collectReferencesFromAdvice(TestClasses.HelperOtherAdvice.name) + collector.prune() + def helperClasses = collector.getSortedHelperClasses() + + then: + assertThatContainsInOrder helperClasses, [ + TestHelperClasses.HelperInterface.name, + TestHelperClasses.Helper.name + ] + assertThatContainsInOrder helperClasses, [ + TestHelperClasses.HelperSuperClass.name, + TestHelperClasses.Helper.name + ] + assertThatContainsInOrder helperClasses, [ + OtherTestHelperClasses.TestEnum.name, + OtherTestHelperClasses.TestEnum.name + '$1', + ] + new HashSet<>(helperClasses) == new HashSet([ + TestHelperClasses.HelperSuperClass.name, + TestHelperClasses.HelperInterface.name, + TestHelperClasses.Helper.name, + OtherTestHelperClasses.Bar.name, + OtherTestHelperClasses.Foo.name, + OtherTestHelperClasses.TestEnum.name, + OtherTestHelperClasses.TestEnum.name + '$1', + OtherTestHelperClasses.name + '$1', + ]) + } + + def "should correctly find external instrumentation classes"() { + when: + def collector = new ReferenceCollector({ it.startsWith("external.instrumentation") }) + collector.collectReferencesFromAdvice(TestClasses.ExternalInstrumentationAdvice.name) + collector.prune() + + then: "should collect references" + def references = collector.getReferences() + references['external.NotInstrumentation'] != null + + then: "should collect helper classes" + def helperClasses = collector.getSortedHelperClasses() + helperClasses == [ExternalHelper.name] + } + + @Unroll + def "should collect helper classes from resource file #desc"() { + when: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromResource(resource) + collector.prune() + + then: "SPI classes are included in helper classes" + def helperClasses = collector.sortedHelperClasses + assertThatContainsInOrder helperClasses, [ + TestHelperClasses.HelperInterface.name, + TestHelperClasses.Helper.name + ] + assertThatContainsInOrder helperClasses, [ + TestHelperClasses.HelperSuperClass.name, + TestHelperClasses.Helper.name + ] + + where: + desc | resource + "Java SPI" | "META-INF/services/test.resource.file" + "AWS SDK v2 global interceptors file" | "software/amazon/awssdk/global/handlers/execution.interceptors" + "AWS SDK v2 service interceptors file" | "software/amazon/awssdk/services/testservice/execution.interceptors" + "AWS SDK v2 service (second level) interceptors file" | "software/amazon/awssdk/services/testservice/testsubservice/execution.interceptors" + "AWS SDK v1 global interceptors file" | "com/amazonaws/global/handlers/request.handler2s" + "AWS SDK v1 service interceptors file" | "com/amazonaws/services/testservice/request.handler2s" + "AWS SDK v1 service (second level) interceptors file" | "com/amazonaws/services/testservice/testsubservice/request.handler2s" + } + + def "should ignore arbitrary resource file"() { + when: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromResource("application.properties") + collector.prune() + + then: + collector.references.isEmpty() + collector.sortedHelperClasses.isEmpty() + } + + def "should collect context store classes"() { + when: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(InstrumentationContextTestClasses.ValidAdvice.name) + collector.prune() + + then: + def contextStore = collector.getContextStoreClasses() + contextStore == [ + (InstrumentationContextTestClasses.Key1.name): Context.name, + (InstrumentationContextTestClasses.Key2.name): Context.name + ] + } + + def "should not collect context store classes for invalid scenario: #desc"() { + when: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(adviceClassName) + collector.prune() + + then: + thrown(MuzzleCompilationException) + + where: + desc | adviceClassName + "passing arbitrary variables or parameters to InstrumentationContext.get()" | InstrumentationContextTestClasses.NotUsingClassRefAdvice.name + "storing class ref in a local var" | InstrumentationContextTestClasses.PassingVariableAdvice.name + } + + private static assertHelperSuperClassMethod(ClassRef reference, boolean isAbstract) { + assertMethod reference, 'abstractMethod', '()I', + VisibilityFlag.PROTECTED, + OwnershipFlag.NON_STATIC, + isAbstract ? ManifestationFlag.ABSTRACT : ManifestationFlag.NON_FINAL + } + + private static assertHelperInterfaceMethod(ClassRef reference, boolean isAbstract) { + assertMethod reference, 'foo', '()V', + VisibilityFlag.PUBLIC, + OwnershipFlag.NON_STATIC, + isAbstract ? ManifestationFlag.ABSTRACT : ManifestationFlag.NON_FINAL + } + + private static assertMethod(ClassRef reference, String methodName, String methodDesc, Flag... flags) { + def method = findMethod reference, methodName, methodDesc + method != null && (method.flags == flags as Set) + } + + private static findMethod(ClassRef reference, String methodName, String methodDesc) { + for (def method : reference.methods) { + if (method.name == methodName && method.descriptor == methodDesc) { + return method + } + } + return null + } + + private static assertField(ClassRef reference, String fieldName, Flag... flags) { + def field = findField reference, fieldName + field != null && (field.flags == flags as Set) + } + + private static FieldRef findField(ClassRef reference, String fieldName) { + for (def field : reference.fields) { + if (field.name == fieldName) { + return field + } + } + return null + } + + private static assertThatContainsInOrder(List list, List sublist) { + def listIt = list.iterator() + def sublistIt = sublist.iterator() + while (listIt.hasNext() && sublistIt.hasNext()) { + def sublistElem = sublistIt.next() + while (listIt.hasNext()) { + def listElem = listIt.next() + if (listElem == sublistElem) { + break + } + } + } + return !sublistIt.hasNext() + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/ReferenceMatcherTest.groovy b/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/ReferenceMatcherTest.groovy new file mode 100644 index 000000000..db95b7f97 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/groovy/muzzle/ReferenceMatcherTest.groovy @@ -0,0 +1,342 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package muzzle + +import static io.opentelemetry.javaagent.extension.muzzle.Flag.ManifestationFlag.ABSTRACT +import static io.opentelemetry.javaagent.extension.muzzle.Flag.ManifestationFlag.INTERFACE +import static io.opentelemetry.javaagent.extension.muzzle.Flag.ManifestationFlag.NON_INTERFACE +import static io.opentelemetry.javaagent.extension.muzzle.Flag.MinimumVisibilityFlag.PACKAGE_OR_HIGHER +import static io.opentelemetry.javaagent.extension.muzzle.Flag.MinimumVisibilityFlag.PRIVATE_OR_HIGHER +import static io.opentelemetry.javaagent.extension.muzzle.Flag.MinimumVisibilityFlag.PROTECTED_OR_HIGHER +import static io.opentelemetry.javaagent.extension.muzzle.Flag.OwnershipFlag.NON_STATIC +import static io.opentelemetry.javaagent.extension.muzzle.Flag.OwnershipFlag.STATIC +import static io.opentelemetry.javaagent.tooling.muzzle.matcher.Mismatch.MissingClass +import static io.opentelemetry.javaagent.tooling.muzzle.matcher.Mismatch.MissingField +import static io.opentelemetry.javaagent.tooling.muzzle.matcher.Mismatch.MissingFlag +import static io.opentelemetry.javaagent.tooling.muzzle.matcher.Mismatch.MissingMethod +import static muzzle.TestClasses.MethodBodyAdvice + +import external.LibraryBaseClass +import io.opentelemetry.instrumentation.TestHelperClasses +import io.opentelemetry.instrumentation.test.utils.ClasspathUtils +import io.opentelemetry.javaagent.extension.muzzle.ClassRef +import io.opentelemetry.javaagent.extension.muzzle.Flag +import io.opentelemetry.javaagent.extension.muzzle.Source +import io.opentelemetry.javaagent.tooling.muzzle.collector.ReferenceCollector +import io.opentelemetry.javaagent.tooling.muzzle.matcher.Mismatch +import io.opentelemetry.javaagent.tooling.muzzle.matcher.ReferenceMatcher +import net.bytebuddy.jar.asm.Type +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +@Unroll +class ReferenceMatcherTest extends Specification { + static final TEST_EXTERNAL_INSTRUMENTATION_PACKAGE = "com.external.otel.instrumentation" + + @Shared + ClassLoader safeClasspath = new URLClassLoader([ClasspathUtils.createJarWithClasses(MethodBodyAdvice.A, + MethodBodyAdvice.B, + MethodBodyAdvice.SomeInterface, + MethodBodyAdvice.SomeImplementation)] as URL[], + (ClassLoader) null) + + @Shared + ClassLoader unsafeClasspath = new URLClassLoader([ClasspathUtils.createJarWithClasses(MethodBodyAdvice.A, + MethodBodyAdvice.SomeInterface, + MethodBodyAdvice.SomeImplementation)] as URL[], + (ClassLoader) null) + + def "match safe classpaths"() { + setup: + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(MethodBodyAdvice.name) + def refMatcher = createMatcher(collector.getReferences()) + + expect: + getMismatchClassSet(refMatcher.getMismatchedReferenceSources(safeClasspath)).empty + getMismatchClassSet(refMatcher.getMismatchedReferenceSources(unsafeClasspath)) == [MissingClass] as Set + } + + def "matching does not hold a strong reference to classloaders"() { + expect: + MuzzleWeakReferenceTest.classLoaderRefIsGarbageCollected() + } + + private static class CountingClassLoader extends URLClassLoader { + int count = 0 + + CountingClassLoader(URL[] urls, ClassLoader parent) { + super(urls, (ClassLoader) parent) + } + + @Override + URL getResource(String name) { + count++ + return super.getResource(name) + } + } + + def "muzzle type pool caches"() { + setup: + def cl = new CountingClassLoader( + [ClasspathUtils.createJarWithClasses(MethodBodyAdvice.A, + MethodBodyAdvice.B, + MethodBodyAdvice.SomeInterface, + MethodBodyAdvice.SomeImplementation)] as URL[], + (ClassLoader) null) + + def collector = new ReferenceCollector({ false }) + collector.collectReferencesFromAdvice(MethodBodyAdvice.name) + + def refMatcher1 = createMatcher(collector.getReferences()) + def refMatcher2 = createMatcher(collector.getReferences()) + assert getMismatchClassSet(refMatcher1.getMismatchedReferenceSources(cl)).empty + int countAfterFirstMatch = cl.count + // the second matcher should be able to used cached type descriptions from the first + assert getMismatchClassSet(refMatcher2.getMismatchedReferenceSources(cl)).empty + + expect: + cl.count == countAfterFirstMatch + } + + def "matching ref #referenceName #referenceFlags against #classToCheck produces #expectedMismatches"() { + setup: + def ref = ClassRef.newBuilder(referenceName) + .addFlag(referenceFlag) + .build() + + when: + def mismatches = createMatcher([(ref.className): ref]).getMismatchedReferenceSources(this.class.classLoader) + + then: + getMismatchClassSet(mismatches) == expectedMismatches as Set + + where: + referenceName | referenceFlag | classToCheck | expectedMismatches + MethodBodyAdvice.B.name | NON_INTERFACE | MethodBodyAdvice.B | [] + MethodBodyAdvice.B.name | INTERFACE | MethodBodyAdvice.B | [MissingFlag] + } + + def "method match #methodTestDesc"() { + setup: + def methodType = Type.getMethodType(methodDesc) + def reference = ClassRef.newBuilder(classToCheck.name) + .addMethod(new Source[0], methodFlags as Flag[], methodName, methodType.returnType, methodType.argumentTypes) + .build() + + when: + def mismatches = createMatcher([(reference.className): reference]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + getMismatchClassSet(mismatches) == expectedMismatches as Set + + where: + methodName | methodDesc | methodFlags | classToCheck | expectedMismatches | methodTestDesc + "method" | "(Ljava/lang/String;)Ljava/lang/String;" | [] | MethodBodyAdvice.B | [] | "match method declared in class" + "hashCode" | "()I" | [] | MethodBodyAdvice.B | [] | "match method declared in superclass" + "someMethod" | "()V" | [] | MethodBodyAdvice.SomeInterface | [] | "match method declared in interface" + "privateStuff" | "()V" | [PRIVATE_OR_HIGHER] | MethodBodyAdvice.B | [] | "match private method" + "privateStuff" | "()V" | [PROTECTED_OR_HIGHER] | MethodBodyAdvice.B2 | [MissingFlag] | "fail match private in supertype" + "staticMethod" | "()V" | [NON_STATIC] | MethodBodyAdvice.B | [MissingFlag] | "static method mismatch" + "missingMethod" | "()V" | [] | MethodBodyAdvice.B | [MissingMethod] | "missing method mismatch" + } + + def "field match #fieldTestDesc"() { + setup: + def reference = ClassRef.newBuilder(classToCheck.name) + .addField(new Source[0], fieldFlags as Flag[], fieldName, Type.getType(fieldType), false) + .build() + + when: + def mismatches = createMatcher([(reference.className): reference]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + getMismatchClassSet(mismatches) == expectedMismatches as Set + + where: + fieldName | fieldType | fieldFlags | classToCheck | expectedMismatches | fieldTestDesc + "missingField" | "Ljava/lang/String;" | [] | MethodBodyAdvice.A | [MissingField] | "mismatch missing field" + "privateField" | "Ljava/lang/String;" | [] | MethodBodyAdvice.A | [MissingField] | "mismatch field type signature" + "privateField" | "Ljava/lang/Object;" | [PRIVATE_OR_HIGHER] | MethodBodyAdvice.A | [] | "match private field" + "privateField" | "Ljava/lang/Object;" | [PROTECTED_OR_HIGHER] | MethodBodyAdvice.A2 | [MissingFlag] | "mismatch private field in supertype" + "protectedField" | "Ljava/lang/Object;" | [STATIC] | MethodBodyAdvice.A | [MissingFlag] | "mismatch static field" + "staticB" | Type.getType(MethodBodyAdvice.B).getDescriptor() | [STATIC, PROTECTED_OR_HIGHER] | MethodBodyAdvice.A | [] | "match static field" + "number" | "I" | [PACKAGE_OR_HIGHER] | MethodBodyAdvice.Primitives | [] | "match primitive int" + "flag" | "Z" | [PACKAGE_OR_HIGHER] | MethodBodyAdvice.Primitives | [] | "match primitive boolean" + } + + def "should not check abstract #desc helper classes"() { + given: + def reference = ClassRef.newBuilder(className) + .setSuperClassName(TestHelperClasses.HelperSuperClass.name) + .addFlag(ABSTRACT) + .addMethod(new Source[0], [ABSTRACT] as Flag[], "unimplemented", Type.VOID_TYPE) + .build() + + when: + def mismatches = createMatcher([(reference.className): reference], [reference.className]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + mismatches.empty + + where: + desc | className + "internal" | "io.opentelemetry.instrumentation.Helper" + "external" | "${TEST_EXTERNAL_INSTRUMENTATION_PACKAGE}.Helper" + } + + def "should not check #desc helper classes with no supertypes"() { + given: + def reference = ClassRef.newBuilder(className) + .setSuperClassName(Object.name) + .addMethod(new Source[0], [] as Flag[], "someMethod", Type.VOID_TYPE) + .build() + + when: + def mismatches = createMatcher([(reference.className): reference], [reference.className]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + mismatches.empty + + where: + desc | className + "internal" | "io.opentelemetry.instrumentation.Helper" + "external" | "${TEST_EXTERNAL_INSTRUMENTATION_PACKAGE}.Helper" + } + + def "should fail #desc helper classes that does not implement all abstract methods"() { + given: + def reference = ClassRef.newBuilder(className) + .setSuperClassName(TestHelperClasses.HelperSuperClass.name) + .addMethod(new Source[0], [] as Flag[], "someMethod", Type.VOID_TYPE) + .build() + + when: + def mismatches = createMatcher([(reference.className): reference], [reference.className]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + getMismatchClassSet(mismatches) == [MissingMethod] as Set + + where: + desc | className + "internal" | "io.opentelemetry.instrumentation.Helper" + "external" | "${TEST_EXTERNAL_INSTRUMENTATION_PACKAGE}.Helper" + } + + def "should fail #desc helper classes that do not implement all abstract methods - even if empty abstract class reference exists"() { + given: + def emptySuperClassRef = ClassRef.newBuilder(TestHelperClasses.HelperSuperClass.name) + .build() + def reference = ClassRef.newBuilder(className) + .setSuperClassName(TestHelperClasses.HelperSuperClass.name) + .addMethod(new Source[0], [] as Flag[], "someMethod", Type.VOID_TYPE) + .build() + + when: + def mismatches = createMatcher( + [(reference.className): reference, (emptySuperClassRef.className): emptySuperClassRef], + [reference.className, emptySuperClassRef.className]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + getMismatchClassSet(mismatches) == [MissingMethod] as Set + + where: + desc | className + "internal" | "io.opentelemetry.instrumentation.Helper" + "external" | "${TEST_EXTERNAL_INSTRUMENTATION_PACKAGE}.Helper" + } + + def "should check #desc helper class whether interface methods are implemented in the super class"() { + given: + def baseHelper = ClassRef.newBuilder("io.opentelemetry.instrumentation.BaseHelper") + .setSuperClassName(Object.name) + .addInterfaceName(TestHelperClasses.HelperInterface.name) + .addMethod(new Source[0], [] as Flag[], "foo", Type.VOID_TYPE) + .build() + // abstract HelperInterface#foo() is implemented by BaseHelper + def helper = ClassRef.newBuilder(className) + .setSuperClassName(baseHelper.className) + .addInterfaceName(TestHelperClasses.AnotherHelperInterface.name) + .addMethod(new Source[0], [] as Flag[], "bar", Type.VOID_TYPE) + .build() + + when: + def mismatches = createMatcher( + [(helper.className): helper, (baseHelper.className): baseHelper], + [helper.className, baseHelper.className]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + mismatches.empty + + where: + desc | className + "internal" | "io.opentelemetry.instrumentation.Helper" + "external" | "${TEST_EXTERNAL_INSTRUMENTATION_PACKAGE}.Helper" + } + + def "should check #desc helper class whether used fields are declared in the super class"() { + given: + def helper = ClassRef.newBuilder(className) + .setSuperClassName(LibraryBaseClass.name) + .addField(new Source[0], new Flag[0], "field", Type.getType("Ljava/lang/Integer;"), false) + .build() + + when: + def mismatches = createMatcher([(helper.className): helper], [helper.className]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + mismatches.empty + + where: + desc | className + "internal" | "io.opentelemetry.instrumentation.Helper" + "external" | "${TEST_EXTERNAL_INSTRUMENTATION_PACKAGE}.Helper" + } + + def "should fail helper class when it uses fields undeclared in the super class: #desc"() { + given: + def helper = ClassRef.newBuilder(className) + .setSuperClassName(LibraryBaseClass.name) + .addField(new Source[0], new Flag[0], fieldName, Type.getType(fieldType), false) + .build() + + when: + def mismatches = createMatcher([(helper.className): helper], [helper.className]) + .getMismatchedReferenceSources(this.class.classLoader) + + then: + getMismatchClassSet(mismatches) == [MissingField] as Set + + where: + desc | className | fieldName | fieldType + "internal helper, different field name" | "io.opentelemetry.instrumentation.Helper" | "differentField" | "Ljava/lang/Integer;" + "internal helper, different field type" | "io.opentelemetry.instrumentation.Helper" | "field" | "Lcom/external/DifferentType;" + "external helper, different field name" | "${TEST_EXTERNAL_INSTRUMENTATION_PACKAGE}.Helper" | "differentField" | "Ljava/lang/Integer;" + "external helper, different field type" | "${TEST_EXTERNAL_INSTRUMENTATION_PACKAGE}.Helper" | "field" | "Lcom/external/DifferentType;" + } + + private static ReferenceMatcher createMatcher(Map references = [:], + List helperClasses = []) { + new ReferenceMatcher(helperClasses, references, { it.startsWith(TEST_EXTERNAL_INSTRUMENTATION_PACKAGE) }) + } + + private static Set getMismatchClassSet(List mismatches) { + Set mismatchClasses = new HashSet<>(mismatches.size()) + for (Mismatch mismatch : mismatches) { + mismatchClasses.add(mismatch.class) + } + return mismatchClasses + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/external/LibraryBaseClass.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/external/LibraryBaseClass.java new file mode 100644 index 000000000..a3b858a84 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/external/LibraryBaseClass.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package external; + +public abstract class LibraryBaseClass { + protected Integer field; +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/external/NotInstrumentation.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/external/NotInstrumentation.java new file mode 100644 index 000000000..df5c51d29 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/external/NotInstrumentation.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package external; + +public class NotInstrumentation { + public void someLibraryCode() {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/external/instrumentation/ExternalHelper.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/external/instrumentation/ExternalHelper.java new file mode 100644 index 000000000..65e987e85 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/external/instrumentation/ExternalHelper.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package external.instrumentation; + +import external.NotInstrumentation; + +public class ExternalHelper { + public void instrument() { + new NotInstrumentation().someLibraryCode(); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/InstrumentationContextTestClasses.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/InstrumentationContextTestClasses.java new file mode 100644 index 000000000..1c6df827b --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/InstrumentationContextTestClasses.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; + +public class InstrumentationContextTestClasses { + public static class ValidAdvice { + public static void advice() { + Runnable.class.getName(); + InstrumentationContext.get(Key1.class, Context.class); + Key2.class.getName(); + Key1.class.getName(); + InstrumentationContext.get(Key2.class, Context.class); + } + } + + public static class NotUsingClassRefAdvice { + public static void advice(Class key, Class context) { + Key2.class.getName(); + Key1.class.getName(); + InstrumentationContext.get(key, context); + } + } + + public static class PassingVariableAdvice { + public static void advice() { + Class context = Context.class; + InstrumentationContext.get(Key1.class, context); + } + } + + public static class Key1 {} + + public static class Key2 {} +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/OtherTestHelperClasses.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/OtherTestHelperClasses.java new file mode 100644 index 000000000..a71afc8cc --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/OtherTestHelperClasses.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation; + +import muzzle.TestClasses; + +public class OtherTestHelperClasses { + public static class Foo implements TestClasses.MethodBodyAdvice.SomeInterface { + @Override + public void someMethod() {} + } + + public static class Bar { + public void doSomething() { + new Foo().someMethod(); + TestEnum.INSTANCE.getAnswer(); + } + } + + public enum TestEnum { + INSTANCE { + @Override + int getAnswer() { + return 42; + } + }; + + abstract int getAnswer(); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/TestHelperClasses.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/TestHelperClasses.java new file mode 100644 index 000000000..9946a1538 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/TestHelperClasses.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation; + +import java.util.ArrayList; +import java.util.List; + +public class TestHelperClasses { + public static class Helper extends HelperSuperClass implements HelperInterface { + + @Override + @SuppressWarnings("ModifiedButNotUsed") + public void foo() { + List list = new ArrayList<>(); + list.add(getStr()); + } + + @Override + protected int abstractMethod() { + return 54321; + } + + private static String getStr() { + return "abc"; + } + } + + public interface HelperInterface { + void foo(); + } + + public interface AnotherHelperInterface extends HelperInterface { + void bar(); + + @Override + int hashCode(); + + @Override + boolean equals(Object other); + + Object clone(); + + @SuppressWarnings("checkstyle:NoFinalizer") + void finalize(); + } + + public abstract static class HelperSuperClass { + protected abstract int abstractMethod(); + + public final String finalMethod() { + return "42"; + } + + static int bar() { + return 12345; + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/test/utils/PortAllocatorTest.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/test/utils/PortAllocatorTest.java new file mode 100644 index 000000000..8f1e8e547 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/test/utils/PortAllocatorTest.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.test.utils; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.Closeable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PortAllocatorTest { + + @Test + public void testSimple() { + PortAllocator portAllocator = getPortAllocator((port) -> true); + int next = PortAllocator.RANGE_MIN + 1; + for (int i = 0; i < 1000; i++) { + Assertions.assertEquals(next, portAllocator.getPort()); + next++; + if (next % PortAllocator.CHUNK_SIZE == 0) { + next++; + } + } + Assertions.assertEquals(next, portAllocator.getPorts(10)); + Assertions.assertEquals(12101, portAllocator.getPorts(PortAllocator.CHUNK_SIZE - 1)); + assertThatThrownBy(() -> portAllocator.getPorts(PortAllocator.CHUNK_SIZE + 1)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void testEven() { + PortAllocator portAllocator = getPortAllocator((port) -> port % 2 == 0); + int next = PortAllocator.RANGE_MIN + 2; + for (int i = 0; i < 1000; i++) { + Assertions.assertEquals(next, portAllocator.getPort()); + next += 2; + if (next % PortAllocator.CHUNK_SIZE == 0) { + next += 2; + } + } + assertThatThrownBy(() -> portAllocator.getPorts(2)).isInstanceOf(IllegalStateException.class); + } + + private static PortAllocator getPortAllocator(PortTest portTest) { + return new PortAllocator(new TestPortBinder(portTest)); + } + + private interface PortTest { + boolean test(int port); + } + + private static class TestPortBinder extends PortAllocator.PortBinder { + private final PortTest portTest; + + TestPortBinder(PortTest portTest) { + this.portTest = portTest; + } + + @Override + Closeable bind(int port) { + if (canBind(port)) { + return () -> {}; + } + return null; + } + + @Override + boolean canBind(int port) { + return portTest.test(port); + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtensionTest.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtensionTest.java new file mode 100644 index 000000000..3d85c2a5a --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtensionTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit; + +import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat; + +import io.opentelemetry.instrumentation.test.utils.TraceUtils; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LibraryInstrumentationExtensionTest { + @RegisterExtension + static final LibraryInstrumentationExtension instrumentation = + LibraryInstrumentationExtension.create(); + + // repeated test verifies that the telemetry data is cleared between test runs + @RepeatedTest(5) + void shouldCollectTraces() throws TimeoutException, InterruptedException { + // when + TraceUtils.runUnderTrace( + "parent", + () -> { + TraceUtils.runInternalSpan("child"); + return null; + }); + + // then + List> traces = instrumentation.waitForTraces(1); + assertThat(traces) + .hasSize(1) + .hasTracesSatisfyingExactly( + trace -> + trace + .hasSize(2) + .hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent"), + childSpan -> childSpan.hasName("child"))); + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/DeclaredFieldTestClass.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/DeclaredFieldTestClass.java new file mode 100644 index 000000000..3caa7edc3 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/DeclaredFieldTestClass.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package muzzle; + +public class DeclaredFieldTestClass { + public static class Advice { + public void instrument() { + new Helper().foo(); + } + } + + public static class Helper extends LibraryBaseClass { + private String helperField; + + public void foo() { + superField.toString(); + } + } + + public static class LibraryBaseClass { + protected Object superField; + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/HelperReferenceWrapperTestClasses.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/HelperReferenceWrapperTestClasses.java new file mode 100644 index 000000000..672fa2646 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/HelperReferenceWrapperTestClasses.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package muzzle; + +@SuppressWarnings({"UnusedMethod", "MethodCanBeStatic"}) +public class HelperReferenceWrapperTestClasses { + interface Interface1 { + void foo(); + } + + interface Interface2 { + void bar(); + } + + abstract static class AbstractClasspathType implements Interface1 { + private Object privateFieldsAreIgnored; + protected Object field; + + static void staticMethodsAreIgnored() {} + + private void privateMethodsToo() {} + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/MuzzleWeakReferenceTest.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/MuzzleWeakReferenceTest.java new file mode 100644 index 000000000..22cb74119 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/MuzzleWeakReferenceTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package muzzle; + +import io.opentelemetry.instrumentation.test.utils.GcUtils; +import io.opentelemetry.javaagent.tooling.muzzle.collector.ReferenceCollector; +import io.opentelemetry.javaagent.tooling.muzzle.matcher.ReferenceMatcher; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collections; + +public class MuzzleWeakReferenceTest { + + // Spock holds strong references to all local variables. For weak reference testing we must create + // our strong references away from Spock in this java class. + // Even returning a WeakReference is enough for spock to create a strong ref. + public static boolean classLoaderRefIsGarbageCollected() throws InterruptedException { + ClassLoader loader = new URLClassLoader(new URL[0], null); + WeakReference clRef = new WeakReference<>(loader); + ReferenceCollector collector = new ReferenceCollector(className -> false); + collector.collectReferencesFromAdvice(TestClasses.MethodBodyAdvice.class.getName()); + ReferenceMatcher refMatcher = + new ReferenceMatcher( + Collections.emptyList(), collector.getReferences(), className -> false); + refMatcher.getMismatchedReferenceSources(loader); + loader = null; + GcUtils.awaitGc(clRef); + return clRef.get() == null; + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/TestClasses.java b/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/TestClasses.java new file mode 100644 index 000000000..3f9756773 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/java/muzzle/TestClasses.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package muzzle; + +import external.instrumentation.ExternalHelper; +import io.opentelemetry.instrumentation.OtherTestHelperClasses; +import io.opentelemetry.instrumentation.TestHelperClasses.Helper; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +public class TestClasses { + + public static class MethodBodyAdvice { + @Advice.OnMethodEnter + public static void methodBodyAdvice() { + A a = new A(); + SomeInterface inter = new SomeImplementation(); + inter.someMethod(); + a.publicB.method("foo"); + a.publicB.methodWithPrimitives(false); + a.publicB.methodWithArrays(new String[0]); + B.staticMethod(); + A.staticB.method("bar"); + } + + public static class A { + public B publicB = new B(); + protected Object protectedField = null; + private final Object privateField = null; + public static B staticB = new B(); + } + + public static class B { + public String method(String s) { + return s; + } + + public void methodWithPrimitives(boolean b) {} + + public Object[] methodWithArrays(String[] s) { + return s; + } + + @SuppressWarnings({"UnusedMethod", "MethodCanBeStatic"}) + private void privateStuff() {} + + protected void protectedMethod() {} + + public static void staticMethod() {} + } + + public static class B2 extends B { + public void stuff() { + B b = new B(); + b.protectedMethod(); + } + } + + public static class A2 extends A {} + + public static class Primitives { + int number = 1; + boolean flag = false; + } + + public interface SomeInterface { + void someMethod(); + } + + public static class SomeImplementation implements SomeInterface { + @Override + public void someMethod() {} + } + + public static class SomeClassWithFields { + public int instanceField = 0; + public static int staticField = 0; + public final int finalField = 0; + } + + public interface AnotherInterface extends SomeInterface {} + } + + public static class LdcAdvice { + public static void ldcMethod() { + MethodBodyAdvice.A.class.getName(); + } + } + + public static class InstanceofAdvice { + public static boolean instanceofMethod(Object a) { + return a instanceof MethodBodyAdvice.A; + } + } + + public static class InvokeDynamicAdvice { + public static MethodBodyAdvice.SomeInterface invokeDynamicMethod( + MethodBodyAdvice.SomeImplementation a) { + Runnable staticMethod = MethodBodyAdvice.B::staticMethod; + return a::someMethod; + } + } + + public static class HelperAdvice { + public static void adviceMethod() { + Helper h = new Helper(); + } + } + + public static class HelperOtherAdvice { + public static void adviceMethod() { + new OtherTestHelperClasses.Bar().doSomething(); + } + } + + public static class ExternalInstrumentationAdvice { + public static void adviceMethod() { + new ExternalHelper().instrument(); + } + } +} diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/resources/META-INF/services/test.resource.file b/opentelemetry-java-instrumentation/testing-common/src/test/resources/META-INF/services/test.resource.file new file mode 100644 index 000000000..9461a51ff --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/resources/META-INF/services/test.resource.file @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.TestHelperClasses$Helper \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/global/handlers/request.handler2s b/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/global/handlers/request.handler2s new file mode 100644 index 000000000..9461a51ff --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/global/handlers/request.handler2s @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.TestHelperClasses$Helper \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/services/testservice/request.handler2s b/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/services/testservice/request.handler2s new file mode 100644 index 000000000..d01e311a0 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/services/testservice/request.handler2s @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.TestHelperClasses$Helper diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/services/testservice/testsubservice/request.handler2s b/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/services/testservice/testsubservice/request.handler2s new file mode 100644 index 000000000..9461a51ff --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/resources/com/amazonaws/services/testservice/testsubservice/request.handler2s @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.TestHelperClasses$Helper \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors b/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors new file mode 100644 index 000000000..9461a51ff --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.TestHelperClasses$Helper \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/services/testservice/execution.interceptors b/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/services/testservice/execution.interceptors new file mode 100644 index 000000000..9461a51ff --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/services/testservice/execution.interceptors @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.TestHelperClasses$Helper \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/services/testservice/testsubservice/execution.interceptors b/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/services/testservice/testsubservice/execution.interceptors new file mode 100644 index 000000000..9461a51ff --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/src/test/resources/software/amazon/awssdk/services/testservice/testsubservice/execution.interceptors @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.TestHelperClasses$Helper \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/testing-common/testing-common.gradle b/opentelemetry-java-instrumentation/testing-common/testing-common.gradle new file mode 100644 index 000000000..5878d76cd --- /dev/null +++ b/opentelemetry-java-instrumentation/testing-common/testing-common.gradle @@ -0,0 +1,53 @@ +description = 'OpenTelemetry Javaagent testing commons' +group = 'io.opentelemetry.javaagent' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +dependencies { + api "org.codehaus.groovy:groovy-all" + api "org.spockframework:spock-core" + implementation "org.junit.jupiter:junit-jupiter-api" + + api "run.mone:opentelemetry-api" + api "run.mone:opentelemetry-semconv" + api "run.mone:opentelemetry-sdk" + api "run.mone:opentelemetry-sdk-metrics" + api "run.mone:opentelemetry-sdk-testing" + + api project(path: ":testing:armeria-shaded-for-testing", configuration: "shadow") + + implementation("run.mone:opentelemetry-proto") { + // Only need the proto, not gRPC. + exclude group: 'io.grpc' + } + + implementation "com.google.guava:guava" + implementation "net.bytebuddy:byte-buddy" + implementation "net.bytebuddy:byte-buddy-agent" + implementation "org.slf4j:slf4j-api" + implementation "ch.qos.logback:logback-classic" + implementation "org.slf4j:log4j-over-slf4j" + implementation "org.slf4j:jcl-over-slf4j" + implementation "org.slf4j:jul-to-slf4j" + implementation "run.mone:opentelemetry-extension-annotations" + implementation "run.mone:opentelemetry-exporter-logging" + implementation project(':instrumentation-api') + + annotationProcessor "com.google.auto.service:auto-service" + compileOnly "com.google.auto.service:auto-service" + + testImplementation "org.assertj:assertj-core" + + testImplementation project(':javaagent-api') + testImplementation project(':javaagent-tooling') + testImplementation project(':javaagent-bootstrap') + testImplementation project(':javaagent-extension-api') + testImplementation project(':instrumentation:external-annotations:javaagent') + + // We have autoservices defined in test subtree, looks like we need this to be able to properly rebuild this + testAnnotationProcessor "com.google.auto.service:auto-service" + testCompileOnly "com.google.auto.service:auto-service" +} + +javadoc.enabled = false diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/agent-exporter.gradle b/opentelemetry-java-instrumentation/testing/agent-exporter/agent-exporter.gradle new file mode 100644 index 000000000..64ac37e0d --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/agent-exporter.gradle @@ -0,0 +1,42 @@ +plugins { + id "otel.shadow-conventions" +} + +apply plugin: "otel.java-conventions" + +dependencies { + compileOnly("net.bytebuddy:byte-buddy") + compileOnly("run.mone:opentelemetry-sdk") + compileOnly("run.mone:opentelemetry-sdk-extension-autoconfigure") + compileOnly("org.slf4j:slf4j-api") + + annotationProcessor "com.google.auto.service:auto-service" + compileOnly "com.google.auto.service:auto-service" + + implementation project(':instrumentation-api') + implementation project(':javaagent-extension-api') + implementation project(':javaagent-tooling') + implementation "run.mone:opentelemetry-proto" + implementation "run.mone:opentelemetry-exporter-otlp" + implementation "run.mone:opentelemetry-exporter-otlp-metrics" + implementation "io.grpc:grpc-testing:1.33.1" + + // Include instrumentations instrumenting core JDK classes tp ensure interoperability with other instrumentation + implementation project(':instrumentation:executors:javaagent') + // FIXME: we should enable this, but currently this fails tests for google http client + //testImplementation project(':instrumentation:http-url-connection:javaagent') + implementation project(':instrumentation:internal:internal-class-loader:javaagent') + implementation project(':instrumentation:internal:internal-eclipse-osgi-3.6:javaagent') + implementation project(':instrumentation:internal:internal-proxy:javaagent') + implementation project(':instrumentation:internal:internal-url-class-loader:javaagent') + + // Many tests use OpenTelemetry API calls, e.g., via TraceUtils.runUnderTrace + implementation project(':instrumentation:opentelemetry-annotations-1.0:javaagent') + // TODO (trask) is full OTel API interop needed, or is @WithSpan enough? + implementation project(':instrumentation:opentelemetry-api-1.0:javaagent') +} + +jar.enabled = false +shadowJar { + archiveFileName = 'testing-agent-classloader.jar' +} \ No newline at end of file diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/bytebuddy/TestAgentExtension.java b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/bytebuddy/TestAgentExtension.java new file mode 100644 index 000000000..8f2c0a590 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/bytebuddy/TestAgentExtension.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.bytebuddy; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.AgentExtension; +import net.bytebuddy.agent.builder.AgentBuilder; + +@AutoService(AgentExtension.class) +public class TestAgentExtension implements AgentExtension { + + @Override + public AgentBuilder extend(AgentBuilder agentBuilder) { + return agentBuilder.with(TestAgentListener.INSTANCE); + } + + @Override + public String extensionName() { + return "test"; + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/bytebuddy/TestAgentListener.java b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/bytebuddy/TestAgentListener.java new file mode 100644 index 000000000..4f2263eec --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/bytebuddy/TestAgentListener.java @@ -0,0 +1,138 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.bytebuddy; + +import io.opentelemetry.javaagent.tooling.ignore.AdditionalLibraryIgnoredTypesConfigurer; +import io.opentelemetry.javaagent.tooling.ignore.IgnoreAllow; +import io.opentelemetry.javaagent.tooling.ignore.IgnoredTypesBuilderImpl; +import io.opentelemetry.javaagent.tooling.ignore.trie.Trie; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestAgentListener implements AgentBuilder.Listener { + + private static final Logger logger = LoggerFactory.getLogger(TestAgentListener.class); + + private static final Trie ADDITIONAL_LIBRARIES_TRIE; + + static { + IgnoredTypesBuilderImpl builder = new IgnoredTypesBuilderImpl(); + new AdditionalLibraryIgnoredTypesConfigurer().configure(builder); + ADDITIONAL_LIBRARIES_TRIE = builder.buildIgnoredTypesTrie(); + } + + public static void reset() { + INSTANCE.transformedClassesNames.clear(); + INSTANCE.instrumentationErrorCount.set(0); + INSTANCE.skipTransformationConditions.clear(); + INSTANCE.skipErrorConditions.clear(); + } + + public static int getInstrumentationErrorCount() { + return INSTANCE.instrumentationErrorCount.get(); + } + + public static List getIgnoredButTransformedClassNames() { + List names = new ArrayList<>(); + for (String name : INSTANCE.transformedClassesNames) { + if (ADDITIONAL_LIBRARIES_TRIE.getOrNull(name) == IgnoreAllow.IGNORE) { + names.add(name); + } + } + return names; + } + + public static void addSkipTransformationCondition(Function condition) { + INSTANCE.skipTransformationConditions.add(condition); + } + + public static void addSkipErrorCondition(BiFunction condition) { + INSTANCE.skipErrorConditions.add(condition); + } + + static final TestAgentListener INSTANCE = new TestAgentListener(); + + private final Set transformedClassesNames = + Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final AtomicInteger instrumentationErrorCount = new AtomicInteger(0); + private final Set> skipTransformationConditions = + Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set> skipErrorConditions = + Collections.newSetFromMap(new ConcurrentHashMap<>()); + + @Override + public void onDiscovery( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { + for (Function skipCondition : skipTransformationConditions) { + if (skipCondition.apply(typeName)) { + throw new AbortTransformationException( + "Aborting transform for class name = " + typeName + ", loader = " + classLoader); + } + } + } + + @Override + public void onTransformation( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + DynamicType dynamicType) { + transformedClassesNames.add(typeDescription.getActualName()); + } + + @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) { + for (BiFunction condition : skipErrorConditions) { + if (condition.apply(typeName, throwable)) { + return; + } + } + if (!(throwable instanceof AbortTransformationException)) { + logger.error( + "Unexpected instrumentation error when instrumenting {} on {}", + typeName, + classLoader, + throwable); + instrumentationErrorCount.incrementAndGet(); + } + } + + @Override + public void onComplete( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {} + + /** Used to signal that a transformation was intentionally aborted and is not an error. */ + private static class AbortTransformationException extends RuntimeException { + public AbortTransformationException(String message) { + super(message); + } + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingExporterFactory.java b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingExporterFactory.java new file mode 100644 index 000000000..2b25518b6 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingExporterFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.exporter; + +import java.util.List; + +public class AgentTestingExporterFactory { + + static final OtlpInMemorySpanExporter spanExporter = new OtlpInMemorySpanExporter(); + static final OtlpInMemoryMetricExporter metricExporter = new OtlpInMemoryMetricExporter(); + + public static List getSpanExportRequests() { + return spanExporter.getCollectedExportRequests(); + } + + public static List getMetricExportRequests() { + return metricExporter.getCollectedExportRequests(); + } + + public static void reset() { + spanExporter.reset(); + metricExporter.reset(); + } + + public static boolean forceFlushCalled() { + return AgentTestingSdkCustomizer.spanProcessor.forceFlushCalled; + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingExporterPropertySource.java b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingExporterPropertySource.java new file mode 100644 index 000000000..c792481e0 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingExporterPropertySource.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.exporter; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.spi.config.PropertySource; +import java.util.HashMap; +import java.util.Map; + +@AutoService(PropertySource.class) +public class AgentTestingExporterPropertySource implements PropertySource { + @Override + public Map getProperties() { + Map properties = new HashMap<>(); + properties.put("otel.traces.exporter", "none"); + properties.put("otel.metrics.exporter", "none"); + return properties; + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingSdkCustomizer.java b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingSdkCustomizer.java new file mode 100644 index 000000000..2f782494f --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingSdkCustomizer.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.exporter; + +import com.google.auto.service.AutoService; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.Collections; + +@AutoService(SdkTracerProviderConfigurer.class) +public class AgentTestingSdkCustomizer implements SdkTracerProviderConfigurer { + + static final AgentTestingSpanProcessor spanProcessor = + new AgentTestingSpanProcessor( + SimpleSpanProcessor.create(AgentTestingExporterFactory.spanExporter)); + + static void reset() { + spanProcessor.forceFlushCalled = false; + } + + @Override + public void configure(SdkTracerProviderBuilder tracerProviderBuilder) { + tracerProviderBuilder.addSpanProcessor(spanProcessor); + + // Until metrics story settles down there is no SPI for it, we rely on the fact that metrics is + // already set up when tracing configuration begins. + IntervalMetricReader.builder() + .setExportIntervalMillis(100) + .setMetricExporter(AgentTestingExporterFactory.metricExporter) + .setMetricProducers(Collections.singleton((SdkMeterProvider) GlobalMeterProvider.get())) + .buildAndStart(); + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingSpanProcessor.java b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingSpanProcessor.java new file mode 100644 index 000000000..092fab36e --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingSpanProcessor.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.exporter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +public class AgentTestingSpanProcessor implements SpanProcessor { + + volatile boolean forceFlushCalled; + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + delegate.onStart(parentContext, span); + } + + @Override + public boolean isStartRequired() { + return delegate.isStartRequired(); + } + + @Override + public void onEnd(ReadableSpan span) { + delegate.onEnd(span); + } + + @Override + public boolean isEndRequired() { + return delegate.isEndRequired(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + + @Override + public CompletableResultCode forceFlush() { + forceFlushCalled = true; + return delegate.forceFlush(); + } + + private final SpanProcessor delegate; + + public AgentTestingSpanProcessor(SpanProcessor delegate) { + this.delegate = delegate; + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/OtlpInMemoryMetricExporter.java b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/OtlpInMemoryMetricExporter.java new file mode 100644 index 000000000..888020227 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/OtlpInMemoryMetricExporter.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.exporter; + +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class OtlpInMemoryMetricExporter implements MetricExporter { + + private static final Logger logger = LoggerFactory.getLogger(OtlpInMemoryMetricExporter.class); + + private final BlockingQueue collectedRequests = + new LinkedBlockingQueue<>(); + + List getCollectedExportRequests() { + return collectedRequests.stream() + .map(ExportMetricsServiceRequest::toByteArray) + .collect(Collectors.toList()); + } + + void reset() { + delegate.flush().join(1, TimeUnit.SECONDS); + collectedRequests.clear(); + } + + private final Server collector; + private final MetricExporter delegate; + + OtlpInMemoryMetricExporter() { + String serverName = InProcessServerBuilder.generateName(); + + collector = + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(new InMemoryOtlpCollector()) + .build(); + try { + collector.start(); + } catch (IOException e) { + throw new AssertionError("Could not start in-process collector.", e); + } + + delegate = + OtlpGrpcMetricExporter.builder() + .setChannel(InProcessChannelBuilder.forName(serverName).directExecutor().build()) + .build(); + } + + @Override + public CompletableResultCode export(Collection metrics) { + return delegate.export(metrics); + } + + @Override + public CompletableResultCode flush() { + return delegate.flush(); + } + + @Override + public CompletableResultCode shutdown() { + collector.shutdown(); + return delegate.shutdown(); + } + + private class InMemoryOtlpCollector extends MetricsServiceGrpc.MetricsServiceImplBase { + + @Override + public void export( + ExportMetricsServiceRequest request, + StreamObserver responseObserver) { + collectedRequests.add(request); + responseObserver.onNext(ExportMetricsServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/OtlpInMemorySpanExporter.java b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/OtlpInMemorySpanExporter.java new file mode 100644 index 000000000..c4a4be89c --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/OtlpInMemorySpanExporter.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing.exporter; + +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class OtlpInMemorySpanExporter implements SpanExporter { + + private static final Logger logger = LoggerFactory.getLogger(OtlpInMemorySpanExporter.class); + + private final BlockingQueue collectedRequests = + new LinkedBlockingQueue<>(); + + List getCollectedExportRequests() { + return collectedRequests.stream() + .map(ExportTraceServiceRequest::toByteArray) + .collect(Collectors.toList()); + } + + void reset() { + delegate.flush().join(1, TimeUnit.SECONDS); + collectedRequests.clear(); + } + + private final Server collector; + private final SpanExporter delegate; + + OtlpInMemorySpanExporter() { + String serverName = InProcessServerBuilder.generateName(); + + collector = + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(new InMemoryOtlpCollector()) + .build(); + try { + collector.start(); + } catch (IOException e) { + throw new AssertionError("Could not start in-process collector.", e); + } + + delegate = + OtlpGrpcSpanExporter.builder() + .setChannel(InProcessChannelBuilder.forName(serverName).directExecutor().build()) + .build(); + } + + @Override + public CompletableResultCode export(Collection spans) { + for (SpanData span : spans) { + logger.info("Exporting span {}", span); + } + return delegate.export(spans); + } + + @Override + public CompletableResultCode flush() { + return delegate.flush(); + } + + @Override + public CompletableResultCode shutdown() { + collector.shutdown(); + return delegate.shutdown(); + } + + private class InMemoryOtlpCollector extends TraceServiceGrpc.TraceServiceImplBase { + + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + collectedRequests.add(request); + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-for-testing/agent-for-testing.gradle b/opentelemetry-java-instrumentation/testing/agent-for-testing/agent-for-testing.gradle new file mode 100644 index 000000000..5a36cf0ef --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-for-testing/agent-for-testing.gradle @@ -0,0 +1,78 @@ +plugins { + id "otel.shadow-conventions" +} + +description = 'OpenTelemetry Javaagent for testing' +group = 'io.opentelemetry.javaagent' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +jar { + manifest { + attributes( + "Main-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Agent-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Premain-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Can-Redefine-Classes": true, + "Can-Retransform-Classes": true, + ) + } +} + +CopySpec isolateSpec(Collection shadowJarTasks) { + return copySpec { + from({ shadowJarTasks.collect { zipTree(it.archiveFile) } }) { + // important to keep prefix 'inst' short, as it is prefixed to lots of strings in runtime mem + into 'inst' + rename '(^.*)\\.class$', '$1.classdata' + // Rename LICENSE file since it clashes with license dir on non-case sensitive FSs (i.e. Mac) + rename '^LICENSE$', 'LICENSE.renamed' + } + } +} + +configurations { + shadowInclude { + canBeResolved = true + canBeConsumed = false + } +} + +evaluationDependsOn(":testing:agent-exporter") + +shadowJar { + configurations = [project.configurations.shadowInclude] + + archiveClassifier.set("") + + dependsOn ':testing:agent-exporter:shadowJar' + with isolateSpec([project(':testing:agent-exporter').tasks.shadowJar]) + + manifest { + inheritFrom project.tasks.jar.manifest + } +} + +jar { + enabled = false +} + +dependencies { + // Dependencies to include without obfuscation. + shadowInclude project(':javaagent-bootstrap') + + testImplementation project(':testing-common') + testImplementation "run.mone:opentelemetry-api" +} + +afterEvaluate { + tasks.withType(Test).configureEach { + inputs.file(shadowJar.archiveFile) + + jvmArgs "-Dotel.javaagent.debug=true" + jvmArgs "-javaagent:${shadowJar.archiveFile.get().asFile.absolutePath}" + + dependsOn shadowJar + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-for-testing/src/test/java/io/opentelemetry/javaagent/testing/AgentForTestingTest.java b/opentelemetry-java-instrumentation/testing/agent-for-testing/src/test/java/io/opentelemetry/javaagent/testing/AgentForTestingTest.java new file mode 100644 index 000000000..5b62062c2 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-for-testing/src/test/java/io/opentelemetry/javaagent/testing/AgentForTestingTest.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.javaagent.testing.common.AgentTestingExporterAccess; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AgentForTestingTest { + + @BeforeEach + void reset() { + AgentTestingExporterAccess.reset(); + } + + @Test + void empty() { + assertEquals(0, AgentTestingExporterAccess.getExportedSpans().size()); + } + + @Test + void exportAndRetrieve() { + GlobalOpenTelemetry.getTracer("test").spanBuilder("test").startSpan().end(); + + List spans = AgentTestingExporterAccess.getExportedSpans(); + assertEquals(1, spans.size()); + assertEquals("test", spans.get(0).getName()); + } +} diff --git a/opentelemetry-java-instrumentation/testing/agent-for-testing/src/test/java/io/opentelemetry/javaagent/testing/Demo2.java b/opentelemetry-java-instrumentation/testing/agent-for-testing/src/test/java/io/opentelemetry/javaagent/testing/Demo2.java new file mode 100644 index 000000000..4014b9df2 --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/agent-for-testing/src/test/java/io/opentelemetry/javaagent/testing/Demo2.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.testing; + +/** + * @author goodjava@qq.com + */ +public class Demo2 { + + @SuppressWarnings("SystemOut") + public static void main(String[] args) { + System.out.println("demo2"); + } + + +} diff --git a/opentelemetry-java-instrumentation/testing/armeria-shaded-for-testing/armeria-shaded-for-testing.gradle b/opentelemetry-java-instrumentation/testing/armeria-shaded-for-testing/armeria-shaded-for-testing.gradle new file mode 100644 index 000000000..ae5e9fa3a --- /dev/null +++ b/opentelemetry-java-instrumentation/testing/armeria-shaded-for-testing/armeria-shaded-for-testing.gradle @@ -0,0 +1,25 @@ +plugins { + id "com.github.johnrengelman.shadow" +} + +group = 'io.opentelemetry.javaagent' + +apply plugin: "otel.java-conventions" +apply plugin: "otel.publish-conventions" + +dependencies { + implementation "com.linecorp.armeria:armeria-junit5:1.8.0" +} + +shadowJar { + // Ensures tests are not affected by Armeria instrumentation + relocate "com.linecorp.armeria", "io.opentelemetry.testing.internal.armeria" + // Allows tests of Netty instrumentations which would otherwise conflict. + // The relocation must end with io.netty to allow Netty to detect shaded native libraries. + // https://github.com/netty/netty/blob/e69107ceaf247099ad9a198b8ef557bdff994a99/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java#L120 + relocate "io.netty", "io.opentelemetry.testing.internal.io.netty" + exclude "META-INF/maven/**" + relocate "META-INF/native/libnetty", "META-INF/native/libio_opentelemetry_testing_internal_netty" + relocate "META-INF/native/netty", "META-INF/native/io_opentelemetry_testing_internal_netty" + mergeServiceFiles() +} diff --git a/opentelemetry-java/.codecov.yaml b/opentelemetry-java/.codecov.yaml new file mode 100644 index 000000000..351d53572 --- /dev/null +++ b/opentelemetry-java/.codecov.yaml @@ -0,0 +1,24 @@ +codecov: + notify: + require_ci_to_pass: yes + strict_yaml_branch: main # only use the latest copy on main branch + +coverage: + precision: 2 + round: down + range: "80...100" + status: + project: + default: + enabled: yes + target: 90% + paths: + - api + - sdk + patch: + default: + enabled: yes + target: 90% + paths: + - api + - sdk diff --git a/opentelemetry-java/.editorconfig b/opentelemetry-java/.editorconfig new file mode 100644 index 000000000..2d83fd57f --- /dev/null +++ b/opentelemetry-java/.editorconfig @@ -0,0 +1,763 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = false +max_line_length = 100 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_wrap_on_typing = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = false +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = false +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = false +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = normal +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = true +ij_java_binary_operation_wrap = normal +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_at_first_column = true +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = normal +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = true +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = false +ij_java_doc_align_param_comments = false +ij_java_doc_do_not_wrap_if_one_line = true +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = true +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = false +ij_java_doc_keep_empty_return_tag = false +ij_java_doc_keep_empty_throws_tag = false +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = normal +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = normal +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = always +ij_java_imports_layout = $*, |, * +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 1 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_control_statement_in_one_line = false +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = true +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = true +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_at_first_column = true +ij_java_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = normal +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = normal +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_record_header = false +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_record_header = false +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = true +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = true +ij_java_ternary_operation_wrap = normal +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = normal +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[*.proto] +ij_continuation_indent_size = 2 +ij_proto_keep_indents_on_empty_lines = false + +[*.scala] +ij_continuation_indent_size = 2 +ij_scala_align_composite_pattern = true +ij_scala_align_extends_with = 0 +ij_scala_align_group_field_declarations = false +ij_scala_align_if_else = false +ij_scala_align_in_columns_case_branch = false +ij_scala_align_multiline_binary_operation = false +ij_scala_align_multiline_chained_methods = false +ij_scala_align_multiline_for = true +ij_scala_align_multiline_parameters = true +ij_scala_align_multiline_parameters_in_calls = false +ij_scala_align_multiline_parenthesized_expression = false +ij_scala_align_tuple_elements = false +ij_scala_align_types_in_multiline_declarations = false +ij_scala_alternate_continuation_indent_for_params = 4 +ij_scala_binary_operation_wrap = off +ij_scala_blank_lines_after_anonymous_class_header = 0 +ij_scala_blank_lines_after_class_header = 0 +ij_scala_blank_lines_after_imports = 1 +ij_scala_blank_lines_after_package = 1 +ij_scala_blank_lines_around_class = 1 +ij_scala_blank_lines_around_field = 0 +ij_scala_blank_lines_around_field_in_inner_scopes = 0 +ij_scala_blank_lines_around_field_in_interface = 0 +ij_scala_blank_lines_around_method = 1 +ij_scala_blank_lines_around_method_in_inner_scopes = 1 +ij_scala_blank_lines_around_method_in_interface = 1 +ij_scala_blank_lines_before_imports = 1 +ij_scala_blank_lines_before_method_body = 0 +ij_scala_blank_lines_before_package = 0 +ij_scala_block_brace_style = end_of_line +ij_scala_block_comment_at_first_column = true +ij_scala_call_parameters_new_line_after_lparen = 0 +ij_scala_call_parameters_right_paren_on_new_line = false +ij_scala_call_parameters_wrap = off +ij_scala_case_clause_brace_force = never +ij_scala_catch_on_new_line = false +ij_scala_class_annotation_wrap = split_into_lines +ij_scala_class_brace_style = end_of_line +ij_scala_closure_brace_force = never +ij_scala_do_not_align_block_expr_params = true +ij_scala_do_not_indent_case_clause_body = false +ij_scala_do_not_indent_tuples_close_brace = true +ij_scala_do_while_brace_force = never +ij_scala_else_on_new_line = false +ij_scala_enable_scaladoc_formatting = true +ij_scala_enforce_functional_syntax_for_unit = true +ij_scala_extends_keyword_wrap = off +ij_scala_extends_list_wrap = off +ij_scala_field_annotation_wrap = split_into_lines +ij_scala_finally_brace_force = never +ij_scala_finally_on_new_line = false +ij_scala_for_brace_force = never +ij_scala_for_statement_wrap = off +ij_scala_formatter = 0 +ij_scala_if_brace_force = never +ij_scala_implicit_value_class_suffix = Ops +ij_scala_indent_braced_function_args = true +ij_scala_indent_case_from_switch = true +ij_scala_indent_first_parameter = true +ij_scala_indent_first_parameter_clause = false +ij_scala_indent_type_arguments = true +ij_scala_indent_type_parameters = true +ij_scala_insert_whitespaces_in_simple_one_line_method = true +ij_scala_keep_blank_lines_before_right_brace = 2 +ij_scala_keep_blank_lines_in_code = 2 +ij_scala_keep_blank_lines_in_declarations = 2 +ij_scala_keep_comments_on_same_line = true +ij_scala_keep_first_column_comment = false +ij_scala_keep_indents_on_empty_lines = false +ij_scala_keep_line_breaks = true +ij_scala_keep_one_line_lambdas_in_arg_list = false +ij_scala_keep_simple_blocks_in_one_line = false +ij_scala_keep_simple_methods_in_one_line = false +ij_scala_keep_xml_formatting = false +ij_scala_line_comment_at_first_column = true +ij_scala_method_annotation_wrap = split_into_lines +ij_scala_method_brace_force = never +ij_scala_method_brace_style = end_of_line +ij_scala_method_call_chain_wrap = off +ij_scala_method_parameters_new_line_after_left_paren = false +ij_scala_method_parameters_right_paren_on_new_line = false +ij_scala_method_parameters_wrap = off +ij_scala_modifier_list_wrap = false +ij_scala_multiline_string_align_dangling_closing_quotes = false +ij_scala_multiline_string_closing_quotes_on_new_line = false +ij_scala_multiline_string_insert_margin_on_enter = true +ij_scala_multiline_string_margin_char = | +ij_scala_multiline_string_margin_indent = 2 +ij_scala_multiline_string_opening_quotes_on_new_line = true +ij_scala_multiline_string_process_margin_on_copy_paste = true +ij_scala_newline_after_annotations = false +ij_scala_not_continuation_indent_for_params = false +ij_scala_parameter_annotation_wrap = off +ij_scala_parentheses_expression_new_line_after_left_paren = false +ij_scala_parentheses_expression_right_paren_on_new_line = false +ij_scala_place_closure_parameters_on_new_line = false +ij_scala_place_self_type_on_new_line = true +ij_scala_prefer_parameters_wrap = false +ij_scala_preserve_space_after_method_declaration_name = false +ij_scala_reformat_on_compile = false +ij_scala_replace_case_arrow_with_unicode_char = false +ij_scala_replace_for_generator_arrow_with_unicode_char = false +ij_scala_replace_lambda_with_greek_letter = false +ij_scala_replace_map_arrow_with_unicode_char = false +ij_scala_scalafmt_reformat_on_files_save = false +ij_scala_scalafmt_show_invalid_code_warnings = true +ij_scala_scalafmt_use_intellij_formatter_for_range_format = true +ij_scala_sd_align_exception_comments = true +ij_scala_sd_align_other_tags_comments = true +ij_scala_sd_align_parameters_comments = true +ij_scala_sd_align_return_comments = true +ij_scala_sd_blank_line_after_parameters_comments = false +ij_scala_sd_blank_line_after_return_comments = false +ij_scala_sd_blank_line_before_parameters = false +ij_scala_sd_blank_line_before_tags = true +ij_scala_sd_blank_line_between_parameters = false +ij_scala_sd_keep_blank_lines_between_tags = false +ij_scala_sd_preserve_spaces_in_tags = false +ij_scala_space_after_comma = true +ij_scala_space_after_for_semicolon = true +ij_scala_space_after_modifiers_constructor = false +ij_scala_space_after_type_colon = true +ij_scala_space_before_brace_method_call = true +ij_scala_space_before_class_left_brace = true +ij_scala_space_before_infix_like_method_parentheses = false +ij_scala_space_before_infix_method_call_parentheses = false +ij_scala_space_before_infix_operator_like_method_call_parentheses = true +ij_scala_space_before_method_call_parentheses = false +ij_scala_space_before_method_left_brace = true +ij_scala_space_before_method_parentheses = false +ij_scala_space_before_type_colon = false +ij_scala_space_before_type_parameter_in_def_list = false +ij_scala_space_before_type_parameter_leading_context_bound_colon = false +ij_scala_space_before_type_parameter_leading_context_bound_colon_hk = true +ij_scala_space_before_type_parameter_list = false +ij_scala_space_before_type_parameter_rest_context_bound_colons = true +ij_scala_space_inside_closure_braces = true +ij_scala_space_inside_self_type_braces = true +ij_scala_space_within_empty_method_call_parentheses = false +ij_scala_spaces_around_at_in_patterns = false +ij_scala_spaces_in_imports = false +ij_scala_spaces_in_one_line_blocks = false +ij_scala_spaces_within_brackets = false +ij_scala_spaces_within_for_parentheses = false +ij_scala_spaces_within_if_parentheses = false +ij_scala_spaces_within_method_call_parentheses = false +ij_scala_spaces_within_method_parentheses = false +ij_scala_spaces_within_parentheses = false +ij_scala_spaces_within_while_parentheses = false +ij_scala_special_else_if_treatment = true +ij_scala_trailing_comma_arg_list_enabled = true +ij_scala_trailing_comma_import_selector_enabled = false +ij_scala_trailing_comma_mode = trailing_comma_keep +ij_scala_trailing_comma_params_enabled = true +ij_scala_trailing_comma_pattern_arg_list_enabled = false +ij_scala_trailing_comma_tuple_enabled = false +ij_scala_trailing_comma_tuple_type_enabled = false +ij_scala_trailing_comma_type_params_enabled = false +ij_scala_try_brace_force = never +ij_scala_type_annotation_exclude_constant = true +ij_scala_type_annotation_exclude_in_dialect_sources = true +ij_scala_type_annotation_exclude_in_test_sources = false +ij_scala_type_annotation_exclude_member_of_anonymous_class = false +ij_scala_type_annotation_exclude_member_of_private_class = false +ij_scala_type_annotation_exclude_when_type_is_stable = true +ij_scala_type_annotation_function_parameter = false +ij_scala_type_annotation_implicit_modifier = true +ij_scala_type_annotation_local_definition = false +ij_scala_type_annotation_private_member = false +ij_scala_type_annotation_protected_member = true +ij_scala_type_annotation_public_member = true +ij_scala_type_annotation_structural_type = true +ij_scala_type_annotation_underscore_parameter = false +ij_scala_type_annotation_unit_type = true +ij_scala_use_alternate_continuation_indent_for_params = false +ij_scala_use_scaladoc2_formatting = false +ij_scala_variable_annotation_wrap = off +ij_scala_while_brace_force = never +ij_scala_while_on_new_line = false +ij_scala_wrap_before_with_keyword = false +ij_scala_wrap_first_method_in_call_chain = false +ij_scala_wrap_long_lines = false + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant, *.bpmn, *.fxml, *.jhm, *.jnlp, *.jrxml, *.plan, *.pom, *.rng, *.tld, *.wadl, *.wsdd, *.wsdl, *.xjb, *.xml, *.xsd, *.xsl, *.xslt, *.xul}] +ij_continuation_indent_size = 2 +ij_xml_align_attributes = false +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal +ij_xml_use_custom_settings = true + +[{*.bash, *.sh, *.zsh}] +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false + +[{*.gant, *.gradle, *.groovy, *.gson, *.gy}] +ij_continuation_indent_size = 2 +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_list_or_map = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = true +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_class_count_to_use_import_on_demand = 999 +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_if_brace_force = never +ij_groovy_import_annotation_wrap = 2 +ij_groovy_indent_case_from_switch = true +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_names_count_to_use_import_on_demand = 999 +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_assert_separator = true +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = true +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_long_lines = false + +[{*.kt, *.kts}] +ij_continuation_indent_size = 2 +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_assignment_wrap = off +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_right_paren_on_new_line = false +ij_kotlin_call_parameters_wrap = off +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = true +ij_kotlin_continuation_indent_for_expression_bodies = true +ij_kotlin_continuation_indent_in_argument_lists = true +ij_kotlin_continuation_indent_in_elvis = true +ij_kotlin_continuation_indent_in_if_conditions = true +ij_kotlin_continuation_indent_in_parameter_lists = true +ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = off +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = false +ij_kotlin_import_nested_classes = false +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = off +ij_kotlin_method_parameters_new_line_after_left_paren = false +ij_kotlin_method_parameters_right_paren_on_new_line = false +ij_kotlin_method_parameters_wrap = off +ij_kotlin_name_count_to_use_star_import = 999 +ij_kotlin_name_count_to_use_star_import_for_members = 999 +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.har, *.jsb2, *.jsb3, *.json, .babelrc, .eslintrc, .stylelintrc, bowerrc, jest.config}] +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.properties, spring.handlers, spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.yaml, *.yml}] +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true diff --git a/opentelemetry-java/.gitignore b/opentelemetry-java/.gitignore new file mode 100644 index 000000000..69c5f8b7c --- /dev/null +++ b/opentelemetry-java/.gitignore @@ -0,0 +1,35 @@ +# Gradle +build +.gradle +local.properties +out/ + +# Maven (proto) +target + +# IntelliJ IDEA +.idea +*.iml + +# Eclipse +.classpath +.project +.settings +bin + +# NetBeans +/.nb-gradle +/.nb-gradle-properties + +# VS Code +.vscode + +# OS X +.DS_Store + +# Emacs +*~ +\#*\# + +# Vim +.swp diff --git a/opentelemetry-java/CHANGELOG.md b/opentelemetry-java/CHANGELOG.md new file mode 100644 index 000000000..cf00ef2fe --- /dev/null +++ b/opentelemetry-java/CHANGELOG.md @@ -0,0 +1,1091 @@ +# Changelog + +## Unreleased: + +### API +#### Enhancements +- Parsing of the W3C Baggage header has been optimized. + +### SDK +#### Behavioral Changes +- The implementation of SpanBuilder will no longer throw exceptions when null parameters are passed in. Instead, +it will treat these calls as no-ops. + +#### Enhancements +- Memory usage of the Tracing SDK has been greatly reduced when exporting via the OTLP or Jaeger exporters. +- The OTLP protobuf version has been updated to v0.9.0 + +### Extensions +- A new experimental extension module has been added to provide a truly no-op implementation of the API. This +is published under the `io.opentelemetry.extension.noopapi` name. +- The `io.opentelemetry.sdk.autoconfigure` module now supports the `OTEL_SERVICE_NAME`/`otel.service.name` +environment variable/system property for configuring the SDK's `Resource` implementation. + +--- + +## Version 1.2.0 - 2021-05-07 + +### General + +#### Enhancements +- The `"Implementation-Version"` attribute has been added to the jar manifests for all published jar artifacts. + +### API + +#### Enhancements +- A new method has been added to the Span and the SpanBuilder to enable adding a set of Attributes in one call, rather than +having to iterate over the contents and add them individually. See `Span.setAllAttributes(Attributes)` and `SpanBuilder.setAllAttributes(Attributes)` + +#### Behavioral Changes +- Previously, an AttributeKey with a null underlying key would preserve the null. Now, this will be converted to an empty String. + +### SDK + +#### Enhancements +- The `IdGenerator.random()` method will now attempt to detect if it is being used in an Android environment, and use +a more Android-friendly `IdGenerator` instance in that case. This will affect any usage of the SDK that does not +explicitly specify a custom `IdGenerator` instance when running on Android. + +#### Behavioral Changes +- The name used for Tracer instances that do not have a name has been changed to be an empty String, rather than the +previously used `"unknown"` value. This change is based on a specification clarification. + +### Propagators + +#### Bugfixes +- The B3 Propagator injectors now only include the relevant fields for the specific injection format. + +#### Behavioral Changes +- The `W3CBaggagePropagator` will no longer explicitly populate an empty `Baggage` instance into the context when +the header is unparsable. It will now return the provided Context instance unaltered, as is required by the specification. +- The `AwsXrayPropagator` will no longer explicitly populate an invalid `Span` instance into the context when +the headers are unparsable. It will now return the provided Context instance unaltered, as is required by the specification. + +### Exporters +- The `jaeger-thrift` exporter has had its dependency on the `jaeger-client` library updated to version `1.6.0`. +- The `zipkin` exporter now has an option to specific a custom timeout. +- The `zipkin`, `jaeger` and `jaeger-thrift` exporters will now report the `otel.dropped_attributes_count` and `otel.dropped_events_count` +tags if the numbers are greater than zero. + +### Semantic Conventions (alpha) + +#### Breaking Changes +- The SemanticAttributes and ResourceAttributes have both been updated to match the OpenTelemetry Specification v1.3.0 release, which +includes several breaking changes. +- Values that were previously defined as `enum`s are now defined as static `public static final ` constants of the appropriate type. + +### OpenTracing Shim (alpha) + +#### Enhancements +- Error logging support in the shim is now implemented according to the v1.2.0 specification. + +### SDK Extensions +- A new `HostResource` Resource and the corresponding `ResourceProvider` has been added. +It will populate the `host.name` and `host.arch` Resource Attributes. +- A new `ExecutorServiceSpanProcessor` has been added to the `opentelemetry-sdk-extension-tracing-incubator` module. This implementation +of a batch SpanProcessor allows you to provide your own ExecutorService to do the background export work. +- The `autoconfigure` module now supports providing the timeout setting for the Jaeger GRPC exporter via +a system property (`otel.exporter.jaeger.timeout`) or environment variable (`OTEL_EXPORTER_JAEGER_TIMEOUT`). +- The `autoconfigure` module now supports providing the timeout setting for the Zipkin exporter via +a system property (`otel.exporter.zipkin.timeout`) or environment variable (`OTEL_EXPORTER_ZIPKIN_TIMEOUT`). +- The `autoconfigure` module now exposes the `EnvironmentResource` class to provide programmatic access to a `Resource` +built from parsing the `otel.resource.attributes` configuration property. + +### Metrics (alpha) + +#### Breaking Changes +- The deprecated `SdkMeterProvider.registerView()` method has been removed. The ViewRegistry is now immutable and cannot +be changed once the `SdkMeterProvider` has been built. + +#### Bugfixes +- OTLP summaries now have the proper percentile value of `1.0` to represent the maximum; previously it was wrongly set to `100.0`. + +#### Enhancements +- There is now full support for delta-aggregations with the `LongSumAggregator` and `DoubleSumAggregator`. +See `AggregatorFactory.sum(AggregationTemporality)`. The previous `AggregatorFactory.sum(boolean)` has been +deprecated and will be removed in the next release. + +--- + +## Version 1.1.0 - 2021-04-07 + +### API + +#### Bugfixes + +- We now use our own internal `@GuardedBy` annotation for errorprone so there won't be an accidental +transitive dependency on a 3rd-party jar. +- The `TraceStateBuilder` now will not crash when an empty value is provided. + +#### Enhancements + +- The `Context` class now provides methods to wrap `java.util.concurrent.Executor` and `java.util.concurrent.ExecutorService` +instances to do context propagation using the current context. See `io.opentelemetry.context.Context.taskWrapping(...)` for +more details. + +### OpenTracing Shim (alpha) + +- The shim now supports methods that take a timestamp as a parameter. +- You can now specify both the `TEXT_MAP` and the `HTTP_HEADER` type propagators for the shim. +See `io.opentelemetry.opentracingshim.OpenTracingPropagators` for details. + +### Extensions + +- The AWS X-Ray propagator is now able to extract 64-bit trace ids. + +### SDK + +#### Bugfixes + +- The `CompletableResultCode.join(long timeout, TimeUnit unit)` method will no longer `fail` the result +when the timeout happens. Nor will `whenComplete` actions be executed in that case. +- The `SimpleSpanProcessor` now keeps track of pending export calls and will wait for them to complete +via a CompletableResultCode when `forceFlush()` is called. Similiarly, this is also done on `shutdown()`. +- The Jaeger Thrift exporter now correctly populates the parent span id into the exporter span. + +#### Enhancements + +- The SpanBuilder provided by the SDK will now ignore `Link` entries that are reference an invalid SpanContext. +This is an update from the OpenTelemetry Specification v1.1.0 release. +- The OTLP Exporters will now log more helpful messages when the collector is unavailable or misconfigured. +- The internals of the `BatchSpanProcessor` have had some optimization done on them, to reduce CPU +usage under load. +- The `Resource` class now has `builder()` and `toBuilder()` methods and a corresponding `ResourceBuilder` class +has been introduced for more fluent creation and modification of `Resource` instances. +- The standard exporters will now throttle error logging when export errors are too frequent. If more than 5 +error messages are logged in a single minute by an exporter, logging will be throttled down to only a single +log message per minute. + +### SDK Extensions + +#### Bugfixes + +- Removed a stacktrace on startup when using the `autoconfigure` module without a metrics SDK on the classpath. + +#### Enhancements + +- The `autoconfigure` module now supports `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` and `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` +settings, in addition to the combined `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. Corresponding +system properties are also supported (`-Dotel.exporter.otlp.metrics.endpoint` and `-Dotel.exporter.otlp.traces.endpoint`). +- An `SdkMeterProviderConfigurer` SPI is now available in the `autoconfigure` module. + +### Semantic Conventions (alpha) + +- The SemanticAttributes and ResourceAttributes have both been updated to match the OpenTelemetry Specification v1.1.0 release. +This includes a breaking changes to the constants defined in the `ResourceAttributes` class: +`ResourceAttributes.CLOUD_ZONE` has been replaced with `ResourceAttributes.CLOUD_AVAILABILITY_ZONE`. + +### Metrics (alpha) + +#### Breaking Changes + +- The `ViewRegistry` now lets you register `View` objects, rather than `AggregatorFactory` instances. +- `GlobalMetricsProvider` has been renamed to `GlobalMeterProvider`. +- `View` registration has been moved to the `SdkMeterProviderBuilder` and the methods on the `SdkMeterProvider` +to add views have been deprecated. They will be removed in the next release. + +#### Enhancements + +- A new option for aggregation as Histograms is now available. + +--- +## Version 1.0.1 - 2021-03-11 + +### Bugfixes + +- AWS resource extensions have been fixed to not throw NullPointerException in actual AWS environment + +--- +## Version 1.0.0 - 2021-02-26 + +### General + +This releases marks the first stable release for the tracing, baggage and context APIs and the SDK. +Please see the [Versioning](VERSIONING.md) document for stability guarantees. + +The best source of lsit of the now stable packages can be found in the +[opentelemetry-bom](https://repo1.maven.org/maven2/io/opentelemetry/opentelemetry-bom/1.0.0/opentelemetry-bom-1.0.0.pom) +artifact in maven central. + +Javadoc is available at javadoc.io. +For example, [javadoc.io](https://javadoc.io/doc/io.opentelemetry/opentelemetry-api/1.0.0/index.html) for +the API module. + +#### Changes + +- The `opentelemetry-proto` module is now versioned as an `alpha` module, as it contains non-stable +metrics and logs signals. It has hence been removed from the main BOM. +- The `opentelemetry-sdk-extension-otproto` module has been removed. The classes in it have been moved +to a new `opentelemetry-exporter-otlp-common` module but have been repackaged into an unsupported, +internal package. + +### Metrics (alpha) + +#### Breaking Changes + +- `PrometheusCollector.Builder` inner class has been moved to the top level as `PrometheusCollectorBuilder`. + +--- +## Version 0.17.1 - 2021-02-19 + +- Removed the unused `ResourceProvider` interface from the SDK. This interface is still available +in the `opentelemetry-sdk-extension-autoconfigure` module, where it is actually used. + +## Version 0.17.0 - 2021-02-17 - RC#3 + +### General + +Note: In an effort to accelerate our work toward a 1.0.0 release, we have skipped the deprecation phase +on a number of breaking changes. We apologize for the inconvenience this may have caused. We are very +aware that these changes will impact users. If you need assistance in migrating from previous releases, +please open a [discussion topic](https://github.com/opentelemetry/opentelemetry-java/discussions) at +[https://github.com/opentelemetry/opentelemetry-java/discussions](https://github.com/opentelemetry/opentelemetry-java/discussions). + +Many classes have been made final that previously were not. Please reach out if you have a need to +provide extended functionality, and we can figure out how best to solve your use-case. + +### API + +#### Breaking Changes + +- `TraceStateBuilder.set(String, String)` has been renamed to `TraceStateBuilder.put(String, String)`. +- `BaggageBuilder.setParent()` and `BaggageBuilder.setNoParent()` have been removed from the Baggage APIs. +In addition, Baggage will no longer be implicitly generated from Baggage that is in the current context. You now must explicitly +get the `Baggage` instance from the `Context` and call `toBuilder()` on it in order to get the entries pre-populated in your builder. +- `TextMapPropagator.Setter` and `TextMapPropagator.Getter` have been moved to the top level and renamed to +`TextMapSetter` and `TextMapGetter` respectively. +- `OpenTelemetry.getDefault()` has been renamed to `OpenTelemetry.noop()`. +- `OpenTelemetry.getPropagating()` has been renamed to `OpenTelemetry.propagating()`. +- `TracerProvider.getDefault()` has been renamed to `TracerProvider.noop()` +- `Tracer.getDefault()` has been removed. +- `TraceId.getTraceIdRandomPart(CharSequence)` has been removed. +- The `B3Propagator.getInstance()` has been renamed to `B3Propagator.injectingSingleHeader()`. +- The `B3Propagator.builder()` method has been removed. As a replacement, you can use `B3Propagator.injectingMultiHeaders()` directly. + +### SDK + +#### Breaking Changes + +- The SPI for configuring Resource auto-populators has been removed from the SDK and moved to the `opentelemetry-sdk-extension-autoconfigure` module. +This means that `Resource.getDefault()` will no longer be populated via SPI, but only include the bare minimum values from the SDK itself. +In order to get the auto-configured Resource attributes, you will need to use the `opentelemetry-sdk-extension-autoconfigure` module directly. +- `InstrumentationLibraryInfo.getEmpty()` has been renamed to `InstrumentationLibraryInfo.empty()`. +- `Resource.getEmpty()` has been renamed to `Resource.empty()`. +- When specifying the endpoints for grpc-based exporters, you now are required to specify the protocol. Hence, you must include +the `http://` or `https://` in front of your endpoint. +- The option on `SpanLimits` to truncate String-valued Span attributes has been removed (this is still pending in the specification). +- The `InMemoryMetricsExporter` has been removed from the `opentelemetry-sdk-testing` module. + +#### Miscellaneous + +- The default values for SpanLimits have been changed to 128, from 1000, to match the spec. + +### Extensions + +#### Breaking Changes + +- In the `opentelemetry-sdk-extension-autoconfigure` module, we have changed the system property used to exclude some Resource auto-populators to be +`otel.java.disabled.resource-providers` instead of `otel.java.disabled.resource_providers`. +- In the `opentelemetry-sdk-extension-autoconfigure` module, you now specify the `OtTracePropagator` with the `"ottrace"` option, rather than `"ottracer"`. +- In the `opentelemetry-sdk-extension-autoconfigure` module, the default exporters are now set to be `"otlp"`, as required by the 1.0.0 specification. +- In the `opentelemetry-sdk-extension-autoconfigure` module, the default propagators are now set to be `"tracecontext,baggage"`, as required by the 1.0.0 specification. +- The `CommonProperties` class has been removed from the `opentelemetry-sdk-extension-otproto` module. + +### Metrics (alpha) + +#### API + +- `Meter.getDefault()` has been removed. +- `MeterProvider.getDefault()` has been renamed to `MeterProvider.noop()`. + +--- +## Version 0.16.0 - 2021-02-08 - RC#2 + +### General + +Note: In an effort to accelerate our work toward a 1.0.0 release, we have skipped the deprecation phase +on a number of breaking changes. We apologize for the inconvenience this may have caused. We are very +aware that these changes will impact users. If you need assistance in migrating from previous releases, +please open a [discussion topic](https://github.com/opentelemetry/opentelemetry-java/discussions) at +[https://github.com/opentelemetry/opentelemetry-java/discussions](https://github.com/opentelemetry/opentelemetry-java/discussions). + +#### Breaking Changes + +- Methods and classes deprecated in 0.15.0 have been removed. + +### API + +#### Breaking Changes + +- The `Span.Kind` enum has been moved to the top level, and named `SpanKind`. +- `DefaultOpenTelemetry` is no longer a public class. If you need the functionality previously provided by this +implementation, it can be accessed via new static methods on the `OpenTelemetry` interface itself. +- The `TraceFlags` interface has been re-introduced. This is now used, rather than a bare `byte` wherever +trace flags is used. In particular, `SpanContext.create()`, `SpanContext.createFromRemoteParent()` now require +a `TraceFlags` instance, and `SpanContext.getTraceFlags()` returns a `TraceFlags` instance. +- The names of static methods on `TraceFlags` have been normalized to match other similar classes, and now +return `TraceFlags` instead of `byte` where appropriate. +- The `Labels` interface and related classes have been moved into the alpha metrics modules and repackaged. +- `TraceId.copyHexInto(byte[] traceId, char[] dest, int destOffset)` has been removed. +- `SpanContext.getTraceIdAsHexString()` has been renamed to `SpanContext.getTraceId()` +- `SpanContext.getSpanIdAsHexString()` has been renamed to `SpanContext.getSpanId()` +- `BaggageEntry.getEntryMetadata()` has been renamed to `BaggageEntry.getMetadata()` +- `BaggageConsumer` has been removed in favor of a standard `java.util.function.BiConsumer` +- `TraceFlags.isSampledFromHex(CharSequence src, int srcOffset)` has been removed. +- `SpanId` and `TraceId` methods that had a `String` parameter now accept `CharSequence` +and assume the id starts at the beginning. +- `SpanId.getSize()` and `TraceId.getSize()` have been removed. +- `SpanId.bytesFromHex()` has been removed. +- `SpanId.asLong(CharSequence)` has been removed. +- `SpanId.asBytes(CharSequence)` has been removed. +- `SpanId.getHexLength()` has been renamed to `SpanId.getLength()` +- `SpanId.bytesToHex()` has been renamed to `SpanId.fromBytes()` +- `TraceId.bytesFromHex()` has been removed. +- `TraceId.traceIdLowBytesAsLong(CharSequence)` has been removed. +- `TraceId.traceIdHighBytesAsLong(CharSequence)` has been removed. +- `TraceId.asBytes(CharSequence)` has been removed. +- `TraceId.getHexLength()` has been renamed to `TraceId.getLength()` +- `TraceId.bytesToHex()` has been renamed to `TraceId.fromBytes()` +- `StrictContextStorage` has been made private. Use -Dio.opentelemetry.context.enableStrictContext=true` to enable it +- `AwsXrayPropagator` has been moved to the `opentelemetry-extension-aws` artifact + +#### Enhancements + +- The `W3CTraceContextPropagator` class now directly implements the `TextMapPropagator` interface. +- The `OpenTelemetry` interface now has a `getDefault()` method which will return a completely no-op implementation. +- The `OpenTelmmetry` interface now has a `getPropagating(ContextPropagators propagators)` method which will +return an implementation that contains propagators, but is otherwise no-op. + +#### Misc Notes + +- The internal `StringUtils` class has had metrics-related methods removed from it. But, you weren't using +internal classes, were you? +- The internal `AbstractWeakConcurrentMap` class has been made non-public. See the line above about internal classes. + +### Extensions + +#### Breaking Changes + +- The `OtTracerPropagator` has been renamed to `OtTracePropagator` in the trace-propagators extension module. + +### SDK + +#### Breaking Changes + +- `TraceConfig` has been renamed to `SpanLimits` and relocated to the `io.opentelemetry.sdk.tracing` package. +All related method names have been renamed to match. +- `SpanData.getTraceState()` has been removed. The TraceState is still available via the SpanContext accessor. +- `SpanData.isSampled()` has been removed. The isSampled property is still available via the SpanContext accessor. + +#### Enhancements + +- `SpanData` now directly exposes the underlying `SpanContext` instance. + +### SDK Extensions + +#### Breaking Changes + +- In the `opentelemetry-autoconfigure` module, three environment variables/system properties +have been renamed to match the spec: + * `OTEL_TRACE_EXPORTER`/`otel.trace.exporter` has been replaced with `OTEL_TRACES_EXPORTER`/`otel.traces.exporter` + * `OTEL_TRACE_SAMPLER`/`otel.trace.sampler` has been replaced with `OTEL_TRACES_SAMPLER`/`otel_traces_sampler` + * `OTEL_TRACE_SAMPLER_ARG`/`otel.trace.sampler.arg` has been replaced with `OTEL_TRACES_SAMPLER_ARG`/`otel.traces.sampler.arg` + +#### Enhancements + +- The `opentelemetry-autoconfigure` module now supports using non-millisecond values for duration & +interval configuration options. See the javadoc on the `io.opentelemetry.sdk.autoconfigure.ConfigProperties.getDuration(String)` +method for details on supported formats. +- The `opentelemetry-autoconfigure` module now provides automatic SPI-based parsing of the `OTEL_RESOURCE_ATTRIBUTES` env var +(and the corresponding `otel.resource.attributes` system property). If you include this module on your +classpath, it will automatically update the `Resource.getDefault()` instance with that configuration. + +### Metrics (alpha) + +#### API + +- The `Labels` interface has been moved into the metrics API module and repackaged into the +`io.opentelemetry.api.metrics.common` package. + +--- +## Version 0.15.0 - 2021-01-29 - RC#1 + +### General + +#### Breaking Changes + +- Methods and classes deprecated in 0.14.x have been removed. + +### Semantic Conventions + +The `opentelemetry-semconv` module has been marked as `-alpha` and removed from the bom. This was done because the OpenTelemetry +project has not decided on a specification for stability of semantic conventions or the specific telemetry produced by +instrumentation. + +#### Deprecations + +- The items in the `io.opentelemetry.semconv.trace.attributes.SemanticAttributes` which were previously +generated form the Resource semantic conventions have been deprecated. Please use the ones in the new +`io.opentelemetry.semconv.resource.attributes.ResourceAttributes` class. + +#### Enhancements + +- A new `io.opentelemetry.semconv.resource.attributes.ResourceAttributes` has been introduced to hold the +generated semantic attributes to be used in creating `Resource`s. + +### SDK + +#### Breaking Changes + +- `SamplingResult.Decision` has been removed in favor of the `io.opentelemetry.sdk.trace.samplers.SamplingDecision` top-level class. +- `Resource.merge(Resource)` now will resolve conflicts by preferring the `Resource` passed in, rather than the original. +- The default Resource (accessible via `Resource.getDefault()`) now includes a fallback `service.name` attribute. The implication +of this is that exporters that have configured fallback service names will only use them if the SDK is intentionally +configured with a Resource that does not utilize the default Resource for its underlying Resource data. +- The `Sampler` is now specified when building the SdkTracerProvider directly, rather than being a part of the TraceConfig. + +#### Bugfixes + +- The Jaeger exporters will now properly populate the process service name from the Resource service.name attribute. + +#### Deprecations + +- Going forward, OTLP exporter endpoint specifications must include a scheme, either `http://` or `https://`. +We will support endpoints without schemes until the next release, at which point not providing a scheme will generate +an error when trying to use them. This applies to the use of system properties, environment variables, or programmatic +specifications of the endpoints. +- The `exportOnlySampled` configuration of the `BatchSpanProcessor` has been deprecated and will be removed in the next +release. +- The `io.opentelemetry.sdk.resources.ResourceAttributes` has been deprecated and will be removed in the next release. +Please use the new `io.opentelemetry.semconv.resource.attributes.ResourceAttributes` class in the `opentelemetry-semconv` +module. +- The serviceName configuration option for the Jaeger and Zipkin exporters has been deprecated. In the next release, those +configuration options will be removed, and the fallback `service.name` will always be pulled from the default Resource. + +#### Enhancements + +- `Resource.getDefault()` now includes a fallback `service.name` attribute. Exporters that require a `service.name` +should acquire the fallback from the default resource, rather than having it configured in. + +### SDK Extensions + +#### Breaking Changes + +- The `otel.bsp.schedule.delay.millis` env var/system property configuration option for the batch span processor has been renamed to +`otel.bsp.schedule.delay` to match the specification. +- The `otel.bsp.export.timeout.millis` env var/system property configuration option for the batch span processor has been renamed to +`otel.bsp.export.timeout` to match the specification. + +#### Enhancements + +- The `opentelemetry-sdk-extension-autoconfigure` module will now additionally register the auto-configured +SDK as the instance of `GlobalOpenTelemetry` when used. +- The `opentelemetry-sdk-extension-autoconfigure` module now supports the `otel.exporter.otlp.certificate` configuration +property for specifying a path to a trusted certificate for the OTLP exporters. + +--- +## Version 0.14.1 - 2021-01-14 + +### General + +- Several more modules have been updated to have `-alpha` appended on their versions: + - `opentelemetry-sdk-extension-jfr-events` + - `opentelemetry-sdk-extension-async-processor` + - `opentelemetry-sdk-extension-logging` + - `opentelemetry-sdk-extension-zpages` + - `opentelemetry-sdk-exporter-prometheus` + - `opentelemetry-sdk-exporter-tracing-incubator` + - `opentelemetry-opentracing-shim` + - `opentelemetry-opencensus-shim` + +### API + +#### Breaking Changes + +- Code that was deprecated in `0.13.0` has been removed from the project. + - Metrics interfaces are no longer available as a part of the `opentelemetry-pom` or from the `opentelemetry-api` modules. + To access the alpha metrics APIs, you will need to explicitly add them as a dependency. + - `OpenTelemetry.setPropagators()` has been removed. You should instead create your + `OpenTelemetry` implementations with the Propagators preset, via the various builder options. For example, use + `DefaultOpenTelemetry.builder().setPropagators(propagators).build()` to configure your no-sdk implementation. + - The `OpenTelemetry.builder()` and the `OpenTelemetryBuilder` interface have been removed. + The builder functionality is now only present on individual implementations of OpenTelemetry. For instance, the + `DefaultOpenTelemetry` class has a builder available. + +#### Deprecations + +- The SemanticAttributes class has been moved to a new module: `opentelemetry-semconv` and repackaged into a new package: +`io.opentelemetry.semconv.trace.attributes`. The old `SemanticAttributes` class will be removed in the next release. +- The SPI interfaces for OpenTelemetry have been deprecated. We are moving to a new auto-configuration approach with the +new SDK auto-configuration module: `io.opentelemetry.sdk.autoconfigure`. This module should be considered the officially +supported auto-configuration library moving forward. + +#### Enhancements + +- The SemanticAttributes have been updated to the latest version of the specification, as of January 7th, 2021. + +### SDK + +#### Bugfixes + +- Environment variables/system properties that are used to set extra headers for the OTLP exporters have been fixed to now +split on commas, rather than semicolons. This has been brought in line with the specification for these environment +variables. This includes `otel.exporter.otlp.span.headers`, `otel.exporter.otlp.metric.headers`, and `otel.exporter.otlp.headers`. +- Passing a null span name when creating a span will no longer cause a NullPointerException. Instead, a default span name will be +provided in place of the missing name. + +#### Breaking Changes + +- The deprecated `SpanData.Link.getContext()` method has been removed in favor of `SpanData.Link.getSpanContext()`. +- The `TracerProviderFactorySdk` SPI class has been renamed to `SdkTracerProviderFactory`. +- The `OpenTelemetrySdkBuilder.build()` method has been renamed to `OpenTelemetrySdkBuilder.buildAndRegisterGlobal()`. +The `build()` method still exists, but no longer sets the instance on the `GlobalOpenTelemetry` when invoked. +- The `SdkTracerManagement.shutdown()` method now returns `CompletableResultCode` which can be used to wait +asynchronously for shutdown to complete. +- The `sampling.probability` sampling attribute previously generated by the `TraceIdRatioBasedSampler` is no longer +generated, as it was not conformant with the specifications. +- The `SpanData` inner classes have been moved to the top level, so `SpanData.Link` -> `LinkData`, `SpanData.Event` -> `EventData` +and `SpanData.Status` -> `StatusData`. + +#### Deprecations + +- `SdkTracerProvider.updateActiveTraceConfig()` and `SdkTracerProvider.addSpanProcessor()` have been deprecated. The methods +will be removed in the next release. +- All existing auto-configuration mechanisms have been deprecated in favor of using the new `io.opentelemetry.sdk.autoconfigure` +module. The existing ones will be removed in the next release. +- The methods with the term "deadline" has been deprecated in the configuration of the grpc-based exporters (OTLP and Jaeger) in favor + of the word "timeout". The deadline-named methods will be removed in the next release. +- The `StringUtils` class in the `opentelemetry-extension-trace-propagators` extension module has been deprecated +and will be made non-public in the next release. +- The `StatusData.isUnset()` and `StatusData.isOk()` methods have been deprecated. They will be removed in the next release. + +#### Enhancements + +- The `OtlpGrpcSpanExporter` now supports setting trusted TLS certificates for secure communication with the collector. +- A new module for supporting auto-configuration of the SDK has been added. The new module, `io.opentelemetry.sdk.autoconfigure` will +be the new path for auto-configuration of the SDK, including via SPI, environment variables and system properties. +- The `TraceConfig` class now exposes a `builder()` method directly, so you don't need to get the default then call `toBuilder()` on it. +- The OTLP protobuf definitions were updated to the latest released version: `0.7.0`. +Both the `Span` and (alpha) `Metric` exporters were updated to match. +- Timeouts in the exporters can now be specified with `java.util.concurrent.TimeUnit` and `java.time.Duration` based configurations, +rather than requiring milliseconds. + +### SDK Extensions + +#### Breaking Changes + +- The ZPages extension now exposes its SpanProcessor implementation. To use it, you will need to add it to your +SDK implementation directly, rather than it adding itself to the global SDK instance. +- The JaegerRemoteSampler builder patterns have been changed and updated to more closely match the rest +of the builders in the project. + +#### Deprecations +- The `AwsXrayIdGenerator` constructor has been deprecated in favor of using a simple `getInstance()` singleton, since +it has no state. +- The `TraceProtoUtils` class in the `opentelemetry-sdk-extension-otproto` module has been deprecated and +will be removed in the next release. + +#### Bugfixes + +- The JaegerRemoteSampler now uses the ParentBased sampler as the basis for any sampling that is done. + +### Metrics (alpha) + +#### SDK: + +- The `InstrumentSelector.newBuilder()` method has been renamed to `InstrumentSelector.builder()` and +the methods on the Builder have changed to use the same naming patterns as the rest of the project. +- The `MeterProviderFactorySdk` class has been renamed to `SdkMeterProviderFactory`. +- The `SdkMeterProvicer.Builder` has been moved to the top level `SdkMeterProviderBuilder`. +- The `InstrumentSelector` now requires an instrument type to be provided, and defaults the name regex to `.*`. + +--- + +## Version 0.13.0 - 2020-12-17 + +### General + +- Starting with 0.13.0, all unstable modules (the 2 metrics modules for now) will have a `-alpha` appended to their + base version numbers to make it clear they are not production ready, and will not be when we get to releasing 1.0. + See our [Rationale](docs/rationale.md) document for details. + +### API + +#### Breaking Changes + +- The `Labels.ArrayBackedLabelsBuilder` class has been made non-public. +You can still access the `LabelsBuilder` functionality via the `Labels.builder()` method. +- Methods deprecated in the 0.12.0 release have been removed or made non-public: + - The `HttpTraceContext` class has been removed. + - The `toBuilder()` method on the OpenTelemetry interface has been removed. + - The `Attributes.builder(Attributes)` method has been removed in favor of `Attributes.toBuilder(Attributes)`. + - The `DefaultContextPropagators` class has made non-public. + - The `TraceMultiPropagator` builder has been removed in favor of a simple factory method. + - The `value()` method on the `StatusCode` enum has been removed. + - The Baggage `EntryMetadata` class has been removed in favor of the `BaggageEntryMetadata` interface. + - The `setCallback()` method on the asynchronous metric instruments has been removed. +- Several public classes have been made `final`. + +#### Enhancements + +- An `asMap` method has been added to the `Labels` interface, to expose them as a `java.util.Map`. +- You can now enable strict Context verification via a system property (`-Dio.opentelemetry.context.enableStrictContext=true`) + Enabling this mode will make sure that all `Scope`s that are created are closed, and generate log messages if they + are not closed before being garbage collected. This mode of operation is CPU intensive, so be careful before + enabling it in high-throughput environments that do not need this strict verification. See the javadoc on the +`io.opentelemetry.context.Context` interface for details. +- Several of the methods on the `Span` interface have been given default implementations. +- The Semantic Attributes constants have been updated to the version in the yaml spec as of Dec 14, 2020. + +#### Miscellaneous + +- The Metrics API has been deprecated in the `opentelemetry-api` module, in preparation for releasing a fully-stable 1.0 + version of that module. The Metrics API will be removed from the module in the next release. +- The API has been broken into separate modules, in preparation for the 1.0 release of the tracing API. + If you depend on the `opentelemetry-api` module, you should get the rest of the API modules as transitive dependencies. +- The `OpenTelemetry.builder()` and the `OpenTelemetryBuilder` interface have been deprecated and will be removed in the next release. + The builder functionality is now only present on individual implementations of OpenTelemetry. For instance, the + `DefaultOpenTelemetry` class has a builder available. +- The `OpenTelemetry.setPropagators()` has been deprecated and will be removed in the next release. You should instead create your + `OpenTelemetry` implementations with the Propagators preset, via the various builder options. For example, use + `DefaultOpenTelemetry.builder().setPropagators(propagators).build()` to configure your no-sdk implementation. + +### SDK + +#### Miscellaneous + +- The `SpanData.Link.getContext()` method has been deprecated in favor of a new `SpanData.Link.getSpanContext()`. + The deprecated method will be removed in the next release of the SDK. +- The internals of the (alpha) Metrics SDK have been significantly updated. +- OTLP adapter classes have been moved into the `opentelemetry-sdk-extension-otproto` module so they can be shared across OTLP usages. +- The zipkin exporter has been updated to have its error code handling match the spec. +- The logging exporter's format has changed to something slightly more human-readable. + +#### Breaking Changes + +- Many SDK classes have been renamed to be prefixed with `Sdk` rather than having `Sdk` being embedded in the middle of the name. + For example, `TracerSdk` has been renamed to `SdkTracer` and `TracerSdkManagement` has been renamed to `SdkTracerManagement`. +- The `ResourcesConfig.builder()` method has been made non-public. +- The `TraceConfig.Builder` class has been moved to the top-level `TraceConfigBuilder` class. +- The built-in exporter `Builder` classes have been moved to the top level, rather than inner classes. Access to the builders + is still available via `builder()` methods on the exporter classes. +- The built-in SpanProcessor `Builder` classes have been moved to the top level, rather than inner classes. Access to the builders + is still available via `builder()` methods on the SpanProcessor implementation classes. +- The built-in ParentBasedSampler `Builder` class has been moved to the top level, rather than inner classes. Access to the builder + is still available via methods on the Sampler interface. +- The `DaemonThreadFactory` class has been moved to an internal module and should not be used outside of this repository. +- The builder class for the `OpenTelemetrySdk` class has been slimmed down. The configurable details have been moved into + the specific provider builders, where they apply more specifically and obviously. +- Many public classes have been made `final`. +- The MetricExporter interface's `shutdown()` method now returns `CompletableResultCode` rather than void. +- The `OpenTelemetrySdk`'s builder class has been moved to the top level, rather than being an inner class. It has been renamed to + `OpenTelemetrySdkBuilder` as a part of that change. +- The OTLP exporters have been split into two separate modules, and the metrics exporter has been tagged with the `-alpha` version. + If you continue to depend on the `opentelemetry-exporters-otlp` module, you will only get the trace exporter as a transitive dependency. + +### Extensions + +#### Bugfixes + +- The `opentelemetry-extension-annotations` module now includes the api module as an `api` dependency, rather than just `implementation`. + +#### Breaking Changes + +- The deprecated `opentelemetry-extension-runtime-metrics` module has been removed. The functionality is available in the + opentelemetry-java-instrumentation project under a different module name. +- The deprecated `trace-utils` module has been removed. +- Several public classes have been made `final`. + +#### Enhancements + +- Some common OTLP adapter utilities have been moved into the `opentelemetry-sdk-extension-otproto` module so they can + be shared across OTLP exporters. + +--- +## Version 0.12.0 - 2020-12-04 + +### API + +#### Bugfixes + +- Usages of tracers and meters on all `OpenTelemetry` instances were being delegated to the global Meter and Tracer. +This has been corrected, and all instances should have independent Tracer and Meter instances. + +#### Breaking Changes + +- The `AttributesBuilder` no long accepts null values for array-valued attributes with numeric or boolean types. +- The `TextMapPropagator.fields()` method now returns a `Collection` rather than a `List`. +- `Labels` has been converted to an interface, from an abstract class. Its API has otherwise remained the same. +- `TraceState` has been converted to an interface, from an abstract class. Its API has otherwise remained the same. +- `Attributes` has been converted to an interface, from an abstract class. Its API has otherwise remained the same. +- The `ReadableAttributes` interface has been removed, as it was redundant with the `Attributes` interface. All APIs that +used or returned `ReadableAttributes` should accept or return standard `Attributes` implementations. +- `SpanContext` has been converted to an interface, from an abstract class. Its API has otherwise remained the same. +- The functional `AttributeConsumer` interface has been removed and replaced with a standard `java.util.function.BiConsumer`. +- The signature of the `BaggageBuilder.put(String, String, EntryMetadata entryMetadata)` +method has been changed to `put(String, String, BaggageEntryMetadata)` + +#### Enhancements + +- A `builder()` method has been added to the OpenTelemetry interface to facilitate constructing implementations. +- An `asMap()` method has been added to the `Attributes` interface to enable conversion to a standard `java.util.Map`. +- An `asMap()` method has been added to the `Baggage` interface to enable conversion to a standard `java.util.Map`. +- An `asMap()` method has been added to the `TraceState` interface to enable conversion to a standard `java.util.Map`. +- The Semantic Attributes constants have been updated to the version in the yaml spec as of Dec 1, 2020. + +#### Miscellaneous + +- The `HttpTraceContext` class has been deprecated in favor of `W3CTraceContextPropagator`. `HttpTraceContext` will be removed in 0.13.0. +- The `toBuilder()` method on the OpenTelemetry interface has been deprecated and will be removed in 0.13.0. +- The `DefaultContextPropagators` class has been deprecated. Access to it will be removed in 0.13.0. +- The `TraceMultiPropagator` builder has been deprecated in favor of a simple factory method. The builder will be removed in 0.13.0. +You can access the same functionality via static methods on the `ContextPropagators` interface. +- The `setCallback()` method on the asynchronous metric instruments has been deprecated and will be removed in 0.13.0. +Instead, use the `setCallback()` method on the builder for the instruments. +- The `value()` method on the `StatusCode` enum has been deprecated and will be removed in 0.13.0. +- The Baggage `EntryMetadata` class has been deprecated in favor of the `BaggageEntryMetadata` interface. The class will be made non-public in 0.13.0. + +### Extensions + +- The `opentelemetry-extension-runtime-metrics` module has been deprecated. The functionality is available in the +opentelemetry-java-instrumentation project under a different module name. The module here will be removed in 0.13.0. +- The `trace-utils` module has been deprecated. If you need this module, please let us know! The module will be removed in 0.13.0. + +### SDK + +#### Breaking Changes + +- The `opentelemetry-sdk-tracing` module has been renamed to `opentelemetry-sdk-trace`. +- The default port the OTLP exporters use has been changed to `4317`. +- The deprecated `SpanData.getCanonicalCode()` method has been removed, along with the implementations. + +#### Enhancements + +- The OpenTelemetrySdk builder now supports the addition of `SpanProcessor`s to the resulting SDK. +- The OpenTelemetrySdk builder now supports the assignment of an `IdGenerator` to the resulting SDK. +- The `ReadableSpan` interface now exposes the `Span.Kind` of the span. +- The SDK no longer depends on the guava library. +- The parent SpanContext is now exposed on the `SpanData` interface. + +#### Miscellaneous + +- The `toBuilder()` method on the OpenTelemetrySdk class has been deprecated and will be removed in 0.13.0. +- The MultiSpanProcessor and MultiSpanExporter have been deprecated. You can access the same functionality via +the `SpanProcessor.composite` and `SpanExporter.composite` methods. The classes will be made non-public in 0.13.0. +- The `SpanData.hasRemoteParent()` method has been deprecated and will be removed in 0.13.0. If you need this information, +you can now call `SpanData.getParentSpanContext().isRemote()`. +- The default timeouts for the 2 OTLP exporters and the Jaeger exporter have been changed to 10s from 1s. + +### Extensions + +#### Breaking Changes + +- The `opentelemetry-sdk-extension-aws-v1-support` module has been renamed to `opentelemetry-sdk-extension-aws` +and the classes in it have been repackaged into the `io.opentelemetry.sdk.extension.aws.*` packages. + +#### Bugfixes: + +- The OpenTracing `TracerShim` now properly handles keys for context extraction in a case-insensitive manner. + +#### Enhancements + +- The `opentelemetry-sdk-extension-resources` now includes resource attributes for the process runtime via the `ProcessRuntimeResource` class. +This is included in the Resource SPI implementation that the module provides. +- The `opentelemetry-sdk-extension-aws` extension now will auto-detect AWS Lambda resource attributes. + +--- +## Version 0.11.0 - 2020-11-18 + +### API + +#### Breaking changes: + +- The SPI interfaces have moved to a package (not a module) separate from the API packages, and now live in `io.opentelemetry.spi.*` package namespace. +- Builder classes have been moved to the top level, rather than being inner classes. +For example, rather than `io.opentelemetry.api.trace.Span.Builder`, the builder is now in its own top-level class: `io.opentelemetry.api.trace.SpanBuilder`. +Methods to create the builders remain in the same place as they were before. +- SpanBuilder.setStartTimestamp, Span.end, and Span.addEvent methods which accept a timestamp now accept a timestamp with a TimeUnit instead of requiring a nanos timestamp. + +#### Enhancements: + +- Versions of SpanBuilder.setStartTimestamp, Span.end, and Span.addEvent added which accept Instant timestamps +- Setting the value of the `io.opentelemetry.context.contextStorageProvider` System property to `default` will enforce that +the default (thread local) ContextStorage will be used for the Context implementation, regardless of what SPI implementations are +available. + +#### Miscellaneous: + +- Invalid W3C `TraceState` entries will now be silently dropped, rather than causing the invalidation of the entire `TraceState`. + +### SDK + +#### Breaking Changes: + +- The builder class for the `OpenTelemetrySdk` now strictly requires its components to be SDK implementations. +You can only build an `OpenTelemetrySdk` with `TracerSdkProvider` and `MeterSdkProvider` instances. + +#### Enhancements: + +- An API has been added to the SDK's MeterProvider implementation (`MeterSdkProvider`) that allows the end-user to configure +how various metrics will be aggregated. This API should be considered a precursor to a full "Views" API, and will most likely +evolve over the coming months before the metrics implementation is complete. See the javadoc for `MeterSdkProvider.registerView()` for details. + +#### Miscellaneous: + +- The `SpanProcessor` interface now includes default method implementations for the `shutdown()` and `forceFlush()` methods. +- The BatchRecorder implementation has been updated to actually batch the recordings, rather than simply passing them through. + +### Extensions + +#### Breaking Changes: + +- The `@WithSpan` annotation has been moved to the `io.opentelemetry.extension.annotations` package in the `opentelemetry-extension-annotations` module + +#### Bugfixes: + +- The memory pool metrics provided by the MemoryPools class in the `opentelemetry-extension-runtime-metrics` module +have been fixed to properly report the committed memory values. + +#### Enhancements: + +- A new module has been added to assist with propagating the OTel context in kotlin co-routines. +See the `opentelemetry-extension-kotlin` module for details. + +--- +## Version 0.10.0 - 2020-11-06 + +### API + +#### Enhancements + +- The W3C Baggage Propagator is now available. +- The B3 Propagator now handles both single and multi-header formats. +- The B3 Propagator defaults to injecting the single B3 header, rather than the multi-header format. +- Mutating a method on `Span` now returns the `Span` to enable call-chaining. + +#### Bug fixes + +- The `package-info` file was removed from the `io.otel.context` package because it made the project incompatible with JPMS. + +#### Breaking changes + +- There have been many updates to the semantic conventions constants. The constants are now auto-generated from the YAML specification files, so the names will now be consistent across languages. For more information, see the [YAML Model for Semantic Conventions](https://github.com/open-telemetry/opentelemetry-specification/tree/master/semantic_conventions). +- All API classes have been moved into the `io.opentelemetry.api.` prefix to support JPMS users. +- The API no longer uses the `grpc-context` as the context implementation. It now uses `io.opentelemetry.context.Context`. This is published in the `opentelemetry-context` artifact. Interactions with the context were mostly moved to static methods in the `Span` and `Baggage` interfaces. +- The Baggage API has been reworked to more closely match the specification. This includes the removal of the `BaggageManager`. Baggage is fully functional within the API, without needing to install an SDK. +- `TracingContextUtils` and `BaggageUtils` were removed from the public API. Instead, use the appropriate static methods on the `Span` and `Baggage` classes, or use methods on the `Context` itself. +- The context propagation APIs have moved into the new `opentelemetry-context` context module. +- `DefaultSpan` was removed from the public API. Instead, use `Span.wrap(spanContext)` if you need a non-functional span that propagates the trace context. +- `DefaultMeter`, `DefaultMeterProvider`, `DefaultTracer` and `DefaultTracerProvider` were removed from the public API. You can access the same functionality with `getDefault()` methods on the `Meter`, `MeterProvider, `Tracer`, and `TracerProvider` classes, respectively. +- Some functionality from the `Tracer` interface is now available either on the `Span` interface or `Context` interface. +- The `OpenTelemetry` class is now an interface, with implementations. Methods on this interface have changed their names to reflect this change. For more information, see [OpenTelemetry.java](/api/src/main/java/io/opentelemetry/api/OpenTelemetry.java). +- All builder-creation methods have been renamed to `.builder()`. +- `StatusCanonicalCode` has been renamed to `StatusCode`. +- `Span.getContext()` has been renamed to `Span.getSpanContext()`. +- `AttributesBuilder` now uses `put` instead of `add` as the method name for adding attributes. +- All parameters are now marked as non-nullable by default. +- `TextMapPropagators` could receive a null carrier passed to the extract method. +- The `TextMapPropagator.Getter` interface has added a method to return the keys that the propagator uses. + +### SDK + +#### Enhancements + +- A new `MetricData` gauge metric type is now available. +- A new `opentelemetry-sdk-testing` module with a JUnit 5 extension to assist with testing is now available. +- The Prometheus metric exporter now consumes `gauge` metrics. +- The Jaeger gRPC exporter now maps `Resource` entries to process tags. +- The OTLP protobuf definitions were updated to the latest released version: 0.6.0. Both the `Span` and `Metric` exporters were updated to match. +- The `Sampler` interface now allows a `Sampler` implementation to update the `TraceState` that is applied to the `SpanContext` for the resulting span. + +#### Breaking changes + +- `TraceConfig` configuration option names (environment variables and system properties) were renamed to match the OpenTelemetery Specification. For more information, see [TraceConfig](./QUICKSTART.md#TraceConfig). +- The Jaeger gRPC exporter was updated to match the OpenTelemetry Specification. The `message` log entry attribute has been renamed to `event` and a new `dropped attributes count` attribute was added. For more information, see the [Overview](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/overview.md). +- The `SpanData.getHasRemoteParent()` and `SpanData.getHasEnded()` methods were renamed to `hasRemoteParent()` and `hasEnded()`, respectively. +- The `IdsGenerator` interface has been renamed to `IdGenerator`, and all implementations and relevant factory methods were similarly renamed. +- The `RandomIdGenerator` is now accessible via a factory method on the `IdGenerator` class, rather than being exposed itself. Use `IdGenerator.random()` to acquire an instance. +- The OTLP metric exporter now emits `gauge` metrics where appropriate. +- `ValueObserver` instruments now generate gauge metrics, rather than non-monotonic counter-style metrics. +- `ValueObserver` instruments now use the `LastValue` aggregation instead of `MinMaxSumCount`. +- The `SpanData.*` implementation classes were removed from the public SDK, but the interfaces are still available. +- `SpanProcessor.onStart` now takes a `Context` as its first parameter. +- The `Sampler` interface now takes a parent `Context` rather than a `SpanContext`. +- Each `Sampler` has been reorganized into their own classes and moved into the `io.opentelemetry.sdk.trace.samplers` package. Factory methods that used to be on the `Samplers` class were moved to the `Sampler` interface. + +### Extensions + +#### Enhancements + +- A new JUnit5 extension was added for writing tests. For more information, see [OpenTelemetryExtension.java](sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtension.java). +- A Jaeger `SpanExporter` which exports via the `thrift-over-http protocol` is now available. +- A Jaeger Propagator is now available. + +#### Breaking changes + +- The in-memory exporter(s) have been moved to the `opentelemetry-sdk-testing` artifact. +- The OpenTracing shim factory class has been renamed from `TraceShim` to `OpenTracingShim`. The factory methods have changed because `BaggageManager` was removed and non-global `OpenTelemetry` instances are now available. +- The 's' was removed from the word "exporters" for every exporter artifact. For example, `opentelemetry-exporters-logging` was renamed to `opentelemetry-exporter-logging`. +- The 's' was removed from the word "extensions" for the package for every SDK extension. For example, `io.opentelemetry.sdk.extensions.otproto.TraceProtoUtils` was renamed to `io.opentelemetry.sdk.extension.otproto.TraceProtoUtils`. + + +### Thanks +Many thanks to everyone who made this release possible! + +@anuraaga @bogdandrutu @Oberon00 @thisthat @HaloFour @jkwatson @kenfinnigan @MariusVolkhart @malafeev @trask @tylerbenson @XiXiaPdx @dengliming @hengyunabc @jarebudev @brianashby-sfx + +--- +## 0.9.1 - 2020-10-07 + +- API + - BREAKING CHANGE: SpanId, TraceId and TraceFlags are no longer used as instances, but only contain helper methods for managing conversion between Strings, bytes and other formats. SpanId and TraceId are now natively String-typed, and the TraceFlags is a single byte. + - BREAKING CHANGE: Propagators now only expose a singleton instance. + - BREAKING CHANGE: The LabelConsumer and AttributeConsumer are now first-class interfaces, and the underlying consumer interface has had the key made additionally generic. Please prefer using the specific interfaces, rather than the underlying `ReadableKeyValuePairs.KeyValueConsumer`. + - BREAKING CHANGE: Minimum JDK version has been updated to 8, with Android API level 24. + - BREAKING CHANGE: Metric Instrument names are now case-insensitive. + - BREAKING CHANGE: The type-safety on Attributes has been moved to a new AttributeKey, and the AttributeValue wrappers have been removed. This impacts all the semantic attribute definitions, and the various APIs that use Attributes. + - BREAKING CHANGE: The obsolete HTTP_STATUS_TEXT semantic attribute has been removed. + - BREAKING CHANGE: The type of the REDIS_DATABASE_INDEX semantic attribute has been changed to be numeric. + - BREAKING CHANGE: Constant Labels have been removed from metric Instrument definitions. + - BREAKING CHANGE: The number of available Span Status options has been greatly reduced (from 16 to 3). + - BREAKING CHANGE: Constant labels have been removed from metric Instrument definitions. + - BREAKING CHANGE: The only way to specify span parenting is via a parent Context + - BREAKING CHANGE: The default TextMapPropagator is now a no-op in the API + - BREAKING CHANGE: CorrelationContext has been renamed to Baggage + - BREAKING CHANGE: Null-valued span attribute behavior has been changed to being "unspecified". + - BREAKING CHANGE: Link and Event interfaces have been removed from the API + - BREAKING CHANGE: The Status object has been removed from the API, in favor of StatusCanonicalCode + - BUGFIX: the `noParent` option on a Span was being ignored if it was set after setting an explicit parent. + - BUGFIX: Attributes and Labels now preserve the latest added entry when an existing key has been used. + - BUGFIX: Updated some of the W3C traceparent validation logic to better match the spec. + - FaaS semantic attributes have been added + - Semantic attribute for "exception.escaped" added + +- SDK + - BREAKING CHANGE: The names of the Sampler.Decision enum values, returned by the Sampler interface, have changed. + - `OpenTelemetrySdk.forceFlush()` now returns a CompletableResultCode + - BREAKING CHANGE: The `ProbabilitySampler` has been renamed to `TraceIdRatioBased` + - BREAKING CHANGE: The environment variables/system properties for specifying exporter and span processor configuration have been updated to match the specification. + - BREAKING CHANGE: Exported zipkin attributes have been changed to match the specification. + - BREAKING CHANGE: Metric Descriptor attributes have been flattened into the MetricData for export. + - BREAKING CHANGE: The OpenTelemetrySdk class now returns a TraceSdkManagement interface, rather than the concrete TracerSdkProvider. + - BUGFIX: Zipkin span durations are now rounded up to 1 microsecond, if less than 1. + - BUGFIX: The insecure option for OTLP export now does the correct thing. + - Added a configuration option for disabling SPI-provided ResourceProviders + - New incubator module with helper classes for working with SpanData + - AWS resources now include the `cloud.provider` attribute. + +- Extensions + - BREAKING CHANGE: Propagators now only expose a singleton instance. + - The auto-config extension has been moved to the instrumentation project. + - New incubator module with some utilities for mutating SpanData instances. + - The AWS Resource extension will now pull in EKS Resource attributes. + - New pre-release extension for handling logging natively. + +### Thanks +Many thanks to all who made this release possible: + +@bogdandrutu @Oberon00 @jkwatson @thisthat @anuraaga @jarebudev @malafeev @quijote @JasonXZLiu @zoercai @eunice98k @dengliming @breedx-nr @iNikem @wangzlei @imavroukakis + +--- +## 0.8.0 - 2020-09-01 + +- Extensions: + - Updated metrics generated by the runtime_metrics module to match the proposed semantic conventions. +- API: + - BREAKING CHANGE: Renamed HttpTextFormat to TextMapPropagator + - Added a toBuilder method to the Attributes class + - Added method to create an Attributes Builder from ReadableAttributes + - Updates to the Attributes' null-handling to conform to the specification + - TraceState validations were updated to match the W3C specification + - recordException Span API now has an additional overload to support additional attributes +- SDK: + - BUGFIX: Bound instruments with no recordings no longer generate data points. + - BREAKING CHANGE: The Exporter interfaces have changed to be async-friendly. + - BREAKING CHANGE: The parent context passed to the Sampler will no longer be nullable, but instead an invalid context will be passed. + - BREAKING CHANGE: The SpanProcessor now takes a ReadWriteSpan for the onStart method + - BREAKING CHANGE: ResourceConstants changed to ResourceAttributes + - BREAKING CHANGE: ParentOrElse Sampler changed to be called ParentBased + - Default Resource include the SDK attributes + - ResourceProvider SPI to enable custom Resource providers + - The individual pieces of the SDK are not published as individual components, in addition to the whole SDK artifact. + - Zipkin and Jaeger exporters now include the InstrumentationLibraryInfo attributes. + - The OTLP protobufs were updated to version 0.5.0 and the OTLP exporters were updated accordingly. + +- Many thanks for contributions from @anuraaga, @dengliming, @iNikem, @huntc, @jarebudev, @MitchellDumovic, @wtyanan, @williamhu99, @Oberon00, @thisthat, @malafeev, @mateuszrzeszutek, @kenfinnigan + +--- +## 0.7.1 - 2020-08-14 + +- BUGFIX: OTLP Span Exporter: fix splitting metadata key-value substring with more than one '=' sign + +--- +## 0.7.0 - 2020-08-02 + +NOTE: This release contains non-backward-compatible breaking SDK changes + +- Added an InMemoryMetricExporter +- Added a toBuilder method to Labels +- Added some semantic attribute constants +- New ZPages extension module with TraceZ and TraceConfigZ pages implemented +- Some overloads added for setting the parent Context +- Some performance improvements in HttpTraceContext implementation +- Removed null checks from the Trace APIs +- The bare API will no longer generate Trace and Span IDs when there is no parent trace context. +- Null Strings are no longer valid keys for Attributes +- BREAKING CHANGE: The Sampler API was changed +- Default endpoint is now set for the OTLP exporters +- BREAKING CHANGE: Jaeger exporter env vars/system properties were updated +- Resource attributes may now be set with a System Property as well as the environment variable. +- Added a propagator for Lightstep OpenTracing propagator +- The ZipkinSpanExporter now defaults to using the OkHttpSender +- The default Sampler in the SDK is now the ParentOrElse sampler +- BUGFIX: SpanWrapper SpanData implementation is now truly Immutable +- Added some simple logging for failed export calls +- Quieted the noisy B3 propagator +- Public constants were added for exporter configuration options +- Added a new configuration option to limit the size of Span attributes +- Many thanks for contributions from @anuraaga, @dengliming, @iNikem, @wtyanan, @williamhu99, @trask, @Oberon00, @MitchellDumovic, @FrankSpitulski, @heyams, @ptravers, @thisthat, @albertteoh, @evantorrie, @neeraj97, + +--- +## 0.6.0 - 2020-07-01 + +NOTE: This release contains non-backward-compatible breaking API and SDK changes + +- Introduction of immutable Attributes for SpansEvents, Links and Resources +- Introduction of immutable Labels for Metric Instruments and Recordings +- BUGFIX: make sure null Points are not propagated to metric exporters +- Added a propagator for AWS X-Ray +- BUGFIX: IntervalMetricReader now handles exceptions thrown by metric exporters +- Renamed contrib modules to "extensions" (Note: this changes the published artifact names, as well) +- Converted CorrelationContext entry keys and values to simple Strings +- Enhanced OTLP exporter configuration options +- Added new SDK Telemetry Resource populator +- Introduced an new MultiTracePropagator to handle multiple propagation formats +- Added new AWS Resource populators +- Added an extension to populate span data into log4j2 log formats. +- Changed the MinMaxSumCount aggregations for ValueRecorders to always aggregate deltas, rather than cumulative +- Updated the OTLP protobuf and exporter to version 0.4.0 of the OTLP protobufs. + +--- +## 0.5.0 - 2020-06-04 + +TODO: fill this out + +- Add helper API to get Tracer/Meter + +--- +## 0.4.0 - 2020-05-04 +- Initial implementation of the Zipkin exporter. +- **Breaking change:** Move B3 propagator to a contrib package +- Add support for Jaeger propagator +- Start implementing support for configuring exporters using Config pattern with support to load from environment variables and system properties. +- Add support to flush the entire SDK processors and exporter pipelines. +- Mark all threads/pools as daemon. +- Add support for Jaeger remote sampler. + +--- +## 0.3.0 - 2020-03-27 +- Initial Java API and SDK for context, trace, metrics, resource. +- Initial implementation of the Jaeger exporter. +- Initial implementation of the OTLP exporters for trace and metrics. diff --git a/opentelemetry-java/CONTRIBUTING.md b/opentelemetry-java/CONTRIBUTING.md new file mode 100644 index 000000000..1a031d6e1 --- /dev/null +++ b/opentelemetry-java/CONTRIBUTING.md @@ -0,0 +1,149 @@ +# Contributing + +Welcome to OpenTelemetry Java repository! + +Before you start - see OpenTelemetry general +[contributing](https://github.com/open-telemetry/community/blob/main/CONTRIBUTING.md) +requirements and recommendations. + +If you want to add new features or change behavior, please make sure your changes follow the +[OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification). +Otherwise file an issue or submit a PR to the specification repo first. + +Make sure to review the projects [license](LICENSE) and sign the +[CNCF CLA](https://identity.linuxfoundation.org/projects/cncf). A signed CLA will be enforced by an +automatic check once you submit a PR, but you can also sign it after opening your PR. + +## Requirements + +Java 11 or higher is required to build the projects in this repository. The built artifacts can be +used on Java 8 or higher. + +## Building opentelemetry-java + +Continuous integration builds the project, runs the tests, and runs multiple +types of static analysis. + +1. Note: Currently, to run the full suite of tests, you'll need to be running a docker daemon. +The tests that require docker are disabled if docker is not present. If you wish to run them, +you must run a local docker daemon. + +2. Clone the repository + + `git clone https://github.com/open-telemetry/opentelemetry-java.git` + +3. Run the following commands to build, run tests and most static analysis, and +check formatting: + + `./gradlew build` + +4. If you are a Windows user, use the alternate command mentioned below to run tests and +check formatting: + + `gradlew.bat` + +## Checks + +Before submitting a PR, you should make sure the style checks and unit tests pass. You can run these +with the `check` task. + +```bash +$ ./gradlew check +``` + +Note: this gradle task will potentially generate changes to files in the `docs/apidiffs/current_vs_latest` +directory. Please make sure to include any changes to these files in your pull request. + +## PR Review +After you submit a PR, it will be reviewed by the project maintainers and approvers. Not all maintainers need to review a +particular PR, but merging to the base branch is authorized to restricted members (administrators). + +## Style guideline + +We follow the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). +Our build will fail if source code is not formatted according to that style. To fix any +style failures the above [checks](#checks) show, automatically apply the formatting with: + +```bash +$ ./gradlew spotlessApply +``` + +To verify code style manually run the following command, +which uses [google-java-format](https://github.com/google/google-java-format) library: + +`./gradlew spotlessCheck` + +### Best practices that we follow + +* This project uses [semantic versioning](https://semver.org/). Except for major versions, a user should be able to update +their dependency version on this project and have nothing break. This means we do not make breaking +changes to the API (e.g., remove a public method) or to the ABI (e.g., change return type from void to non-void). +* Avoid exposing publicly any class/method/variable that don't need to be public. +* By default, all arguments/members are treated as non-null. Every argument/member that can be `null` must be annotated with `@Nullable`. +* The project aims to provide a consistent experience across all the public APIs. It is important to ensure consistency (same look and feel) across different public packages. +* Use `final` for public classes everywhere it is possible, this ensures that these classes cannot be extended when the API does not intend to offer that functionality. +* In general, we use the following ordering of class members: + * static fields (final before non-final) + * non-static fields (final before non-final) + * constructors + * static methods + * instance methods + * inner classes +* Adding `toString()` overrides on classes is encouraged, but we only use `toString()` to provide debugging assistance. The implementations +of all `toString()` methods should be considered to be unstable unless explicitly documented otherwise. + +If you notice any practice being applied in the project consistently that isn't listed here, please consider a pull request to add it. + +### Pre-commit hook +To completely delegate code style formatting to the machine, +you can add [git pre-commit hook](https://git-scm.com/docs/githooks). +We provide an example script in `buildscripts/pre-commit` file. +Just copy or symlink it into `.git/hooks` folder. + +### Editorconfig +As additional convenience for IntelliJ Idea users, we provide `.editorconfig` file. +Idea will automatically use it to adjust its code formatting settings. +It does not support all required rules, so you still have to run `spotlessApply` from time to time. + +### 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. +* We do not use `@author` tags in our javadoc. +* Our javadoc is available via [javadoc.io}(https://javadoc.io/doc/io.opentelemetry/opentelemetry-api) + +### 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. + + +### Unit Tests + +* Unit tests target Java 8, so language features such as lambda and streams can be used in tests. + +## Common tasks + +### Updating OTLP proto dependency version + +The OTLP proto dependency version is defined [here](proto/build.gradle). To bump the version, + +1. Find the latest release version [here](https://github.com/open-telemetry/opentelemetry-proto/releases/latest) +2. Download the zip source code archive +3. Run `shasum -a 256 ~/path/to/downloaded.zip` to compute its checksum +4. Update `protoVersion` and `protoChecksum` in the build file with the new version and checksum diff --git a/opentelemetry-java/LICENSE b/opentelemetry-java/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/opentelemetry-java/LICENSE @@ -0,0 +1,201 @@ + 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/opentelemetry-java/QUICKSTART.md b/opentelemetry-java/QUICKSTART.md new file mode 100644 index 000000000..46358aac9 --- /dev/null +++ b/opentelemetry-java/QUICKSTART.md @@ -0,0 +1,7 @@ +# OpenTelemetry QuickStart + +Our quickstart guide has been migrated to the [OpenTelemetry documentation website](https://opentelemetry.io/docs/java/manual_instrumentation/). + +The source for the documentation website can be found in our [website_docs](website_docs) folder in +this repository. If you have updates to the documentation, please open a pull request that updates the documents +in that location. Thanks! \ No newline at end of file diff --git a/opentelemetry-java/README.md b/opentelemetry-java/README.md new file mode 100644 index 000000000..b3879dfd4 --- /dev/null +++ b/opentelemetry-java/README.md @@ -0,0 +1,7 @@ +# Overview +This is the SDK project that opentelemetry-java-instrumentation depends on. + +For more details, configurations, and design principles, please refer to the open-source version of opentelemetry-java: + + +https://github.com/open-telemetry/opentelemetry-java \ No newline at end of file diff --git a/opentelemetry-java/README_cn.md b/opentelemetry-java/README_cn.md new file mode 100644 index 000000000..89d4ac7b4 --- /dev/null +++ b/opentelemetry-java/README_cn.md @@ -0,0 +1,6 @@ +# 概况 +这是opentelemetry-java-instrumentation项目所依赖的sdk工程。 + +更多细节、配置、设计原理可以查看开源版opentelemetry-java: + +https://github.com/open-telemetry/opentelemetry-java \ No newline at end of file diff --git a/opentelemetry-java/RELEASING.md b/opentelemetry-java/RELEASING.md new file mode 100644 index 000000000..25306650e --- /dev/null +++ b/opentelemetry-java/RELEASING.md @@ -0,0 +1,126 @@ +# OpenTelemetry Release Process + +## Starting the Release + +Before releasing, it is a good idea to run `./gradlew japicmp` on the main branch +and verify that there are no unexpected public API changes seen in the `docs/apidiffs/current_vs_latest` +directory. + +Open the release build workflow in your browser [here](https://github.com/open-telemetry/opentelemetry-java/actions/workflows/release-build.yml). + +You will see a button that says "Run workflow". Press the button, enter the version number you want +to release in the input field that pops up, and then press "Run workflow". + +This triggers the release process, which builds the artifacts. It will not automatically update the +documentation, because the Github Actions cannot push changes to the main branch. + +## Announcement + +Once the GitHub workflow completes, go to Github [release +page](https://github.com/open-telemetry/opentelemetry-java/releases), press +`Draft a new release` to write release notes about the new release. If there is already a draft +release notes, just point it at the created tag. + +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/open-telemetry/opentelemetry-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/open-telemetry/opentelemetry-java/blob/main/CHANGELOG.md) +for a list of major changes since last release. + +## Update release versions in documentations and CHANGELOG files + +After releasing is done, you need to first update the docs. + +``` +./gradlew updateVersionInDocs -Prelease.version=x.y.z +./gradlew japicmp -PapiBaseVersion=a.b.c -PapiNewVersion=x.y.z +./gradlew japicmp +``` + +Where `x.y.z` is the version just released and `a.b.c` is the previous version. + +Next, update the +[CHANGELOG.md](https://github.com/open-telemetry/opentelemetry-java/blob/main/CHANGELOG.md). + +Create a PR to mark the new release in README.md and CHANGELOG.md on the main branch. + +Finally, update the files `website_docs` directory to point at the newly released version. Once that has +been merged to the main branch, use the "Update OpenTelemetry Website" github action to create a PR +in the website repository with the changes. + +## Patch Release + +All patch releases should include only bug-fixes, and must avoid +adding/modifying the public APIs. + +Open the patch release build workflow in your browser [here](https://github.com/open-telemetry/opentelemetry-java/actions/workflows/patch-release-build.yml). + +You will see a button that says "Run workflow". Press the button, enter the version number you want +to release in the input field for version that pops up and the commits you want to cherrypick for the +patch as a comma-separated list. Then, press "Run workflow". + +If the commits cannot be cleanly applied to the release branch, for example because it has diverged +too much from main, then the workflow will fail before building. In this case, you will need to +prepare the release branch manually. + +This example will assume patching into release branch `v1.2.x` from a git repository with remotes +named `origin` and `upstream`. + +``` +$ git remote -v +origin git@github.com:username/opentelemetry-java.git (fetch) +origin git@github.com:username/opentelemetry-java.git (push) +upstream git@github.com:open-telemetry/opentelemetry-java.git (fetch) +upstream git@github.com:open-telemetry/opentelemetry-java.git (push) +``` + +First, checkout the release branch + +``` +git fetch upstream v1.2.x +git checkout upstream/v1.2.x +``` + +Apply cherrypicks manually and commit. It is ok to apply multiple cherrypicks in a single commit. +Use a commit message such as "Manual cherrypick for commits commithash1, commithash2". + +After commiting the change, push to your fork's branch. + +``` +git push origin v1.2.x +``` + +Create a PR to have code review and merge this into upstream's release branch. As this was not +applied automatically, we need to do code review to make sure the manual cherrypick is correct. + +After it is merged, Run the patch release workflow again, but leave the commits input field blank. +The release will be made with the current state of the release branch, which is what you prepared +above. + +## Credentials + +The following credentials are required for publishing (and automatically set in Circle CI): + +* `GPG_PRIVATE_KEY` and `GPG_PASSWORD`: GPG private key and password for signing + - Note, currently only @anuraaga has this and we need to find a way to safely share secrets in the + OpenTelemetry project, for example with a password manager. In the worst case if you need to + release manually and cannot get a hold of it, you can generate a new key but don't forget to + upload the public key to keyservers. + +* `SONATYPE_USER` and `SONATYPE_KEY`: Sonatype username and password. + +## Releasing from the local setup + +Releasing from the local setup can be done providing the previously mentioned four credential values, i.e. +`GPG_PRIVATE_KEY`, `GPG_PASSWORD`, `SONATYPE_USER` and `SONATYPE_KEY`: + +```sh +export SONATYPE_USER=my_maven_user +export SONATYPE_KEY=my_maven_password +export GPG_PRIVATE_KEY=$(cat ~/tmp/gpg.key.txt) +export GPG_PASSWORD= +export RELEASE_VERSION=2.4.5 # Set version you want to release +./gradlew final -Prelease.version=${RELEASE_VERSION} +``` diff --git a/opentelemetry-java/VERSIONING.md b/opentelemetry-java/VERSIONING.md new file mode 100644 index 000000000..47608b6ab --- /dev/null +++ b/opentelemetry-java/VERSIONING.md @@ -0,0 +1,80 @@ +# OpenTelemetry Java Versioning + +## Compatibility requirements + +This codebase strictly follows [Semantic Versioning 2.0.0](https://semver.org/). This means +that all artifacts have a version of the format `MAJOR.MINOR.PATCH` or `MAJOR.MINOR.PATCH-alpha`. +For any artifact with a stable release, that is its version does not end in `-alpha`, no backwards-incompatible +changes will be made unless incrementing the `MAJOR` version number. In practice, this means that +backwards-incompatible changes will be avoided as long as possible. Most releases are made by +incrementing the `MINOR` version. Patch releases with urgent cherry-picked bugfixes will be made by +incrementing the `PATCH` version. + +A backwards-incompatible change affects the public API of a module. The public API is any public +class or method that is not in a package which includes the word `internal`. Examples of incompatible +changes are: + +- API changes that could require code using the artifact to be changed, e.g., removing a method, + reordering parameters, adding a method to an interface or abstract class without adding a default + implementation. + +- ABI changes that could require code using the artifact to be recompiled, but not changed, e.g., + changing the return type of a method from `void` to non-`void`, changing a `class` to an `interface`. + The [JLS](https://docs.oracle.com/javase/specs/jls/se7/html/jls-13.html) has more information on + what constitutes compatible changes. + +- Behavior changes that can require code using the artifact to be changed, e.g., throwing an exception + in code that previously could not. Note, the opposite is not true, replacing an exception with a + no-op is acceptable if the no-op does not have a chance of causing errors in other parts of the + application. + +Such changes will be avoided - if they must be made, the `MAJOR` version of the artifact will be +incremented. + +Backwards incompatible changes to `internal` packages are expected. Versions of published artifacts +are expected to be aligned by using BOMs we publish. We will always provide BOMs to allow alignment +of versions. + +Changes may be made that require changes to the an app's dependency declarations aside from just +incrementing the version on `MINOR` version updates. For example, code may be separated out to a +new artifact which requires adding the new artifact to dependency declarations. + +As a user, if you always depend on the latest version of the BOM for a given `MAJOR` version, and +you do not use classes in the `internal` package (which you MUST NOT do), you can be assured that +your app will always function and have access to the latest features of OpenTelemetry without needing +any changes to code. + +## API vs SDK + +This codebase is broadly split into two large pieces, the OpenTelemetry API and the OpenTelemetry SDK, +including extensions respectively. Until a `MAJOR` version bump, all artifacts in the codebase, both +for API and SDK, will be released together with identical `MAJOR.MINOR.PATCH` versions. If one of the +two has its `MAJOR` version incremented independently, for example SDK v2 is released while still +targeting API v1, then all artifacts in that category will be released together. The details for this +will be fleshed out at the time - it can be expected that the repository is split in some way to +ensure all artifacts within a single repository are at the same version number. + +When incrementing the `MAJOR` version of the API, previously released `MAJOR` versions will be supported +for at least three more years. This includes + +- No backwards incompatible changes, as defined above +- A version of the SDK supporting the latest minor version of this API will be maintained +- Bug and security fixes will be backported. +- Additional features generally will not be backported + +When incrementing the `MAJOR` version of the SDK, previously released `MAJOR` versions will be supported +for at least one year. + +## Stable vs alpha + +Not all of our artifacts are published as stable artifacts - any non-stable artifact has the suffix +`-alpha` on its version. NONE of the guarantees described above apply to alpha artifacts. They may +require code or environment changes on every release and are not meant for consumption for users +where versioning stability is important. + +When an alpha artifact is ready to be made stable, the next release will be made as usual by bumping +the minor version, while the `-alpha` suffix will be removed. For example, if the previous release +of `opentelemetry-sdk` is `1.2.0` and of `opentelemetry-sdk-metrics` is `1.2.0-alpha`, the next +release when making metrics stable will have both artifacts with the version `1.3.0`. Notably, +`MAJOR` versions are only used to indicate a backwards-incompatible change and are not used to +announce new features. diff --git a/opentelemetry-java/all/README.md b/opentelemetry-java/all/README.md new file mode 100644 index 000000000..3aafd768e --- /dev/null +++ b/opentelemetry-java/all/README.md @@ -0,0 +1,5 @@ +# opentelemetry-all (utility project) + +This is a utility project which depends on all other projects in this repository. We use it for +global checks, for example API checks with archunit, and collecting all coverage reports from all +modules for uploading to codecov. diff --git a/opentelemetry-java/all/build.gradle.kts b/opentelemetry-java/all/build.gradle.kts new file mode 100644 index 000000000..8cac65167 --- /dev/null +++ b/opentelemetry-java/all/build.gradle.kts @@ -0,0 +1,91 @@ +plugins { + java +} + +description = "OpenTelemetry All" +extra["moduleName"] = "io.opentelemetry.all" + +tasks { + // We don't compile much here, just some API boundary tests. This project is mostly for + // aggregating jacoco reports and it doesn't work if this isn't at least as high as the + // highest supported Java version in any of our projects. Most of our projects target + // Java 8, except for jfr-events. + withType(JavaCompile::class) { + options.release.set(11) + } + + named("testJava8") { + enabled = false + } +} + +dependencies { + rootProject.subprojects.forEach { subproject -> + // Generate aggregate coverage report for published modules that enable jacoco. + subproject.plugins.withId("jacoco") { + subproject.plugins.withId("maven-publish") { + implementation(project(subproject.path)) { + isTransitive = false + } + } + } + } + testImplementation("com.tngtech.archunit:archunit-junit4") +} + +// https://docs.gradle.org/current/samples/sample_jvm_multi_project_with_code_coverage.html + +val sourcesPath by configurations.creating { + isVisible = false + isCanBeResolved = true + isCanBeConsumed = false + extendsFrom(configurations.implementation.get()) + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION)) + attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("source-folders")) + } +} + +val coverageDataPath by configurations.creating { + isVisible = false + isCanBeResolved = true + isCanBeConsumed = false + extendsFrom(configurations.implementation.get()) + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION)) + attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("jacoco-coverage-data")) + } +} + +tasks.named("jacocoTestReport") { + enabled = true + + configurations.runtimeClasspath.get().forEach { + additionalClassDirs(zipTree(it).filter { + // Exclude mrjar (jacoco complains), shaded, and generated code + !it.absolutePath.contains("META-INF/versions/") && + !it.absolutePath.contains("/internal/shaded/") && + !it.absolutePath.contains("io/opentelemetry/proto/") && + !it.absolutePath.contains("io/opentelemetry/exporter/jaeger/proto/") && + !it.absolutePath.contains("io/opentelemetry/sdk/extension/trace/jaeger/proto/") && + !it.absolutePath.contains("io/opentelemetry/semconv/trace/attributes/") && + !it.absolutePath.contains("AutoValue_") && + // TODO(anuraaga): Remove exclusion after enabling coverage for jfr-events + !it.absolutePath.contains("io/opentelemetry/sdk/extension/jfr") + }) + } + additionalSourceDirs(sourcesPath.incoming.artifactView { lenient(true) }.files) + executionData(coverageDataPath.incoming.artifactView { lenient(true) }.files.filter { it.exists() }) + + reports { + // xml is usually used to integrate code coverage with + // other tools like SonarQube, Coveralls or Codecov + xml.isEnabled = true + + // HTML reports can be used to see code coverage + // without any external tools + html.isEnabled = true + } +} diff --git a/opentelemetry-java/all/src/test/java/io/opentelemetry/all/InternalApiProtectionTest.java b/opentelemetry-java/all/src/test/java/io/opentelemetry/all/InternalApiProtectionTest.java new file mode 100644 index 000000000..157d15de8 --- /dev/null +++ b/opentelemetry-java/all/src/test/java/io/opentelemetry/all/InternalApiProtectionTest.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.all; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.syntax.elements.ClassesShouldConjunction; +import org.junit.jupiter.api.Test; + +class InternalApiProtectionTest { + + private static final String OTEL_BASE_PACKAGE = "io.opentelemetry"; + private static final JavaClasses ALL_OTEL_CLASSES = + new ClassFileImporter().importPackages(OTEL_BASE_PACKAGE); + + @Test + void extensionsShouldNotUseInternalApi() { + ClassesShouldConjunction contribRule = + noClasses() + .that() + .resideInAPackage("..extension..") + .should() + .dependOnClassesThat() + .resideInAPackage(OTEL_BASE_PACKAGE + ".internal"); + + contribRule.check(ALL_OTEL_CLASSES); + } + + @Test + void exportersShouldNotUseInternalApi() { + ClassesShouldConjunction contribRule = + noClasses() + .that() + .resideInAPackage("..exporter..") + .should() + .dependOnClassesThat() + .resideInAPackage(OTEL_BASE_PACKAGE + ".internal"); + + contribRule.check(ALL_OTEL_CLASSES); + } +} diff --git a/opentelemetry-java/all/src/test/java/io/opentelemetry/all/SdkDesignTest.java b/opentelemetry-java/all/src/test/java/io/opentelemetry/all/SdkDesignTest.java new file mode 100644 index 000000000..e5c05ab96 --- /dev/null +++ b/opentelemetry-java/all/src/test/java/io/opentelemetry/all/SdkDesignTest.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.all; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.base.Optional; +import com.tngtech.archunit.base.PackageMatcher; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClassList; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import com.tngtech.archunit.lang.syntax.elements.MethodsShouldConjunction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class SdkDesignTest { + + private static final JavaClasses SDK_OTEL_CLASSES = + new ClassFileImporter().importPackages("io.opentelemetry.sdk"); + + /** + * Ensures that all SDK methods that: - are defined in classes that extend or implement API model + * and are public (to exclude protected builders) - are public (avoids issues with protected + * methods returning classes unavailable to test's CL) - override or implement parent method + * return only API, Context or generic Java type. + */ + @Test + void sdkImplementationOfApiClassesShouldReturnApiTypeOnly() { + MethodsShouldConjunction covariantReturnRule = + ArchRuleDefinition.methods() + .that() + .areDeclaredInClassesThat() + .areAssignableTo(inPackage("io.opentelemetry.api..")) + .and() + .areDeclaredInClassesThat() + .arePublic() + .and() + .arePublic() + .and(implementOrOverride()) + .should() + .haveRawReturnType( + inPackage("io.opentelemetry.api..", "io.opentelemetry.context..", "java..")) + .orShould() + .haveRawReturnType("void"); + + covariantReturnRule.check(SDK_OTEL_CLASSES); + } + + static DescribedPredicate implementOrOverride() { + return new DescribedPredicate<>("implement or override a method") { + @Override + public boolean apply(JavaMethod input) { + JavaClassList params = input.getRawParameterTypes(); + Class[] paramsType = new Class[params.size()]; + for (int i = 0, n = params.size(); i < n; i++) { + paramsType[i] = params.get(i).reflect(); + } + String name = input.getName(); + + List parents = new ArrayList<>(input.getOwner().getAllRawSuperclasses()); + parents.addAll(input.getOwner().getAllRawInterfaces()); + + for (JavaClass parent : parents) { + Optional found = parent.tryGetMethod(name, paramsType); + if (found.isPresent()) { + return true; + } + } + return false; + } + }; + } + + static DescribedPredicate inPackage(String... requiredPackages) { + return new DescribedPredicate<>("are in " + Arrays.toString(requiredPackages)) { + @Override + public boolean apply(JavaClass member) { + for (String requiredPackage : requiredPackages) { + if (PackageMatcher.of(requiredPackage).matches(member.getPackageName())) { + return true; + } + } + return false; + } + }; + } +} diff --git a/opentelemetry-java/all/src/test/java/io/opentelemetry/extension/noopapi/NoopOpenTelemetryTest.java b/opentelemetry-java/all/src/test/java/io/opentelemetry/extension/noopapi/NoopOpenTelemetryTest.java new file mode 100644 index 000000000..422373a98 --- /dev/null +++ b/opentelemetry-java/all/src/test/java/io/opentelemetry/extension/noopapi/NoopOpenTelemetryTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.noopapi; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class NoopOpenTelemetryTest { + + private static final SpanContext SPAN_CONTEXT = + SpanContext.create( + "00000000000000000000000000000061", + "0000000000000061", + TraceFlags.getDefault(), + TraceState.getDefault()); + + @Test + void contextNoOp() { + // Context.root() is not a no-op Context, so the default context is never root. + Context context = Context.current(); + assertThat(context).isNotSameAs(Context.root()); + // No allocations + assertThat(context.with(Span.wrap(SPAN_CONTEXT))).isSameAs(context); + assertThat(SPAN_CONTEXT.isValid()).isTrue(); + try (Scope ignored = Context.current().with(Span.wrap(SPAN_CONTEXT)).makeCurrent()) { + // No context mounted, so always an invalid span. + assertThat(Span.fromContext(Context.current()).getSpanContext().isValid()).isFalse(); + } + } + + @Test + void tracerNoOp() { + SpanBuilder span1 = NoopOpenTelemetry.getInstance().getTracer("test").spanBuilder("test"); + SpanBuilder span2 = NoopOpenTelemetry.getInstance().getTracer("test").spanBuilder("test"); + // No allocations + assertThat(span1).isSameAs(span2); + + // No crash + span1.setParent(Context.current()); + span1.setNoParent(); + span1.addLink(SPAN_CONTEXT); + span1.addLink(SPAN_CONTEXT, Attributes.empty()); + span1.setAttribute("key", "value"); + span1.setAttribute("key", 1L); + span1.setAttribute("key", 1.0); + span1.setAttribute("key", true); + span1.setAttribute(AttributeKey.stringKey("key"), "value"); + span1.setSpanKind(SpanKind.CLIENT); + span1.setStartTimestamp(1, TimeUnit.DAYS); + + // No allocations + assertThat(span1.startSpan()).isSameAs(Span.getInvalid()); + } +} diff --git a/opentelemetry-java/api/all/README.md b/opentelemetry-java/api/all/README.md new file mode 100644 index 000000000..c1d88ac02 --- /dev/null +++ b/opentelemetry-java/api/all/README.md @@ -0,0 +1,18 @@ +# OpenTelemetry API + +[![Javadocs][javadoc-image]][javadoc-url] + +* The code in this module is the implementation of stable OpenTelemetry signals. +* Semantic Conventions for OpenTelemetry are in the `opentelemetry-semconv` module. +* The default implementation of the interfaces in this module is in the OpenTelemetry SDK module. +* The interfaces in this directory can be implemented to create alternative + implementations of the OpenTelemetry library. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-api.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-api + +--- +#### Running micro-benchmarks +From the root of the repo run `./gradlew clean :api:jmh` to run all the benchmarks +or run `./gradlew clean :api:jmh -PjmhIncludeSingleClass=` +to run a specific benchmark class. diff --git a/opentelemetry-java/api/all/build.gradle.kts b/opentelemetry-java/api/all/build.gradle.kts new file mode 100644 index 000000000..5c003c4ef --- /dev/null +++ b/opentelemetry-java/api/all/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java-library") + id("maven-publish") + + id("me.champeau.jmh") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry API" +extra["moduleName"] = "io.opentelemetry.api" +base.archivesBaseName = "opentelemetry-api" + +dependencies { + api(project(":context")) + + annotationProcessor("com.google.auto.value:auto-value") + + testImplementation("edu.berkeley.cs.jqf:jqf-fuzz") + testImplementation("com.google.guava:guava-testlib") +} diff --git a/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/baggage/BaggageBenchmark.java b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/baggage/BaggageBenchmark.java new file mode 100644 index 000000000..0377c3d17 --- /dev/null +++ b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/baggage/BaggageBenchmark.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import java.util.ArrayList; +import java.util.List; +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.Measurement; +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.State; +import org.openjdk.jmh.annotations.Warmup; + +@SuppressWarnings("JavadocMethod") +@State(Scope.Thread) +public class BaggageBenchmark { + + @Param({"0", "1", "10", "100"}) + public int itemsToAdd; + + // pre-allocate the keys & values to remove one possible confounding factor + private static final List keys = new ArrayList<>(100); + private static final List values = new ArrayList<>(100); + + static { + for (int i = 0; i < 100; i++) { + keys.add("key" + i); + values.add("value" + i); + } + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public Baggage baggageItemBenchmark() { + BaggageBuilder builder = Baggage.builder(); + for (int i = 0; i < itemsToAdd; i++) { + builder.put(keys.get(i), values.get(i)); + } + return builder.build(); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public Baggage baggageToBuilderBenchmark() { + Baggage baggage = Baggage.empty(); + for (int i = 0; i < itemsToAdd; i++) { + baggage = baggage.toBuilder().put(keys.get(i), values.get(i)).build(); + } + return baggage; + } +} diff --git a/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorBenchmark.java b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorBenchmark.java new file mode 100644 index 000000000..5f24acee5 --- /dev/null +++ b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorBenchmark.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage.propagation; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.annotation.Nullable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@SuppressWarnings("JavadocMethod") +@State(Scope.Thread) +public class W3CBaggagePropagatorBenchmark { + + private static final Map SMALL_BAGGAGE; + private static final Map LARGE_BAGGAGE; + + static { + List baggages = + IntStream.range(0, 100) + .mapToObj( + i -> + "key" + + i + + " = value" + + i + + ";metaKey" + + i + + "=\tmetaVal" + + i + + ",broken)key" + + i + + "=value") + .collect(Collectors.toList()); + SMALL_BAGGAGE = Collections.singletonMap("baggage", String.join(",", baggages.subList(0, 5))); + LARGE_BAGGAGE = Collections.singletonMap("baggage", String.join(",", baggages)); + } + + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(3) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Warmup(iterations = 5, time = 1) + public Context smallBaggage() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + return propagator.extract(Context.root(), SMALL_BAGGAGE, getter); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(3) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Warmup(iterations = 5, time = 1) + public Context largeBaggage() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + return propagator.extract(Context.root(), LARGE_BAGGAGE, getter); + } +} diff --git a/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/common/AttributesBenchmark.java b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/common/AttributesBenchmark.java new file mode 100644 index 000000000..e0c7de41b --- /dev/null +++ b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/common/AttributesBenchmark.java @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import java.util.ArrayList; +import java.util.List; +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@SuppressWarnings("JavadocMethod") +@State(Scope.Thread) +public class AttributesBenchmark { + + // pre-allocate the keys & values to remove one possible confounding factor + private static final List> keys = new ArrayList<>(10); + private static final List values = new ArrayList<>(10); + + static { + for (int i = 0; i < 10; i++) { + keys.add(AttributeKey.stringKey("key" + i)); + values.add("value" + i); + } + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public Attributes ofOne() { + return Attributes.of(keys.get(0), values.get(0)); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public Attributes ofTwo() { + return Attributes.of(keys.get(0), values.get(0), keys.get(1), values.get(1)); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public Attributes ofThree() { + return Attributes.of( + keys.get(0), values.get(0), keys.get(1), values.get(1), keys.get(2), values.get(2)); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public Attributes ofFour() { + return Attributes.of( + keys.get(0), + values.get(0), + keys.get(1), + values.get(1), + keys.get(2), + values.get(2), + keys.get(3), + values.get(3)); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public Attributes ofFive() { + return Attributes.of( + keys.get(0), + values.get(0), + keys.get(1), + values.get(1), + keys.get(2), + values.get(2), + keys.get(3), + values.get(3), + keys.get(4), + values.get(4)); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public Attributes builderTenItems() { + AttributesBuilder attributesBuilder = Attributes.builder(); + for (int i = 0; i < 10; i++) { + attributesBuilder.put(keys.get(i), values.get(i)); + } + return attributesBuilder.build(); + } +} diff --git a/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/DefaultTracerBenchmarks.java b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/DefaultTracerBenchmarks.java new file mode 100644 index 000000000..be612f377 --- /dev/null +++ b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/DefaultTracerBenchmarks.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Thread) +public class DefaultTracerBenchmarks { + + private final Tracer tracer = DefaultTracer.getInstance(); + @Nullable private Span span = null; + + /** Benchmark the full span lifecycle. */ + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public void measureFullSpanLifecycle() { + span = tracer.spanBuilder("span").startSpan(); + try (io.opentelemetry.context.Scope ignored = span.makeCurrent()) { + // no-op + } finally { + span.end(); + } + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public void measureSpanBuilding() { + span = tracer.spanBuilder("span").startSpan(); + } + + /** Benchmark just the scope lifecycle. */ + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public void measureScopeLifecycle() { + try (io.opentelemetry.context.Scope ignored = span.makeCurrent()) { + // no-op + } + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + public void measureGetCurrentSpan() { + Span.current(); + } + + @TearDown(Level.Iteration) + public void tearDown() { + if (span != null) { + span.end(); + } + } +} diff --git a/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/SpanIdBenchmark.java b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/SpanIdBenchmark.java new file mode 100644 index 000000000..47dd55b2b --- /dev/null +++ b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/SpanIdBenchmark.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 15, time = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(3) +@Threads(1) +public class SpanIdBenchmark { + + @Benchmark + public byte[] getSpanIdBytes() { + return SpanContext.getInvalid().getSpanIdBytes(); + } +} diff --git a/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorBenchmark.java b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorBenchmark.java new file mode 100644 index 000000000..582f4a586 --- /dev/null +++ b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorBenchmark.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace.propagation; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Thread) +public class W3CTraceContextPropagatorBenchmark { + + private static final String TRACEPARENT = "traceparent"; + private static final int COUNT = 5; + private static final List traceparentsHeaders = + Arrays.asList( + "00-905734c59b913b4a905734c59b913b4a-9909983295041501-01", + "00-21196a77f299580e21196a77f299580e-993a97ee3691eb26-00", + "00-2e7d0ad2390617702e7d0ad239061770-d49582a2de984b86-01", + "00-905734c59b913b4a905734c59b913b4a-776ff807b787538a-00", + "00-68ec932c33b3f2ee68ec932c33b3f2ee-68ec932c33b3f2ee-00"); + private final TextMapPropagator w3cTraceContextPropagator = + W3CTraceContextPropagator.getInstance(); + private final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + private final Map carrier = new HashMap<>(); + private final TextMapSetter> setter = Map::put; + + private static final List> carriers = + getCarrierForHeader(traceparentsHeaders); + + /** Benchmark for measuring HttpTraceContext extract. */ + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + @OperationsPerInvocation(COUNT) + @Nullable + public Context measureExtractCreateInject() { + Context result = null; + for (int i = 0; i < COUNT; i++) { + result = w3cTraceContextPropagator.extract(Context.root(), carriers.get(i), getter); + SpanContext current = Span.fromContext(result).getSpanContext(); + SpanContext clientSpanContext = + SpanContext.create( + current.getTraceId(), + current.getSpanId(), + current.getTraceFlags(), + current.getTraceState()); + result = Span.wrap(clientSpanContext).storeInContext(result); + w3cTraceContextPropagator.inject(result, carrier, setter); + if (carrier.size() != 1) { + throw new IllegalStateException("Fail test"); + } + carrier.clear(); + } + return result; + } + + private static List> getCarrierForHeader(List headers) { + List> carriers = new ArrayList<>(); + for (String header : headers) { + Map carrier = new HashMap<>(); + carrier.put(TRACEPARENT, header); + carriers.add(carrier); + } + return carriers; + } +} diff --git a/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorExtractBenchmark.java b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorExtractBenchmark.java new file mode 100644 index 000000000..03d384b3f --- /dev/null +++ b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorExtractBenchmark.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace.propagation; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Thread) +public class W3CTraceContextPropagatorExtractBenchmark { + + private static final String TRACEPARENT = "traceparent"; + private static final int COUNT = 5; + private static final List traceparentsHeaders = + Arrays.asList( + "00-905734c59b913b4a905734c59b913b4a-9909983295041501-01", + "00-21196a77f299580e21196a77f299580e-993a97ee3691eb26-00", + "00-2e7d0ad2390617702e7d0ad239061770-d49582a2de984b86-01", + "00-905734c59b913b4a905734c59b913b4a-776ff807b787538a-00", + "00-68ec932c33b3f2ee68ec932c33b3f2ee-68ec932c33b3f2ee-00"); + private final TextMapPropagator w3cTraceContextPropagator = + W3CTraceContextPropagator.getInstance(); + private final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + private static final List> carriers = + getCarrierForHeader(traceparentsHeaders); + + /** Benchmark for measuring HttpTraceContext extract. */ + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + @OperationsPerInvocation(COUNT) + @Nullable + public Context measureExtract() { + Context result = null; + for (int i = 0; i < COUNT; i++) { + result = w3cTraceContextPropagator.extract(Context.root(), carriers.get(i), getter); + } + return result; + } + + private static List> getCarrierForHeader(List headers) { + List> carriers = new ArrayList<>(); + for (String header : headers) { + Map carrier = new HashMap<>(); + carrier.put(TRACEPARENT, header); + carriers.add(carrier); + } + return carriers; + } +} diff --git a/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorInjectBenchmark.java b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorInjectBenchmark.java new file mode 100644 index 000000000..176fd1bd0 --- /dev/null +++ b/opentelemetry-java/api/all/src/jmh/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorInjectBenchmark.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace.propagation; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Thread) +public class W3CTraceContextPropagatorInjectBenchmark { + + private static final List spanContexts = + Arrays.asList( + createTestSpanContext("905734c59b913b4a905734c59b913b4a", "9909983295041501"), + createTestSpanContext("21196a77f299580e21196a77f299580e", "993a97ee3691eb26"), + createTestSpanContext("2e7d0ad2390617702e7d0ad239061770", "d49582a2de984b86"), + createTestSpanContext("905734c59b913b4a905734c59b913b4a", "776ff807b787538a"), + createTestSpanContext("68ec932c33b3f2ee68ec932c33b3f2ee", "68ec932c33b3f2ee")); + private static final int COUNT = 5; // spanContexts.size() + private final TextMapPropagator w3cTraceContextPropagator = + W3CTraceContextPropagator.getInstance(); + private final Map carrier = new HashMap<>(); + private final TextMapSetter> setter = Map::put; + private final List contexts = createContexts(spanContexts); + + /** Benchmark for measuring inject with default trace state and sampled trace options. */ + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @Fork(1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 1) + @OperationsPerInvocation(COUNT) + public Map measureInject() { + for (int i = 0; i < COUNT; i++) { + w3cTraceContextPropagator.inject(contexts.get(i), carrier, setter); + } + return carrier; + } + + private static SpanContext createTestSpanContext(String traceId, String spanId) { + return SpanContext.create(traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()); + } + + private static List createContexts(List spanContexts) { + List contexts = new ArrayList<>(); + for (SpanContext context : spanContexts) { + contexts.add(Context.root().with(Span.wrap(context))); + } + return contexts; + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/DefaultOpenTelemetry.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/DefaultOpenTelemetry.java new file mode 100644 index 000000000..64f9844e2 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/DefaultOpenTelemetry.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api; + +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.propagation.ContextPropagators; +import javax.annotation.concurrent.ThreadSafe; + +/** + * The default OpenTelemetry API, which tries to find API implementations via SPI or otherwise falls + * back to no-op default implementations. + */ +@ThreadSafe +final class DefaultOpenTelemetry implements OpenTelemetry { + private static final OpenTelemetry NO_OP = new DefaultOpenTelemetry(ContextPropagators.noop()); + + static OpenTelemetry getNoop() { + return NO_OP; + } + + static OpenTelemetry getPropagating(ContextPropagators propagators) { + return new DefaultOpenTelemetry(propagators); + } + + private final ContextPropagators propagators; + + DefaultOpenTelemetry(ContextPropagators propagators) { + this.propagators = propagators; + } + + @Override + public TracerProvider getTracerProvider() { + return TracerProvider.noop(); + } + + @Override + public ContextPropagators getPropagators() { + return propagators; + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java new file mode 100644 index 000000000..9f91cf6a3 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java @@ -0,0 +1,195 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api; + +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.propagation.ContextPropagators; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** + * A global singleton for the entrypoint to telemetry functionality for tracing, metrics and + * baggage. + * + *

    If using the OpenTelemetry SDK, you may want to instantiate the {@link OpenTelemetry} to + * provide configuration, for example of {@code Resource} or {@code Sampler}. See {@code + * OpenTelemetrySdk} and {@code OpenTelemetrySdk.builder} for information on how to construct the + * SDK {@link OpenTelemetry}. + * + * @see TracerProvider + * @see ContextPropagators + */ +public final class GlobalOpenTelemetry { + + private static final Logger logger = Logger.getLogger(GlobalOpenTelemetry.class.getName()); + + private static final Object mutex = new Object(); + + @Nullable private static volatile ObfuscatedOpenTelemetry globalOpenTelemetry; + + @GuardedBy("mutex") + @Nullable + private static Throwable setGlobalCaller; + + private GlobalOpenTelemetry() {} + + /** + * Returns the registered global {@link OpenTelemetry}. + * + * @throws IllegalStateException if a provider has been specified by system property using the + * interface FQCN but the specified provider cannot be found. + */ + public static OpenTelemetry get() { + if (globalOpenTelemetry == null) { + synchronized (mutex) { + if (globalOpenTelemetry == null) { + + OpenTelemetry autoConfigured = maybeAutoConfigure(); + if (autoConfigured != null) { + return autoConfigured; + } + + set(OpenTelemetry.noop()); + return OpenTelemetry.noop(); + } + } + } + return globalOpenTelemetry; + } + + /** + * Sets the {@link OpenTelemetry} that should be the global instance. Future calls to {@link + * #get()} will return the provided {@link OpenTelemetry} instance. This should be called once as + * early as possible in your application initialization logic, often in a {@code static} block in + * your main class. It should only be called once - an attempt to call it a second time will + * result in an error. If trying to set the global {@link OpenTelemetry} multiple times in tests, + * use {@link GlobalOpenTelemetry#resetForTest()} between them. + * + *

    If you are using the OpenTelemetry SDK, you should generally use {@code + * OpenTelemetrySdk.builder().buildAndRegisterGlobal()} instead of calling this method directly. + */ + public static void set(OpenTelemetry openTelemetry) { + synchronized (mutex) { + if (globalOpenTelemetry != null) { + throw new IllegalStateException( + "GlobalOpenTelemetry.set has already been called. GlobalOpenTelemetry.set must be " + + "called only once before any calls to GlobalOpenTelemetry.get. If you are using " + + "the OpenTelemetrySdk, use OpenTelemetrySdkBuilder.buildAndRegisterGlobal " + + "instead. Previous invocation set to cause of this exception.", + setGlobalCaller); + } + globalOpenTelemetry = new ObfuscatedOpenTelemetry(openTelemetry); + setGlobalCaller = new Throwable(); + } + } + + /** Returns the globally registered {@link TracerProvider}. */ + public static TracerProvider getTracerProvider() { + return get().getTracerProvider(); + } + + /** + * Gets or creates a named tracer instance from the globally registered {@link TracerProvider}. + * + *

    This is a shortcut method for {@code getTracerProvider().get(instrumentationName)} + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library (e.g., "io.opentelemetry.contrib.mongodb"). Must not be null. + * @return a tracer instance. + */ + public static Tracer getTracer(String instrumentationName) { + return get().getTracer(instrumentationName); + } + + /** + * Gets or creates a named and versioned tracer instance from the globally registered {@link + * TracerProvider}. + * + *

    This is a shortcut method for {@code getTracerProvider().get(instrumentationName, + * instrumentationVersion)} + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library (e.g., "io.opentelemetry.contrib.mongodb"). Must not be null. + * @param instrumentationVersion The version of the instrumentation library (e.g., "1.0.0"). + * @return a tracer instance. + */ + public static Tracer getTracer(String instrumentationName, String instrumentationVersion) { + return get().getTracer(instrumentationName, instrumentationVersion); + } + + /** + * Unsets the global {@link OpenTelemetry}. This is only meant to be used from tests which need to + * reconfigure {@link OpenTelemetry}. + */ + public static void resetForTest() { + globalOpenTelemetry = null; + } + + /** + * Returns the globally registered {@link ContextPropagators} for remote propagation of a context. + */ + public static ContextPropagators getPropagators() { + return get().getPropagators(); + } + + @Nullable + private static OpenTelemetry maybeAutoConfigure() { + final Class openTelemetrySdkAutoConfiguration; + try { + openTelemetrySdkAutoConfiguration = + Class.forName("io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration"); + } catch (ClassNotFoundException e) { + return null; + } + + try { + Method initialize = openTelemetrySdkAutoConfiguration.getMethod("initialize"); + return (OpenTelemetry) initialize.invoke(null); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException( + "OpenTelemetrySdkAutoConfiguration detected on classpath " + + "but could not invoke initialize method. This is a bug in OpenTelemetry.", + e); + } catch (InvocationTargetException t) { + logger.log( + Level.SEVERE, + "Error automatically configuring OpenTelemetry SDK. OpenTelemetry will not be enabled.", + t.getTargetException()); + return null; + } + } + + /** + * Static global instances are obfuscated when they are returned from the API to prevent users + * from casting them to their SDK-specific implementation. For example, we do not want users to + * use patterns like {@code (OpenTelemetrySdk) GlobalOpenTelemetry.get()}. + */ + @ThreadSafe + static class ObfuscatedOpenTelemetry implements OpenTelemetry { + + private final OpenTelemetry delegate; + + ObfuscatedOpenTelemetry(OpenTelemetry delegate) { + this.delegate = delegate; + } + + @Override + public TracerProvider getTracerProvider() { + return delegate.getTracerProvider(); + } + + @Override + public ContextPropagators getPropagators() { + return delegate.getPropagators(); + } + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/OpenTelemetry.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/OpenTelemetry.java new file mode 100644 index 000000000..2e764d3c4 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/OpenTelemetry.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.propagation.ContextPropagators; + +/** + * The entrypoint to telemetry functionality for tracing, metrics and baggage. + * + *

    If using the OpenTelemetry SDK, you may want to instantiate the {@link OpenTelemetry} to + * provide configuration, for example of {@code Resource} or {@code Sampler}. See {@code + * OpenTelemetrySdk} and {@code OpenTelemetrySdk.builder} for information on how to construct the + * SDK {@link OpenTelemetry}. + * + * @see TracerProvider + * @see ContextPropagators + */ +public interface OpenTelemetry { + /** Returns a completely no-op {@link OpenTelemetry}. */ + static OpenTelemetry noop() { + return DefaultOpenTelemetry.getNoop(); + } + + /** + * Returns an {@link OpenTelemetry} which will do remote propagation of {@link + * io.opentelemetry.context.Context} using the provided {@link ContextPropagators} and is no-op + * otherwise. + */ + static OpenTelemetry propagating(ContextPropagators propagators) { + return DefaultOpenTelemetry.getPropagating(propagators); + } + + /** Returns the {@link TracerProvider} for this {@link OpenTelemetry}. */ + TracerProvider getTracerProvider(); + + /** + * Gets or creates a named tracer instance from the {@link TracerProvider} for this {@link + * OpenTelemetry}. + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library (e.g., "io.opentelemetry.contrib.mongodb"). Must not be null. + * @return a tracer instance. + */ + default Tracer getTracer(String instrumentationName) { + return getTracerProvider().get(instrumentationName); + } + + /** + * Gets or creates a named and versioned tracer instance from the {@link TracerProvider} in this + * {@link OpenTelemetry}. + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library (e.g., "io.opentelemetry.contrib.mongodb"). Must not be null. + * @param instrumentationVersion The version of the instrumentation library (e.g., "1.0.0"). + * @return a tracer instance. + */ + default Tracer getTracer(String instrumentationName, String instrumentationVersion) { + return getTracerProvider().get(instrumentationName, instrumentationVersion); + } + + /** Returns the {@link ContextPropagators} for this {@link OpenTelemetry}. */ + ContextPropagators getPropagators(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/Baggage.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/Baggage.java new file mode 100644 index 000000000..5dd2924d4 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/Baggage.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ImplicitContextKeyed; +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * A map from {@link String} to {@link BaggageEntry} that can be used to label anything that is + * associated with a specific operation. + * + *

    For example, {@code Baggage}s can be used to label stats, log messages, or debugging + * information. + * + *

    Implementations of this interface *must* be immutable and have well-defined value-based + * equals/hashCode implementations. If an implementation does not strictly conform to these + * requirements, behavior of the OpenTelemetry APIs and default SDK cannot be guaranteed. + * + *

    For this reason, it is strongly suggested that you use the implementation that is provided + * here via the factory methods and the {@link BaggageBuilder}. + */ +@Immutable +public interface Baggage extends ImplicitContextKeyed { + + /** Baggage with no entries. */ + static Baggage empty() { + return ImmutableBaggage.empty(); + } + + /** Creates a new {@link BaggageBuilder} for creating Baggage. */ + static BaggageBuilder builder() { + return ImmutableBaggage.builder(); + } + + /** + * Returns Baggage from the current {@link Context}, falling back to empty Baggage if none is in + * the current Context. + */ + static Baggage current() { + return fromContext(Context.current()); + } + + /** + * Returns the {@link Baggage} from the specified {@link Context}, falling back to a empty {@link + * Baggage} if there is no baggage in the context. + */ + static Baggage fromContext(Context context) { + Baggage baggage = context.get(BaggageContextKey.KEY); + return baggage != null ? baggage : empty(); + } + + /** + * Returns the {@link Baggage} from the specified {@link Context}, or {@code null} if there is no + * baggage in the context. + */ + @Nullable + static Baggage fromContextOrNull(Context context) { + return context.get(BaggageContextKey.KEY); + } + + @Override + default Context storeInContext(Context context) { + return context.with(BaggageContextKey.KEY, this); + } + + /** Returns the number of entries in this {@link Baggage}. */ + int size(); + + /** Returns whether this {@link Baggage} is empty, containing no entries. */ + default boolean isEmpty() { + return size() == 0; + } + + /** Iterates over all the entries in this {@link Baggage}. */ + void forEach(BiConsumer consumer); + + /** Returns a read-only view of this {@link Baggage} as a {@link Map}. */ + Map asMap(); + + /** + * Returns the {@code String} value associated with the given key, without metadata. + * + * @param entryKey entry key to return the value for. + * @return the value associated with the given key, or {@code null} if no {@code Entry} with the + * given {@code entryKey} is in this {@code Baggage}. + */ + @Nullable + String getEntryValue(String entryKey); + + /** + * Create a Builder pre-initialized with the contents of this Baggage. The returned Builder will + * be set to not use an implicit parent, so any parent assignment must be done manually. + */ + BaggageBuilder toBuilder(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageBuilder.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageBuilder.java new file mode 100644 index 000000000..5cf6c7891 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageBuilder.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +/** + * A builder of {@link Baggage}. + * + * @see Baggage#builder() + */ +public interface BaggageBuilder { + + /** + * Adds the key/value pair and metadata regardless of whether the key is present. + * + * @param key the {@code String} key which will be set. + * @param value the {@code String} value to set for the given key. + * @param entryMetadata the {@code BaggageEntryMetadata} metadata to set for the given key. + * @return this + */ + BaggageBuilder put(String key, String value, BaggageEntryMetadata entryMetadata); + + /** + * Adds the key/value pair with empty metadata regardless of whether the key is present. + * + * @param key the {@code String} key which will be set. + * @param value the {@code String} value to set for the given key. + * @return this + */ + default BaggageBuilder put(String key, String value) { + return put(key, value, BaggageEntryMetadata.empty()); + } + + /** + * Removes the key if it exists. + * + * @param key the {@code String} key which will be removed. + * @return this + */ + BaggageBuilder remove(String key); + + /** + * Creates a {@code Baggage} from this builder. + * + * @return a {@code Baggage} with the same entries as this builder. + */ + Baggage build(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageContextKey.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageContextKey.java new file mode 100644 index 000000000..06825652b --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageContextKey.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import io.opentelemetry.context.ContextKey; +import javax.annotation.concurrent.Immutable; + +/** Util class to hold on to the key for storing Baggage in the Context. */ +@Immutable +class BaggageContextKey { + static final ContextKey KEY = ContextKey.named("opentelemetry-baggage-key"); + + private BaggageContextKey() {} +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageEntry.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageEntry.java new file mode 100644 index 000000000..01525f1ba --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageEntry.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import javax.annotation.concurrent.Immutable; + +/** An entry in a set of baggage. */ +@Immutable +public interface BaggageEntry { + + /** Returns the entry's value. */ + String getValue(); + + /** Returns the entry's {@link BaggageEntryMetadata}. */ + BaggageEntryMetadata getMetadata(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageEntryMetadata.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageEntryMetadata.java new file mode 100644 index 000000000..56da2b95a --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/BaggageEntryMetadata.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import javax.annotation.concurrent.Immutable; + +/** + * Metadata associated with an {@link BaggageEntry}. For the moment this is an opaque wrapper for a + * String metadata value. + */ +@Immutable +public interface BaggageEntryMetadata { + + /** Returns an empty {@link BaggageEntryMetadata}. */ + static BaggageEntryMetadata empty() { + return ImmutableEntryMetadata.EMPTY; + } + + /** Returns a new {@link BaggageEntryMetadata} with the given value. */ + static BaggageEntryMetadata create(String metadata) { + return ImmutableEntryMetadata.create(metadata); + } + + /** Returns the String value of this {@link BaggageEntryMetadata}. */ + String getValue(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableBaggage.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableBaggage.java new file mode 100644 index 000000000..81c70aa9c --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableBaggage.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import io.opentelemetry.api.internal.ImmutableKeyValuePairs; +import io.opentelemetry.api.internal.StringUtils; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class ImmutableBaggage extends ImmutableKeyValuePairs + implements Baggage { + + private static final Baggage EMPTY = new ImmutableBaggage.Builder().build(); + + private ImmutableBaggage(Object[] data) { + super(data); + } + + static Baggage empty() { + return EMPTY; + } + + static BaggageBuilder builder() { + return new Builder(); + } + + @Nullable + @Override + public String getEntryValue(String entryKey) { + BaggageEntry entry = get(entryKey); + return entry != null ? entry.getValue() : null; + } + + @Override + public BaggageBuilder toBuilder() { + return new Builder(new ArrayList<>(data())); + } + + private static Baggage sortAndFilterToBaggage(Object[] data) { + return new ImmutableBaggage(data); + } + + // TODO: Migrate to AutoValue.Builder + // @AutoValue.Builder + static class Builder implements BaggageBuilder { + + private final List data; + + Builder() { + this.data = new ArrayList<>(); + } + + Builder(List data) { + this.data = data; + } + + @Override + public BaggageBuilder put(String key, String value, BaggageEntryMetadata entryMetadata) { + if (!isKeyValid(key) || !isValueValid(value) || entryMetadata == null) { + return this; + } + data.add(key); + data.add(ImmutableEntry.create(value, entryMetadata)); + + return this; + } + + @Override + public BaggageBuilder remove(String key) { + if (key == null) { + return this; + } + data.add(key); + data.add(null); + return this; + } + + @Override + public Baggage build() { + return sortAndFilterToBaggage(data.toArray()); + } + } + + /** + * Determines whether the given {@code String} is a valid entry key. + * + * @param name the entry key name to be validated. + * @return whether the name is valid. + */ + private static boolean isKeyValid(String name) { + return name != null && !name.isEmpty() && StringUtils.isPrintableString(name); + } + + /** + * Determines whether the given {@code String} is a valid entry value. + * + * @param value the entry value to be validated. + * @return whether the value is valid. + */ + private static boolean isValueValid(String value) { + return value != null && StringUtils.isPrintableString(value); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntry.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntry.java new file mode 100644 index 000000000..73c752afd --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntry.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import com.google.auto.value.AutoValue; +import javax.annotation.concurrent.Immutable; + +/** String-String key-value pair, along with {@link ImmutableEntryMetadata}. */ +@Immutable +@AutoValue +abstract class ImmutableEntry implements BaggageEntry { + + ImmutableEntry() {} + + /** + * Creates an {@code Entry} from the given key, value and metadata. + * + * @param value the entry value. + * @param entryMetadata the entry metadata. + * @return a {@code Entry}. + */ + static ImmutableEntry create(String value, BaggageEntryMetadata entryMetadata) { + return new AutoValue_ImmutableEntry(value, entryMetadata); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java new file mode 100644 index 000000000..03b7f9b31 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import com.google.auto.value.AutoValue; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +abstract class ImmutableEntryMetadata implements BaggageEntryMetadata { + /** Returns an empty metadata. */ + static final ImmutableEntryMetadata EMPTY = create(""); + + ImmutableEntryMetadata() {} + + /** + * Creates an {@link ImmutableEntryMetadata} with the given value. + * + * @param metadata TTL of an {@code Entry}. + * @return an {@code EntryMetadata}. + */ + static ImmutableEntryMetadata create(String metadata) { + if (metadata == null) { + return EMPTY; + } + return new AutoValue_ImmutableEntryMetadata(metadata); + } + + /** + * Returns the String value of this {@link ImmutableEntryMetadata}. + * + * @return the raw metadata value. + */ + @Override + public abstract String getValue(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/package-info.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/package-info.java new file mode 100644 index 000000000..7cf942a91 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * API for associating entries with scoped operations. + * + *

    This package manages a set of entries in the {@link io.opentelemetry.context.Context}. The + * entries can be used to label anything that is associated with a specific operation. For example, + * the {@code opentelemetry.stats} package labels all stats with the current entries. + * + *

    Note that entries are independent of the tracing data that is propagated in the {@link + * io.opentelemetry.context.Context}, such as trace ID. + */ +// TODO: Add code examples. +@ParametersAreNonnullByDefault +package io.opentelemetry.api.baggage; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/Element.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/Element.java new file mode 100644 index 000000000..7c8fc474c --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/Element.java @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage.propagation; + +import java.util.BitSet; + +/** + * Represents single element of a W3C baggage header (key or value). Allows tracking parsing of a + * header string, keeping the state and validating allowed characters. Parsing state can be reset + * with {@link #reset(int)} allowing instance re-use. + */ +class Element { + + private static final BitSet EXCLUDED_KEY_CHARS = new BitSet(128); + private static final BitSet EXCLUDED_VALUE_CHARS = new BitSet(128); + + static { + for (char c : + new char[] { + '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}' + }) { + EXCLUDED_KEY_CHARS.set(c); + } + for (char c : new char[] {'"', ',', ';', '\\'}) { + EXCLUDED_VALUE_CHARS.set(c); + } + } + + private final BitSet excluded; + + private boolean leadingSpace; + private boolean readingValue; + private boolean trailingSpace; + private int start; + private int end; + private String value; + + static Element createKeyElement() { + return new Element(EXCLUDED_KEY_CHARS); + } + + static Element createValueElement() { + return new Element(EXCLUDED_VALUE_CHARS); + } + + /** + * Constructs element instance. + * + * @param excluded characters that are not allowed for this type of an element + */ + private Element(BitSet excluded) { + this.excluded = excluded; + reset(0); + } + + String getValue() { + return value; + } + + void reset(int start) { + this.start = start; + leadingSpace = true; + readingValue = false; + trailingSpace = false; + value = null; + } + + boolean tryTerminating(int index, String header) { + if (this.readingValue) { + markEnd(index); + } + if (this.trailingSpace) { + setValue(header); + return true; + } else { + // leading spaces - no content, invalid + return false; + } + } + + private void markEnd(int end) { + this.end = end; + this.readingValue = false; + trailingSpace = true; + } + + private void setValue(String header) { + this.value = header.substring(this.start, this.end); + } + + boolean tryNextChar(char character, int index) { + if (isWhitespace(character)) { + return tryNextWhitespace(index); + } else if (isExcluded(character)) { + return false; + } else { + return tryNextTokenChar(index); + } + } + + private static boolean isWhitespace(char character) { + return character == ' ' || character == '\t'; + } + + private boolean tryNextWhitespace(int index) { + if (readingValue) { + markEnd(index); + } + return true; + } + + private boolean isExcluded(char character) { + return (character <= 32 || character >= 127 || excluded.get(character)); + } + + private boolean tryNextTokenChar(int index) { + if (leadingSpace) { + markStart(index); + } + return !trailingSpace; + } + + private void markStart(int start) { + this.start = start; + readingValue = true; + leadingSpace = false; + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/Parser.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/Parser.java new file mode 100644 index 000000000..0eb6a2ef8 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/Parser.java @@ -0,0 +1,143 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage.propagation; + +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.baggage.BaggageEntryMetadata; + +/** + * Implements single-pass Baggage parsing in accordance with https://w3c.github.io/baggage/ Key / + * value are restricted in accordance with https://www.ietf.org/rfc/rfc2616.txt + * + *

    Note: following aspects are not specified in RFC: - some invalid elements (key or value) - + * parser will include valid ones, disregard invalid - empty "value" is regarded as invalid - meta - + * anything besides element terminator (comma) + */ +class Parser { + + private enum State { + KEY, + VALUE, + META + } + + private final String baggageHeader; + + private final Element key = Element.createKeyElement(); + private final Element value = Element.createValueElement(); + private String meta; + + private State state; + private int metaStart; + + private boolean skipToNext; + + public Parser(String baggageHeader) { + this.baggageHeader = baggageHeader; + reset(0); + } + + void parseInto(BaggageBuilder baggageBuilder) { + for (int i = 0, n = baggageHeader.length(); i < n; i++) { + char current = baggageHeader.charAt(i); + + if (skipToNext) { + if (current == ',') { + reset(i + 1); + } + continue; + } + + switch (current) { + case '=': + { + if (state == State.KEY) { + if (key.tryTerminating(i, baggageHeader)) { + setState(State.VALUE, i + 1); + } else { + skipToNext = true; + } + } + break; + } + case ';': + { + if (state == State.VALUE) { + skipToNext = !value.tryTerminating(i, baggageHeader); + setState(State.META, i + 1); + } + break; + } + case ',': + { + switch (state) { + case VALUE: + value.tryTerminating(i, baggageHeader); + break; + case META: + meta = baggageHeader.substring(metaStart, i).trim(); + break; + case KEY: // none + } + baggageBuilder.put(key.getValue(), value.getValue(), BaggageEntryMetadata.create(meta)); + reset(i + 1); + break; + } + default: + { + switch (state) { + case KEY: + skipToNext = !key.tryNextChar(current, i); + break; + case VALUE: + skipToNext = !value.tryNextChar(current, i); + break; + case META: // none + } + } + } + } + // need to finish parsing if there was no list element termination comma + switch (state) { + case KEY: + break; + case META: + { + String rest = baggageHeader.substring(metaStart).trim(); + baggageBuilder.put(key.getValue(), value.getValue(), BaggageEntryMetadata.create(rest)); + break; + } + case VALUE: + { + if (!skipToNext) { + value.tryTerminating(baggageHeader.length(), baggageHeader); + baggageBuilder.put(key.getValue(), value.getValue()); + break; + } + } + } + } + + /** + * Resets parsing state, preparing to start a new list element (see spec). + * + * @param index index where parser should start new element scan + */ + private void reset(int index) { + this.skipToNext = false; + this.state = State.KEY; + this.key.reset(index); + this.value.reset(index); + this.meta = ""; + this.metaStart = 0; + } + + /** Switches parser state (element of a list member). */ + private void setState(State state, int start) { + this.state = state; + this.metaStart = start; + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagator.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagator.java new file mode 100644 index 000000000..ee83c4ca7 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagator.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage.propagation; + +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; + +/** + * {@link TextMapPropagator} that implements the W3C specification for baggage header propagation. + */ +public final class W3CBaggagePropagator implements TextMapPropagator { + + private static final String FIELD = "baggage"; + private static final List FIELDS = singletonList(FIELD); + private static final W3CBaggagePropagator INSTANCE = new W3CBaggagePropagator(); + + /** Singleton instance of the W3C Baggage Propagator. */ + public static W3CBaggagePropagator getInstance() { + return INSTANCE; + } + + private W3CBaggagePropagator() {} + + @Override + public Collection fields() { + return FIELDS; + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null || setter == null) { + return; + } + Baggage baggage = Baggage.fromContext(context); + if (baggage.isEmpty()) { + return; + } + StringBuilder headerContent = new StringBuilder(); + baggage.forEach( + (key, baggageEntry) -> { + headerContent.append(key).append("=").append(baggageEntry.getValue()); + String metadataValue = baggageEntry.getMetadata().getValue(); + if (metadataValue != null && !metadataValue.isEmpty()) { + headerContent.append(";").append(metadataValue); + } + headerContent.append(","); + }); + if (headerContent.length() > 0) { + headerContent.setLength(headerContent.length() - 1); + setter.set(carrier, FIELD, headerContent.toString()); + } + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Context.root(); + } + if (getter == null) { + return context; + } + + String baggageHeader = getter.get(carrier, FIELD); + if (baggageHeader == null) { + return context; + } + if (baggageHeader.isEmpty()) { + return context; + } + + BaggageBuilder baggageBuilder = Baggage.builder(); + try { + extractEntries(baggageHeader, baggageBuilder); + } catch (RuntimeException e) { + return context; + } + return context.with(baggageBuilder.build()); + } + + private static void extractEntries(String baggageHeader, BaggageBuilder baggageBuilder) { + new Parser(baggageHeader).parseInto(baggageBuilder); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/package-info.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/package-info.java new file mode 100644 index 000000000..2d085fbe3 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Default OpenTelemetry remote baggage propagators. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.api.baggage.propagation; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java new file mode 100644 index 000000000..da0c3c7f5 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributes.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import io.opentelemetry.api.internal.ImmutableKeyValuePairs; +import java.util.ArrayList; +import java.util.Comparator; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class ArrayBackedAttributes extends ImmutableKeyValuePairs, Object> + implements Attributes { + + // We only compare the key name, not type, when constructing, to allow deduping keys with the + // same name but different type. + private static final Comparator> KEY_COMPARATOR_FOR_CONSTRUCTION = + Comparator.comparing(AttributeKey::getKey); + + static final Attributes EMPTY = Attributes.builder().build(); + + private ArrayBackedAttributes(Object[] data, Comparator> keyComparator) { + super(data, keyComparator); + } + + @Override + public AttributesBuilder toBuilder() { + return new ArrayBackedAttributesBuilder(new ArrayList<>(data())); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T get(AttributeKey key) { + return (T) super.get(key); + } + + static Attributes sortAndFilterToAttributes(Object... data) { + // null out any empty keys or keys with null values + // so they will then be removed by the sortAndFilter method. + for (int i = 0; i < data.length; i += 2) { + AttributeKey key = (AttributeKey) data[i]; + if (key != null && key.getKey().isEmpty()) { + data[i] = null; + } + } + return new ArrayBackedAttributes(data, KEY_COMPARATOR_FOR_CONSTRUCTION); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java new file mode 100644 index 000000000..9c2918d6e --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/ArrayBackedAttributesBuilder.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class ArrayBackedAttributesBuilder implements AttributesBuilder { + private final List data; + + ArrayBackedAttributesBuilder() { + data = new ArrayList<>(); + } + + ArrayBackedAttributesBuilder(List data) { + this.data = data; + } + + @Override + public Attributes build() { + return ArrayBackedAttributes.sortAndFilterToAttributes(data.toArray()); + } + + @Override + public AttributesBuilder put(AttributeKey key, int value) { + return put(key, (long) value); + } + + @Override + public AttributesBuilder put(AttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + data.add(key); + data.add(value); + return this; + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public AttributesBuilder putAll(Attributes attributes) { + if (attributes == null) { + return this; + } + // Attributes must iterate over their entries with matching types for key / value, so this + // downcast to the raw type is safe. + attributes.forEach((key, value) -> put((AttributeKey) key, value)); + return this; + } + + static List toList(double... values) { + Double[] boxed = new Double[values.length]; + for (int i = 0; i < values.length; i++) { + boxed[i] = values[i]; + } + return Arrays.asList(boxed); + } + + static List toList(long... values) { + Long[] boxed = new Long[values.length]; + for (int i = 0; i < values.length; i++) { + boxed[i] = values[i]; + } + return Arrays.asList(boxed); + } + + static List toList(boolean... values) { + Boolean[] boxed = new Boolean[values.length]; + for (int i = 0; i < values.length; i++) { + boxed[i] = values[i]; + } + return Arrays.asList(boxed); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java new file mode 100644 index 000000000..6d6e66eac --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeKey.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * This interface provides a handle for setting the values of {@link Attributes}. The type of value + * that can be set with an implementation of this key is denoted by the type parameter. + * + *

    Implementations MUST be immutable, as these are used as the keys to Maps. + * + * @param The type of value that can be set with the key. + */ +@SuppressWarnings("rawtypes") +@Immutable +public interface AttributeKey { + /** Returns the underlying String representation of the key. */ + String getKey(); + + /** Returns the type of attribute for this key. Useful for building switch statements. */ + AttributeType getType(); + + /** Returns a new AttributeKey for String valued attributes. */ + static AttributeKey stringKey(String key) { + return AttributeKeyImpl.create(key, AttributeType.STRING); + } + + /** Returns a new AttributeKey for Boolean valued attributes. */ + static AttributeKey booleanKey(String key) { + return AttributeKeyImpl.create(key, AttributeType.BOOLEAN); + } + + /** Returns a new AttributeKey for Long valued attributes. */ + static AttributeKey longKey(String key) { + return AttributeKeyImpl.create(key, AttributeType.LONG); + } + + /** Returns a new AttributeKey for Double valued attributes. */ + static AttributeKey doubleKey(String key) { + return AttributeKeyImpl.create(key, AttributeType.DOUBLE); + } + + /** Returns a new AttributeKey for List<String> valued attributes. */ + static AttributeKey> stringArrayKey(String key) { + return AttributeKeyImpl.create(key, AttributeType.STRING_ARRAY); + } + + /** Returns a new AttributeKey for List<Boolean> valued attributes. */ + static AttributeKey> booleanArrayKey(String key) { + return AttributeKeyImpl.create(key, AttributeType.BOOLEAN_ARRAY); + } + + /** Returns a new AttributeKey for List<Long> valued attributes. */ + static AttributeKey> longArrayKey(String key) { + return AttributeKeyImpl.create(key, AttributeType.LONG_ARRAY); + } + + /** Returns a new AttributeKey for List<Double> valued attributes. */ + static AttributeKey> doubleArrayKey(String key) { + return AttributeKeyImpl.create(key, AttributeType.DOUBLE_ARRAY); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeKeyImpl.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeKeyImpl.java new file mode 100644 index 000000000..2e67911d6 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeKeyImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; + +@SuppressWarnings("rawtypes") +@AutoValue +abstract class AttributeKeyImpl implements AttributeKey { + + // Used by auto-instrumentation agent. Check with auto-instrumentation before making changes to + // this method. + // + // In particular, do not change this return type to AttributeKeyImpl because auto-instrumentation + // hijacks this method and returns a bridged implementation of Context. + // + // Ideally auto-instrumentation would hijack the public AttributeKey.*Key() instead of this + // method, but auto-instrumentation also needs to inject its own implementation of AttributeKey + // into the class loader at the same time, which causes a problem because injecting a class into + // the class loader automatically resolves its super classes (interfaces), which in this case is + // Context, which would be the same class (interface) being instrumented at that time, + // which would lead to the JVM throwing a LinkageError "attempted duplicate interface definition" + static AttributeKey create(@Nullable String key, AttributeType type) { + return new AutoValue_AttributeKeyImpl<>(type, key != null ? key : ""); + } + + @Override + public abstract String getKey(); + + @Override + public final String toString() { + return getKey(); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java new file mode 100644 index 000000000..1c51e36d6 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +/** + * An enum that represents all the possible value types for an {@code AttributeKey} and hence the + * types of values that are allowed for {@link Attributes}. + */ +public enum AttributeType { + STRING, + BOOLEAN, + LONG, + DOUBLE, + STRING_ARRAY, + BOOLEAN_ARRAY, + LONG_ARRAY, + DOUBLE_ARRAY +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java new file mode 100644 index 000000000..418d67931 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/Attributes.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import static io.opentelemetry.api.common.ArrayBackedAttributes.sortAndFilterToAttributes; + +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * An immutable container for attributes. + * + *

    The keys are {@link AttributeKey}s and the values are Object instances that match the type of + * the provided key. + * + *

    Null keys will be silently dropped. + * + *

    Note: The behavior of null-valued attributes is undefined, and hence strongly discouraged. + * + *

    Implementations of this interface *must* be immutable and have well-defined value-based + * equals/hashCode implementations. If an implementation does not strictly conform to these + * requirements, behavior of the OpenTelemetry APIs and default SDK cannot be guaranteed. + * + *

    For this reason, it is strongly suggested that you use the implementation that is provided + * here via the factory methods and the {@link AttributesBuilder}. + */ +@SuppressWarnings("rawtypes") +@Immutable +public interface Attributes { + + /** Returns the value for the given {@link AttributeKey}, or {@code null} if not found. */ + @Nullable + T get(AttributeKey key); + + /** Iterates over all the key-value pairs of attributes contained by this instance. */ + void forEach(BiConsumer, ? super Object> consumer); + + /** The number of attributes contained in this. */ + int size(); + + /** Whether there are any attributes contained in this. */ + boolean isEmpty(); + + /** Returns a read-only view of this {@link Attributes} as a {@link Map}. */ + Map, Object> asMap(); + + /** Returns a {@link Attributes} instance with no attributes. */ + static Attributes empty() { + return ArrayBackedAttributes.EMPTY; + } + + /** Returns a {@link Attributes} instance with a single key-value pair. */ + static Attributes of(AttributeKey key, T value) { + return sortAndFilterToAttributes(key, value); + } + + /** + * Returns a {@link Attributes} instance with two key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + static Attributes of(AttributeKey key1, T value1, AttributeKey key2, U value2) { + return sortAndFilterToAttributes(key1, value1, key2, value2); + } + + /** + * Returns a {@link Attributes} instance with three key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + static Attributes of( + AttributeKey key1, + T value1, + AttributeKey key2, + U value2, + AttributeKey key3, + V value3) { + return sortAndFilterToAttributes(key1, value1, key2, value2, key3, value3); + } + + /** + * Returns a {@link Attributes} instance with four key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + static Attributes of( + AttributeKey key1, + T value1, + AttributeKey key2, + U value2, + AttributeKey key3, + V value3, + AttributeKey key4, + W value4) { + return sortAndFilterToAttributes(key1, value1, key2, value2, key3, value3, key4, value4); + } + + /** + * Returns a {@link Attributes} instance with five key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + static Attributes of( + AttributeKey key1, + T value1, + AttributeKey key2, + U value2, + AttributeKey key3, + V value3, + AttributeKey key4, + W value4, + AttributeKey key5, + X value5) { + return sortAndFilterToAttributes( + key1, value1, + key2, value2, + key3, value3, + key4, value4, + key5, value5); + } + + /** + * Returns a {@link Attributes} instance with the given key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + static Attributes of( + AttributeKey key1, + T value1, + AttributeKey key2, + U value2, + AttributeKey key3, + V value3, + AttributeKey key4, + W value4, + AttributeKey key5, + X value5, + AttributeKey key6, + Y value6) { + return sortAndFilterToAttributes( + key1, value1, + key2, value2, + key3, value3, + key4, value4, + key5, value5, + key6, value6); + } + + /** Returns a new {@link AttributesBuilder} instance for creating arbitrary {@link Attributes}. */ + static AttributesBuilder builder() { + return new ArrayBackedAttributesBuilder(); + } + + /** + * Returns a new {@link AttributesBuilder} instance populated with the data of this {@link + * Attributes}. + */ + AttributesBuilder toBuilder(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java new file mode 100644 index 000000000..c37185a34 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/AttributesBuilder.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import static io.opentelemetry.api.common.ArrayBackedAttributesBuilder.toList; +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import java.util.Arrays; + +/** A builder of {@link Attributes} supporting an arbitrary number of key-value pairs. */ +public interface AttributesBuilder { + /** Create the {@link Attributes} from this. */ + Attributes build(); + + /** + * Puts a {@link AttributeKey} with associated value into this. + * + *

    The type parameter is unused. + */ + // The type parameter was added unintentionally and unfortunately it is an API break for + // implementations of this interface to remove it. It doesn't affect users of the interface in + // any way, and has almost no effect on implementations, so we leave it until a future major + // version. + AttributesBuilder put(AttributeKey key, int value); + + /** Puts a {@link AttributeKey} with associated value into this. */ + AttributesBuilder put(AttributeKey key, T value); + + /** + * Puts a String attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, String value) { + return put(stringKey(key), value); + } + + /** + * Puts a long attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, long value) { + return put(longKey(key), value); + } + + /** + * Puts a double attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, double value) { + return put(doubleKey(key), value); + } + + /** + * Puts a boolean attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, boolean value) { + return put(booleanKey(key), value); + } + + /** + * Puts a String array attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, String... value) { + if (value == null) { + return this; + } + return put(stringArrayKey(key), Arrays.asList(value)); + } + + /** + * Puts a Long array attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, long... value) { + if (value == null) { + return this; + } + return put(longArrayKey(key), toList(value)); + } + + /** + * Puts a Double array attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, double... value) { + if (value == null) { + return this; + } + return put(doubleArrayKey(key), toList(value)); + } + + /** + * Puts a Boolean array attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + default AttributesBuilder put(String key, boolean... value) { + if (value == null) { + return this; + } + return put(booleanArrayKey(key), toList(value)); + } + + /** + * Puts all the provided attributes into this Builder. + * + * @return this Builder + */ + AttributesBuilder putAll(Attributes attributes); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/package-info.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/package-info.java new file mode 100644 index 000000000..219c1ccf8 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/common/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This package contains code common across the OpenTelemetry APIs, including {@link + * io.opentelemetry.api.common.Attributes} and classes/utilities for interacting with them. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.api.common; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/GuardedBy.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/GuardedBy.java new file mode 100644 index 000000000..4cf630a56 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/GuardedBy.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The field or method to which this annotation is applied can only be accessed when holding a + * particular lock, which may be a built-in (synchronization) lock, or may be an explicit {@link + * java.util.concurrent.locks.Lock}. + * + *

    The argument determines which lock guards the annotated field or method: + * + *

      + *
    • this : The string literal "this" means that this field is guarded by the class in which it + * is defined. + *
    • class-name.this : For inner classes, it may be necessary to disambiguate 'this'; the + * class-name.this designation allows you to specify which 'this' reference is intended + *
    • itself : For reference fields only; the object to which the field refers. + *
    • field-name : The lock object is referenced by the (instance or static) field specified by + * field-name. + *
    • class-name.field-name : The lock object is reference by the static field specified by + * class-name.field-name. + *
    • method-name() : The lock object is returned by calling the named nil-ary method. + *
    • class-name.class : The Class object for the specified class should be used as the lock + * object. + *
    + * + *

    This annotation is similar to {@link javax.annotation.concurrent.GuardedBy} but has {@link + * RetentionPolicy#SOURCE} so it is not in published artifacts. We only apply this to private + * members, so there is no reason to publish them and we avoid requiring end users to have to depend + * on the annotations in their own build. See the original issue for more info. + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.SOURCE) +public @interface GuardedBy { + /** The name of the object guarding the target. */ + String value(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/ImmutableKeyValuePairs.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/ImmutableKeyValuePairs.java new file mode 100644 index 000000000..1a2e93c07 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/ImmutableKeyValuePairs.java @@ -0,0 +1,262 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import static io.opentelemetry.api.internal.Utils.checkArgument; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * An immutable set of key-value pairs. + * + *

    Key-value pairs are dropped for {@code null} or empty keys. + * + *

    Note: for subclasses of this, null keys will be removed, but if your key has another concept + * of being "empty", you'll need to remove them before calling the constructor, assuming you don't + * want the "empty" keys to be kept in your collection. + * + * @param The type of the values contained in this. + */ +@Immutable +public abstract class ImmutableKeyValuePairs { + private final Object[] data; + + /** + * Sorts and dedupes the key/value pairs in {@code data}. {@code null} values will be removed. + * Keys must be {@link Comparable}. + */ + protected ImmutableKeyValuePairs(Object[] data) { + this(data, Comparator.naturalOrder()); + } + + /** + * Sorts and dedupes the key/value pairs in {@code data}. {@code null} values will be removed. + * Keys will be compared with the given {@link Comparator}. + */ + protected ImmutableKeyValuePairs(Object[] data, Comparator keyComparator) { + this.data = sortAndFilter(data, keyComparator); + } + + // TODO: Improve this to avoid one allocation, for the moment only some Builders and the asMap + // calls this. + protected final List data() { + return Arrays.asList(data); + } + + public final int size() { + return data.length / 2; + } + + public final boolean isEmpty() { + return data.length == 0; + } + + public final Map asMap() { + return ReadOnlyArrayMap.wrap(data()); + } + + /** Returns the value for the given {@code key}, or {@code null} if the key is not present. */ + @Nullable + @SuppressWarnings("unchecked") + public final V get(K key) { + if (key == null) { + return null; + } + for (int i = 0; i < data.length; i += 2) { + if (key.equals(data[i])) { + return (V) data[i + 1]; + } + } + return null; + } + + /** Iterates over all the key-value pairs of labels contained by this instance. */ + @SuppressWarnings("unchecked") + public final void forEach(BiConsumer consumer) { + if (consumer == null) { + return; + } + for (int i = 0; i < data.length; i += 2) { + consumer.accept((K) data[i], (V) data[i + 1]); + } + } + + /** + * Sorts and dedupes the key/value pairs in {@code data}. {@code null} values will be removed. + * Keys will be compared with the given {@link Comparator}. + */ + private static Object[] sortAndFilter(Object[] data, Comparator keyComparator) { + checkArgument( + data.length % 2 == 0, "You must provide an even number of key/value pair arguments."); + + if (data.length == 0) { + return data; + } + + mergeSort(data, keyComparator); + return dedupe(data, keyComparator); + } + + // note: merge sort implementation cribbed from this wikipedia article: + // https://en.wikipedia.org/wiki/Merge_sort (this is the top-down variant) + private static void mergeSort(Object[] data, Comparator keyComparator) { + Object[] workArray = new Object[data.length]; + System.arraycopy(data, 0, workArray, 0, data.length); + splitAndMerge( + workArray, + 0, + data.length, + data, + keyComparator); // sort data from workArray[] into sourceArray[] + } + + /** + * Sort the given run of array targetArray[] using array workArray[] as a source. beginIndex is + * inclusive; endIndex is exclusive (targetArray[endIndex] is not in the set). + */ + private static void splitAndMerge( + Object[] workArray, + int beginIndex, + int endIndex, + Object[] targetArray, + Comparator keyComparator) { + if (endIndex - beginIndex <= 2) { // if single element in the run, it's sorted + return; + } + // split the run longer than 1 item into halves + int midpoint = ((endIndex + beginIndex) / 4) * 2; // note: due to it's being key/value pairs + // recursively sort both runs from array targetArray[] into workArray[] + splitAndMerge(targetArray, beginIndex, midpoint, workArray, keyComparator); + splitAndMerge(targetArray, midpoint, endIndex, workArray, keyComparator); + // merge the resulting runs from array workArray[] into targetArray[] + merge(workArray, beginIndex, midpoint, endIndex, targetArray, keyComparator); + } + + /** + * Left source half is sourceArray[ beginIndex:middleIndex-1]. Right source half is sourceArray[ + * middleIndex:endIndex-1]. Result is targetArray[ beginIndex:endIndex-1]. + */ + @SuppressWarnings("unchecked") + private static void merge( + Object[] sourceArray, + int beginIndex, + int middleIndex, + int endIndex, + Object[] targetArray, + Comparator keyComparator) { + int leftKeyIndex = beginIndex; + int rightKeyIndex = middleIndex; + + // While there are elements in the left or right runs, fill in the target array from left to + // right + for (int k = beginIndex; k < endIndex; k += 2) { + // If left run head exists and is <= existing right run head. + if (leftKeyIndex < middleIndex - 1 + && (rightKeyIndex >= endIndex - 1 + || compareToNullSafe( + (K) sourceArray[leftKeyIndex], (K) sourceArray[rightKeyIndex], keyComparator) + <= 0)) { + targetArray[k] = sourceArray[leftKeyIndex]; + targetArray[k + 1] = sourceArray[leftKeyIndex + 1]; + leftKeyIndex = leftKeyIndex + 2; + } else { + targetArray[k] = sourceArray[rightKeyIndex]; + targetArray[k + 1] = sourceArray[rightKeyIndex + 1]; + rightKeyIndex = rightKeyIndex + 2; + } + } + } + + private static int compareToNullSafe( + @Nullable K key, @Nullable K pivotKey, Comparator keyComparator) { + if (key == null) { + return pivotKey == null ? 0 : -1; + } + if (pivotKey == null) { + return 1; + } + return keyComparator.compare(key, pivotKey); + } + + @SuppressWarnings("unchecked") + private static Object[] dedupe(Object[] data, Comparator keyComparator) { + Object previousKey = null; + int size = 0; + + // Implement the "last one in wins" behavior. + for (int i = 0; i < data.length; i += 2) { + Object key = data[i]; + Object value = data[i + 1]; + // Skip entries with key null. + if (key == null) { + continue; + } + // If the previously added key is equal with the current key, we overwrite what we have. + if (previousKey != null && keyComparator.compare((K) key, (K) previousKey) == 0) { + size -= 2; + } + // Skip entries with null value, we do it here because we want them to overwrite and remove + // entries with same key that we already added. + if (value == null) { + continue; + } + previousKey = key; + data[size++] = key; + data[size++] = value; + } + // Elements removed from the array, copy the array. We optimize for the case where we don't have + // duplicates or invalid entries. + if (data.length != size) { + Object[] result = new Object[size]; + System.arraycopy(data, 0, result, 0, size); + return result; + } + return data; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ImmutableKeyValuePairs)) { + return false; + } + ImmutableKeyValuePairs that = (ImmutableKeyValuePairs) o; + return Arrays.equals(this.data, that.data); + } + + @Override + public int hashCode() { + int result = 1; + result *= 1000003; + result ^= Arrays.hashCode(data); + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + for (int i = 0; i < data.length; i += 2) { + // Quote string values + Object value = data[i + 1]; + String valueStr = value instanceof String ? '"' + (String) value + '"' : value.toString(); + sb.append(data[i]).append("=").append(valueStr).append(", "); + } + // get rid of that last pesky comma + if (sb.length() > 1) { + sb.setLength(sb.length() - 2); + } + sb.append("}"); + return sb.toString(); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/OtelEncodingUtils.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/OtelEncodingUtils.java new file mode 100644 index 000000000..3014ec28f --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/OtelEncodingUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import java.util.Arrays; +import javax.annotation.concurrent.Immutable; + +@Immutable +public final class OtelEncodingUtils { + static final int LONG_BYTES = Long.SIZE / Byte.SIZE; + static final int BYTE_BASE16 = 2; + static final int LONG_BASE16 = BYTE_BASE16 * LONG_BYTES; + private static final String ALPHABET = "0123456789abcdef"; + private static final int NUM_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[NUM_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; + } + + /** + * Returns the {@code long} value whose base16 representation is stored in the first 16 chars of + * {@code chars} starting from the {@code offset}. + * + * @param chars the base16 representation of the {@code long}. + * @param offset the starting offset in the {@code CharSequence}. + */ + public static long longFromBase16String(CharSequence chars, int offset) { + return (byteFromBase16(chars.charAt(offset), chars.charAt(offset + 1)) & 0xFFL) << 56 + | (byteFromBase16(chars.charAt(offset + 2), chars.charAt(offset + 3)) & 0xFFL) << 48 + | (byteFromBase16(chars.charAt(offset + 4), chars.charAt(offset + 5)) & 0xFFL) << 40 + | (byteFromBase16(chars.charAt(offset + 6), chars.charAt(offset + 7)) & 0xFFL) << 32 + | (byteFromBase16(chars.charAt(offset + 8), chars.charAt(offset + 9)) & 0xFFL) << 24 + | (byteFromBase16(chars.charAt(offset + 10), chars.charAt(offset + 11)) & 0xFFL) << 16 + | (byteFromBase16(chars.charAt(offset + 12), chars.charAt(offset + 13)) & 0xFFL) << 8 + | (byteFromBase16(chars.charAt(offset + 14), chars.charAt(offset + 15)) & 0xFFL); + } + + /** + * Appends the base16 encoding of the specified {@code value} to the {@code dest}. + * + * @param value the value to be converted. + * @param dest the destination char array. + * @param destOffset the starting offset in the destination char array. + */ + public static void longToBase16String(long value, char[] dest, int destOffset) { + byteToBase16((byte) (value >> 56 & 0xFFL), dest, destOffset); + byteToBase16((byte) (value >> 48 & 0xFFL), dest, destOffset + BYTE_BASE16); + byteToBase16((byte) (value >> 40 & 0xFFL), dest, destOffset + 2 * BYTE_BASE16); + byteToBase16((byte) (value >> 32 & 0xFFL), dest, destOffset + 3 * BYTE_BASE16); + byteToBase16((byte) (value >> 24 & 0xFFL), dest, destOffset + 4 * BYTE_BASE16); + byteToBase16((byte) (value >> 16 & 0xFFL), dest, destOffset + 5 * BYTE_BASE16); + byteToBase16((byte) (value >> 8 & 0xFFL), dest, destOffset + 6 * BYTE_BASE16); + byteToBase16((byte) (value & 0xFFL), dest, destOffset + 7 * BYTE_BASE16); + } + + /** Returns the {@code byte[]} decoded from the given hex {@link CharSequence}. */ + public static byte[] bytesFromBase16(CharSequence value, int length) { + byte[] result = new byte[length / 2]; + for (int i = 0; i < length; i += 2) { + result[i / 2] = byteFromBase16(value.charAt(i), value.charAt(i + 1)); + } + return result; + } + + /** Fills {@code dest} with the hex encoding of {@code bytes}. */ + public static void bytesToBase16(byte[] bytes, char[] dest, int length) { + for (int i = 0; i < length; i++) { + byteToBase16(bytes[i], dest, i * 2); + } + } + + /** + * Encodes the specified byte, and returns the encoded {@code String}. + * + * @param value the value to be converted. + * @param dest the destination char array. + * @param destOffset the starting offset in the destination char array. + */ + public static void byteToBase16(byte value, char[] dest, int destOffset) { + int b = value & 0xFF; + dest[destOffset] = ENCODING[b]; + dest[destOffset + 1] = ENCODING[b | 0x100]; + } + + /** + * Decodes the specified two character sequence, and returns the resulting {@code byte}. + * + * @param first the first hex character. + * @param second the second hex character. + * @return the resulting {@code byte} + */ + public static byte byteFromBase16(char first, char second) { + if (first >= NUM_ASCII_CHARACTERS || DECODING[first] == -1) { + throw new IllegalArgumentException("invalid character " + first); + } + if (second >= NUM_ASCII_CHARACTERS || DECODING[second] == -1) { + throw new IllegalArgumentException("invalid character " + second); + } + int decoded = DECODING[first] << 4 | DECODING[second]; + return (byte) decoded; + } + + /** Returns whether the {@link CharSequence} is a valid hex string. */ + public static boolean isValidBase16String(CharSequence value) { + for (int i = 0; i < value.length(); i++) { + char b = value.charAt(i); + if (!isValidBase16Character(b)) { + return false; + } + } + return true; + } + + /** Returns whether the given {@code char} is a valid hex character. */ + public static boolean isValidBase16Character(char b) { + // 48..57 && 97..102 are valid + return (48 <= b && b <= 57) || (97 <= b && b <= 102); + } + + private OtelEncodingUtils() {} +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/ReadOnlyArrayMap.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/ReadOnlyArrayMap.java new file mode 100644 index 000000000..d4f27959f --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/ReadOnlyArrayMap.java @@ -0,0 +1,312 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2013-2020 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package io.opentelemetry.api.internal; + +import java.lang.reflect.Array; +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import javax.annotation.Nullable; + +/** A read-only view of an array of key-value pairs. */ +@SuppressWarnings("unchecked") +public final class ReadOnlyArrayMap extends AbstractMap { + + /** Returns a read-only view of the given {@code array}. */ + public static Map wrap(List array) { + if (array.isEmpty()) { + return Collections.emptyMap(); + } + return new ReadOnlyArrayMap<>(array); + } + + private final List array; + private final int size; + + private ReadOnlyArrayMap(List array) { + this.array = array; + this.size = array.size() / 2; + } + + @Override + public int size() { + return size; + } + + @Override + public boolean containsKey(Object o) { + if (o == null) { + return false; // null keys are not allowed + } + return arrayIndexOfKey(o) != -1; + } + + @Override + public boolean containsValue(Object o) { + for (int i = 0; i < array.size(); i += 2) { + if (value(i + 1).equals(o)) { + return true; + } + } + return false; + } + + @Override + @Nullable + public V get(Object o) { + if (o == null) { + return null; // null keys are not allowed + } + int i = arrayIndexOfKey(o); + return i != -1 ? value(i + 1) : null; + } + + int arrayIndexOfKey(Object o) { + int result = -1; + for (int i = 0; i < array.size(); i += 2) { + if (o.equals(key(i))) { + return i; + } + } + return result; + } + + K key(int i) { + return (K) array.get(i); + } + + V value(int i) { + return (V) array.get(i); + } + + @Override + public Set keySet() { + return new KeySetView(); + } + + @Override + public Collection values() { + return new ValuesView(); + } + + @Override + public Set> entrySet() { + return new EntrySetView(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public V put(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + final class KeySetView extends SetView { + @Override + K elementAtArrayIndex(int i) { + return key(i); + } + + @Override + public boolean contains(Object o) { + return containsKey(o); + } + } + + final class ValuesView extends SetView { + @Override + V elementAtArrayIndex(int i) { + return value(i + 1); + } + + @Override + public boolean contains(Object o) { + return containsValue(o); + } + } + + final class EntrySetView extends SetView> { + @Override + Map.Entry elementAtArrayIndex(int i) { + return new AbstractMap.SimpleImmutableEntry<>(key(i), value(i + 1)); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry) || ((Map.Entry) o).getKey() == null) { + return false; + } + Map.Entry that = (Map.Entry) o; + int i = arrayIndexOfKey(that.getKey()); + if (i == -1) { + return false; + } + return value(i + 1).equals(that.getValue()); + } + } + + abstract class SetView implements Set { + @Override + public int size() { + return size; + } + + // By abstracting this, {@link #keySet()} {@link #values()} and {@link #entrySet()} only + // implement need implement two methods based on {@link #}: this method and and {@link + // #contains(Object)}. + abstract E elementAtArrayIndex(int i); + + @Override + public Iterator iterator() { + return new ReadOnlyIterator(); + } + + @Override + public Object[] toArray() { + return copyTo(new Object[size]); + } + + @Override + public T[] toArray(T[] a) { + T[] result = + a.length >= size ? a : (T[]) Array.newInstance(a.getClass().getComponentType(), size()); + return copyTo(result); + } + + T[] copyTo(T[] dest) { + for (int i = 0, d = 0; i < array.size(); i += 2) { + dest[d++] = (T) elementAtArrayIndex(i); + } + return dest; + } + + final class ReadOnlyIterator implements Iterator { + int current = 0; + + @Override + public boolean hasNext() { + return current < array.size(); + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + E result = elementAtArrayIndex(current); + current += 2; + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean containsAll(Collection c) { + if (c == null) { + return false; + } + if (c.isEmpty()) { + return true; + } + + for (Object element : c) { + if (!contains(element)) { + return false; + } + } + return true; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("ReadOnlyArrayMap{"); + for (int i = 0; i < array.size(); i += 2) { + result.append(key(i)).append('=').append(value(i + 1)); + result.append(','); + } + result.setLength(result.length() - 1); + return result.append("}").toString(); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/StringUtils.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/StringUtils.java new file mode 100644 index 000000000..0af3b5cdb --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/StringUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import java.util.Objects; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Utilities for working with strings. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@Immutable +public final class StringUtils { + + /** + * Returns {@code true} if the given string is null or is the empty string. + * + *

    This method was copied verbatim from Guava library method + * com.google.common.base.Strings#isNullOrEmpty(java.lang.String). + * + * @param string a string reference to check + * @return {@code true} if the string is null or is the empty string + */ + public static boolean isNullOrEmpty(@Nullable String string) { + return string == null || string.isEmpty(); + } + + /** + * Pads a given string on the left with leading 0's up the length. + * + * @param value the string to pad + * @param minLength the minimum length the resulting padded string must have. Can be zero or + * negative, in which case the input string is always returned. + * @return the padded string + */ + public static String padLeft(String value, int minLength) { + return padStart(value, minLength, '0'); + } + + /** + * Returns a string, of length at least {@code minLength}, consisting of {@code string} prepended + * with as many copies of {@code padChar} as are necessary to reach that length. For example, + * + *

      + *
    • {@code padStart("7", 3, '0')} returns {@code "007"} + *
    • {@code padStart("2010", 3, '0')} returns {@code "2010"} + *
    + * + *

    See {@link java.util.Formatter} for a richer set of formatting capabilities. + * + *

    This method was copied almost verbatim from Guava library method + * com.google.common.base.Strings#padStart(java.lang.String, int, char). + * + * @param string the string which should appear at the end of the result + * @param minLength the minimum length the resulting string must have. Can be zero or negative, in + * which case the input string is always returned. + * @param padChar the character to insert at the beginning of the result until the minimum length + * is reached + * @return the padded string + */ + private static String padStart(String string, int minLength, char padChar) { + Objects.requireNonNull(string); + if (string.length() >= minLength) { + return string; + } + StringBuilder sb = new StringBuilder(minLength); + for (int i = string.length(); i < minLength; i++) { + sb.append(padChar); + } + sb.append(string); + return sb.toString(); + } + + /** + * 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/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/TemporaryBuffers.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/TemporaryBuffers.java new file mode 100644 index 000000000..43a415fbe --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/TemporaryBuffers.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +/** + * {@link ThreadLocal} buffers for use when creating new derived objects such as {@link String}s. + * These buffers are reused within a single thread - it is _not safe_ to use the buffer to generate + * multiple derived objects at the same time because the same memory will be used. In general, you + * should get a temporary buffer, fill it with data, and finish by converting into the derived + * object within the same method to avoid multiple usages of the same buffer. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class TemporaryBuffers { + + private static final ThreadLocal CHAR_ARRAY = new ThreadLocal<>(); + + /** + * A {@link ThreadLocal} {@code char[]} of size {@code len}. Take care when using a large value of + * {@code len} as this buffer will remain for the lifetime of the thread. The returned buffer will + * not be zeroed and may be larger than the requested size, you must make sure to fill the entire + * content to the desired value and set the length explicitly when converting to a {@link String}. + */ + public static char[] chars(int len) { + char[] buffer = CHAR_ARRAY.get(); + if (buffer == null || buffer.length < len) { + buffer = new char[len]; + CHAR_ARRAY.set(buffer); + } + return buffer; + } + + // Visible for testing + static void clearChars() { + CHAR_ARRAY.set(null); + } + + private TemporaryBuffers() {} +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/Utils.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/Utils.java new file mode 100644 index 000000000..2f6603e0d --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/Utils.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import javax.annotation.concurrent.Immutable; + +/** General internal utility methods. */ +@Immutable +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. + */ + public static void checkArgument(boolean isValid, String errorMessage) { + if (!isValid) { + throw new IllegalArgumentException(errorMessage); + } + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/package-info.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/package-info.java new file mode 100644 index 000000000..7ed63c90c --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/internal/package-info.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Interfaces and implementations that are internal to OpenTelemetry. + * + *

    All the content under this package and its subpackages are considered not part of the public + * API, and must not be used by users of the OpenTelemetry library. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.api.internal; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/package-info.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/package-info.java new file mode 100644 index 000000000..cd68fbe22 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/package-info.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The OpenTelemetry API. + * + *

    Individual features of OpenTelemetry, such as tracing and metrics, can be accessed through + * methods on {@link io.opentelemetry.api.OpenTelemetry}. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.api; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceState.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceState.java new file mode 100644 index 000000000..2aa493216 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceState.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.internal.ReadOnlyArrayMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +abstract class ArrayBasedTraceState implements TraceState { + + @Override + @Nullable + public String get(String key) { + if (key == null) { + return null; + } + List entries = getEntries(); + for (int i = 0; i < entries.size(); i += 2) { + if (entries.get(i).equals(key)) { + return entries.get(i + 1); + } + } + return null; + } + + @Override + public int size() { + return getEntries().size() / 2; + } + + @Override + public boolean isEmpty() { + return getEntries().isEmpty(); + } + + @Override + public void forEach(BiConsumer consumer) { + if (consumer == null) { + return; + } + List entries = getEntries(); + for (int i = 0; i < entries.size(); i += 2) { + consumer.accept(entries.get(i), entries.get(i + 1)); + } + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public Map asMap() { + return ReadOnlyArrayMap.wrap((List) getEntries()); + } + + abstract List getEntries(); + + @Override + public TraceStateBuilder toBuilder() { + return new ArrayBasedTraceStateBuilder(this); + } + + static ArrayBasedTraceState create(List entries) { + return new AutoValue_ArrayBasedTraceState(Collections.unmodifiableList(entries)); + } + + ArrayBasedTraceState() {} +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceStateBuilder.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceStateBuilder.java new file mode 100644 index 000000000..987b28bad --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceStateBuilder.java @@ -0,0 +1,179 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import io.opentelemetry.api.internal.StringUtils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +final class ArrayBasedTraceStateBuilder implements TraceStateBuilder { + + private static final int MAX_VENDOR_ID_SIZE = 13; + + // Needs to be in this class to avoid initialization deadlock because super class depends on + // subclass (the auto-value generate class). + private static final ArrayBasedTraceState EMPTY = + ArrayBasedTraceState.create(Collections.emptyList()); + + private static final int MAX_KEY_VALUE_PAIRS = 32; + private static final int KEY_MAX_SIZE = 256; + private static final int VALUE_MAX_SIZE = 256; + private static final int MAX_TENANT_ID_SIZE = 240; + + private final List entries; + + static TraceState empty() { + return EMPTY; + } + + ArrayBasedTraceStateBuilder() { + entries = new ArrayList<>(); + } + + ArrayBasedTraceStateBuilder(ArrayBasedTraceState parent) { + entries = new ArrayList<>(parent.getEntries()); + } + + /** + * Allows key value pairs to be added to the TraceState. + * + * @param key is an 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 /. For multi-tenant vendor scenarios, an at sign (@) can + * be used to prefix the vendor name. The tenant id (before the '@') is limited to 240 + * characters and the vendor id is limited to 13 characters. If in the multi-tenant vendor + * format, then the first character may additionally be numeric. + */ + @Override + public TraceStateBuilder put(String key, String value) { + if (!isKeyValid(key) + || !isValueValid(value) + || (entries != null && entries.size() >= MAX_KEY_VALUE_PAIRS)) { + return this; + } + removeEntry(key); + // Inserts the element at the front of this list. (note: probably pretty inefficient with an + // ArrayList as the underlying implementation!) + entries.add(0, key); + entries.add(1, value); + return this; + } + + @Override + public TraceStateBuilder remove(String key) { + if (key == null) { + return this; + } + removeEntry(key); + return this; + } + + private void removeEntry(String key) { + int currentSize = entries.size(); + for (int i = 0; i < currentSize; i += 2) { + if (entries.get(i).equals(key)) { + // remove twice at i to get the key & the value (yes, this is pretty ugly). + entries.remove(i); + entries.remove(i); + // Exit now because the entries list cannot contain duplicates. + break; + } + } + } + + @Override + public TraceState build() { + return ArrayBasedTraceState.create(entries); + } + + /** + * Checks the validity of a key. + * + * @param key is an 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 /. For multi-tenant vendor scenarios, an at sign (@) can + * be used to prefix the vendor name. The tenant id (before the '@') is limited to 240 + * characters and the vendor id is limited to 13 characters. If in the multi-tenant vendor + * format, then the first character may additionally be numeric. + * @return boolean representing key validity + */ + // todo: benchmark this implementation + private static boolean isKeyValid(@Nullable String key) { + if (key == null) { + return false; + } + if (key.length() > KEY_MAX_SIZE + || key.isEmpty() + || isNotLowercaseLetterOrDigit(key.charAt(0))) { + return false; + } + boolean isMultiTenantVendorKey = false; + for (int i = 1; i < key.length(); i++) { + char c = key.charAt(i); + if (isNotLegalKeyCharacter(c)) { + return false; + } + if (c == '@') { + // you can't have 2 '@' signs + if (isMultiTenantVendorKey) { + return false; + } + isMultiTenantVendorKey = true; + // tenant id (the part to the left of the '@' sign) must be 240 characters or less + if (i > MAX_TENANT_ID_SIZE) { + return false; + } + // vendor id (the part to the right of the '@' sign) must be 1-13 characters long + int remainingKeyChars = key.length() - i - 1; + if (remainingKeyChars > MAX_VENDOR_ID_SIZE || remainingKeyChars == 0) { + return false; + } + } + } + if (!isMultiTenantVendorKey) { + // if it's not the vendor format (with an '@' sign), the key must start with a letter. + return isNotDigit(key.charAt(0)); + } + return true; + } + + private static boolean isNotLegalKeyCharacter(char c) { + return isNotLowercaseLetterOrDigit(c) + && c != '_' + && c != '-' + && c != '@' + && c != '*' + && c != '/'; + } + + private static boolean isNotLowercaseLetterOrDigit(char ch) { + return (ch < 'a' || ch > 'z') && isNotDigit(ch); + } + + private static boolean isNotDigit(char ch) { + return ch < '0' || ch > '9'; + } + + // 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 isValueValid(@Nullable String value) { + if (StringUtils.isNullOrEmpty(value)) { + return false; + } + 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; + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java new file mode 100644 index 000000000..7463d30a1 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** No-op implementations of {@link Tracer}. */ +@ThreadSafe +final class DefaultTracer implements Tracer { + + private static final Tracer INSTANCE = new DefaultTracer(); + + static Tracer getInstance() { + return INSTANCE; + } + + @Override + public SpanBuilder spanBuilder(String spanName) { + return NoopSpanBuilder.create(); + } + + private DefaultTracer() {} + + // Noop implementation of Span.Builder. + private static final class NoopSpanBuilder implements SpanBuilder { + static NoopSpanBuilder create() { + return new NoopSpanBuilder(); + } + + @Nullable private SpanContext spanContext; + + @Override + public Span startSpan() { + if (spanContext == null) { + spanContext = Span.current().getSpanContext(); + } + + return Span.wrap(spanContext); + } + + @Override + public NoopSpanBuilder setParent(Context context) { + if (context == null) { + return this; + } + spanContext = Span.fromContext(context).getSpanContext(); + return this; + } + + @Override + public NoopSpanBuilder setNoParent() { + spanContext = SpanContext.getInvalid(); + return this; + } + + @Override + public NoopSpanBuilder addLink(SpanContext spanContext) { + return this; + } + + @Override + public NoopSpanBuilder addLink(SpanContext spanContext, Attributes attributes) { + return this; + } + + @Override + public NoopSpanBuilder setAttribute(String key, String value) { + return this; + } + + @Override + public NoopSpanBuilder setAttribute(String key, long value) { + return this; + } + + @Override + public NoopSpanBuilder setAttribute(String key, double value) { + return this; + } + + @Override + public NoopSpanBuilder setAttribute(String key, boolean value) { + return this; + } + + @Override + public NoopSpanBuilder setAttribute(AttributeKey key, T value) { + return this; + } + + @Override + public NoopSpanBuilder setAllAttributes(Attributes attributes) { + return this; + } + + @Override + public NoopSpanBuilder setSpanKind(SpanKind spanKind) { + return this; + } + + @Override + public NoopSpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { + return this; + } + + private NoopSpanBuilder() {} + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracerProvider.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracerProvider.java new file mode 100644 index 000000000..ec300a420 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracerProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +class DefaultTracerProvider implements TracerProvider { + + private static final TracerProvider INSTANCE = new DefaultTracerProvider(); + + static TracerProvider getInstance() { + return INSTANCE; + } + + @Override + public Tracer get(String instrumentationName) { + return DefaultTracer.getInstance(); + } + + @Override + public Tracer get(String instrumentationName, String instrumentationVersion) { + return DefaultTracer.getInstance(); + } + + private DefaultTracerProvider() {} +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/HeraContext.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/HeraContext.java new file mode 100644 index 000000000..9ed4da46e --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/HeraContext.java @@ -0,0 +1,39 @@ +package io.opentelemetry.api.trace; + +import java.util.HashMap; +import java.util.Map; + +public class HeraContext { + public static final int LIMIT_LENGTH = 8; + public static final String HERA_CONTEXT_PROPAGATOR_KEY = "heracontext"; + public static final String ENTRY_SPLIT = ";"; + public static final String KEY_VALUE_SPLIT = ":"; + + private HeraContext(){ + + } + + public static Map getInvalid() { + return wrap(null); + } + + public static Map getDefault() { + return wrap(null); + } + + public static boolean isValid(Map heraContext){ + if(heraContext != null && heraContext.size() > 0){ + String heraContextString = heraContext.get(HERA_CONTEXT_PROPAGATOR_KEY); + if(heraContextString != null && !heraContextString.isEmpty()){ + return heraContextString.split(ENTRY_SPLIT).length <= LIMIT_LENGTH; + } + } + return false; + } + + public static Map wrap(String heraContext){ + Map result = new HashMap<>(); + result.put(HERA_CONTEXT_PROPAGATOR_KEY, heraContext); + return result; + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableSpanContext.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableSpanContext.java new file mode 100644 index 000000000..355fc1b89 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableSpanContext.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import com.google.auto.value.AutoValue; +import java.util.Map; + +@AutoValue +abstract class ImmutableSpanContext implements SpanContext { + + static final SpanContext INVALID = + createInternal( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getDefault(), + TraceState.getDefault(), + /* remote= */ false, + /* valid= */ false,HeraContext.getInvalid()); + + private static AutoValue_ImmutableSpanContext createInternal( + String traceId, + String spanId, + TraceFlags traceFlags, + TraceState traceState, + boolean remote, + boolean valid, Map heraContext) { + return new AutoValue_ImmutableSpanContext( + traceId, spanId, traceFlags, traceState, remote, heraContext, valid); + } + + static SpanContext create( + String traceIdHex, + String spanIdHex, + TraceFlags traceFlags, + TraceState traceState, + boolean remote, Map heraContext) { + if (SpanId.isValid(spanIdHex) && TraceId.isValid(traceIdHex)) { + return createInternal( + traceIdHex, spanIdHex, traceFlags, traceState, remote, /* valid= */ true, HeraContext.isValid(heraContext)?heraContext:HeraContext.getInvalid()); + } + return createInternal( + TraceId.getInvalid(), + SpanId.getInvalid(), + traceFlags, + traceState, + remote, + /* valid= */ false, HeraContext.isValid(heraContext)?heraContext:HeraContext.getInvalid()); + } + + static SpanContext createInvalidWithHeraContext(Map heraContext) { + return createInternal( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getDefault(), + TraceState.getDefault(), + /* remote= */false, + /* valid= */ false, HeraContext.isValid(heraContext)?heraContext:HeraContext.getInvalid()); + } + + @Override + public abstract boolean isValid(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableTraceFlags.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableTraceFlags.java new file mode 100644 index 000000000..7cd15f607 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableTraceFlags.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import java.util.Objects; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class ImmutableTraceFlags implements TraceFlags { + private static final ImmutableTraceFlags[] INSTANCES = buildInstances(); + // Bit to represent whether trace is sampled or not. + private static final byte SAMPLED_BIT = 0x01; + + static final ImmutableTraceFlags DEFAULT = fromByte((byte) 0x00); + static final ImmutableTraceFlags SAMPLED = fromByte(SAMPLED_BIT); + static final int HEX_LENGTH = 2; + + private final String hexRep; + private final byte byteRep; + + // Implementation of the TraceFlags.fromHex(). + static ImmutableTraceFlags fromHex(CharSequence src, int srcOffset) { + Objects.requireNonNull(src, "src"); + return fromByte( + OtelEncodingUtils.byteFromBase16(src.charAt(srcOffset), src.charAt(srcOffset + 1))); + } + + // Implementation of the TraceFlags.fromByte(). + static ImmutableTraceFlags fromByte(byte traceFlagsByte) { + // Equivalent with Byte.toUnsignedInt(), but cannot use it because of Android. + return INSTANCES[traceFlagsByte & 255]; + } + + private static ImmutableTraceFlags[] buildInstances() { + ImmutableTraceFlags[] instances = new ImmutableTraceFlags[256]; + for (int i = 0; i < 256; i++) { + instances[i] = new ImmutableTraceFlags((byte) i); + } + return instances; + } + + private ImmutableTraceFlags(byte byteRep) { + char[] result = new char[2]; + OtelEncodingUtils.byteToBase16(byteRep, result, 0); + this.hexRep = new String(result); + this.byteRep = byteRep; + } + + @Override + public boolean isSampled() { + return (this.byteRep & SAMPLED_BIT) != 0; + } + + @Override + public String asHex() { + return this.hexRep; + } + + @Override + public byte asByte() { + return this.byteRep; + } + + @Override + public String toString() { + return asHex(); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/PropagatedSpan.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/PropagatedSpan.java new file mode 100644 index 000000000..24c145b05 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/PropagatedSpan.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.Immutable; + +/** + * The default {@link Span} that is used when no {@code Span} implementation is available. All + * operations are no-op except context propagation. + */ +@Immutable +final class PropagatedSpan implements Span { + + static final PropagatedSpan INVALID = new PropagatedSpan(SpanContext.getInvalid()); + + // Used by auto-instrumentation agent. Check with auto-instrumentation before making changes to + // this method. + // + // In particular, do not change this return type to PropagatedSpan because auto-instrumentation + // hijacks this method and returns a bridged implementation of Span. + // + // Ideally auto-instrumentation would hijack the public Span.wrap() instead of this + // method, but auto-instrumentation also needs to inject its own implementation of Span + // into the class loader at the same time, which causes a problem because injecting a class into + // the class loader automatically resolves its super classes (interfaces), which in this case is + // Span, which would be the same class (interface) being instrumented at that time, + // which would lead to the JVM throwing a LinkageError "attempted duplicate interface definition" + static Span create(SpanContext spanContext) { + return new PropagatedSpan(spanContext); + } + + private final SpanContext spanContext; + + private PropagatedSpan(SpanContext spanContext) { + this.spanContext = spanContext; + } + + @Override + public Span setAttribute(String key, String value) { + return this; + } + + @Override + public Span setAttribute(String key, long value) { + return this; + } + + @Override + public Span setAttribute(String key, double value) { + return this; + } + + @Override + public Span setAttribute(String key, boolean value) { + return this; + } + + @Override + public Span setAttribute(AttributeKey key, T value) { + return this; + } + + @Override + public Span setAllAttributes(Attributes attributes) { + return this; + } + + @Override + public Span addEvent(String name) { + return this; + } + + @Override + public Span addEvent(String name, long timestamp, TimeUnit unit) { + return this; + } + + @Override + public Span addEvent(String name, Attributes attributes) { + return this; + } + + @Override + public Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + return this; + } + + @Override + public Span setStatus(StatusCode statusCode) { + return this; + } + + @Override + public Span setStatus(StatusCode statusCode, String description) { + return this; + } + + @Override + public Span recordException(Throwable exception) { + return this; + } + + @Override + public Span recordException(Throwable exception, Attributes additionalAttributes) { + return this; + } + + @Override + public Span updateName(String name) { + return this; + } + + @Override + public void end() {} + + @Override + public void end(long timestamp, TimeUnit unit) {} + + @Override + public SpanContext getSpanContext() { + return spanContext; + } + + @Override + public boolean isRecording() { + return false; + } + + @Override + public String toString() { + return "PropagatedSpan{" + spanContext + '}'; + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/Span.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/Span.java new file mode 100644 index 000000000..f73955684 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/Span.java @@ -0,0 +1,425 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ImplicitContextKeyed; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** + * An interface that represents a span. It has an associated {@link SpanContext}. + * + *

    Spans are created by the {@link SpanBuilder#startSpan} method. + * + *

    {@code Span} must be ended by calling {@link #end()}. + */ +@ThreadSafe +public interface Span extends ImplicitContextKeyed { + + /** + * Returns the {@link Span} from the current {@link Context}, falling back to a default, no-op + * {@link Span} if there is no span in the current context. + */ + static Span current() { + Span span = Context.current().get(SpanContextKey.KEY); + return span == null ? getInvalid() : span; + } + + /** + * Returns the {@link Span} from the specified {@link Context}, falling back to a default, no-op + * {@link Span} if there is no span in the context. + */ + static Span fromContext(Context context) { + if (context == null) { + return Span.getInvalid(); + } + Span span = context.get(SpanContextKey.KEY); + return span == null ? getInvalid() : span; + } + + /** + * Returns the {@link Span} from the specified {@link Context}, or {@code null} if there is no + * span in the context. + */ + @Nullable + static Span fromContextOrNull(Context context) { + if (context == null) { + return null; + } + return context.get(SpanContextKey.KEY); + } + + /** + * Returns an invalid {@link Span}. An invalid {@link Span} is used when tracing is disabled, + * usually because there is no OpenTelemetry SDK installed. + */ + static Span getInvalid() { + return PropagatedSpan.INVALID; + } + + /** + * Returns a non-recording {@link Span} that holds the provided {@link SpanContext} but has no + * functionality. It will not be exported and all tracing operations are no-op, but it can be used + * to propagate a valid {@link SpanContext} downstream. + */ + static Span wrap(SpanContext spanContext) { + if (spanContext == null || !spanContext.isValid()) { + Map heraContext = spanContext.getHeraContext(); + if(heraContext == null || heraContext.size() == 0) { + return getInvalid(); + } + } + return PropagatedSpan.create(spanContext); + } + + /** + * 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. + * + *

    If a null or empty String {@code value} is passed in, the behavior is undefined, and hence + * strongly discouraged. + * + *

    Note: It is strongly recommended to use {@link #setAttribute(AttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + default Span setAttribute(String key, String value) { + return setAttribute(AttributeKey.stringKey(key), value); + } + + /** + * 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. + * + *

    Note: It is strongly recommended to use {@link #setAttribute(AttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + default Span setAttribute(String key, long value) { + return setAttribute(AttributeKey.longKey(key), value); + } + + /** + * 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. + * + *

    Note: It is strongly recommended to use {@link #setAttribute(AttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + default Span setAttribute(String key, double value) { + return setAttribute(AttributeKey.doubleKey(key), value); + } + + /** + * 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. + * + *

    Note: It is strongly recommended to use {@link #setAttribute(AttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + default Span setAttribute(String key, boolean value) { + return setAttribute(AttributeKey.booleanKey(key), value); + } + + /** + * 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. + * + *

    Note: the behavior of null values is undefined, and hence strongly discouraged. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + Span setAttribute(AttributeKey key, T value); + + /** + * 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. + * @return this. + */ + default Span setAttribute(AttributeKey key, int value) { + return setAttribute(key, (long) value); + } + + /** + * Sets attributes to the {@link Span}. If the {@link Span} previously contained a mapping for any + * of the keys, the old values are replaced by the specified values. + * + * @param attributes the attributes + * @return this. + * @since 1.2.0 + */ + @SuppressWarnings("unchecked") + default Span setAllAttributes(Attributes attributes) { + if (attributes == null || attributes.isEmpty()) { + return this; + } + attributes.forEach( + (attributeKey, value) -> this.setAttribute((AttributeKey) attributeKey, value)); + return this; + } + + /** + * Adds an event to the {@link Span}. The timestamp of the event will be the current time. + * + * @param name the name of the event. + * @return this. + */ + default Span addEvent(String name) { + return addEvent(name, Attributes.empty()); + } + + /** + * Adds an event to the {@link Span} with the given {@code timestamp}, as nanos since epoch. Note, + * this {@code timestamp} is not the same as {@link System#nanoTime()} but may be computed using + * it, for example, by taking a difference of readings from {@link System#nanoTime()} and adding + * to the span start time. + * + *

    When possible, it is preferred to use {@link #addEvent(String)} at the time the event + * occurred. + * + * @param name the name of the event. + * @param timestamp the explicit event timestamp since epoch. + * @param unit the unit of the timestamp + * @return this. + */ + default Span addEvent(String name, long timestamp, TimeUnit unit) { + return addEvent(name, Attributes.empty(), timestamp, unit); + } + + /** + * Adds an event to the {@link Span} with the given {@code timestamp}, as nanos since epoch. Note, + * this {@code timestamp} is not the same as {@link System#nanoTime()} but may be computed using + * it, for example, by taking a difference of readings from {@link System#nanoTime()} and adding + * to the span start time. + * + *

    When possible, it is preferred to use {@link #addEvent(String)} at the time the event + * occurred. + * + * @param name the name of the event. + * @param timestamp the explicit event timestamp since epoch. + * @return this. + */ + default Span addEvent(String name, Instant timestamp) { + if (timestamp == null) { + return addEvent(name); + } + return addEvent( + name, SECONDS.toNanos(timestamp.getEpochSecond()) + timestamp.getNano(), NANOSECONDS); + } + + /** + * Adds an event to the {@link Span} with the given {@link Attributes}. The timestamp of the event + * will be the current time. + * + * @param name the name of the event. + * @param attributes the attributes that will be added; these are associated with this event, not + * the {@code Span} as for {@code setAttribute()}. + * @return this. + */ + Span addEvent(String name, Attributes attributes); + + /** + * Adds an event to the {@link Span} with the given {@link Attributes} and {@code timestamp}. + * Note, this {@code timestamp} is not the same as {@link System#nanoTime()} but may be computed + * using it, for example, by taking a difference of readings from {@link System#nanoTime()} and + * adding to the span start time. + * + *

    When possible, it is preferred to use {@link #addEvent(String)} at the time the event + * occurred. + * + * @param name the name of the event. + * @param attributes the attributes that will be added; these are associated with this event, not + * the {@code Span} as for {@code setAttribute()}. + * @param timestamp the explicit event timestamp since epoch. + * @param unit the unit of the timestamp + * @return this. + */ + Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit); + + /** + * Adds an event to the {@link Span} with the given {@link Attributes} and {@code timestamp}. + * Note, this {@code timestamp} is not the same as {@link System#nanoTime()} but may be computed + * using it, for example, by taking a difference of readings from {@link System#nanoTime()} and + * adding to the span start time. + * + *

    When possible, it is preferred to use {@link #addEvent(String)} at the time the event + * occurred. + * + * @param name the name of the event. + * @param attributes the attributes that will be added; these are associated with this event, not + * the {@code Span} as for {@code setAttribute()}. + * @param timestamp the explicit event timestamp since epoch. + * @return this. + */ + default Span addEvent(String name, Attributes attributes, Instant timestamp) { + if (timestamp == null) { + return addEvent(name, attributes); + } + return addEvent( + name, + attributes, + SECONDS.toNanos(timestamp.getEpochSecond()) + timestamp.getNano(), + NANOSECONDS); + } + + /** + * Sets the status to the {@code Span}. + * + *

    If used, this will override the default {@code Span} status. Default status code is {@link + * StatusCode#UNSET}. + * + *

    Only the value of the last call will be recorded, and implementations are free to ignore + * previous calls. + * + * @param statusCode the {@link StatusCode} to set. + * @return this. + */ + default Span setStatus(StatusCode statusCode) { + return setStatus(statusCode, ""); + } + + /** + * Sets the status to the {@code Span}. + * + *

    If used, this will override the default {@code Span} status. Default status code is {@link + * StatusCode#UNSET}. + * + *

    Only the value of the last call will be recorded, and implementations are free to ignore + * previous calls. + * + * @param statusCode the {@link StatusCode} to set. + * @param description the description of the {@code Status}. + * @return this. + */ + Span setStatus(StatusCode statusCode, String description); + + /** + * Records information about the {@link Throwable} to the {@link Span}. + * + *

    Note that the EXCEPTION_ESCAPED value from the Semantic Conventions cannot be determined by + * this function. You should record this attribute manually using {@link + * #recordException(Throwable, Attributes)} if you know that an exception is escaping. + * + * @param exception the {@link Throwable} to record. + * @return this. + */ + default Span recordException(Throwable exception) { + return recordException(exception, Attributes.empty()); + } + + /** + * Records information about the {@link Throwable} to the {@link Span}. + * + * @param exception the {@link Throwable} to record. + * @param additionalAttributes the additional {@link Attributes} to record. + * @return this. + */ + Span recordException(Throwable exception, Attributes additionalAttributes); + + /** + * Updates the {@code Span} name. + * + *

    If used, this will override the name provided via {@code Span.Builder}. + * + *

    Upon this update, any sampling behavior based on {@code Span} name will depend on the + * implementation. + * + * @param name the {@code Span} name. + * @return this. + */ + Span updateName(String name); + + /** + * Marks the end of {@code Span} execution. + * + *

    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. + */ + void end(); + + /** + * Marks the end of {@code Span} execution with the specified timestamp. + * + *

    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. + * + *

    Use this method for specifying explicit end options, such as end {@code Timestamp}. When no + * explicit values are required, use {@link #end()}. + * + * @param timestamp the explicit timestamp from the epoch, for this {@code Span}. {@code 0} + * indicates current time should be used. + * @param unit the unit of the timestamp + */ + void end(long timestamp, TimeUnit unit); + + /** + * Marks the end of {@code Span} execution with the specified timestamp. + * + *

    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. + * + *

    Use this method for specifying explicit end options, such as end {@code Timestamp}. When no + * explicit values are required, use {@link #end()}. + * + * @param timestamp the explicit timestamp from the epoch, for this {@code Span}. {@code 0} + * indicates current time should be used. + */ + default void end(Instant timestamp) { + if (timestamp == null) { + end(); + return; + } + end(SECONDS.toNanos(timestamp.getEpochSecond()) + timestamp.getNano(), NANOSECONDS); + } + + /** + * Returns the {@code SpanContext} associated with this {@code Span}. + * + * @return the {@code SpanContext} associated with this {@code Span}. + */ + SpanContext getSpanContext(); + + /** + * Returns {@code true} if this {@code Span} records tracing events (e.g. {@link + * #addEvent(String)}, {@link #setAttribute(String, long)}). + * + * @return {@code true} if this {@code Span} records tracing events. + */ + boolean isRecording(); + + @Override + default Context storeInContext(Context context) { + return context.with(SpanContextKey.KEY, this); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanBuilder.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanBuilder.java new file mode 100644 index 000000000..ba9185783 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanBuilder.java @@ -0,0 +1,328 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +/** + * {@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. + * + *

    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: + * + *

    {@code
    + * class MyClass {
    + *   private static final Tracer tracer = OpenTelemetry.get().getTracer("com.anyco.rpc");
    + *
    + *   void doWork {
    + *     // Create a Span as a child of the current Span.
    + *     Span span = tracer.spanBuilder("MyChildSpan").startSpan();
    + *     try (Scope ss = span.makeCurrent()) {
    + *       span.addEvent("my event");
    + *       doSomeWork();  // Here the new span is in the current Context, so it can be used
    + *                      // implicitly anywhere down the stack.
    + *     } finally {
    + *       span.end();
    + *     }
    + *   }
    + * }
    + * }
    + * + *

    There might be cases where you do not perform all the work inside one static scope and the + * Context is automatically propagated: + * + *

    {@code
    + * class MyRpcServerInterceptorListener implements RpcServerInterceptor.Listener {
    + *   private static final Tracer tracer = OpenTelemetry.get().getTracer("com.example.rpc");
    + *   private Span mySpan;
    + *
    + *   public MyRpcInterceptor() {}
    + *
    + *   public void onRequest(String rpcName, Metadata metadata) {
    + *     // Create a Span as a child of the remote Span.
    + *     mySpan = tracer.spanBuilder(rpcName)
    + *         .setParent(extractContextFromMetadata(metadata)).startSpan();
    + *   }
    + *
    + *   public void onExecuteHandler(ServerCallHandler serverCallHandler) {
    + *     try (Scope ws = mySpan.makeCurrent()) {
    + *       Span.current().addEvent("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.setStatus(StatusCode.ERROR);
    + *     mySpan.end();
    + *   }
    + *
    + *   // 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.setStatus(rpcStatusToCanonicalTraceStatus(status);
    + *     mySpan.end();
    + *   }
    + * }
    + * }
    + * + *

    This is a simple example where all the work is being done within a single scope and the + * Context is manually propagated: + * + *

    {@code
    + * class MyClass {
    + *   private static final Tracer tracer = OpenTelemetry.get().getTracer("com.example.rpc");
    + *
    + *   void doWork(Span parent) {
    + *     Span childSpan = tracer.spanBuilder("MyChildSpan")
    + *         .setParent(Context.current().with(parent))
    + *         .startSpan();
    + *     childSpan.addEvent("my event");
    + *     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.
    + *     }
    + *   }
    + * }
    + * }
    + * + *

    see {@link SpanBuilder#startSpan} for usage examples. + */ +public interface SpanBuilder { + + /** + * Sets the parent to use from the specified {@code Context}. If not set, the value of {@code + * Span.current()} at {@link #startSpan()} time will be used as parent. + * + *

    If no {@link Span} is available in the specified {@code Context}, the resulting {@code Span} + * will become a root instance, as if {@link #setNoParent()} had been called. + * + *

    If called multiple times, only the last specified value will be used. Observe that the state + * defined by a previous call to {@link #setNoParent()} will be discarded. + * + * @param context the {@code Context}. + * @return this. + */ + SpanBuilder setParent(Context context); + + /** + * Sets the option to become a root {@code Span} for a new trace. If not set, the value of {@code + * Span.current()} at {@link #startSpan()} time will be used as parent. + * + *

    Observe that any previously set parent will be discarded. + * + * @return this. + */ + SpanBuilder setNoParent(); + + /** + * Adds a link to the newly created {@code Span}. + * + *

    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 or + * the same trace. + * + *

    Implementations may ignore calls with an {@linkplain SpanContext#isValid() invalid span + * context}. + * + * @param spanContext the context of the linked {@code Span}. + * @return this. + */ + SpanBuilder addLink(SpanContext spanContext); + + /** + * Adds a link to the newly created {@code Span}. + * + *

    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 or + * the same trace. + * + *

    Implementations may ignore calls with an {@linkplain SpanContext#isValid() invalid span + * context}. + * + * @param spanContext the context of the linked {@code Span}. + * @param attributes the attributes of the {@code Link}. + * @return this. + */ + SpanBuilder addLink(SpanContext spanContext, Attributes attributes); + + /** + * Sets an attribute to the newly created {@code Span}. If {@code SpanBuilder} previously + * contained a mapping for the key, the old value is replaced by the specified value. + * + *

    If a null or empty String {@code value} is passed in, the behavior is undefined, and hence + * strongly discouraged. + * + *

    Note: It is strongly recommended to use {@link #setAttribute(AttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + SpanBuilder setAttribute(String key, String value); + + /** + * Sets an attribute to the newly created {@code Span}. If {@code SpanBuilder} previously + * contained a mapping for the key, the old value is replaced by the specified value. + * + *

    Note: It is strongly recommended to use {@link #setAttribute(AttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + SpanBuilder setAttribute(String key, long value); + + /** + * Sets an attribute to the newly created {@code Span}. If {@code SpanBuilder} previously + * contained a mapping for the key, the old value is replaced by the specified value. + * + *

    Note: It is strongly recommended to use {@link #setAttribute(AttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + SpanBuilder setAttribute(String key, double value); + + /** + * Sets an attribute to the newly created {@code Span}. If {@code SpanBuilder} previously + * contained a mapping for the key, the old value is replaced by the specified value. + * + *

    Note: It is strongly recommended to use {@link #setAttribute(AttributeKey, Object)}, and + * pre-allocate your keys, if possible. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + SpanBuilder setAttribute(String key, boolean value); + + /** + * Sets an attribute to the newly created {@code Span}. If {@code SpanBuilder} previously + * contained a mapping for the key, the old value is replaced by the specified value. + * + *

    Note: the behavior of null values is undefined, and hence strongly discouraged. + * + * @param key the key for this attribute. + * @param value the value for this attribute. + * @return this. + */ + SpanBuilder setAttribute(AttributeKey key, T value); + + /** + * Sets attributes to the {@link SpanBuilder}. If the {@link SpanBuilder} previously contained a + * mapping for any of the keys, the old values are replaced by the specified values. + * + * @param attributes the attributes + * @return this. + * @since 1.2.0 + */ + @SuppressWarnings("unchecked") + default SpanBuilder setAllAttributes(Attributes attributes) { + if (attributes == null || attributes.isEmpty()) { + return this; + } + attributes.forEach( + (attributeKey, value) -> setAttribute((AttributeKey) attributeKey, value)); + return this; + } + + /** + * Sets the {@link SpanKind} for the newly created {@code Span}. If not called, the implementation + * will provide a default value {@link SpanKind#INTERNAL}. + * + * @param spanKind the kind of the newly created {@code Span}. + * @return this. + */ + SpanBuilder setSpanKind(SpanKind spanKind); + + /** + * Sets an explicit start timestamp for the newly created {@code Span}. + * + *

    LIRInstruction.Use this method to specify an explicit start timestamp. If not called, the + * implementation will use the timestamp value at {@link #startSpan()} time, which should be the + * default case. + * + *

    Important this is NOT equivalent with System.nanoTime(). + * + * @param startTimestamp the explicit start timestamp from the epoch of the newly created {@code + * Span}. + * @param unit the unit of the timestamp. + * @return this. + */ + SpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit); + + /** + * Sets an explicit start timestamp for the newly created {@code Span}. + * + *

    Use this method to specify an explicit start timestamp. If not called, the implementation + * will use the timestamp value at {@link #startSpan()} time, which should be the default case. + * + *

    Important this is NOT equivalent with System.nanoTime(). + * + * @param startTimestamp the explicit start timestamp from the epoch of the newly created {@code + * Span}. + * @return this. + */ + default SpanBuilder setStartTimestamp(Instant startTimestamp) { + if (startTimestamp == null) { + return this; + } + return setStartTimestamp( + SECONDS.toNanos(startTimestamp.getEpochSecond()) + startTimestamp.getNano(), NANOSECONDS); + } + + /** + * Starts a new {@link Span}. + * + *

    Users must manually call {@link Span#end()} to end this {@code Span}. + * + *

    Does not install the newly created {@code Span} to the current Context. + * + *

    IMPORTANT: This method can be called only once per {@link SpanBuilder} instance and as the + * last method called. After this method is called calling any method is undefined behavior. + * + *

    Example of usage: + * + *

    {@code
    +   * class MyClass {
    +   *   private static final Tracer tracer = OpenTelemetry.get().getTracer("com.example.rpc");
    +   *
    +   *   void doWork(Span parent) {
    +   *     Span childSpan = tracer.spanBuilder("MyChildSpan")
    +   *          .setParent(Context.current().with(parent))
    +   *          .startSpan();
    +   *     childSpan.addEvent("my event");
    +   *     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.
    +   *     }
    +   *   }
    +   * }
    +   * }
    + * + * @return the newly created {@code Span}. + */ + Span startSpan(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanContext.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanContext.java new file mode 100644 index 000000000..f3cc979d7 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanContext.java @@ -0,0 +1,152 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import java.util.Map; + +/** + * 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 options + * (currently only whether the context is sampled or not), as well as the {@link TraceState + * traceState} and the {@link boolean remote} flag. + * + *

    Implementations of this interface *must* be immutable and have well-defined value-based + * equals/hashCode implementations. If an implementation does not strictly conform to these + * requirements, behavior of the OpenTelemetry APIs and default SDK cannot be guaranteed. It is + * strongly suggested that you use the implementation that is provided here via {@link + * #create(String, String, TraceFlags, TraceState, Map)} or {@link #createFromRemoteParent(String, + * String, TraceFlags, TraceState, Map)}. + */ +public interface SpanContext { + + /** + * Returns the invalid {@code SpanContext} that can be used for no-op operations. + * + * @return the invalid {@code SpanContext}. + */ + static SpanContext getInvalid() { + return ImmutableSpanContext.INVALID; + } + + static SpanContext getInValidWithHeraContxt(Map heraContext) { + return ImmutableSpanContext.createInvalidWithHeraContext(heraContext); + } + + /** + * Creates a new {@code SpanContext} with the given identifiers and options. + * + *

    If the traceId or the spanId are invalid (ie. do not conform to the requirements for + * hexadecimal ids of the appropriate lengths), both will be replaced with the standard "invalid" + * versions (i.e. all '0's). See {@link SpanId#isValid(CharSequence)} and {@link + * TraceId#isValid(CharSequence)} for details. + * + * @param traceIdHex the trace identifier of the {@code SpanContext}. + * @param spanIdHex the span identifier of the {@code SpanContext}. + * @param traceFlags the trace flags of the {@code SpanContext}. + * @param traceState the trace state for the {@code SpanContext}. + * @return a new {@code SpanContext} with the given identifiers and options. + */ + static SpanContext create( + String traceIdHex, String spanIdHex, TraceFlags traceFlags, TraceState traceState, Map heraContext) { + return ImmutableSpanContext.create( + traceIdHex, spanIdHex, traceFlags, traceState, /* remote=*/ false, heraContext); + } + + /** + * Creates a new {@code SpanContext} that was propagated from a remote parent, with the given + * identifiers and options. + * + *

    If the traceId or the spanId are invalid (ie. do not conform to the requirements for + * hexadecimal ids of the appropriate lengths), both will be replaced with the standard "invalid" + * versions (i.e. all '0's). See {@link SpanId#isValid(CharSequence)} and {@link + * TraceId#isValid(CharSequence)} for details. + * + * @param traceIdHex the trace identifier of the {@code SpanContext}. + * @param spanIdHex the span identifier of the {@code SpanContext}. + * @param traceFlags the trace flags of the {@code SpanContext}. + * @param traceState the trace state for the {@code SpanContext}. + * @return a new {@code SpanContext} with the given identifiers and options. + */ + static SpanContext createFromRemoteParent( + String traceIdHex, String spanIdHex, TraceFlags traceFlags, TraceState traceState, Map heraContext) { + return ImmutableSpanContext.create( + traceIdHex, spanIdHex, traceFlags, traceState, /* remote=*/ true, heraContext); + } + + /** + * Returns the trace identifier associated with this {@link SpanContext} as 32 character lowercase + * hex String. + * + * @return the trace identifier associated with this {@link SpanContext} as lowercase hex. + */ + String getTraceId(); + + /** + * Returns the trace identifier associated with this {@link SpanContext} as 16-byte array. + * + * @return the trace identifier associated with this {@link SpanContext} as 16-byte array. + */ + default byte[] getTraceIdBytes() { + return OtelEncodingUtils.bytesFromBase16(getTraceId(), TraceId.getLength()); + } + + /** + * Returns the span identifier associated with this {@link SpanContext} as 16 character lowercase + * hex String. + * + * @return the span identifier associated with this {@link SpanContext} as 16 character lowercase + * hex (base16) String. + */ + String getSpanId(); + + /** + * Returns the span identifier associated with this {@link SpanContext} as 8-byte array. + * + * @return the span identifier associated with this {@link SpanContext} as 8-byte array. + */ + default byte[] getSpanIdBytes() { + return OtelEncodingUtils.bytesFromBase16(getSpanId(), SpanId.getLength()); + } + + /** Whether the span in this context is sampled. */ + default boolean isSampled() { + return getTraceFlags().isSampled(); + } + + /** + * Returns the trace flags associated with this {@link SpanContext}. + * + * @return the trace flags associated with this {@link SpanContext}. + */ + TraceFlags getTraceFlags(); + + /** + * Returns the {@code TraceState} associated with this {@code SpanContext}. + * + * @return the {@code TraceState} associated with this {@code SpanContext}. + */ + TraceState getTraceState(); + + /** + * Returns {@code true} if this {@code SpanContext} is valid. + * + * @return {@code true} if this {@code SpanContext} is valid. + */ + default boolean isValid() { + return TraceId.isValid(getTraceId()) && SpanId.isValid(getSpanId()); + } + + /** + * Returns {@code true} if the {@code SpanContext} was propagated from a remote parent. + * + * @return {@code true} if the {@code SpanContext} was propagated from a remote parent. + */ + boolean isRemote(); + + Map getHeraContext(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanContextKey.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanContextKey.java new file mode 100644 index 000000000..be8c27938 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanContextKey.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import io.opentelemetry.context.ContextKey; +import javax.annotation.concurrent.Immutable; + +/** Util class to hold on to the key for storing a Span in the Context. */ +@Immutable +final class SpanContextKey { + static final ContextKey KEY = ContextKey.named("opentelemetry-trace-span-key"); + + private SpanContextKey() {} +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java new file mode 100644 index 000000000..786adfa59 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import io.opentelemetry.api.internal.TemporaryBuffers; +import javax.annotation.concurrent.Immutable; + +/** + * Helper methods for dealing with a span identifier. A valid span identifier is a 16 character + * lowercase hex (base16) String, where at least one of the characters is not a "0". + * + *

    There are two more other representation that this class helps with: + * + *

      + *
    • Bytes: a 8-byte array, where valid means that at least one of the bytes is not `\0`. + *
    • Long: a {@code long} value, where valid means that the value is non-zero. + *
    + */ +@Immutable +public final class SpanId { + private static final int BYTES_LENGTH = 8; + private static final int HEX_LENGTH = 2 * BYTES_LENGTH; + private static final String INVALID = "0000000000000000"; + + private SpanId() {} + + /** + * Returns the length of the lowercase hex (base16) representation of the {@code SpanId}. + * + * @return the length of the lowercase hex (base16) representation of the {@code SpanId}. + */ + public static int getLength() { + return HEX_LENGTH; + } + + /** + * Returns the invalid {@code SpanId} in lowercase hex (base16) representation. All characters are + * "0". + * + * @return the invalid {@code SpanId} lowercase in hex (base16) representation. + */ + public static String getInvalid() { + return INVALID; + } + + /** + * Returns whether the span identifier is valid. A valid span identifier is a 16 character hex + * String, where at least one of the characters is not a '0'. + * + * @return {@code true} if the span identifier is valid. + */ + public static boolean isValid(CharSequence spanId) { + return spanId != null + && spanId.length() == HEX_LENGTH + && !INVALID.contentEquals(spanId) + && OtelEncodingUtils.isValidBase16String(spanId); + } + + /** + * Returns the lowercase hex (base16) representation of the {@code SpanId} converted from the + * given bytes representation, or {@link #getInvalid()} if input is {@code null} or the given byte + * array is too short. + * + *

    It converts the first 8 bytes of the given byte array. + * + * @param spanIdBytes the bytes (8-byte array) representation of the {@code SpanId}. + * @return the lowercase hex (base16) representation of the {@code SpanId}. + */ + public static String fromBytes(byte[] spanIdBytes) { + if (spanIdBytes == null || spanIdBytes.length < BYTES_LENGTH) { + return INVALID; + } + char[] result = TemporaryBuffers.chars(HEX_LENGTH); + OtelEncodingUtils.bytesToBase16(spanIdBytes, result, BYTES_LENGTH); + return new String(result, 0, HEX_LENGTH); + } + + /** + * Returns the lowercase hex (base16) representation of the {@code SpanId} converted from the + * given {@code long} value representation. + * + *

    There is no restriction on the specified values, other than the already established validity + * rules applying to {@code SpanId}. Specifying 0 for the long value will effectively return + * {@link #getInvalid()}. + * + *

    This is equivalent to calling {@link #fromBytes(byte[])} with the specified value stored as + * big-endian. + * + * @param id {@code long} value representation of the {@code SpanId}. + * @return the lowercase hex (base16) representation of the {@code SpanId}. + */ + public static String fromLong(long id) { + if (id == 0) { + return getInvalid(); + } + char[] result = TemporaryBuffers.chars(HEX_LENGTH); + OtelEncodingUtils.longToBase16String(id, result, 0); + return new String(result, 0, HEX_LENGTH); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanKind.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanKind.java new file mode 100644 index 000000000..6a774e872 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/SpanKind.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +/** + * Type of {@link Span}. Can be used to specify additional relationships between spans in addition + * to a parent/child relationship. + */ +public enum SpanKind { + /** Default value. Indicates that the span is used internally. */ + INTERNAL, + + /** Indicates that the span covers server-side handling of an RPC or other remote request. */ + SERVER, + + /** + * Indicates that the span covers the client-side wrapper around an RPC or other remote request. + */ + CLIENT, + + /** + * Indicates that the span describes producer sending a message to a broker. Unlike client and + * server, there is no direct critical path latency relationship between producer and consumer + * spans. + */ + PRODUCER, + + /** + * Indicates that the span describes consumer receiving a message from a broker. Unlike client and + * server, there is no direct critical path latency relationship between producer and consumer + * spans. + */ + CONSUMER +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/StatusCode.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/StatusCode.java new file mode 100644 index 000000000..6259e32d2 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/StatusCode.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +/** + * 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. + */ +public enum StatusCode { + /** The default status. */ + UNSET, + + /** + * The operation has been validated by an Application developers or Operator to have completed + * successfully. + */ + OK, + + /** The operation contains an error. */ + ERROR +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlags.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlags.java new file mode 100644 index 000000000..936a1a6e0 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlags.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import javax.annotation.concurrent.Immutable; + +/** + * A valid trace flags is a byte or 2 character lowercase hex (base16) String. + * + *

    These options are propagated to all child {@link Span spans}. These determine features such as + * whether a {@code Span} should be traced. + */ +@Immutable +public interface TraceFlags { + /** + * Returns the length of the lowercase hex (base16) representation of the {@link TraceFlags}. + * + * @return the length of the lowercase hex (base16) representation of the {@link TraceFlags}. + */ + static int getLength() { + return ImmutableTraceFlags.HEX_LENGTH; + } + + /** + * Returns the default (with all flag bits off) byte representation of the {@link TraceFlags}. + * + * @return the default (with all flag bits off) byte representation of the {@link TraceFlags}. + */ + static TraceFlags getDefault() { + return ImmutableTraceFlags.DEFAULT; + } + + /** + * Returns the lowercase hex (base16) representation of the {@link TraceFlags} with the sampling + * flag bit on. + * + * @return the lowercase hex (base16) representation of the {@link TraceFlags} with the sampling + * flag bit on. + */ + static TraceFlags getSampled() { + return ImmutableTraceFlags.SAMPLED; + } + + /** + * Returns the {@link TraceFlags} converted from the given lowercase hex (base16) representation. + * + *

    This may throw runtime exceptions if the input is invalid. + * + * @param src the buffer where the hex (base16) representation of the {@link TraceFlags} is. + * @param srcOffset the offset int buffer. + * @return the {@link TraceFlags} converted from the given lowercase hex (base16) representation. + */ + static TraceFlags fromHex(CharSequence src, int srcOffset) { + return ImmutableTraceFlags.fromHex(src, srcOffset); + } + + /** + * Returns the {@link TraceFlags} converted from the given byte representation. + * + * @param traceFlagsByte the byte representation of the {@link TraceFlags}. + * @return the {@link TraceFlags} converted from the given byte representation. + */ + static TraceFlags fromByte(byte traceFlagsByte) { + return ImmutableTraceFlags.fromByte(traceFlagsByte); + } + + /** + * Returns {@code true} if the sampling bit is on for this {@link TraceFlags}, otherwise {@code + * false}. + * + * @return {@code true} if the sampling bit is on for this {@link TraceFlags}, otherwise {@code * + * false}. + */ + boolean isSampled(); + + /** + * Returns the lowercase hex (base16) representation of this {@link TraceFlags}. + * + * @return the byte representation of the {@link TraceFlags}. + */ + String asHex(); + + /** + * Returns the byte representation of this {@link TraceFlags}. + * + * @return the byte representation of the {@link TraceFlags}. + */ + byte asByte(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java new file mode 100644 index 000000000..59587bd44 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import io.opentelemetry.api.internal.TemporaryBuffers; +import javax.annotation.concurrent.Immutable; + +/** + * Helper methods for dealing with a trace identifier. A valid trace identifier is a 32 character + * lowercase hex (base16) String, where at least one of the characters is not a "0". + * + *

    There are two more other representation that this class helps with: + * + *

      + *
    • Bytes: a 16-byte array, where valid means that at least one of the bytes is not `\0`. + *
    • Long: two {@code long} values, where valid means that at least one of values is non-zero. + * To avoid allocating new objects this representation uses two parts, "high part" + * representing the left most part of the {@code TraceId} and "low part" representing the + * right most part of the {@code TraceId}. This is equivalent with the values being stored as + * big-endian. + *
    + */ +@Immutable +public final class TraceId { + private static final int BYTES_LENGTH = 16; + private static final int HEX_LENGTH = 2 * BYTES_LENGTH; + private static final String INVALID = "00000000000000000000000000000000"; + + private TraceId() {} + + /** + * Returns the length of the lowercase hex (base16) representation of the {@code TraceId}. + * + * @return the length of the lowercase hex (base16) representation of the {@code TraceId}. + */ + public static int getLength() { + return HEX_LENGTH; + } + + /** + * Returns the invalid {@code TraceId} in lowercase hex (base16) representation. All characters + * are "0". + * + * @return the invalid {@code TraceId} in lowercase hex (base16) representation. + */ + public static String getInvalid() { + return INVALID; + } + + /** + * Returns whether the {@code TraceId} is valid. A valid trace identifier is a 32 character hex + * String, where at least one of the characters is not a '0'. + * + * @return {@code true} if the {@code TraceId} is valid. + */ + public static boolean isValid(CharSequence traceId) { + return traceId != null + && traceId.length() == HEX_LENGTH + && !INVALID.contentEquals(traceId) + && OtelEncodingUtils.isValidBase16String(traceId); + } + + /** + * Returns the lowercase hex (base16) representation of the {@code TraceId} converted from the + * given bytes representation, or {@link #getInvalid()} if input is {@code null} or the given byte + * array is too short. + * + *

    It converts the first 26 bytes of the given byte array. + * + * @param traceIdBytes the bytes (16-byte array) representation of the {@code TraceId}. + * @return the lowercase hex (base16) representation of the {@code TraceId}. + */ + public static String fromBytes(byte[] traceIdBytes) { + if (traceIdBytes == null || traceIdBytes.length < BYTES_LENGTH) { + return INVALID; + } + char[] result = TemporaryBuffers.chars(HEX_LENGTH); + OtelEncodingUtils.bytesToBase16(traceIdBytes, result, BYTES_LENGTH); + return new String(result, 0, HEX_LENGTH); + } + + /** + * Returns the bytes (16-byte array) representation of the {@code TraceId} converted from the + * given two {@code long} values representing the lower and higher parts. + * + *

    There is no restriction on the specified values, other than the already established validity + * rules applying to {@code TraceId}. Specifying 0 for both values will effectively return {@link + * #getInvalid()}. + * + *

    This is equivalent to calling {@link #fromBytes(byte[])} with the specified values stored as + * big-endian. + * + * @param traceIdLongHighPart the higher part of the long values representation of the {@code + * TraceId}. + * @param traceIdLongLowPart the lower part of the long values representation of the {@code + * TraceId}. + * @return the lowercase hex (base16) representation of the {@code TraceId}. + */ + public static String fromLongs(long traceIdLongHighPart, long traceIdLongLowPart) { + if (traceIdLongHighPart == 0 && traceIdLongLowPart == 0) { + return getInvalid(); + } + char[] chars = TemporaryBuffers.chars(HEX_LENGTH); + OtelEncodingUtils.longToBase16String(traceIdLongHighPart, chars, 0); + OtelEncodingUtils.longToBase16String(traceIdLongLowPart, chars, 16); + return new String(chars, 0, HEX_LENGTH); + } +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceState.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceState.java new file mode 100644 index 000000000..fb8e9fac4 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceState.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +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. + * + *

    Implementation is optimized for a small list of key-value pairs. + * + *

    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 /. + * + *

    Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the + * range 0x20 to 0x7E) except comma , and =. + * + *

    Implementations of this interface *must* be immutable and have well-defined value-based + * equals/hashCode implementations. If an implementation does not strictly conform to these + * requirements, behavior of the OpenTelemetry APIs and default SDK cannot be guaranteed. + * + *

    Implementations of this interface that do not conform to the W3C specification risk + * incompatibility with W3C-compatible implementations. + * + *

    For these reasons, it is strongly suggested that you use the implementation that is provided + * here via the {@link TraceState#builder}. + */ +@Immutable +public interface TraceState { + + /** + * Returns the default {@code TraceState} with no entries. + * + *

    This method is equivalent to calling {@code #builder().build()}, but avoids new allocations. + * + * @return the default {@code TraceState} with no entries. + */ + static TraceState getDefault() { + return ArrayBasedTraceStateBuilder.empty(); + } + + /** Returns an empty {@code TraceStateBuilder}. */ + static TraceStateBuilder builder() { + return new ArrayBasedTraceStateBuilder(); + } + + /** + * 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. + */ + @Nullable + String get(String key); + + /** Returns the number of entries in this {@link TraceState}. */ + int size(); + + /** Returns whether this {@link TraceState} is empty, containing no entries. */ + boolean isEmpty(); + + /** Iterates over all the key-value entries contained in this {@link TraceState}. */ + void forEach(BiConsumer consumer); + + /** Returns a read-only view of this {@link TraceState} as a {@link Map}. */ + Map asMap(); + + /** + * Returns a {@code Builder} based on this {@code TraceState}. + * + * @return a {@code Builder} based on this {@code TraceState}. + */ + TraceStateBuilder toBuilder(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceStateBuilder.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceStateBuilder.java new file mode 100644 index 000000000..153570dea --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TraceStateBuilder.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +/** + * A builder of {@link TraceState}. This implementation does full validation of the keys and values + * in the entries, and will ignore any entries that do not conform to the W3C specification. + */ +public interface TraceStateBuilder { + /** + * 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. + */ + TraceStateBuilder put(String key, String value); + + /** + * 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. + */ + TraceStateBuilder remove(String key); + + /** + * 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. + */ + TraceState build(); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/Tracer.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/Tracer.java new file mode 100644 index 000000000..f3b0703b3 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/Tracer.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * Tracer is the interface for {@link Span} creation and interaction with the in-process context. + * + *

    Users may choose to use manual or automatic Context propagation. Because of that this class + * offers APIs to facilitate both usages. + * + *

    The automatic context propagation is done using {@link io.opentelemetry.context.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.opentelemetry.context.Context} between different threads. + * + *

    Example usage with automatic context propagation: + * + *

    {@code
    + * class MyClass {
    + *   private static final Tracer tracer =
    + *     openTelemetry.getTracer("instrumentation-library-name", "1.0.0");
    + *   void doWork() {
    + *     Span span = tracer.spanBuilder("MyClass.DoWork").startSpan();
    + *     try (Scope ignored = span.makeCurrent()) {
    + *       Span.current().addEvent("Starting the work.");
    + *       doWorkInternal();
    + *       Span.current().addEvent("Finished working.");
    + *     } finally {
    + *       span.end();
    + *     }
    + *   }
    + * }
    + * }
    + * + *

    Example usage with manual context propagation: + * + *

    {@code
    + * class MyClass {
    + *   private static final Tracer tracer =
    + *     openTelemetry.getTracer("instrumentation-library-name", "1.0.0");
    + *   void doWork(Span parent) {
    + *     Span childSpan = tracer.spanBuilder("MyChildSpan")
    + *         setParent(parent).startSpan();
    + *     childSpan.addEvent("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.
    + *     }
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface Tracer { + + /** + * Returns a {@link SpanBuilder} to create and start a new {@link Span}. + * + *

    See {@link SpanBuilder} for usage examples. + * + * @param spanName The name of the returned Span. + * @return a {@code Span.Builder} to create and start a new {@code Span}. + */ + SpanBuilder spanBuilder(String spanName); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TracerProvider.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TracerProvider.java new file mode 100644 index 000000000..a47f9462a --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/TracerProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * A registry for creating named {@link Tracer}s. The name Provider is for consistency with * + * other languages and it is NOT loaded using reflection. + * + * @see Tracer + */ +@ThreadSafe +public interface TracerProvider { + + /** + * Returns a no-op {@link TracerProvider} which only creates no-op {@link Span}s which do not + * record nor are emitted. + */ + static TracerProvider noop() { + return DefaultTracerProvider.getInstance(); + } + + /** + * Gets or creates a named tracer instance. + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library (e.g., "io.opentelemetry.contrib.mongodb"). Must not be null. If the + * instrumented library is providing its own instrumentation, this should match the library + * name. + * @return a tracer instance. + */ + Tracer get(String instrumentationName); + + /** + * Gets or creates a named and versioned tracer instance. + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library (e.g., "io.opentelemetry.contrib.mongodb"). Must not be null. If the + * instrumented library is providing its own instrumentation, this should match the library + * name. + * @param instrumentationVersion The version of the instrumentation library (e.g., "1.0.0"). + * @return a tracer instance. + */ + Tracer get(String instrumentationName, String instrumentationVersion); +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/package-info.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/package-info.java new file mode 100644 index 000000000..2970a7eea --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * API for distributed tracing. + * + *

    Distributed tracing, also called distributed request tracing, is a technique that helps + * debugging distributed applications. + * + *

    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. + * + *

    {@link io.opentelemetry.api.trace.Span} represents a single operation within a trace. + * + *

    {@link io.opentelemetry.api.trace.Span Spans} are propagated in-process in the {@link + * io.opentelemetry.context.Context} and between process using one of the wire propagation formats + * supported in the {@code opentelemetry.trace.propagation} package. + */ +// TODO: Add code examples. +@ParametersAreNonnullByDefault +package io.opentelemetry.api.trace; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagator.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagator.java new file mode 100644 index 000000000..3b161873b --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagator.java @@ -0,0 +1,285 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace.propagation; + +import static io.opentelemetry.api.internal.Utils.checkArgument; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import io.opentelemetry.api.internal.TemporaryBuffers; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.TraceStateBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Implementation of the W3C TraceContext propagation protocol. See W3C Trace Context. + * + *

    This is the default propagator for {@link SpanContext}s. The {@link SpanContext} type is + * designed to support all the data propagated via W3C propagation natively. + */ +@Immutable +@SuppressWarnings("SystemOut") +public final class W3CTraceContextPropagator implements TextMapPropagator { + private static final Logger logger = Logger.getLogger(W3CTraceContextPropagator.class.getName()); + + static final String TRACE_PARENT = "traceparent"; + static final String TRACE_STATE = "tracestate"; + private static final List FIELDS = + Collections.unmodifiableList(Arrays.asList(TRACE_PARENT, TRACE_STATE)); + + private static final String VERSION = "00"; + private static final int VERSION_SIZE = 2; + private static final char TRACEPARENT_DELIMITER = '-'; + private static final int TRACEPARENT_DELIMITER_SIZE = 1; + private static final int TRACE_ID_HEX_SIZE = TraceId.getLength(); + private static final int SPAN_ID_HEX_SIZE = SpanId.getLength(); + private static final int TRACE_OPTION_HEX_SIZE = TraceFlags.getLength(); + private static final int TRACE_ID_OFFSET = VERSION_SIZE + TRACEPARENT_DELIMITER_SIZE; + private static final int SPAN_ID_OFFSET = + TRACE_ID_OFFSET + TRACE_ID_HEX_SIZE + TRACEPARENT_DELIMITER_SIZE; + private static final int TRACE_OPTION_OFFSET = + SPAN_ID_OFFSET + SPAN_ID_HEX_SIZE + TRACEPARENT_DELIMITER_SIZE; + private static final int TRACEPARENT_HEADER_SIZE = TRACE_OPTION_OFFSET + TRACE_OPTION_HEX_SIZE; + private static final int TRACESTATE_MAX_SIZE = 512; + private static final int TRACESTATE_MAX_MEMBERS = 32; + private static final char TRACESTATE_KEY_VALUE_DELIMITER = '='; + private static final char TRACESTATE_ENTRY_DELIMITER = ','; + private static final Pattern TRACESTATE_ENTRY_DELIMITER_SPLIT_PATTERN = + Pattern.compile("[ \t]*" + TRACESTATE_ENTRY_DELIMITER + "[ \t]*"); + private static final Set VALID_VERSIONS; + private static final String VERSION_00 = "00"; + private static final W3CTraceContextPropagator INSTANCE = new W3CTraceContextPropagator(); + + static { + // A valid version is 1 byte representing an 8-bit unsigned integer, version ff is invalid. + VALID_VERSIONS = new HashSet<>(); + for (int i = 0; i < 255; i++) { + String version = Long.toHexString(i); + if (version.length() < 2) { + version = '0' + version; + } + VALID_VERSIONS.add(version); + } + } + + private W3CTraceContextPropagator() { + // singleton + } + + /** + * Returns a singleton instance of a {@link TextMapPropagator} implementing the W3C TraceContext + * propagation. + */ + public static W3CTraceContextPropagator getInstance() { + return INSTANCE; + } + + @Override + public Collection fields() { + return FIELDS; + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null || setter == null) { + return; + } + + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + if (!spanContext.isValid()) { + return; + } + + char[] chars = TemporaryBuffers.chars(TRACEPARENT_HEADER_SIZE); + chars[0] = VERSION.charAt(0); + chars[1] = VERSION.charAt(1); + chars[2] = TRACEPARENT_DELIMITER; + + String traceId = spanContext.getTraceId(); + traceId.getChars(0, traceId.length(), chars, TRACE_ID_OFFSET); + + chars[SPAN_ID_OFFSET - 1] = TRACEPARENT_DELIMITER; + + String spanId = spanContext.getSpanId(); + spanId.getChars(0, spanId.length(), chars, SPAN_ID_OFFSET); + + chars[TRACE_OPTION_OFFSET - 1] = TRACEPARENT_DELIMITER; + String traceFlagsHex = spanContext.getTraceFlags().asHex(); + chars[TRACE_OPTION_OFFSET] = traceFlagsHex.charAt(0); + chars[TRACE_OPTION_OFFSET + 1] = traceFlagsHex.charAt(1); + setter.set(carrier, TRACE_PARENT, new String(chars, 0, TRACEPARENT_HEADER_SIZE)); + Map heraContext = spanContext.getHeraContext(); + if (heraContext != null && heraContext.size() > 0 && heraContext.get(HeraContext.HERA_CONTEXT_PROPAGATOR_KEY) != null){ + setter.set(carrier, HeraContext.HERA_CONTEXT_PROPAGATOR_KEY, heraContext.get(HeraContext.HERA_CONTEXT_PROPAGATOR_KEY)); + } + TraceState traceState = spanContext.getTraceState(); + if (traceState.isEmpty()) { + // No need to add an empty "tracestate" header. + return; + } + StringBuilder stringBuilder = new StringBuilder(TRACESTATE_MAX_SIZE); + traceState.forEach( + (key, value) -> { + if (stringBuilder.length() != 0) { + stringBuilder.append(TRACESTATE_ENTRY_DELIMITER); + } + stringBuilder.append(key).append(TRACESTATE_KEY_VALUE_DELIMITER).append(value); + }); + setter.set(carrier, TRACE_STATE, stringBuilder.toString()); + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Context.root(); + } + if (getter == null) { + return context; + } + + SpanContext spanContext = extractImpl(carrier, getter); + if (!spanContext.isValid()) { + Map heraContext = spanContext.getHeraContext(); + if(heraContext == null || heraContext.size() == 0) { + return context; + } + } + + return context.with(Span.wrap(spanContext)); + } + + private static SpanContext extractImpl(@Nullable C carrier, TextMapGetter getter) { + String traceParent = getter.get(carrier, TRACE_PARENT); + String heraContext = getter.get(carrier, HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + if (traceParent == null) { + if(heraContext == null || heraContext.isEmpty()) { + return SpanContext.getInvalid(); + }else{ + return SpanContext.getInValidWithHeraContxt(HeraContext.wrap(heraContext)); + } + } + SpanContext contextFromParentHeader = extractContextFromTraceParent(traceParent,heraContext); + if (!contextFromParentHeader.isValid()) { + return contextFromParentHeader; + } + + String traceStateHeader = getter.get(carrier, TRACE_STATE); + if (traceStateHeader == null || traceStateHeader.isEmpty()) { + return contextFromParentHeader; + } + + try { + TraceState traceState = extractTraceState(traceStateHeader); + return SpanContext.createFromRemoteParent( + contextFromParentHeader.getTraceId(), + contextFromParentHeader.getSpanId(), + contextFromParentHeader.getTraceFlags(), + traceState,HeraContext.wrap(heraContext)); + } catch (IllegalArgumentException e) { + logger.fine("Unparseable tracestate header. Returning span context without state."); + return contextFromParentHeader; + } + } + + private static SpanContext extractContextFromTraceParent(String traceparent,String heraContext) { + // TODO(bdrutu): Do we need to verify that version is hex and that + // for the version the length is the expected one? + boolean isValid = + (traceparent.length() == TRACEPARENT_HEADER_SIZE + || (traceparent.length() > TRACEPARENT_HEADER_SIZE + && traceparent.charAt(TRACEPARENT_HEADER_SIZE) == TRACEPARENT_DELIMITER)) + && traceparent.charAt(TRACE_ID_OFFSET - 1) == TRACEPARENT_DELIMITER + && traceparent.charAt(SPAN_ID_OFFSET - 1) == TRACEPARENT_DELIMITER + && traceparent.charAt(TRACE_OPTION_OFFSET - 1) == TRACEPARENT_DELIMITER; + if (!isValid) { + logger.fine("Unparseable traceparent header. Returning INVALID span context."); + if(heraContext == null || heraContext.isEmpty()){ + return SpanContext.getInvalid(); + }else{ + return SpanContext.getInValidWithHeraContxt(HeraContext.wrap(heraContext)); + } + + } + + String version = traceparent.substring(0, 2); + if (!VALID_VERSIONS.contains(version)) { + if(heraContext == null || heraContext.isEmpty()){ + return SpanContext.getInvalid(); + }else{ + return SpanContext.getInValidWithHeraContxt(HeraContext.wrap(heraContext)); + } + } + if (version.equals(VERSION_00) && traceparent.length() > TRACEPARENT_HEADER_SIZE) { + if(heraContext == null || heraContext.isEmpty()){ + return SpanContext.getInvalid(); + }else{ + return SpanContext.getInValidWithHeraContxt(HeraContext.wrap(heraContext)); + } + } + + String traceId = traceparent.substring(TRACE_ID_OFFSET, TRACE_ID_OFFSET + TraceId.getLength()); + String spanId = traceparent.substring(SPAN_ID_OFFSET, SPAN_ID_OFFSET + SpanId.getLength()); + char firstTraceFlagsChar = traceparent.charAt(TRACE_OPTION_OFFSET); + char secondTraceFlagsChar = traceparent.charAt(TRACE_OPTION_OFFSET + 1); + + if (!OtelEncodingUtils.isValidBase16Character(firstTraceFlagsChar) + || !OtelEncodingUtils.isValidBase16Character(secondTraceFlagsChar)) { + if(heraContext == null || heraContext.isEmpty()){ + return SpanContext.getInvalid(); + }else{ + return SpanContext.getInValidWithHeraContxt(HeraContext.wrap(heraContext)); + } + } + + TraceFlags traceFlags = + TraceFlags.fromByte( + OtelEncodingUtils.byteFromBase16(firstTraceFlagsChar, secondTraceFlagsChar)); + return SpanContext.createFromRemoteParent(traceId, spanId, traceFlags, TraceState.getDefault(), + HeraContext.wrap(heraContext)); + } + + private static TraceState extractTraceState(String traceStateHeader) { + TraceStateBuilder traceStateBuilder = TraceState.builder(); + String[] listMembers = TRACESTATE_ENTRY_DELIMITER_SPLIT_PATTERN.split(traceStateHeader); + checkArgument( + listMembers.length <= TRACESTATE_MAX_MEMBERS, "TraceState has too many elements."); + // Iterate in reverse order because when call builder set the elements is added in the + // front of the list. + for (int i = listMembers.length - 1; i >= 0; i--) { + String listMember = listMembers[i]; + int index = listMember.indexOf(TRACESTATE_KEY_VALUE_DELIMITER); + checkArgument(index != -1, "Invalid TraceState list-member format."); + traceStateBuilder.put(listMember.substring(0, index), listMember.substring(index + 1)); + } + TraceState traceState = traceStateBuilder.build(); + if (traceState.size() != listMembers.length) { + // Validation failure, drop the tracestate + return TraceState.getDefault(); + } + return traceState; + } + +} diff --git a/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/propagation/package-info.java b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/propagation/package-info.java new file mode 100644 index 000000000..bd0c0ac35 --- /dev/null +++ b/opentelemetry-java/api/all/src/main/java/io/opentelemetry/api/trace/propagation/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Default OpenTelemetry remote trace propagators. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.api.trace.propagation; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/OpenTelemetryTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/OpenTelemetryTest.java new file mode 100644 index 000000000..1d633473b --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/OpenTelemetryTest.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.propagation.ContextPropagators; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class OpenTelemetryTest { + + @BeforeAll + static void beforeClass() { + GlobalOpenTelemetry.resetForTest(); + } + + @AfterEach + void after() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + void testDefault() { + assertThat(OpenTelemetry.noop().getTracerProvider()).isSameAs(TracerProvider.noop()); + assertThat(OpenTelemetry.noop().getPropagators()).isSameAs(ContextPropagators.noop()); + } + + @Test + void propagating() { + ContextPropagators contextPropagators = mock(ContextPropagators.class); + OpenTelemetry openTelemetry = OpenTelemetry.propagating(contextPropagators); + + assertThat(openTelemetry.getTracerProvider()).isSameAs(TracerProvider.noop()); + assertThat(openTelemetry.getPropagators()).isSameAs(contextPropagators); + } + + @Test + void testGlobalBeforeSet() { + assertThat(GlobalOpenTelemetry.getTracerProvider()).isSameAs(TracerProvider.noop()); + assertThat(GlobalOpenTelemetry.getTracerProvider()) + .isSameAs(GlobalOpenTelemetry.getTracerProvider()); + assertThat(GlobalOpenTelemetry.getPropagators()).isSameAs(GlobalOpenTelemetry.getPropagators()); + } + + @Test + void independentNonGlobalPropagators() { + ContextPropagators propagators1 = mock(ContextPropagators.class); + OpenTelemetry otel1 = OpenTelemetry.propagating(propagators1); + ContextPropagators propagators2 = mock(ContextPropagators.class); + OpenTelemetry otel2 = OpenTelemetry.propagating(propagators2); + + assertThat(otel1.getPropagators()).isSameAs(propagators1); + assertThat(otel2.getPropagators()).isSameAs(propagators2); + } + + @Test + void setThenSet() { + setOpenTelemetry(); + assertThatThrownBy(() -> GlobalOpenTelemetry.set(OpenTelemetry.noop())) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("GlobalOpenTelemetry.set has already been called") + .hasStackTraceContaining("setOpenTelemetry"); + } + + @Test + void getThenSet() { + assertThat(getOpenTelemetry()).isInstanceOf(DefaultOpenTelemetry.class); + assertThatThrownBy(() -> GlobalOpenTelemetry.set(OpenTelemetry.noop())) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("GlobalOpenTelemetry.set has already been called") + .hasStackTraceContaining("getOpenTelemetry"); + } + + private static void setOpenTelemetry() { + GlobalOpenTelemetry.set(OpenTelemetry.noop()); + } + + private static OpenTelemetry getOpenTelemetry() { + return GlobalOpenTelemetry.get(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageContextTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageContextTest.java new file mode 100644 index 000000000..b8dfd63e1 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageContextTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.junit.jupiter.api.Test; + +class BaggageContextTest { + + @Test + void testGetCurrentBaggage_Default() { + try (Scope s = Context.root().makeCurrent()) { + Baggage baggage = Baggage.current(); + assertThat(baggage).isSameAs(Baggage.empty()); + } + } + + @Test + void testGetCurrentBaggage_SetCorrContext() { + Baggage baggage = Baggage.empty(); + try (Scope ignored = Context.root().with(baggage).makeCurrent()) { + assertThat(Baggage.current()).isSameAs(baggage); + } + } + + @Test + void testGetBaggage_DefaultContext() { + Baggage baggage = Baggage.fromContext(Context.root()); + assertThat(baggage).isSameAs(Baggage.empty()); + } + + @Test + void testGetBaggage_ExplicitContext() { + Baggage baggage = Baggage.empty(); + Context context = Context.root().with(baggage); + assertThat(Baggage.fromContext(context)).isSameAs(baggage); + } + + @Test + void testGetBaggageWithoutDefault_DefaultContext() { + Baggage baggage = Baggage.fromContextOrNull(Context.root()); + assertThat(baggage).isNull(); + } + + @Test + void testGetBaggageWithoutDefault_ExplicitContext() { + Baggage baggage = Baggage.empty(); + Context context = Context.root().with(baggage); + assertThat(Baggage.fromContextOrNull(context)).isSameAs(baggage); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageEntryMetadataTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageEntryMetadataTest.java new file mode 100644 index 000000000..be5dc952c --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageEntryMetadataTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.testing.EqualsTester; +import org.junit.jupiter.api.Test; + +class BaggageEntryMetadataTest { + + @Test + void getValue() { + BaggageEntryMetadata entryMetadata = BaggageEntryMetadata.create("metadata;value=foo"); + assertThat(entryMetadata.getValue()).isEqualTo("metadata;value=foo"); + } + + @Test + void nullValue() { + assertThat(BaggageEntryMetadata.create(null)).isEqualTo(BaggageEntryMetadata.empty()); + } + + @Test + void testEquals() { + new EqualsTester() + .addEqualityGroup( + BaggageEntryMetadata.create("value"), BaggageEntryMetadata.create("value")) + .addEqualityGroup(BaggageEntryMetadata.create("other value")) + .testEquals(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageTest.java new file mode 100644 index 000000000..4c141276e --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/BaggageTest.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.junit.jupiter.api.Test; + +class BaggageTest { + @Test + void current_empty() { + try (Scope scope = Context.root().makeCurrent()) { + assertThat(Baggage.current()).isEqualTo(Baggage.empty()); + } + } + + @Test + void current() { + try (Scope scope = + Context.root().with(Baggage.builder().put("foo", "bar").build()).makeCurrent()) { + Baggage result = Baggage.current(); + assertThat(result.getEntryValue("foo")).isEqualTo("bar"); + } + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ImmutableBaggageTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ImmutableBaggageTest.java new file mode 100644 index 000000000..ca78124fb --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ImmutableBaggageTest.java @@ -0,0 +1,202 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import com.google.common.testing.EqualsTester; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link Baggage} and {@link BaggageBuilder}. + * + *

    Tests for scope management with {@link Baggage} are in {@link ScopedBaggageTest}. + */ +class ImmutableBaggageTest { + + private static final BaggageEntryMetadata TMD = BaggageEntryMetadata.create("tmd"); + + private static final String K1 = "k1"; + private static final String K2 = "k2"; + + private static final String V1 = "v1"; + private static final String V2 = "v2"; + + private static final Baggage ONE_ENTRY = Baggage.builder().put(K1, V1, TMD).build(); + private static final Baggage TWO_ENTRIES = ONE_ENTRY.toBuilder().put(K2, V2, TMD).build(); + + @Test + void getEntryValue() { + assertThat(ONE_ENTRY.getEntryValue(K1)).isEqualTo(V1); + } + + @Test + void getEntryValue_nullKey() { + assertThat(ONE_ENTRY.getEntryValue(null)).isNull(); + } + + @Test + void getEntries_empty() { + Baggage baggage = Baggage.empty(); + assertThat(baggage.size()).isZero(); + assertThat(baggage.isEmpty()).isTrue(); + } + + @Test + void getEntries_nonEmpty() { + Baggage baggage = TWO_ENTRIES; + assertThat(baggage.asMap()) + .containsOnly( + entry(K1, ImmutableEntry.create(V1, TMD)), entry(K2, ImmutableEntry.create(V2, TMD))); + assertThat(baggage.size()).isEqualTo(2); + assertThat(baggage.isEmpty()).isFalse(); + } + + @Test + void getEntries_chain() { + Baggage baggage = TWO_ENTRIES.toBuilder().put(K1, V2, TMD).build(); + assertThat(baggage.asMap()) + .containsOnly( + entry(K1, ImmutableEntry.create(V2, TMD)), entry(K2, ImmutableEntry.create(V2, TMD))); + } + + @Test + void put_newKey() { + assertThat(ONE_ENTRY.toBuilder().put(K2, V2, TMD).build().asMap()) + .containsOnly( + entry(K1, ImmutableEntry.create(V1, TMD)), entry(K2, ImmutableEntry.create(V2, TMD))); + } + + @Test + void put_existingKey() { + assertThat(ONE_ENTRY.toBuilder().put(K1, V2, TMD).build().asMap()) + .containsOnly(entry(K1, ImmutableEntry.create(V2, TMD))); + } + + @Test + void put_nullKey() { + BaggageBuilder builder = ONE_ENTRY.toBuilder(); + Baggage built = builder.build(); + builder.put(null, V2, TMD); + assertThat(builder.build()).isEqualTo(built); + } + + @Test + void put_nullValue() { + BaggageBuilder builder = ONE_ENTRY.toBuilder(); + Baggage built = builder.build(); + builder.put(K2, null, TMD); + assertThat(builder.build()).isEqualTo(built); + } + + @Test + void put_nullMetadata() { + BaggageBuilder builder = ONE_ENTRY.toBuilder(); + Baggage built = builder.build(); + builder.put(K2, V2, null); + assertThat(builder.build()).isEqualTo(built); + } + + @Test + void put_keyUnprintableChars() { + BaggageBuilder builder = ONE_ENTRY.toBuilder(); + Baggage built = builder.build(); + builder.put("\2ab\3cd", "value"); + assertThat(builder.build()).isEqualTo(built); + } + + @Test + void put_keyEmpty() { + BaggageBuilder builder = ONE_ENTRY.toBuilder(); + Baggage built = builder.build(); + builder.put("", "value"); + assertThat(builder.build()).isEqualTo(built); + } + + @Test + void put_valueUnprintableChars() { + BaggageBuilder builder = ONE_ENTRY.toBuilder(); + Baggage built = builder.build(); + builder.put(K2, "\2ab\3cd"); + assertThat(builder.build()).isEqualTo(built); + } + + @Test + void remove_existingKey() { + BaggageBuilder builder = Baggage.builder(); + builder.put(K1, V1, TMD); + builder.put(K2, V2, TMD); + + assertThat(builder.remove(K1).build().asMap()) + .containsOnly(entry(K2, ImmutableEntry.create(V2, TMD))); + } + + @Test + void remove_differentKey() { + BaggageBuilder builder = Baggage.builder(); + builder.put(K1, V1, TMD); + builder.put(K2, V2, TMD); + + assertThat(builder.remove(K2).build().asMap()) + .containsOnly(entry(K1, ImmutableEntry.create(V1, TMD))); + } + + @Test + void remove_keyFromParent() { + assertThat(TWO_ENTRIES.toBuilder().remove(K1).build().asMap()) + .containsOnly(entry(K2, ImmutableEntry.create(V2, TMD))); + } + + @Test + void remove_nullKey() { + BaggageBuilder builder = Baggage.builder(); + builder.put(K2, V2); + Baggage built = builder.build(); + builder.remove(null); + assertThat(builder.build()).isEqualTo(built); + } + + @Test + void toBuilder_keepsOriginalState() { + assertThat(Baggage.empty().toBuilder().build()).isEqualTo(Baggage.empty()); + + Baggage originalBaggage = Baggage.builder().put("key", "value").build(); + assertThat(originalBaggage.toBuilder().build()).isEqualTo(originalBaggage); + } + + @Test + void toBuilder_allowChanges() { + Baggage singleItemNoParent = Baggage.builder().put("key1", "value1").build(); + Baggage singleItemWithParent = Baggage.builder().put("key1", "value1").build(); + + assertThat(Baggage.empty().toBuilder().put("key1", "value1").build()) + .isEqualTo(singleItemNoParent); + assertThat(singleItemNoParent.toBuilder().put("key2", "value2").build()) + .isEqualTo(Baggage.builder().put("key1", "value1").put("key2", "value2").build()); + assertThat(singleItemNoParent.toBuilder().put("key1", "value2").build()) + .isEqualTo(Baggage.builder().put("key1", "value2").build()); + + assertThat(singleItemWithParent.toBuilder().put("key1", "value2").build()) + .isEqualTo(Baggage.builder().put("key1", "value2").build()); + } + + @Test + void testEquals() { + Baggage baggage1 = Baggage.builder().put(K1, V1).build(); + Baggage baggage2 = baggage1.toBuilder().put(K1, V2).build(); + Baggage baggage3 = Baggage.builder().put(K1, V2).build(); + new EqualsTester() + .addEqualityGroup( + Baggage.builder().put(K1, V1, TMD).put(K2, V2, TMD).build(), + Baggage.builder().put(K1, V1, TMD).put(K2, V2, TMD).build(), + Baggage.builder().put(K2, V2, TMD).put(K1, V1, TMD).build()) + .addEqualityGroup(Baggage.builder().put(K1, V1, TMD).put(K2, V1, TMD).build()) + .addEqualityGroup(Baggage.builder().put(K1, V2, TMD).put(K2, V1, TMD).build()) + .addEqualityGroup(baggage2, baggage3) + .testEquals(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ImmutableEntryTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ImmutableEntryTest.java new file mode 100644 index 000000000..b7765c2ff --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ImmutableEntryTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.testing.EqualsTester; +import org.junit.jupiter.api.Test; + +class ImmutableEntryTest { + + private static final String VALUE = "VALUE"; + private static final String VALUE_2 = "VALUE2"; + private static final BaggageEntryMetadata SAMPLE_METADATA = + BaggageEntryMetadata.create("propagation=unlimited"); + + @Test + void testGetEntryMetadata() { + assertThat(ImmutableEntry.create(VALUE, SAMPLE_METADATA).getMetadata()) + .isEqualTo(SAMPLE_METADATA); + } + + @Test + void testEntryEquals() { + new EqualsTester() + .addEqualityGroup( + ImmutableEntry.create(VALUE, SAMPLE_METADATA), + ImmutableEntry.create(VALUE, SAMPLE_METADATA)) + .addEqualityGroup(ImmutableEntry.create(VALUE_2, SAMPLE_METADATA)) + .addEqualityGroup(ImmutableEntry.create(VALUE, BaggageEntryMetadata.create("other"))) + .testEquals(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ScopedBaggageTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ScopedBaggageTest.java new file mode 100644 index 000000000..2a018b5fa --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/ScopedBaggageTest.java @@ -0,0 +1,119 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.context.Scope; +import org.junit.jupiter.api.Test; + +/** Unit tests for the methods in {@link Baggage} that interact with the current {@link Baggage}. */ +class ScopedBaggageTest { + + private static final String KEY_1 = "key 1"; + private static final String KEY_2 = "key 2"; + private static final String KEY_3 = "key 3"; + + private static final String VALUE_1 = "value 1"; + private static final String VALUE_2 = "value 2"; + private static final String VALUE_3 = "value 3"; + private static final String VALUE_4 = "value 4"; + + private static final BaggageEntryMetadata METADATA_UNLIMITED_PROPAGATION = + BaggageEntryMetadata.create("unlimited"); + private static final BaggageEntryMetadata METADATA_NO_PROPAGATION = + BaggageEntryMetadata.create("noprop"); + + @Test + void emptyBaggage() { + Baggage defaultBaggage = Baggage.current(); + assertThat(defaultBaggage.isEmpty()).isTrue(); + } + + @Test + void withContext() { + assertThat(Baggage.current().isEmpty()).isTrue(); + Baggage scopedEntries = + Baggage.builder().put(KEY_1, VALUE_1, METADATA_UNLIMITED_PROPAGATION).build(); + try (Scope scope = scopedEntries.makeCurrent()) { + assertThat(Baggage.current()).isSameAs(scopedEntries); + } + assertThat(Baggage.current().isEmpty()).isTrue(); + } + + @Test + void createBuilderFromCurrentEntries() { + Baggage scopedBaggage = + Baggage.builder().put(KEY_1, VALUE_1, METADATA_UNLIMITED_PROPAGATION).build(); + try (Scope scope = scopedBaggage.makeCurrent()) { + Baggage newEntries = + Baggage.current().toBuilder().put(KEY_2, VALUE_2, METADATA_UNLIMITED_PROPAGATION).build(); + assertThat(newEntries.asMap()) + .containsOnly( + entry(KEY_1, ImmutableEntry.create(VALUE_1, METADATA_UNLIMITED_PROPAGATION)), + entry(KEY_2, ImmutableEntry.create(VALUE_2, METADATA_UNLIMITED_PROPAGATION))); + assertThat(Baggage.current()).isSameAs(scopedBaggage); + } + } + + @Test + void setCurrentEntriesWithBuilder() { + assertThat(Baggage.current().isEmpty()).isTrue(); + Baggage scopedBaggage = + Baggage.builder().put(KEY_1, VALUE_1, METADATA_UNLIMITED_PROPAGATION).build(); + try (Scope scope = scopedBaggage.makeCurrent()) { + assertThat(Baggage.current().asMap()) + .containsOnly( + entry(KEY_1, ImmutableEntry.create(VALUE_1, METADATA_UNLIMITED_PROPAGATION))); + assertThat(Baggage.current()).isSameAs(scopedBaggage); + } + assertThat(Baggage.current().isEmpty()).isTrue(); + } + + @Test + void addToCurrentEntriesWithBuilder() { + Baggage scopedBaggage = + Baggage.builder().put(KEY_1, VALUE_1, METADATA_UNLIMITED_PROPAGATION).build(); + try (Scope scope1 = scopedBaggage.makeCurrent()) { + Baggage innerBaggage = + Baggage.current().toBuilder().put(KEY_2, VALUE_2, METADATA_UNLIMITED_PROPAGATION).build(); + try (Scope scope2 = innerBaggage.makeCurrent()) { + assertThat(Baggage.current().asMap()) + .containsOnly( + entry(KEY_1, ImmutableEntry.create(VALUE_1, METADATA_UNLIMITED_PROPAGATION)), + entry(KEY_2, ImmutableEntry.create(VALUE_2, METADATA_UNLIMITED_PROPAGATION))); + assertThat(Baggage.current()).isSameAs(innerBaggage); + } + assertThat(Baggage.current()).isSameAs(scopedBaggage); + } + } + + @Test + void multiScopeBaggageWithMetadata() { + Baggage scopedBaggage = + Baggage.builder() + .put(KEY_1, VALUE_1, METADATA_UNLIMITED_PROPAGATION) + .put(KEY_2, VALUE_2, METADATA_UNLIMITED_PROPAGATION) + .build(); + try (Scope scope1 = scopedBaggage.makeCurrent()) { + Baggage innerBaggage = + Baggage.current().toBuilder() + .put(KEY_3, VALUE_3, METADATA_NO_PROPAGATION) + .put(KEY_2, VALUE_4, METADATA_NO_PROPAGATION) + .build(); + try (Scope scope2 = innerBaggage.makeCurrent()) { + assertThat(Baggage.current().asMap()) + .containsOnly( + entry(KEY_1, ImmutableEntry.create(VALUE_1, METADATA_UNLIMITED_PROPAGATION)), + entry(KEY_2, ImmutableEntry.create(VALUE_4, METADATA_NO_PROPAGATION)), + entry(KEY_3, ImmutableEntry.create(VALUE_3, METADATA_NO_PROPAGATION))); + assertThat(Baggage.current()).isSameAs(innerBaggage); + } + assertThat(Baggage.current()).isSameAs(scopedBaggage); + } + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorFuzzTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorFuzzTest.java new file mode 100644 index 000000000..0388b6722 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorFuzzTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage.propagation; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import edu.berkeley.cs.jqf.fuzz.Fuzz; +import edu.berkeley.cs.jqf.fuzz.JQF; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.runner.RunWith; + +@RunWith(JQF.class) +@SuppressWarnings("JavadocMethod") +public class W3CBaggagePropagatorFuzzTest { + private final W3CBaggagePropagator baggagePropagator = W3CBaggagePropagator.getInstance(); + + @Fuzz + public void safeForRandomInputs(String baggage) { + Context context = + baggagePropagator.extract( + Context.root(), + ImmutableMap.of("baggage", baggage), + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }); + assertThat(context).isNotNull(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorTest.java new file mode 100644 index 000000000..5f7c91722 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorTest.java @@ -0,0 +1,460 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.baggage.propagation; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageEntryMetadata; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class W3CBaggagePropagatorTest { + + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + @Test + void fields() { + assertThat(W3CBaggagePropagator.getInstance().fields()).containsExactly("baggage"); + } + + @Test + void extract_key_duplicate() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + + Context result = + propagator.extract( + Context.root(), ImmutableMap.of("baggage", "key=value1,key=value2"), getter); + + Baggage expectedBaggage = Baggage.builder().put("key", "value2").build(); + assertThat(Baggage.fromContext(result)).isEqualTo(expectedBaggage); + } + + @Test + void extract_key_leadingSpaces() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + + Context result = + propagator.extract(Context.root(), ImmutableMap.of("baggage", " key=value1"), getter); + + Baggage expectedBaggage = Baggage.builder().put("key", "value1").build(); + assertThat(Baggage.fromContext(result)).isEqualTo(expectedBaggage); + } + + @Test + void extract_key_trailingSpaces() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + + Context result = + propagator.extract(Context.root(), ImmutableMap.of("baggage", "key =value1"), getter); + + Baggage expectedBaggage = Baggage.builder().put("key", "value1").build(); + assertThat(Baggage.fromContext(result)).isEqualTo(expectedBaggage); + } + + @Test + void extract_key_onlySpaces() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + + Context result = + propagator.extract(Context.root(), ImmutableMap.of("baggage", " =value1"), getter); + + assertThat(Baggage.fromContext(result)).isEqualTo(Baggage.empty()); + } + + @Test + void extract_key_withInnerSpaces() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + + Context result = + propagator.extract(Context.root(), ImmutableMap.of("baggage", "ke y=value1"), getter); + + assertThat(Baggage.fromContext(result)).isEqualTo(Baggage.empty()); + } + + @Test + void extract_key_withSeparators() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + + Context result = + propagator.extract(Context.root(), ImmutableMap.of("baggage", "ke?y=value1"), getter); + + assertThat(Baggage.fromContext(result)).isEqualTo(Baggage.empty()); + } + + @Test + void extract_key_singleValid_multipleInvalid() { + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + + Context result = + propagator.extract( + Context.root(), + ImmutableMap.of( + "baggage", + "ke carrier = new HashMap<>(); + propagator.inject(Context.root(), carrier, Map::put); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_emptyBaggage() { + Baggage baggage = Baggage.empty(); + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + Map carrier = new HashMap<>(); + propagator.inject(Context.root().with(baggage), carrier, Map::put); + assertThat(carrier).isEmpty(); + } + + @Test + void inject() { + Baggage baggage = + Baggage.builder() + .put("nometa", "nometa-value") + .put("meta", "meta-value", BaggageEntryMetadata.create("somemetadata; someother=foo")) + .build(); + W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance(); + Map carrier = new HashMap<>(); + propagator.inject(Context.root().with(baggage), carrier, Map::put); + assertThat(carrier) + .containsExactlyInAnyOrderEntriesOf( + singletonMap( + "baggage", "meta=meta-value;somemetadata; someother=foo,nometa=nometa-value")); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + W3CBaggagePropagator.getInstance().inject(null, carrier, Map::put); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = Context.current().with(Baggage.builder().put("cat", "meow").build()); + W3CBaggagePropagator.getInstance().inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/common/AttributeKeyTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/common/AttributeKeyTest.java new file mode 100644 index 000000000..c54a1a6ed --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/common/AttributeKeyTest.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +class AttributeKeyTest { + + @Test + @SuppressWarnings("AutoValueSubclassLeaked") + void equalsVerifier() { + EqualsVerifier.forClass(AutoValue_AttributeKeyImpl.class).verify(); + } + + @Test + void nullToEmpty() { + assertThat(AttributeKey.stringKey(null).getKey()).isEmpty(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java new file mode 100644 index 000000000..816f24124 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/common/AttributesTest.java @@ -0,0 +1,419 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.common; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link Attributes}s. */ +@SuppressWarnings("rawtypes") +class AttributesTest { + + @Test + void forEach() { + final Map entriesSeen = new LinkedHashMap<>(); + + Attributes attributes = Attributes.of(stringKey("key1"), "value1", longKey("key2"), 333L); + + attributes.forEach(entriesSeen::put); + + assertThat(entriesSeen) + .containsExactly(entry(stringKey("key1"), "value1"), entry(longKey("key2"), 333L)); + } + + @Test + void forEach_singleAttribute() { + final Map entriesSeen = new HashMap<>(); + + Attributes attributes = Attributes.of(stringKey("key"), "value"); + attributes.forEach(entriesSeen::put); + assertThat(entriesSeen).containsExactly(entry(stringKey("key"), "value")); + } + + @Test + void putAll() { + Attributes attributes = Attributes.of(stringKey("key1"), "value1", longKey("key2"), 333L); + assertThat(Attributes.builder().put(booleanKey("key3"), true).putAll(attributes).build()) + .isEqualTo( + Attributes.of( + stringKey("key1"), "value1", longKey("key2"), 333L, booleanKey("key3"), true)); + } + + @Test + void putAll_null() { + assertThat(Attributes.builder().put(booleanKey("key3"), true).putAll(null).build()) + .isEqualTo(Attributes.of(booleanKey("key3"), true)); + } + + @SuppressWarnings("CollectionIncompatibleType") + @Test + void asMap() { + Attributes attributes = Attributes.of(stringKey("key1"), "value1", longKey("key2"), 333L); + + Map, Object> map = attributes.asMap(); + assertThat(map) + .containsExactly(entry(stringKey("key1"), "value1"), entry(longKey("key2"), 333L)); + + assertThat(map.get(stringKey("key1"))).isEqualTo("value1"); + assertThat(map.get(longKey("key2"))).isEqualTo(333L); + // Map of AttributeKey, not String + assertThat(map.get("key1")).isNull(); + assertThat(map.get(null)).isNull(); + assertThat(map.keySet()).containsExactlyInAnyOrder(stringKey("key1"), longKey("key2")); + assertThat(map.values()).containsExactlyInAnyOrder("value1", 333L); + assertThat(map.entrySet()) + .containsExactlyInAnyOrder( + entry(stringKey("key1"), "value1"), entry(longKey("key2"), 333L)); + assertThat(map.entrySet().contains(entry(stringKey("key1"), "value1"))).isTrue(); + assertThat(map.entrySet().contains(entry(stringKey("key1"), "value2"))).isFalse(); + assertThat(map.isEmpty()).isFalse(); + assertThat(map.containsKey(stringKey("key1"))).isTrue(); + assertThat(map.containsKey(longKey("key2"))).isTrue(); + assertThat(map.containsKey(stringKey("key3"))).isFalse(); + assertThat(map.containsKey(null)).isFalse(); + assertThat(map.containsValue("value1")).isTrue(); + assertThat(map.containsValue(333L)).isTrue(); + assertThat(map.containsValue("cat")).isFalse(); + assertThatThrownBy(() -> map.put(stringKey("animal"), "cat")) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.remove(stringKey("key1"))) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.putAll(Collections.emptyMap())) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(map::clear).isInstanceOf(UnsupportedOperationException.class); + + assertThat(map.keySet().contains(stringKey("key1"))).isTrue(); + assertThat(map.keySet().contains(stringKey("key3"))).isFalse(); + assertThat(map.keySet().containsAll(Arrays.asList(stringKey("key1"), longKey("key2")))) + .isTrue(); + assertThat(map.keySet().containsAll(Arrays.asList(stringKey("key1"), longKey("key3")))) + .isFalse(); + assertThat(map.keySet().containsAll(null)).isFalse(); + assertThat(map.keySet().containsAll(Collections.emptyList())).isTrue(); + assertThat(map.keySet().size()).isEqualTo(2); + assertThat(map.keySet().toArray()) + .containsExactlyInAnyOrder(stringKey("key1"), longKey("key2")); + AttributeKey[] keys = new AttributeKey[2]; + map.keySet().toArray(keys); + assertThat(keys).containsExactlyInAnyOrder(stringKey("key1"), longKey("key2")); + keys = new AttributeKey[0]; + assertThat(map.keySet().toArray(keys)) + .containsExactlyInAnyOrder(stringKey("key1"), longKey("key2")); + assertThat(keys).isEmpty(); // Didn't use input array. + assertThatThrownBy(() -> map.keySet().iterator().remove()) + .isInstanceOf(UnsupportedOperationException.class); + assertThat(map.keySet().containsAll(singletonList(stringKey("key1")))).isTrue(); + assertThat(map.keySet().containsAll(Arrays.asList(stringKey("key1"), stringKey("key3")))) + .isFalse(); + assertThat(map.keySet().isEmpty()).isFalse(); + assertThatThrownBy(() -> map.keySet().add(stringKey("key3"))) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.keySet().remove(stringKey("key1"))) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.keySet().addAll(Collections.singletonList(stringKey("key3")))) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.keySet().retainAll(Collections.singletonList(stringKey("key3")))) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.keySet().removeAll(Collections.singletonList(stringKey("key3")))) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> map.keySet().clear()) + .isInstanceOf(UnsupportedOperationException.class); + + assertThat(map.values().contains("value1")).isTrue(); + assertThat(map.values().contains("value3")).isFalse(); + + assertThat(map.toString()).isEqualTo("ReadOnlyArrayMap{key1=value1,key2=333}"); + + Map, Object> emptyMap = Attributes.builder().build().asMap(); + assertThat(emptyMap.isEmpty()).isTrue(); + assertThatThrownBy(() -> emptyMap.entrySet().iterator().next()) + .isInstanceOf(NoSuchElementException.class); + } + + @Test + void builder_nullKey() { + Attributes attributes = Attributes.builder().put(stringKey(null), "value").build(); + assertThat(attributes).isEqualTo(Attributes.empty()); + } + + @Test + void forEach_empty() { + final AtomicBoolean sawSomething = new AtomicBoolean(false); + Attributes emptyAttributes = Attributes.empty(); + emptyAttributes.forEach((key, value) -> sawSomething.set(true)); + assertThat(sawSomething.get()).isFalse(); + } + + @Test + void orderIndependentEquality() { + Attributes one = + Attributes.of( + stringKey("key1"), "value1", + stringKey("key2"), "value2"); + Attributes two = + Attributes.of( + stringKey("key2"), "value2", + stringKey("key1"), "value1"); + + assertThat(one).isEqualTo(two); + + Attributes three = + Attributes.of( + stringKey("key1"), "value1", + stringKey("key2"), "value2", + stringKey(""), "empty", + stringKey("key3"), "value3", + stringKey("key4"), "value4"); + Attributes four = + Attributes.of( + null, + "null", + stringKey("key2"), + "value2", + stringKey("key1"), + "value1", + stringKey("key4"), + "value4", + stringKey("key3"), + "value3"); + + assertThat(three).isEqualTo(four); + } + + @Test + void deduplication() { + Attributes one = + Attributes.of( + stringKey("key1"), "valueX", + stringKey("key1"), "value1"); + Attributes two = Attributes.of(stringKey("key1"), "value1"); + + assertThat(one).isEqualTo(two); + } + + @Test + void deduplication_oddNumberElements() { + Attributes one = + Attributes.builder() + .put(stringKey("key2"), "valueX") + .put(stringKey("key2"), "value2") + .put(stringKey("key1"), "value1") + .build(); + Attributes two = + Attributes.builder() + .put(stringKey("key2"), "value2") + .put(stringKey("key1"), "value1") + .build(); + + assertThat(one).isEqualTo(two); + } + + @Test + void emptyAndNullKey() { + Attributes noAttributes = Attributes.of(stringKey(""), "empty", null, "null"); + + assertThat(noAttributes.size()).isEqualTo(0); + } + + @Test + void builder() { + Attributes attributes = + Attributes.builder() + .put("string", "value1") + .put("long", 100) + .put(longKey("long2"), 10) + .put("double", 33.44) + .put("boolean", "duplicateShouldBeRemoved") + .put("boolean", false) + .build(); + + Attributes wantAttributes = + Attributes.of( + stringKey("string"), + "value1", + longKey("long"), + 100L, + longKey("long2"), + 10L, + doubleKey("double"), + 33.44, + booleanKey("boolean"), + false); + assertThat(attributes).isEqualTo(wantAttributes); + + AttributesBuilder newAttributes = attributes.toBuilder(); + newAttributes.put("newKey", "newValue"); + assertThat(newAttributes.build()) + .isEqualTo( + Attributes.of( + stringKey("string"), + "value1", + longKey("long"), + 100L, + longKey("long2"), + 10L, + doubleKey("double"), + 33.44, + booleanKey("boolean"), + false, + stringKey("newKey"), + "newValue")); + // Original not mutated. + assertThat(attributes).isEqualTo(wantAttributes); + } + + @Test + void builder_arrayTypes() { + Attributes attributes = + Attributes.builder() + .put("string", "value1", "value2", null) + .put("long", 100L, 200L) + .put("double", 33.44, -44.33) + .put("boolean", "duplicateShouldBeRemoved") + .put(stringKey("boolean"), "true") + .put("boolean", false, true) + .build(); + + assertThat(attributes) + .isEqualTo( + Attributes.of( + stringArrayKey("string"), Arrays.asList("value1", "value2", null), + longArrayKey("long"), Arrays.asList(100L, 200L), + doubleArrayKey("double"), Arrays.asList(33.44, -44.33), + booleanArrayKey("boolean"), Arrays.asList(false, true))); + } + + @Test + @SuppressWarnings("unchecked") + void get_Null() { + assertThat(Attributes.empty().get(stringKey("foo"))).isNull(); + assertThat(Attributes.of(stringKey("key"), "value").get(stringKey("foo"))).isNull(); + assertThat(Attributes.of(stringKey("key"), "value").get((AttributeKey) null)).isNull(); + } + + @Test + void get() { + assertThat(Attributes.of(stringKey("key"), "value").get(stringKey("key"))).isEqualTo("value"); + assertThat(Attributes.of(stringKey("key"), "value").get(stringKey("value"))).isNull(); + Attributes threeElements = + Attributes.of( + stringKey("string"), "value", booleanKey("boolean"), true, longKey("long"), 1L); + assertThat(threeElements.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(threeElements.get(stringKey("string"))).isEqualTo("value"); + assertThat(threeElements.get(longKey("long"))).isEqualTo(1L); + Attributes twoElements = + Attributes.of(stringKey("string"), "value", booleanKey("boolean"), true); + assertThat(twoElements.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(twoElements.get(stringKey("string"))).isEqualTo("value"); + Attributes fourElements = + Attributes.of( + stringKey("string"), + "value", + booleanKey("boolean"), + true, + longKey("long"), + 1L, + stringArrayKey("array"), + Arrays.asList("one", "two", "three")); + assertThat(fourElements.get(stringArrayKey("array"))) + .isEqualTo(Arrays.asList("one", "two", "three")); + assertThat(threeElements.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(threeElements.get(stringKey("string"))).isEqualTo("value"); + assertThat(threeElements.get(longKey("long"))).isEqualTo(1L); + } + + @Test + void toBuilder() { + Attributes filled = Attributes.builder().put("cat", "meow").put("dog", "bark").build(); + + Attributes fromEmpty = + Attributes.empty().toBuilder().put("cat", "meow").put("dog", "bark").build(); + assertThat(fromEmpty).isEqualTo(filled); + // Original not mutated. + assertThat(Attributes.empty().isEmpty()).isTrue(); + + Attributes partial = Attributes.builder().put("cat", "meow").build(); + Attributes fromPartial = partial.toBuilder().put("dog", "bark").build(); + assertThat(fromPartial).isEqualTo(filled); + // Original not mutated. + assertThat(partial).isEqualTo(Attributes.builder().put("cat", "meow").build()); + } + + @Test + void nullsAreNoOps() { + AttributesBuilder builder = Attributes.builder(); + builder.put(stringKey("attrValue"), "attrValue"); + builder.put("string", "string"); + builder.put("long", 10); + builder.put("double", 1.0); + builder.put("bool", true); + builder.put("arrayString", new String[] {"string"}); + builder.put("arrayLong", new long[] {10L}); + builder.put("arrayDouble", new double[] {1.0}); + builder.put("arrayBool", new boolean[] {true}); + assertThat(builder.build().size()).isEqualTo(9); + + // note: currently these are no-op calls; that behavior is not required, so if it needs to + // change, that is fine. + builder.put(stringKey("attrValue"), null); + builder.put("string", (String) null); + builder.put("arrayString", (String[]) null); + builder.put("arrayLong", (long[]) null); + builder.put("arrayDouble", (double[]) null); + builder.put("arrayBool", (boolean[]) null); + + Attributes attributes = builder.build(); + assertThat(attributes.size()).isEqualTo(9); + assertThat(attributes.get(stringKey("string"))).isEqualTo("string"); + assertThat(attributes.get(stringArrayKey("arrayString"))).isEqualTo(singletonList("string")); + assertThat(attributes.get(longArrayKey("arrayLong"))).isEqualTo(singletonList(10L)); + assertThat(attributes.get(doubleArrayKey("arrayDouble"))).isEqualTo(singletonList(1.0d)); + assertThat(attributes.get(booleanArrayKey("arrayBool"))).isEqualTo(singletonList(true)); + } + + @Test + void attributesToString() { + Attributes attributes = + Attributes.builder() + .put("otel.status_code", "OK") + .put("http.response_size", 100) + .put("process.cpu_consumed", 33.44) + .put("error", true) + .put("success", "true") + .build(); + + assertThat(attributes.toString()) + .isEqualTo( + "{error=true, http.response_size=100, " + + "otel.status_code=\"OK\", process.cpu_consumed=33.44, success=\"true\"}"); + } + + @Test + void onlySameTypeCanRetrieveValue() { + Attributes attributes = Attributes.of(stringKey("animal"), "cat"); + assertThat(attributes.get(stringKey("animal"))).isEqualTo("cat"); + assertThat(attributes.get(longKey("animal"))).isNull(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/ImmutableKeyValuePairsTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/ImmutableKeyValuePairsTest.java new file mode 100644 index 000000000..d39db2cd4 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/ImmutableKeyValuePairsTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.jupiter.api.Test; + +class ImmutableKeyValuePairsTest { + + @Test + void get() { + assertThat(new TestPairs(new Object[0]).get("one")).isNull(); + assertThat(new TestPairs(new Object[] {"one", 55}).get("one")).isEqualTo(55); + assertThat(new TestPairs(new Object[] {"one", 55}).get("two")).isNull(); + assertThat(new TestPairs(new Object[] {"one", 55, "two", "b"}).get("one")).isEqualTo(55); + assertThat(new TestPairs(new Object[] {"one", 55, "two", "b"}).get("two")).isEqualTo("b"); + assertThat(new TestPairs(new Object[] {"one", 55, "two", "b"}).get("three")).isNull(); + } + + @Test + void size() { + assertThat(new TestPairs(new Object[0]).size()).isEqualTo(0); + assertThat(new TestPairs(new Object[] {"one", 55}).size()).isEqualTo(1); + assertThat(new TestPairs(new Object[] {"one", 55, "two", "b"}).size()).isEqualTo(2); + } + + @Test + void isEmpty() { + assertThat(new TestPairs(new Object[0]).isEmpty()).isTrue(); + assertThat(new TestPairs(new Object[] {"one", 55}).isEmpty()).isFalse(); + assertThat(new TestPairs(new Object[] {"one", 55, "two", "b"}).isEmpty()).isFalse(); + } + + @Test + void toStringIsHumanReadable() { + assertThat(new TestPairs(new Object[0]).toString()).isEqualTo("{}"); + assertThat(new TestPairs(new Object[] {"one", 55}).toString()).isEqualTo("{one=55}"); + assertThat(new TestPairs(new Object[] {"one", 55, "two", "b"}).toString()) + .isEqualTo("{one=55, two=\"b\"}"); + } + + @Test + void doesNotCrash() { + TestPairs pairs = new TestPairs(new Object[0]); + assertThat(pairs.get(null)).isNull(); + assertThatCode(() -> pairs.forEach(null)).doesNotThrowAnyException(); + } + + static class TestPairs extends ImmutableKeyValuePairs { + TestPairs(Object[] data) { + super(data); + } + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/OtelEncodingUtilsTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/OtelEncodingUtilsTest.java new file mode 100644 index 000000000..7f75413ca --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/OtelEncodingUtilsTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.CharBuffer; +import org.junit.jupiter.api.Test; + +class OtelEncodingUtilsTest { + + private static final long FIRST_LONG = 0x1213141516171819L; + private static final char[] FIRST_CHAR_ARRAY = + new char[] {'1', '2', '1', '3', '1', '4', '1', '5', '1', '6', '1', '7', '1', '8', '1', '9'}; + private static final long SECOND_LONG = 0xFFEEDDCCBBAA9988L; + private static final char[] SECOND_CHAR_ARRAY = + new char[] {'f', 'f', 'e', 'e', 'd', 'd', 'c', 'c', 'b', 'b', 'a', 'a', '9', '9', '8', '8'}; + private static final char[] BOTH_CHAR_ARRAY = + new char[] { + '1', '2', '1', '3', '1', '4', '1', '5', '1', '6', '1', '7', '1', '8', '1', '9', 'f', 'f', + 'e', 'e', 'd', 'd', 'c', 'c', 'b', 'b', 'a', 'a', '9', '9', '8', '8' + }; + + @Test + void longToBase16String() { + char[] chars1 = new char[OtelEncodingUtils.LONG_BASE16]; + OtelEncodingUtils.longToBase16String(FIRST_LONG, chars1, 0); + assertThat(chars1).isEqualTo(FIRST_CHAR_ARRAY); + + char[] chars2 = new char[OtelEncodingUtils.LONG_BASE16]; + OtelEncodingUtils.longToBase16String(SECOND_LONG, chars2, 0); + assertThat(chars2).isEqualTo(SECOND_CHAR_ARRAY); + + char[] chars3 = new char[2 * OtelEncodingUtils.LONG_BASE16]; + OtelEncodingUtils.longToBase16String(FIRST_LONG, chars3, 0); + OtelEncodingUtils.longToBase16String(SECOND_LONG, chars3, OtelEncodingUtils.LONG_BASE16); + assertThat(chars3).isEqualTo(BOTH_CHAR_ARRAY); + } + + @Test + void longFromBase16String_InputTooSmall() { + // Valid base16 strings always have an even length. + assertThatThrownBy(() -> OtelEncodingUtils.longFromBase16String("12345678", 1)) + .isInstanceOf(StringIndexOutOfBoundsException.class); + } + + @Test + void longFromBase16String_UnrecognizedCharacters() { + // These contain bytes not in the decoding. + assertThatThrownBy(() -> OtelEncodingUtils.longFromBase16String("0123456789gbcdef", 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid character g"); + } + + @Test + void validHex() { + assertThat(OtelEncodingUtils.isValidBase16String("abcdef1234567890")).isTrue(); + assertThat(OtelEncodingUtils.isValidBase16String("abcdefg1234567890")).isFalse(); + assertThat(OtelEncodingUtils.isValidBase16String(" OtelEncodingUtils.byteFromBase16('g', 'f')) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid character g"); + assertThatThrownBy(() -> OtelEncodingUtils.byteFromBase16('\u0129', 'f')) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid character \u0129"); + assertThatThrownBy(() -> OtelEncodingUtils.byteFromBase16('f', 'g')) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid character g"); + assertThatThrownBy(() -> OtelEncodingUtils.byteFromBase16('f', '\u0129')) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid character \u0129"); + } + + private static void toFromBase16StringValidate(long value) { + char[] dest = new char[OtelEncodingUtils.LONG_BASE16]; + OtelEncodingUtils.longToBase16String(value, dest, 0); + assertThat(OtelEncodingUtils.longFromBase16String(CharBuffer.wrap(dest), 0)).isEqualTo(value); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/ReadOnlyArrayMapTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/ReadOnlyArrayMapTest.java new file mode 100644 index 000000000..54313d24b --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/ReadOnlyArrayMapTest.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import com.google.common.testing.EqualsTester; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ReadOnlyArrayMapTest { + + @Test + void equalsHashCode() { + Map one = ReadOnlyArrayMap.wrap(Arrays.asList("a", "b")); + Map two = ReadOnlyArrayMap.wrap(Arrays.asList("a", "b")); + Map three = ReadOnlyArrayMap.wrap(Arrays.asList("c", "d")); + Map empty = ReadOnlyArrayMap.wrap(Collections.emptyList()); + new EqualsTester() + .addEqualityGroup(one, two) + .addEqualityGroup(three) + .addEqualityGroup(empty, empty) + .testEquals(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/StringUtilsTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/StringUtilsTest.java new file mode 100644 index 000000000..cc214bc32 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/StringUtilsTest.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class StringUtilsTest { + + @Test + @SuppressWarnings("AvoidEscapedUnicodeCharacters") + void isPrintableString() { + assertThat(StringUtils.isPrintableString("abcd")).isTrue(); + assertThat(StringUtils.isPrintableString("\u0002ab")).isFalse(); + assertThat(StringUtils.isPrintableString("\u0127ab")).isFalse(); + } + + @Test + void isNullOrEmpty() { + assertThat(StringUtils.isNullOrEmpty("")).isTrue(); + assertThat(StringUtils.isNullOrEmpty(null)).isTrue(); + assertThat(StringUtils.isNullOrEmpty("hello")).isFalse(); + assertThat(StringUtils.isNullOrEmpty(" ")).isFalse(); + } + + @Test + void padLeft() { + assertThat(StringUtils.padLeft("value", 10)).isEqualTo("00000value"); + } + + @Test + void padLeft_throws_for_null_value() { + assertThatThrownBy(() -> StringUtils.padLeft(null, 10)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void padLeft_length_does_not_exceed_length() { + assertThat(StringUtils.padLeft("value", 3)).isEqualTo("value"); + assertThat(StringUtils.padLeft("value", -10)).isEqualTo("value"); + assertThat(StringUtils.padLeft("value", 0)).isEqualTo("value"); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/TemporaryBuffersTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/TemporaryBuffersTest.java new file mode 100644 index 000000000..15e2caedf --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/TemporaryBuffersTest.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class TemporaryBuffersTest { + + @Test + void chars() { + TemporaryBuffers.clearChars(); + char[] buffer10 = TemporaryBuffers.chars(10); + assertThat(buffer10).hasSize(10); + char[] buffer8 = TemporaryBuffers.chars(8); + // Buffer was reused even though smaller. + assertThat(buffer8).isSameAs(buffer10); + char[] buffer20 = TemporaryBuffers.chars(20); + assertThat(buffer20).hasSize(20); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/UtilsTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/UtilsTest.java new file mode 100644 index 000000000..5609ba6bc --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/internal/UtilsTest.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.internal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class UtilsTest { + private static final String TEST_MESSAGE = "test message"; + + @Test + void checkArgument() { + Utils.checkArgument(true, TEST_MESSAGE); + assertThatThrownBy(() -> Utils.checkArgument(false, TEST_MESSAGE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(TEST_MESSAGE); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/DefaultTracerProviderTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/DefaultTracerProviderTest.java new file mode 100644 index 000000000..72a98f29f --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/DefaultTracerProviderTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class DefaultTracerProviderTest { + + @Test + void returnsDefaultTracer() { + assertThat(TracerProvider.noop().get("test")).isInstanceOf(DefaultTracer.class); + assertThat(TracerProvider.noop().get("test", "1.0")).isInstanceOf(DefaultTracer.class); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/DefaultTracerTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/DefaultTracerTest.java new file mode 100644 index 000000000..8478c7498 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/DefaultTracerTest.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DefaultTracer}. */ +// Need to suppress warnings for MustBeClosed because Android 14 does not support +// try-with-resources. +@SuppressWarnings("MustBeClosedChecker") +class DefaultTracerTest { + private static final Tracer defaultTracer = DefaultTracer.getInstance(); + private static final String SPAN_NAME = "MySpanName"; + private static final SpanContext spanContext = + SpanContext.create( + "00000000000000000000000000000061", + "0000000000000061", + TraceFlags.getDefault(), + TraceState.getDefault(),HeraContext.getDefault()); + + @Test + void defaultSpanBuilderWithName() { + assertThat(defaultTracer.spanBuilder(SPAN_NAME).startSpan().getSpanContext().isValid()) + .isFalse(); + } + + @Test + void testSpanContextPropagationExplicitParent() { + Span span = + defaultTracer + .spanBuilder(SPAN_NAME) + .setParent(Context.root().with(Span.wrap(spanContext))) + .startSpan(); + assertThat(span.getSpanContext()).isSameAs(spanContext); + } + + @Test + void testSpanContextPropagation() { + Span parent = Span.wrap(spanContext); + + Span span = + defaultTracer.spanBuilder(SPAN_NAME).setParent(Context.root().with(parent)).startSpan(); + assertThat(span.getSpanContext()).isSameAs(spanContext); + } + + @Test + void noSpanContextMakesInvalidSpans() { + Span span = defaultTracer.spanBuilder(SPAN_NAME).startSpan(); + assertThat(span.getSpanContext()).isSameAs(SpanContext.getInvalid()); + } + + @Test + void testSpanContextPropagation_fromContext() { + Context context = Context.current().with(Span.wrap(spanContext)); + + Span span = defaultTracer.spanBuilder(SPAN_NAME).setParent(context).startSpan(); + assertThat(span.getSpanContext()).isSameAs(spanContext); + } + + @Test + void testSpanContextPropagation_fromContextAfterNoParent() { + Context context = Context.current().with(Span.wrap(spanContext)); + + Span span = defaultTracer.spanBuilder(SPAN_NAME).setNoParent().setParent(context).startSpan(); + assertThat(span.getSpanContext()).isSameAs(spanContext); + } + + @Test + void testSpanContextPropagation_fromContextThenNoParent() { + Context context = Context.current().with(Span.wrap(spanContext)); + + Span span = defaultTracer.spanBuilder(SPAN_NAME).setParent(context).setNoParent().startSpan(); + assertThat(span.getSpanContext()).isEqualTo(SpanContext.getInvalid()); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/PropagatedSpanTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/PropagatedSpanTest.java new file mode 100644 index 000000000..7c3807775 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/PropagatedSpanTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class PropagatedSpanTest { + + @Test + void notRecording() { + assertThat(Span.getInvalid().isRecording()).isFalse(); + } + + @Test + void hasInvalidContextAndDefaultSpanOptions() { + SpanContext context = Span.getInvalid().getSpanContext(); + assertThat(context.getTraceFlags()).isEqualTo(TraceFlags.getDefault()); + assertThat(context.getTraceState()).isEqualTo(TraceState.getDefault()); + } + + @Test + void doNotCrash() { + Span span = Span.getInvalid(); + span.setAttribute(stringKey("MyStringAttributeKey"), "MyStringAttributeValue"); + span.setAttribute(booleanKey("MyBooleanAttributeKey"), true); + span.setAttribute(longKey("MyLongAttributeKey"), 123L); + span.setAttribute(longKey("MyLongAttributeKey"), 123); + span.setAttribute("NullString", null); + span.setAttribute("EmptyString", ""); + span.setAttribute("long", 1); + span.setAttribute("double", 1.0); + span.setAttribute("boolean", true); + span.setAttribute(stringArrayKey("NullArrayString"), null); + span.setAttribute(booleanArrayKey("NullArrayBoolean"), null); + span.setAttribute(longArrayKey("NullArrayLong"), null); + span.setAttribute(doubleArrayKey("NullArrayDouble"), null); + span.setAttribute((String) null, null); + span.setAllAttributes(null); + span.setAllAttributes(Attributes.empty()); + span.setAllAttributes( + Attributes.of(stringKey("MyStringAttributeKey"), "MyStringAttributeValue")); + span.addEvent("event"); + span.addEvent("event", 0, TimeUnit.NANOSECONDS); + span.addEvent("event", Instant.EPOCH); + span.addEvent("event", Attributes.of(booleanKey("MyBooleanAttributeKey"), true)); + span.addEvent( + "event", Attributes.of(booleanKey("MyBooleanAttributeKey"), true), 0, TimeUnit.NANOSECONDS); + span.setStatus(StatusCode.OK); + span.setStatus(StatusCode.OK, "null"); + span.recordException(new IllegalStateException()); + span.recordException(new IllegalStateException(), Attributes.empty()); + span.updateName("name"); + span.end(); + span.end(0, TimeUnit.NANOSECONDS); + span.end(Instant.EPOCH); + } + + @Test + void defaultSpan_ToString() { + Span span = Span.getInvalid(); + assertThat(span.toString()) + .isEqualTo( + "PropagatedSpan{ImmutableSpanContext{traceId=00000000000000000000000000000000, " + + "spanId=0000000000000000, traceFlags=00, " + + "traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=false}}"); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanBuilderTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanBuilderTest.java new file mode 100644 index 000000000..a7ed2dc27 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanBuilderTest.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link SpanBuilder}. */ +class SpanBuilderTest { + private final Tracer tracer = DefaultTracer.getInstance(); + + @Test + void doNotCrash_NoopImplementation() { + assertThatCode( + () -> { + SpanBuilder spanBuilder = tracer.spanBuilder(null); + spanBuilder.setSpanKind(null); + spanBuilder.setParent(null); + spanBuilder.setNoParent(); + spanBuilder.addLink(null); + spanBuilder.addLink(null, Attributes.empty()); + spanBuilder.addLink(SpanContext.getInvalid(), null); + spanBuilder.setAttribute((String) null, "foo"); + spanBuilder.setAttribute("foo", null); + spanBuilder.setAttribute(null, 0L); + spanBuilder.setAttribute(null, 0.0); + spanBuilder.setAttribute(null, false); + spanBuilder.setAttribute((AttributeKey) null, "foo"); + spanBuilder.setAttribute(stringKey(null), "foo"); + spanBuilder.setAttribute(stringKey(""), "foo"); + spanBuilder.setAttribute(stringKey("foo"), null); + spanBuilder.setStartTimestamp(-1, TimeUnit.MILLISECONDS); + spanBuilder.setStartTimestamp(1, null); + spanBuilder.setParent(Context.root().with(Span.wrap(null))); + spanBuilder.setParent(Context.root()); + spanBuilder.setNoParent(); + spanBuilder.addLink(Span.getInvalid().getSpanContext()); + spanBuilder.addLink(Span.getInvalid().getSpanContext(), Attributes.empty()); + spanBuilder.setAttribute("key", "value"); + spanBuilder.setAttribute("key", 12345L); + spanBuilder.setAttribute("key", .12345); + spanBuilder.setAttribute("key", true); + spanBuilder.setAttribute(stringKey("key"), "value"); + spanBuilder.setAllAttributes(Attributes.of(stringKey("key"), "value")); + spanBuilder.setAllAttributes(Attributes.empty()); + spanBuilder.setAllAttributes(null); + spanBuilder.setStartTimestamp(12345L, TimeUnit.NANOSECONDS); + spanBuilder.setStartTimestamp(Instant.EPOCH); + spanBuilder.setStartTimestamp(null); + assertThat(spanBuilder.startSpan().getSpanContext().isValid()).isFalse(); + }) + .doesNotThrowAnyException(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanContextTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanContextTest.java new file mode 100644 index 000000000..8796d3420 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanContextTest.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import java.util.HashMap; +import java.util.Map; + +/** Unit tests for {@link SpanContext}. */ +class SpanContextTest { + private static final String FIRST_TRACE_ID = "00000000000000000000000000000061"; + private static final String SECOND_TRACE_ID = "00000000000000300000000000000000"; + private static final String FIRST_SPAN_ID = "0000000000000061"; + private static final String SECOND_SPAN_ID = "3000000000000000"; + private static final Map HERA_CONTEXT = new HashMap<>(); + private static final TraceState FIRST_TRACE_STATE = + TraceState.builder().put("foo", "bar").build(); + private static final TraceState SECOND_TRACE_STATE = + TraceState.builder().put("foo", "baz").build(); + private static final SpanContext first = + SpanContext.create(FIRST_TRACE_ID, FIRST_SPAN_ID, TraceFlags.getDefault(), FIRST_TRACE_STATE,HERA_CONTEXT); + private static final SpanContext second = + SpanContext.create( + SECOND_TRACE_ID, SECOND_SPAN_ID, TraceFlags.getSampled(), SECOND_TRACE_STATE,HERA_CONTEXT); + private static final SpanContext remote = + SpanContext.createFromRemoteParent( + SECOND_TRACE_ID, SECOND_SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault(),HERA_CONTEXT); + + @Test + void invalidSpanContext() { + assertThat(SpanContext.getInvalid().getTraceId()).isEqualTo(TraceId.getInvalid()); + assertThat(SpanContext.getInvalid().getSpanId()).isEqualTo(SpanId.getInvalid()); + assertThat(SpanContext.getInvalid().getTraceFlags()).isEqualTo(TraceFlags.getDefault()); + } + + @Test + void isValid() { + assertThat(SpanContext.getInvalid().isValid()).isFalse(); + assertThat( + SpanContext.create( + FIRST_TRACE_ID, + SpanId.getInvalid(), + TraceFlags.getDefault(), + TraceState.getDefault(),HERA_CONTEXT) + .isValid()) + .isFalse(); + assertThat( + SpanContext.create( + TraceId.getInvalid(), + FIRST_SPAN_ID, + TraceFlags.getDefault(), + TraceState.getDefault(),HERA_CONTEXT) + .isValid()) + .isFalse(); + assertThat(first.isValid()).isTrue(); + assertThat(second.isValid()).isTrue(); + } + + @Test + void getTraceId() { + assertThat(first.getTraceId()).isEqualTo(FIRST_TRACE_ID); + assertThat(second.getTraceId()).isEqualTo(SECOND_TRACE_ID); + } + + @Test + void getSpanId() { + assertThat(first.getSpanId()).isEqualTo(FIRST_SPAN_ID); + assertThat(second.getSpanId()).isEqualTo(SECOND_SPAN_ID); + } + + @Test + void getTraceFlags() { + assertThat(first.getTraceFlags()).isEqualTo(TraceFlags.getDefault()); + assertThat(second.getTraceFlags()).isEqualTo(TraceFlags.getSampled()); + } + + @Test + void getTraceState() { + assertThat(first.getTraceState()).isEqualTo(FIRST_TRACE_STATE); + assertThat(second.getTraceState()).isEqualTo(SECOND_TRACE_STATE); + } + + @Test + void isRemote() { + assertThat(first.isRemote()).isFalse(); + assertThat(second.isRemote()).isFalse(); + assertThat(remote.isRemote()).isTrue(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanIdTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanIdTest.java new file mode 100644 index 000000000..6e60be353 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanIdTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link SpanId}. */ +class SpanIdTest { + private static final String first = "0000000000000061"; + private static final String second = "ff00000000000041"; + + @Test + void invalid() { + assertThat(SpanId.getInvalid()).isEqualTo("0000000000000000"); + } + + @Test + void isValid() { + assertThat(SpanId.isValid(null)).isFalse(); + assertThat(SpanId.isValid("001")).isFalse(); + assertThat(SpanId.isValid("000000000000z000")).isFalse(); + assertThat(SpanId.isValid(SpanId.getInvalid())).isFalse(); + + assertThat(SpanId.isValid(first)).isTrue(); + assertThat(SpanId.isValid(second)).isTrue(); + } + + @Test + void fromLong() { + assertThat(SpanId.fromLong(0)).isEqualTo(SpanId.getInvalid()); + assertThat(SpanId.fromLong(0x61)).isEqualTo(first); + assertThat(SpanId.fromLong(0xff00000000000041L)).isEqualTo(second); + } + + @Test + void fromBytes() { + String spanId = "090a0b0c0d0e0f00"; + assertThat(SpanId.fromBytes(OtelEncodingUtils.bytesFromBase16(spanId, SpanId.getLength()))) + .isEqualTo(spanId); + } + + @Test + void fromBytes_Invalid() { + assertThat(SpanId.fromBytes(null)).isEqualTo(SpanId.getInvalid()); + assertThat(SpanId.fromBytes(new byte[] {0, 1, 2, 3, 4})).isEqualTo(SpanId.getInvalid()); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanTest.java new file mode 100644 index 000000000..12a25484b --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/SpanTest.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.junit.jupiter.api.Test; + +class SpanTest { + @Test + void testGetCurrentSpan_Default() { + Span span = Span.current(); + assertThat(span).isSameAs(Span.getInvalid()); + } + + @Test + void testGetCurrentSpan_SetSpan() { + Span span = Span.wrap(SpanContext.getInvalid()); + try (Scope ignored = Context.current().with(span).makeCurrent()) { + assertThat(Span.current()).isSameAs(span); + } + } + + @Test + void testGetSpan_DefaultContext() { + Span span = Span.fromContext(Context.root()); + assertThat(span).isSameAs(Span.getInvalid()); + } + + @Test + void testGetSpan_ExplicitContext() { + Span span = Span.wrap(SpanContext.getInvalid()); + Context context = Context.root().with(span); + assertThat(Span.fromContext(context)).isSameAs(span); + } + + @Test + void testGetSpanWithoutDefault_DefaultContext() { + Span span = Span.fromContextOrNull(Context.root()); + assertThat(span).isNull(); + } + + @Test + void testGetSpanWithoutDefault_ExplicitContext() { + Span span = Span.wrap(SpanContext.getInvalid()); + Context context = Context.root().with(span); + assertThat(Span.fromContextOrNull(context)).isSameAs(span); + } + + @Test + void testInProcessContext() { + Span span = Span.wrap(SpanContext.getInvalid()); + try (Scope scope = span.makeCurrent()) { + assertThat(Span.current()).isSameAs(span); + Span secondSpan = Span.wrap(SpanContext.getInvalid()); + try (Scope secondScope = secondSpan.makeCurrent()) { + assertThat(Span.current()).isSameAs(secondSpan); + } finally { + assertThat(Span.current()).isSameAs(span); + } + } + assertThat(Span.current().getSpanContext().isValid()).isFalse(); + } + + @Test + void fromContext_null() { + assertThat(Span.fromContext(null)).isEqualTo(Span.getInvalid()); + assertThat(Span.fromContextOrNull(null)).isNull(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceFlagsTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceFlagsTest.java new file mode 100644 index 000000000..d6ef529f0 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceFlagsTest.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link TraceFlags}. */ +class TraceFlagsTest { + + @Test + void defaultInstances() { + assertThat(TraceFlags.getDefault().asHex()).isEqualTo("00"); + assertThat(TraceFlags.getSampled().asHex()).isEqualTo("01"); + } + + @Test + void isSampled() { + assertThat(TraceFlags.fromByte((byte) 0xff).isSampled()).isTrue(); + assertThat(TraceFlags.fromByte((byte) 0x01).isSampled()).isTrue(); + assertThat(TraceFlags.fromByte((byte) 0x05).isSampled()).isTrue(); + assertThat(TraceFlags.fromByte((byte) 0x00).isSampled()).isFalse(); + } + + @Test + void toFromHex() { + for (int i = 0; i < 256; i++) { + String hex = Integer.toHexString(i); + if (hex.length() == 1) { + hex = "0" + hex; + } + assertThat(TraceFlags.fromHex(hex, 0).asHex()).isEqualTo(hex); + } + } + + @Test + void toFromByte() { + for (int i = 0; i < 256; i++) { + assertThat(TraceFlags.fromByte((byte) i).asByte()).isEqualTo((byte) i); + } + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceIdTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceIdTest.java new file mode 100644 index 000000000..e41700167 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceIdTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.internal.OtelEncodingUtils; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link TraceId}. */ +class TraceIdTest { + private static final String first = "00000000000000000000000000000061"; + private static final String second = "ff000000000000000000000000000041"; + + @Test + void invalid() { + assertThat(TraceId.getInvalid()).isEqualTo("00000000000000000000000000000000"); + } + + @Test + void isValid() { + assertThat(TraceId.isValid(null)).isFalse(); + assertThat(TraceId.isValid("001")).isFalse(); + assertThat(TraceId.isValid("000000000000004z0000000000000016")).isFalse(); + assertThat(TraceId.isValid(TraceId.getInvalid())).isFalse(); + + assertThat(TraceId.isValid(first)).isTrue(); + assertThat(TraceId.isValid(second)).isTrue(); + } + + @Test + void fromLongs() { + assertThat(TraceId.fromLongs(0, 0)).isEqualTo(TraceId.getInvalid()); + assertThat(TraceId.fromLongs(0, 0x61)).isEqualTo(first); + assertThat(TraceId.fromLongs(0xff00000000000000L, 0x41)).isEqualTo(second); + assertThat(TraceId.fromLongs(0xff01020304050600L, 0xff0a0b0c0d0e0f00L)) + .isEqualTo("ff01020304050600ff0a0b0c0d0e0f00"); + } + + @Test + void fromBytes() { + String traceId = "0102030405060708090a0b0c0d0e0f00"; + assertThat(TraceId.fromBytes(OtelEncodingUtils.bytesFromBase16(traceId, TraceId.getLength()))) + .isEqualTo(traceId); + } + + @Test + void fromBytes_Invalid() { + assertThat(TraceId.fromBytes(null)).isEqualTo(TraceId.getInvalid()); + assertThat(TraceId.fromBytes(new byte[] {0, 1, 2, 3, 4})).isEqualTo(TraceId.getInvalid()); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceStateTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceStateTest.java new file mode 100644 index 000000000..dece72b73 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/TraceStateTest.java @@ -0,0 +1,312 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.InstanceOfAssertFactories.type; + +import com.google.common.testing.EqualsTester; +import java.util.Arrays; +import java.util.LinkedHashMap; +import org.junit.jupiter.api.Test; + +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"; + + private final TraceState firstTraceState = + TraceState.builder().put(FIRST_KEY, FIRST_VALUE).build(); + private final TraceState secondTraceState = + TraceState.builder().put(SECOND_KEY, SECOND_VALUE).build(); + private final TraceState multiValueTraceState = + TraceState.builder().put(FIRST_KEY, FIRST_VALUE).put(SECOND_KEY, SECOND_VALUE).build(); + + @Test + 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); + assertThat(firstTraceState.get("dog")).isNull(); + assertThat(firstTraceState.get(null)).isNull(); + } + + @Test + void sizeAndEmpty() { + assertThat(TraceState.getDefault().size()).isZero(); + assertThat(TraceState.getDefault().isEmpty()).isTrue(); + + assertThat(firstTraceState.size()).isOne(); + assertThat(firstTraceState.isEmpty()).isFalse(); + + assertThat(multiValueTraceState.size()).isEqualTo(2); + assertThat(multiValueTraceState.isEmpty()).isFalse(); + } + + @Test + void forEach() { + LinkedHashMap entries = new LinkedHashMap<>(); + firstTraceState.forEach(entries::put); + assertThat(entries).containsExactly(entry(FIRST_KEY, FIRST_VALUE)); + + entries.clear(); + secondTraceState.forEach(entries::put); + assertThat(entries).containsExactly(entry(SECOND_KEY, SECOND_VALUE)); + + entries.clear(); + multiValueTraceState.forEach(entries::put); + // Reverse order of input. + assertThat(entries) + .containsExactly(entry(SECOND_KEY, SECOND_VALUE), entry(FIRST_KEY, FIRST_VALUE)); + + assertThatCode(() -> firstTraceState.forEach(null)).doesNotThrowAnyException(); + } + + @Test + void asMap() { + assertThat(firstTraceState.asMap()).containsExactly(entry(FIRST_KEY, FIRST_VALUE)); + + assertThat(secondTraceState.asMap()).containsExactly(entry(SECOND_KEY, SECOND_VALUE)); + + // Reverse order of input. + assertThat(multiValueTraceState.asMap()) + .containsExactly(entry(SECOND_KEY, SECOND_VALUE), entry(FIRST_KEY, FIRST_VALUE)); + } + + @Test + void disallowsNullKey() { + assertThat(TraceState.builder().put(null, FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void disallowsEmptyKey() { + assertThat(TraceState.builder().put("", FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void invalidFirstKeyCharacter() { + assertThat(TraceState.builder().put("$_key", FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void firstKeyCharacterDigitIsAllowed() { + // note: a digit is only allowed if the key is in the tenant format (with an '@') + TraceState result = TraceState.builder().put("1@tenant", FIRST_VALUE).build(); + assertThat(result.get("1@tenant")).isEqualTo(FIRST_VALUE); + } + + @Test + void testValidLongTenantId() { + TraceState result = TraceState.builder().put("12345678901234567890@nr", FIRST_VALUE).build(); + assertThat(result.get("12345678901234567890@nr")).isEqualTo(FIRST_VALUE); + } + + @Test + void invalidKeyCharacters() { + assertThat(TraceState.builder().put("kEy_1", FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void testValidAtSignVendorNamePrefix() { + TraceState result = TraceState.builder().put("1@nr", FIRST_VALUE).build(); + assertThat(result.get("1@nr")).isEqualTo(FIRST_VALUE); + } + + @Test + void testVendorIdLongerThan13Characters() { + assertThat(TraceState.builder().put("1@nrabcdefghijkl", FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void testVendorIdLongerThan13Characters_longTenantId() { + assertThat(TraceState.builder().put("12345678901234567890@nrabcdefghijkl", FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void tenantIdLongerThan240Characters() { + char[] chars = new char[241]; + Arrays.fill(chars, 'a'); + String tenantId = new String(chars); + assertThat(TraceState.builder().put(tenantId + "@nr", FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void testNonVendorFormatFirstKeyCharacter() { + assertThat(TraceState.builder().put("1acdfrgs", FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void testMultipleAtSignNotAllowed() { + assertThat(TraceState.builder().put("1@n@r@", FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void invalidKeySize() { + char[] chars = new char[257]; + Arrays.fill(chars, 'a'); + String longKey = new String(chars); + assertThat(TraceState.builder().put(longKey, FIRST_VALUE).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + 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(TraceState.builder().put(allowedKey, FIRST_VALUE).build().get(allowedKey)) + .isEqualTo(FIRST_VALUE); + } + + @Test + void invalidValueSize() { + char[] chars = new char[257]; + Arrays.fill(chars, 'a'); + String longValue = new String(chars); + assertThat(TraceState.builder().put(FIRST_KEY, longValue).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + 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(TraceState.builder().put(FIRST_KEY, allowedValue).build().get(FIRST_KEY)) + .isEqualTo(allowedValue); + } + + @Test + @SuppressWarnings("checkstyle:AvoidEscapedUnicodeCharacters") + void invalidValues() { + assertThat(TraceState.builder().put(FIRST_KEY, null).build()) + .isEqualTo(TraceState.getDefault()); + assertThat(TraceState.builder().put("foo", "bar,").build()).isEqualTo(TraceState.getDefault()); + assertThat(TraceState.builder().put("foo", "bar ").build()).isEqualTo(TraceState.getDefault()); + assertThat(TraceState.builder().put("foo", "bar=").build()).isEqualTo(TraceState.getDefault()); + assertThat(TraceState.builder().put("foo", "bar\u0019").build()) + .isEqualTo(TraceState.getDefault()); + assertThat(TraceState.builder().put("foo", "bar\u007F").build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void addEntry() { + assertThat(firstTraceState.toBuilder().put(SECOND_KEY, SECOND_VALUE).build()) + .isEqualTo(multiValueTraceState); + } + + @Test + void updateEntry() { + assertThat(firstTraceState.toBuilder().put(FIRST_KEY, SECOND_VALUE).build().get(FIRST_KEY)) + .isEqualTo(SECOND_VALUE); + TraceState updatedMultiValueTraceState = + multiValueTraceState.toBuilder().put(FIRST_KEY, SECOND_VALUE).build(); + assertThat(updatedMultiValueTraceState.get(FIRST_KEY)).isEqualTo(SECOND_VALUE); + assertThat(updatedMultiValueTraceState.get(SECOND_KEY)).isEqualTo(SECOND_VALUE); + } + + @Test + void addAndUpdateEntry() { + assertThat( + firstTraceState.toBuilder() + .put(FIRST_KEY, SECOND_VALUE) // update the existing entry + .put(SECOND_KEY, FIRST_VALUE) // add a new entry + .build()) + .asInstanceOf(type(ArrayBasedTraceState.class)) + .extracting(ArrayBasedTraceState::getEntries) + .asList() + .containsExactly(SECOND_KEY, FIRST_VALUE, FIRST_KEY, SECOND_VALUE); + } + + @Test + void addSameKey() { + assertThat( + TraceState.builder() + .put(FIRST_KEY, SECOND_VALUE) // update the existing entry + .put(FIRST_KEY, FIRST_VALUE) // add a new entry + .build()) + .asInstanceOf(type(ArrayBasedTraceState.class)) + .extracting(ArrayBasedTraceState::getEntries) + .asList() + .containsExactly(FIRST_KEY, FIRST_VALUE); + } + + @Test + void remove() { + assertThat(multiValueTraceState.toBuilder().remove(SECOND_KEY).build()) + .isEqualTo(firstTraceState); + } + + @Test + void addAndRemoveEntry() { + assertThat( + TraceState.builder() + .put(FIRST_KEY, SECOND_VALUE) // update the existing entry + .remove(FIRST_KEY) // add a new entry + .build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void remove_NullNotAllowed() { + assertThat(multiValueTraceState.toBuilder().remove(null).build()) + .isEqualTo(multiValueTraceState); + } + + @Test + void traceState_EqualsAndHashCode() { + EqualsTester tester = new EqualsTester(); + tester.addEqualityGroup( + TraceState.getDefault(), + TraceState.getDefault(), + TraceState.getDefault().toBuilder().build(), + TraceState.builder().build()); + tester.addEqualityGroup( + firstTraceState, TraceState.builder().put(FIRST_KEY, FIRST_VALUE).build()); + tester.addEqualityGroup( + secondTraceState, TraceState.builder().put(SECOND_KEY, SECOND_VALUE).build()); + tester.testEquals(); + } + + @Test + void traceState_ToString() { + assertThat(TraceState.getDefault().toString()).isEqualTo("ArrayBasedTraceState{entries=[]}"); + } + + @Test + void doesNotCrash() { + assertThat(TraceState.getDefault().get(null)).isNull(); + assertThatCode(() -> TraceState.getDefault().forEach(null)).doesNotThrowAnyException(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorFuzzTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorFuzzTest.java new file mode 100644 index 000000000..4c720ff61 --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorFuzzTest.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace.propagation; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import edu.berkeley.cs.jqf.fuzz.Fuzz; +import edu.berkeley.cs.jqf.fuzz.JQF; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.runner.RunWith; + +@RunWith(JQF.class) +@SuppressWarnings("JavadocMethod") +public class W3CTraceContextPropagatorFuzzTest { + private final TextMapPropagator w3cTraceContextPropagator = + W3CTraceContextPropagator.getInstance(); + + @Fuzz + public void safeForRandomInputs(String traceParentHeader, String traceStateHeader) { + Context context = + w3cTraceContextPropagator.extract( + Context.root(), + ImmutableMap.of("traceparent", traceParentHeader, "tracestate", traceStateHeader), + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }); + + assertThat(context).isNotNull(); + } +} diff --git a/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorTest.java b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorTest.java new file mode 100644 index 000000000..0d4a3516e --- /dev/null +++ b/opentelemetry-java/api/all/src/test/java/io/opentelemetry/api/trace/propagation/W3CTraceContextPropagatorTest.java @@ -0,0 +1,544 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.trace.propagation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** Unit tests for {@link W3CTraceContextPropagator}. */ +class W3CTraceContextPropagatorTest { + + private static final TraceState TRACE_STATE = + TraceState.builder().put("foo", "bar").put("bar", "baz").build(); + private static final String TRACE_ID_BASE16 = "ff000000000000000000000000000041"; + private static final String SPAN_ID_BASE16 = "ff00000000000041"; + private static final String TRACEPARENT_HEADER_SAMPLED = + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-01"; + private static final String TRACEPARENT_HEADER_NOT_SAMPLED = + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-00"; + private static final TextMapSetter> setter = Map::put; + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + // Encoding preserves the order which is the reverse order of adding. + private static final String TRACESTATE_NOT_DEFAULT_ENCODING = "bar=baz,foo=bar"; + private static final String TRACESTATE_NOT_DEFAULT_ENCODING_WITH_SPACES = + "bar=baz , foo=bar"; + private final TextMapPropagator w3cTraceContextPropagator = + W3CTraceContextPropagator.getInstance(); + + private static SpanContext getSpanContext(Context context) { + return Span.fromContext(context).getSpanContext(); + } + + private static Context withSpanContext(SpanContext spanContext, Context context) { + return context.with(Span.wrap(spanContext)); + } + + @Test + void inject_Nothing() { + Map carrier = new LinkedHashMap<>(); + w3cTraceContextPropagator.inject(Context.current(), carrier, setter); + assertThat(carrier).hasSize(0); + } + + @Test + void inject_NullCarrierUsage() { + final Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()); + w3cTraceContextPropagator.inject( + context, + null, + (TextMapSetter>) (ignored, key, value) -> carrier.put(key, value)); + assertThat(carrier) + .containsExactly(entry(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_SAMPLED)); + } + + @Test + void inject_invalidContext() { + Map carrier = new LinkedHashMap<>(); + w3cTraceContextPropagator.inject( + withSpanContext( + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.builder().put("foo", "bar").build()), + Context.current()), + carrier, + setter); + assertThat(carrier).hasSize(0); + } + + @Test + void inject_SampledContext() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()); + w3cTraceContextPropagator.inject(context, carrier, setter); + assertThat(carrier) + .containsExactly(entry(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_SAMPLED)); + } + + @Test + void inject_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + w3cTraceContextPropagator.inject(context, carrier, setter); + assertThat(carrier) + .containsExactly( + entry(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_NOT_SAMPLED)); + } + + @Test + void inject_SampledContext_WithTraceState() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TRACE_STATE), + Context.current()); + w3cTraceContextPropagator.inject(context, carrier, setter); + assertThat(carrier) + .containsExactly( + entry(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_SAMPLED), + entry(W3CTraceContextPropagator.TRACE_STATE, TRACESTATE_NOT_DEFAULT_ENCODING)); + } + + @Test + void inject_NotSampledContext_WithTraceState() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TRACE_STATE), + Context.current()); + w3cTraceContextPropagator.inject(context, carrier, setter); + assertThat(carrier) + .containsExactly( + entry(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_NOT_SAMPLED), + entry(W3CTraceContextPropagator.TRACE_STATE, TRACESTATE_NOT_DEFAULT_ENCODING)); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + w3cTraceContextPropagator.inject(null, carrier, setter); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TRACE_STATE), + Context.current()); + w3cTraceContextPropagator.inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } + + @Test + void extract_Nothing() { + // Context remains untouched. + assertThat(w3cTraceContextPropagator.extract(Context.current(), Collections.emptyMap(), getter)) + .isSameAs(Context.current()); + } + + @Test + void extract_SampledContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_SAMPLED); + assertThat( + getSpanContext(w3cTraceContextPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NullCarrier() { + Map carrier = new LinkedHashMap<>(); + carrier.put(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_SAMPLED); + assertThat( + getSpanContext(w3cTraceContextPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extractAndInject_MoreFlags() { + String traceParent = "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-03"; + Map extractCarrier = new LinkedHashMap<>(); + extractCarrier.put(W3CTraceContextPropagator.TRACE_PARENT, traceParent); + Context context = w3cTraceContextPropagator.extract(Context.current(), extractCarrier, getter); + Map injectCarrier = new LinkedHashMap<>(); + w3cTraceContextPropagator.inject(context, injectCarrier, setter); + assertThat(extractCarrier).isEqualTo(injectCarrier); + } + + @Test + void extract_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_NOT_SAMPLED); + assertThat( + getSpanContext(w3cTraceContextPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_WithTraceState() { + Map carrier = new LinkedHashMap<>(); + carrier.put(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_SAMPLED); + carrier.put(W3CTraceContextPropagator.TRACE_STATE, TRACESTATE_NOT_DEFAULT_ENCODING); + assertThat( + getSpanContext(w3cTraceContextPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TRACE_STATE)); + } + + @Test + void extract_NotSampledContext_WithTraceState() { + Map carrier = new LinkedHashMap<>(); + carrier.put(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_NOT_SAMPLED); + carrier.put(W3CTraceContextPropagator.TRACE_STATE, TRACESTATE_NOT_DEFAULT_ENCODING); + assertThat( + getSpanContext(w3cTraceContextPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TRACE_STATE)); + } + + @Test + void extract_NotSampledContext_NextVersion() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + W3CTraceContextPropagator.TRACE_PARENT, + "01-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-00-02"); + assertThat( + getSpanContext(w3cTraceContextPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext_EmptyTraceState() { + Map carrier = new LinkedHashMap<>(); + carrier.put(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_NOT_SAMPLED); + carrier.put(W3CTraceContextPropagator.TRACE_STATE, ""); + assertThat( + getSpanContext(w3cTraceContextPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext_TraceStateWithSpaces() { + Map carrier = new LinkedHashMap<>(); + carrier.put(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_NOT_SAMPLED); + carrier.put(W3CTraceContextPropagator.TRACE_STATE, TRACESTATE_NOT_DEFAULT_ENCODING_WITH_SPACES); + assertThat( + getSpanContext(w3cTraceContextPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TRACE_STATE)); + } + + @Test + void extract_EmptyHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(W3CTraceContextPropagator.TRACE_PARENT, ""); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_invalidDelimiters() { + Map carrier = new LinkedHashMap<>(); + Context input = Context.current(); + Context result; + + carrier.put( + W3CTraceContextPropagator.TRACE_PARENT, + "01+" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-00-02"); + result = w3cTraceContextPropagator.extract(input, carrier, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isEqualTo(SpanContext.getInvalid()); + + carrier.put( + W3CTraceContextPropagator.TRACE_PARENT, + "01-" + TRACE_ID_BASE16 + "+" + SPAN_ID_BASE16 + "-00-02"); + result = w3cTraceContextPropagator.extract(input, carrier, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isEqualTo(SpanContext.getInvalid()); + + carrier.put( + W3CTraceContextPropagator.TRACE_PARENT, + "01-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "+00-02"); + result = w3cTraceContextPropagator.extract(input, carrier, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isEqualTo(SpanContext.getInvalid()); + + carrier.put( + W3CTraceContextPropagator.TRACE_PARENT, + "01-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-00+02"); + result = w3cTraceContextPropagator.extract(input, carrier, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isEqualTo(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTraceId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + "abcdefghijklmnopabcdefghijklmnop" + "-" + SPAN_ID_BASE16 + "-01"); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "00-" + SPAN_ID_BASE16 + "-01"); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + "abcdefghijklmnop" + "-01"); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_Size() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "00-01"); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceFlags() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-gh"); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceFlags_Size() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-0100"); + verifyInvalidBehavior(invalidHeaders); + } + + private void verifyInvalidBehavior(Map invalidHeaders) { + Context input = Context.current(); + Context result = w3cTraceContextPropagator.extract(input, invalidHeaders, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTracestate_EntriesDelimiter() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-01"); + invalidHeaders.put(W3CTraceContextPropagator.TRACE_STATE, "foo=bar;test=test"); + assertThat( + getSpanContext( + w3cTraceContextPropagator.extract(Context.current(), invalidHeaders, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_InvalidTracestate_KeyValueDelimiter() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-01"); + invalidHeaders.put(W3CTraceContextPropagator.TRACE_STATE, "foo=bar,test-test"); + assertThat( + getSpanContext( + w3cTraceContextPropagator.extract(Context.current(), invalidHeaders, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_InvalidTracestate_EmptyValue() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-01"); + invalidHeaders.put(W3CTraceContextPropagator.TRACE_STATE, "foo=,test=test"); + assertThat( + getSpanContext( + w3cTraceContextPropagator.extract(Context.current(), invalidHeaders, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_InvalidTracestate_OneString() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-01"); + invalidHeaders.put(W3CTraceContextPropagator.TRACE_STATE, "test-test"); + assertThat( + getSpanContext( + w3cTraceContextPropagator.extract(Context.current(), invalidHeaders, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_InvalidVersion_ff() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "ff-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-01"); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceparent_extraTrailing() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "00-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-00-01"); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_ValidTraceparent_nextVersion_extraTrailing() { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put( + W3CTraceContextPropagator.TRACE_PARENT, + "01-" + TRACE_ID_BASE16 + "-" + SPAN_ID_BASE16 + "-00-01"); + assertThat( + getSpanContext( + w3cTraceContextPropagator.extract(Context.current(), invalidHeaders, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void fieldsList() { + assertThat(w3cTraceContextPropagator.fields()) + .containsExactly( + W3CTraceContextPropagator.TRACE_PARENT, W3CTraceContextPropagator.TRACE_STATE); + } + + @Test + void headerNames() { + assertThat(W3CTraceContextPropagator.TRACE_PARENT).isEqualTo("traceparent"); + assertThat(W3CTraceContextPropagator.TRACE_STATE).isEqualTo("tracestate"); + } + + @Test + void extract_emptyCarrier() { + Map emptyHeaders = new HashMap<>(); + assertThat( + getSpanContext( + w3cTraceContextPropagator.extract(Context.current(), emptyHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_nullContext() { + assertThat(w3cTraceContextPropagator.extract(null, Collections.emptyMap(), getter)) + .isSameAs(Context.root()); + } + + @Test + void extract_nullGetter() { + Context context = + withSpanContext( + SpanContext.create( + TRACE_ID_BASE16, SPAN_ID_BASE16, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + assertThat(w3cTraceContextPropagator.extract(context, Collections.emptyMap(), null)) + .isSameAs(context); + } + + // Tests transplanted from the w3c test suite + + @ParameterizedTest + @ValueSource( + strings = {"foo@=1,bar=2", "@foo=1,bar=2", "foo@@bar=1,bar=2", "foo@bar@baz=1,bar=2"}) + void test_tracestate_key_illegal_vendor_format(String traceState) { + Map invalidHeaders = new HashMap<>(); + invalidHeaders.put(W3CTraceContextPropagator.TRACE_PARENT, TRACEPARENT_HEADER_SAMPLED); + invalidHeaders.put(W3CTraceContextPropagator.TRACE_STATE, traceState); + Context context = + W3CTraceContextPropagator.getInstance().extract(Context.root(), invalidHeaders, getter); + assertThat(Span.fromContext(context).getSpanContext().getTraceState().get("bar")).isNull(); + } +} diff --git a/opentelemetry-java/api/build.gradle.kts b/opentelemetry-java/api/build.gradle.kts new file mode 100644 index 000000000..29b0b3361 --- /dev/null +++ b/opentelemetry-java/api/build.gradle.kts @@ -0,0 +1,10 @@ +subprojects { + // Workaround https://github.com/gradle/gradle/issues/847 + group = "io.opentelemetry.api" + val proj = this + plugins.withId("java") { + configure { + archivesBaseName = "opentelemetry-api-${proj.name}" + } + } +} diff --git a/opentelemetry-java/api/metrics/README.md b/opentelemetry-java/api/metrics/README.md new file mode 100644 index 000000000..9452f1b19 --- /dev/null +++ b/opentelemetry-java/api/metrics/README.md @@ -0,0 +1,9 @@ +# OpenTelemetry Metrics API + +[![Javadocs][javadoc-image]][javadoc-url] + +* The interfaces in this directory can be implemented to create alternative + implementations of the OpenTelemetry library. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-api-metrics.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-api-metrics \ No newline at end of file diff --git a/opentelemetry-java/api/metrics/build.gradle.kts b/opentelemetry-java/api/metrics/build.gradle.kts new file mode 100644 index 000000000..501fa4357 --- /dev/null +++ b/opentelemetry-java/api/metrics/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("java-library") + id("maven-publish") + + id("me.champeau.jmh") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry API" +extra["moduleName"] = "io.opentelemetry.api.metrics" + +dependencies { + api(project(":api:all")) + + annotationProcessor("com.google.auto.value:auto-value") + + testImplementation("edu.berkeley.cs.jqf:jqf-fuzz") + testImplementation("com.google.guava:guava-testlib") +} diff --git a/opentelemetry-java/api/metrics/gradle.properties b/opentelemetry-java/api/metrics/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/api/metrics/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/AsynchronousInstrument.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/AsynchronousInstrument.java new file mode 100644 index 000000000..4d68b81b6 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/AsynchronousInstrument.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.ThreadSafe; + +/** + * {@code AsynchronousInstrument} is an interface that defines a type of instruments that are used + * to report measurements asynchronously. + * + *

    They are reported by a callback, once per collection interval, and lack Context. They are + * permitted to report only one value per distinct label set per period. If the application observes + * multiple values for the same label set, in a single callback, the last value is the only value + * kept. + */ +@ThreadSafe +public interface AsynchronousInstrument extends Instrument { + + /** The result pass to the updater. */ + interface LongResult { + void observe(long value, Labels labels); + } + + /** The result pass to the updater. */ + interface DoubleResult { + void observe(double value, Labels labels); + } +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/AsynchronousInstrumentBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/AsynchronousInstrumentBuilder.java new file mode 100644 index 000000000..046f87ed5 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/AsynchronousInstrumentBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import java.util.function.Consumer; + +/** Builder class for {@link AsynchronousInstrument}. */ +public interface AsynchronousInstrumentBuilder extends InstrumentBuilder { + /** + * Sets a consumer that gets executed every collection interval. + * + *

    Evaluation is deferred until needed, if this {@code AsynchronousInstrument} metric is not + * exported then it will never be called. + * + * @param updater the consumer to be executed before export. + */ + AsynchronousInstrumentBuilder setUpdater(Consumer updater); + + @Override + AsynchronousInstrument build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BatchRecorder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BatchRecorder.java new file mode 100644 index 000000000..f7075839c --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BatchRecorder.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * Util class that can be use to atomically record measurements associated with a set of Metrics. + * + *

    This class is equivalent with individually calling record on every Measure, but has the + * advantage that all these operations are recorded atomically and it is more efficient. + */ +@ThreadSafe +public interface BatchRecorder { + /** + * Associates the {@link LongValueRecorder} with the given value. Subsequent updates to the same + * {@link LongValueRecorder} will overwrite the previous value. + * + * @param valueRecorder the {@link LongValueRecorder}. + * @param value the value to be associated with {@code valueRecorder}. + * @return this. + */ + BatchRecorder put(LongValueRecorder valueRecorder, long value); + + /** + * Associates the {@link DoubleValueRecorder} with the given value. Subsequent updates to the same + * {@link DoubleValueRecorder} will overwrite the previous value. + * + * @param valueRecorder the {@link DoubleValueRecorder}. + * @param value the value to be associated with {@code valueRecorder}. + * @return this. + */ + BatchRecorder put(DoubleValueRecorder valueRecorder, double value); + + /** + * Associates the {@link LongCounter} with the given value. Subsequent updates to the same {@link + * LongCounter} will overwrite the previous value. + * + * @param counter the {@link LongCounter}. + * @param value the value to be associated with {@code counter}. + * @return this. + */ + BatchRecorder put(LongCounter counter, long value); + + /** + * Associates the {@link DoubleCounter} with the given value. Subsequent updates to the same + * {@link DoubleCounter} will overwrite the previous value. + * + * @param counter the {@link DoubleCounter}. + * @param value the value to be associated with {@code counter}. + * @return this. + */ + BatchRecorder put(DoubleCounter counter, double value); + + /** + * Associates the {@link LongUpDownCounter} with the given value. Subsequent updates to the same + * {@link LongCounter} will overwrite the previous value. + * + * @param upDownCounter the {@link LongCounter}. + * @param value the value to be associated with {@code counter}. + * @return this. + */ + BatchRecorder put(LongUpDownCounter upDownCounter, long value); + + /** + * Associates the {@link DoubleUpDownCounter} with the given value. Subsequent updates to the same + * {@link DoubleCounter} will overwrite the previous value. + * + * @param upDownCounter the {@link DoubleCounter}. + * @param value the value to be associated with {@code counter}. + * @return this. + */ + BatchRecorder put(DoubleUpDownCounter upDownCounter, double value); + + /** + * Records all of measurements at the same time. + * + *

    This method records all measurements every time it is called, so make sure it is not called + * twice if not needed. + */ + void record(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleCounter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleCounter.java new file mode 100644 index 000000000..6a3c450c8 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleCounter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** A {@code Bound Instrument} for a {@link DoubleCounter}. */ +@ThreadSafe +public interface BoundDoubleCounter extends BoundSynchronousInstrument { + /** + * Adds the given {@code increment} to the current value. The values cannot be negative. + * + *

    The value added is associated with the current {@code Context}. + * + * @param increment the value to add. + */ + void add(double increment); + + @Override + void unbind(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleUpDownCounter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleUpDownCounter.java new file mode 100644 index 000000000..24223cb17 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleUpDownCounter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** A {@code Bound Instrument} for a {@link DoubleUpDownCounter}. */ +@ThreadSafe +public interface BoundDoubleUpDownCounter extends BoundSynchronousInstrument { + /** + * Adds the given {@code increment} to the current value. + * + *

    The value added is associated with the current {@code Context}. + * + * @param increment the value to add. + */ + void add(double increment); + + @Override + void unbind(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleValueRecorder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleValueRecorder.java new file mode 100644 index 000000000..6fc3866d1 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundDoubleValueRecorder.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** A {@code Bound Instrument} for a {@link DoubleValueRecorder}. */ +@ThreadSafe +public interface BoundDoubleValueRecorder extends BoundSynchronousInstrument { + /** + * Records the given measurement, associated with the current {@code Context}. + * + * @param value the measurement to record. + * @throws IllegalArgumentException if value is negative. + */ + void record(double value); + + @Override + void unbind(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongCounter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongCounter.java new file mode 100644 index 000000000..c7fa15e05 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongCounter.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** A {@code Bound Instrument} for a {@link LongCounter}. */ +@ThreadSafe +public interface BoundLongCounter extends BoundSynchronousInstrument { + + /** + * Adds the given {@code increment} to the current value. The values cannot be negative. + * + *

    The value added is associated with the current {@code Context}. + * + * @param increment the value to add. + */ + void add(long increment); + + @Override + void unbind(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongUpDownCounter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongUpDownCounter.java new file mode 100644 index 000000000..b0c253f1a --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongUpDownCounter.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** A {@code Bound Instrument} for a {@link LongUpDownCounter}. */ +@ThreadSafe +public interface BoundLongUpDownCounter extends BoundSynchronousInstrument { + + /** + * Adds the given {@code increment} to the current value. + * + *

    The value added is associated with the current {@code Context}. + * + * @param increment the value to add. + */ + void add(long increment); + + @Override + void unbind(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongValueRecorder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongValueRecorder.java new file mode 100644 index 000000000..f7c716c76 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundLongValueRecorder.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** A {@code Bound Instrument} for a {@link LongValueRecorder}. */ +@ThreadSafe +public interface BoundLongValueRecorder extends BoundSynchronousInstrument { + /** + * Records the given measurement, associated with the current {@code Context}. + * + * @param value the measurement to record. + * @throws IllegalArgumentException if value is negative. + */ + void record(long value); + + @Override + void unbind(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundSynchronousInstrument.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundSynchronousInstrument.java new file mode 100644 index 000000000..17fdc0da4 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/BoundSynchronousInstrument.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +interface BoundSynchronousInstrument { + /** + * Unbinds the current {@code Bound} from the Instrument. + * + *

    After this method returns the current instance {@code Bound} is considered invalid (not + * being managed by the instrument). + */ + void unbind(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DefaultMeter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DefaultMeter.java new file mode 100644 index 000000000..0a61f9337 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DefaultMeter.java @@ -0,0 +1,661 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.internal.Utils; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Objects; +import java.util.function.Consumer; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; + +/** No-op implementations of {@link Meter}. */ +@ThreadSafe +final class DefaultMeter implements Meter { + + private static final DefaultMeter INSTANCE = new DefaultMeter(); + private static final String COUNTERS_CAN_ONLY_INCREASE = "Counters can only increase"; + + /* VisibleForTesting */ static final String ERROR_MESSAGE_INVALID_NAME = + "Name should be a ASCII string with a length no greater than " + + MetricsStringUtils.METRIC_NAME_MAX_LENGTH + + " characters."; + + static Meter getInstance() { + return INSTANCE; + } + + @Override + public DoubleCounterBuilder doubleCounterBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopDoubleCounter.NoopBuilder(); + } + + @Override + public LongCounterBuilder longCounterBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopLongCounter.NoopBuilder(); + } + + @Override + public DoubleUpDownCounterBuilder doubleUpDownCounterBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopDoubleUpDownCounter.NoopBuilder(); + } + + @Override + public LongUpDownCounterBuilder longUpDownCounterBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopLongUpDownCounter.NoopBuilder(); + } + + @Override + public DoubleValueRecorderBuilder doubleValueRecorderBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopDoubleValueRecorder.NoopBuilder(); + } + + @Override + public LongValueRecorderBuilder longValueRecorderBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopLongValueRecorder.NoopBuilder(); + } + + @Override + public DoubleSumObserverBuilder doubleSumObserverBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopDoubleSumObserver.NoopBuilder(); + } + + @Override + public LongSumObserverBuilder longSumObserverBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopLongSumObserver.NoopBuilder(); + } + + @Override + public DoubleUpDownSumObserverBuilder doubleUpDownSumObserverBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopDoubleUpDownSumObserver.NoopBuilder(); + } + + @Override + public LongUpDownSumObserverBuilder longUpDownSumObserverBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopLongUpDownSumObserver.NoopBuilder(); + } + + @Override + public DoubleValueObserverBuilder doubleValueObserverBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopDoubleValueObserver.NoopBuilder(); + } + + @Override + public LongValueObserverBuilder longValueObserverBuilder(String name) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + return new NoopLongValueObserver.NoopBuilder(); + } + + @Override + public BatchRecorder newBatchRecorder(String... keyValuePairs) { + validateLabelPairs(keyValuePairs); + return NoopBatchRecorder.INSTANCE; + } + + private DefaultMeter() {} + + /** No-op implementation of {@link DoubleCounter} interface. */ + @Immutable + private static final class NoopDoubleCounter implements DoubleCounter { + + private NoopDoubleCounter() {} + + @Override + public void add(double increment, Labels labels) { + Objects.requireNonNull(labels, "labels"); + Utils.checkArgument(increment >= 0.0, COUNTERS_CAN_ONLY_INCREASE); + } + + @Override + public void add(double increment) { + add(increment, Labels.empty()); + } + + @Override + public NoopBoundDoubleCounter bind(Labels labels) { + Objects.requireNonNull(labels, "labels"); + return NoopBoundDoubleCounter.INSTANCE; + } + + @Immutable + private enum NoopBoundDoubleCounter implements BoundDoubleCounter { + INSTANCE; + + @Override + public void add(double increment) { + Utils.checkArgument(increment >= 0.0, COUNTERS_CAN_ONLY_INCREASE); + } + + @Override + public void unbind() {} + } + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements DoubleCounterBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public DoubleCounter build() { + return new NoopDoubleCounter(); + } + } + } + + /** No-op implementation of {@link LongCounter} interface. */ + @Immutable + private static final class NoopLongCounter implements LongCounter { + + private NoopLongCounter() {} + + @Override + public void add(long increment, Labels labels) { + Objects.requireNonNull(labels, "labels"); + Utils.checkArgument(increment >= 0, COUNTERS_CAN_ONLY_INCREASE); + } + + @Override + public void add(long increment) { + add(increment, Labels.empty()); + } + + @Override + public NoopBoundLongCounter bind(Labels labels) { + Objects.requireNonNull(labels, "labels"); + return NoopBoundLongCounter.INSTANCE; + } + + @Immutable + private enum NoopBoundLongCounter implements BoundLongCounter { + INSTANCE; + + @Override + public void add(long increment) { + Utils.checkArgument(increment >= 0, COUNTERS_CAN_ONLY_INCREASE); + } + + @Override + public void unbind() {} + } + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements LongCounterBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public LongCounter build() { + return new NoopLongCounter(); + } + } + } + + /** No-op implementation of {@link DoubleUpDownCounter} interface. */ + @Immutable + private static final class NoopDoubleUpDownCounter implements DoubleUpDownCounter { + + private NoopDoubleUpDownCounter() {} + + @Override + public void add(double increment, Labels labels) { + Objects.requireNonNull(labels, "labels"); + } + + @Override + public void add(double increment) { + add(increment, Labels.empty()); + } + + @Override + public NoopBoundDoubleUpDownCounter bind(Labels labels) { + Objects.requireNonNull(labels, "labels"); + return NoopBoundDoubleUpDownCounter.INSTANCE; + } + + @Immutable + private enum NoopBoundDoubleUpDownCounter implements BoundDoubleUpDownCounter { + INSTANCE; + + @Override + public void add(double increment) {} + + @Override + public void unbind() {} + } + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements DoubleUpDownCounterBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public DoubleUpDownCounter build() { + return new NoopDoubleUpDownCounter(); + } + } + } + + /** No-op implementation of {@link LongUpDownCounter} interface. */ + @Immutable + private static final class NoopLongUpDownCounter implements LongUpDownCounter { + + private NoopLongUpDownCounter() {} + + @Override + public void add(long increment, Labels labels) { + Objects.requireNonNull(labels, "labels"); + } + + @Override + public void add(long increment) { + add(increment, Labels.empty()); + } + + @Override + public NoopBoundLongUpDownCounter bind(Labels labels) { + Objects.requireNonNull(labels, "labels"); + return NoopBoundLongUpDownCounter.INSTANCE; + } + + @Immutable + private enum NoopBoundLongUpDownCounter implements BoundLongUpDownCounter { + INSTANCE; + + @Override + public void add(long increment) {} + + @Override + public void unbind() {} + } + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements LongUpDownCounterBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public LongUpDownCounter build() { + return new NoopLongUpDownCounter(); + } + } + } + + /** No-op implementation of {@link DoubleValueRecorder} interface. */ + @Immutable + private static final class NoopDoubleValueRecorder implements DoubleValueRecorder { + + private NoopDoubleValueRecorder() {} + + @Override + public void record(double value, Labels labels) { + Objects.requireNonNull(labels, "labels"); + } + + @Override + public void record(double value) { + record(value, Labels.empty()); + } + + @Override + public NoopBoundDoubleValueRecorder bind(Labels labels) { + Objects.requireNonNull(labels, "labels"); + return NoopBoundDoubleValueRecorder.INSTANCE; + } + + @Immutable + private enum NoopBoundDoubleValueRecorder implements BoundDoubleValueRecorder { + INSTANCE; + + @Override + public void record(double value) {} + + @Override + public void unbind() {} + } + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements DoubleValueRecorderBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public DoubleValueRecorder build() { + return new NoopDoubleValueRecorder(); + } + } + } + + /** No-op implementation of {@link LongValueRecorder} interface. */ + @Immutable + private static final class NoopLongValueRecorder implements LongValueRecorder { + + private NoopLongValueRecorder() {} + + @Override + public void record(long value, Labels labels) { + Objects.requireNonNull(labels, "labels"); + } + + @Override + public void record(long value) { + record(value, Labels.empty()); + } + + @Override + public NoopBoundLongValueRecorder bind(Labels labels) { + Objects.requireNonNull(labels, "labels"); + return NoopBoundLongValueRecorder.INSTANCE; + } + + @Immutable + private enum NoopBoundLongValueRecorder implements BoundLongValueRecorder { + INSTANCE; + + @Override + public void record(long value) {} + + @Override + public void unbind() {} + } + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements LongValueRecorderBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public LongValueRecorder build() { + return new NoopLongValueRecorder(); + } + } + } + + /** No-op implementation of {@link DoubleSumObserver} interface. */ + @Immutable + private static final class NoopDoubleSumObserver implements DoubleSumObserver { + + private NoopDoubleSumObserver() {} + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements DoubleSumObserverBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public DoubleSumObserverBuilder setUpdater(Consumer updater) { + Objects.requireNonNull(updater, "callback"); + return this; + } + + @Override + public DoubleSumObserver build() { + return new NoopDoubleSumObserver(); + } + } + } + + /** No-op implementation of {@link LongSumObserver} interface. */ + @Immutable + private static final class NoopLongSumObserver implements LongSumObserver { + + private NoopLongSumObserver() {} + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements LongSumObserverBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public NoopBuilder setUpdater(Consumer updater) { + Objects.requireNonNull(updater, "callback"); + return this; + } + + @Override + public LongSumObserver build() { + return new NoopLongSumObserver(); + } + } + } + + /** No-op implementation of {@link DoubleUpDownSumObserver} interface. */ + @Immutable + private static final class NoopDoubleUpDownSumObserver implements DoubleUpDownSumObserver { + + private NoopDoubleUpDownSumObserver() {} + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements DoubleUpDownSumObserverBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public DoubleUpDownSumObserverBuilder setUpdater(Consumer updater) { + Objects.requireNonNull(updater, "callback"); + return this; + } + + @Override + public DoubleUpDownSumObserver build() { + return new NoopDoubleUpDownSumObserver(); + } + } + } + + /** No-op implementation of {@link LongUpDownSumObserver} interface. */ + @Immutable + private static final class NoopLongUpDownSumObserver implements LongUpDownSumObserver { + + private NoopLongUpDownSumObserver() {} + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements LongUpDownSumObserverBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public LongUpDownSumObserverBuilder setUpdater(Consumer updater) { + Objects.requireNonNull(updater, "callback"); + return this; + } + + @Override + public LongUpDownSumObserver build() { + return new NoopLongUpDownSumObserver(); + } + } + } + + /** No-op implementation of {@link DoubleValueObserver} interface. */ + @Immutable + private static final class NoopDoubleValueObserver implements DoubleValueObserver { + + private NoopDoubleValueObserver() {} + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements DoubleValueObserverBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public DoubleValueObserverBuilder setUpdater(Consumer updater) { + Objects.requireNonNull(updater, "callback"); + return this; + } + + @Override + public DoubleValueObserver build() { + return new NoopDoubleValueObserver(); + } + } + } + + /** No-op implementation of {@link LongValueObserver} interface. */ + @Immutable + private static final class NoopLongValueObserver implements LongValueObserver { + + private NoopLongValueObserver() {} + + private static final class NoopBuilder extends NoopAbstractInstrumentBuilder + implements LongValueObserverBuilder { + + @Override + protected NoopBuilder getThis() { + return this; + } + + @Override + public LongValueObserverBuilder setUpdater(Consumer updater) { + Objects.requireNonNull(updater, "callback"); + return this; + } + + @Override + public LongValueObserver build() { + return new NoopLongValueObserver(); + } + } + } + + /** No-op implementation of {@link BatchRecorder} interface. */ + private enum NoopBatchRecorder implements BatchRecorder { + INSTANCE; + + @Override + public BatchRecorder put(LongValueRecorder valueRecorder, long value) { + Objects.requireNonNull(valueRecorder, "valueRecorder"); + return this; + } + + @Override + public BatchRecorder put(DoubleValueRecorder valueRecorder, double value) { + Objects.requireNonNull(valueRecorder, "valueRecorder"); + return this; + } + + @Override + public BatchRecorder put(LongCounter counter, long value) { + Objects.requireNonNull(counter, "counter"); + Utils.checkArgument(value >= 0, COUNTERS_CAN_ONLY_INCREASE); + return this; + } + + @Override + public BatchRecorder put(DoubleCounter counter, double value) { + Objects.requireNonNull(counter, "counter"); + Utils.checkArgument(value >= 0.0, COUNTERS_CAN_ONLY_INCREASE); + return this; + } + + @Override + public BatchRecorder put(LongUpDownCounter upDownCounter, long value) { + Objects.requireNonNull(upDownCounter, "upDownCounter"); + return this; + } + + @Override + public BatchRecorder put(DoubleUpDownCounter upDownCounter, double value) { + Objects.requireNonNull(upDownCounter, "upDownCounter"); + return this; + } + + @Override + public void record() {} + } + + private abstract static class NoopAbstractInstrumentBuilder< + B extends NoopAbstractInstrumentBuilder> + implements InstrumentBuilder { + + @Override + public B setDescription(String description) { + Objects.requireNonNull(description, "description"); + return getThis(); + } + + @Override + public B setUnit(String unit) { + Objects.requireNonNull(unit, "unit"); + return getThis(); + } + + protected abstract B getThis(); + } + + /** + * Validates that the array of Strings is 1) even in length, and 2) they can be formed into valid + * pairs where the first item in the pair is not null. + * + * @param keyValuePairs The String[] to validate for correctness. + * @throws IllegalArgumentException if any of the preconditions are violated. + */ + private static void validateLabelPairs(String[] keyValuePairs) { + Utils.checkArgument( + keyValuePairs.length % 2 == 0, + "You must provide an even number of key/value pair arguments."); + for (int i = 0; i < keyValuePairs.length; i += 2) { + String key = keyValuePairs[i]; + Objects.requireNonNull(key, "You cannot provide null keys for label creation."); + } + } +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DefaultMeterProvider.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DefaultMeterProvider.java new file mode 100644 index 000000000..91db00977 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DefaultMeterProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +final class DefaultMeterProvider implements MeterProvider { + + private static final MeterProvider INSTANCE = new DefaultMeterProvider(); + + static MeterProvider getInstance() { + return INSTANCE; + } + + @Override + public Meter get(String instrumentationName) { + return get(instrumentationName, null); + } + + @Override + public Meter get(String instrumentationName, String instrumentationVersion) { + return DefaultMeter.getInstance(); + } + + private DefaultMeterProvider() {} +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleCounter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleCounter.java new file mode 100644 index 000000000..48134d007 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleCounter.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Counter is the most common synchronous instrument. This instrument supports an {@link + * #add(double, Labels)}` function for reporting an increment, and is restricted to non-negative + * increments. The default aggregation is `Sum`. + * + *

    Example: + * + *

    {@code
    + * class YourClass {
    + *   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + *   private static final DoubleCounter counter =
    + *       meter.
    + *           .doubleCounterBuilder("allocated_resources")
    + *           .setDescription("Total allocated resources")
    + *           .setUnit("1")
    + *           .build();
    + *
    + *   // It is recommended that the API user keep references to a Bound Counters.
    + *   private static final BoundDoubleCounter someWorkBound =
    + *       counter.bind("work_name", "some_work");
    + *
    + *   void doSomeWork() {
    + *      someWorkBound.add(10.2);  // Resources needed for this task.
    + *      // Your code here.
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface DoubleCounter extends SynchronousInstrument { + + /** + * Adds the given {@code increment} to the current value. The values cannot be negative. + * + *

    The value added is associated with the current {@code Context} and provided set of labels. + * + * @param increment the value to add. + * @param labels the labels to be associated to this recording. + */ + void add(double increment, Labels labels); + + /** + * Adds the given {@code increment} to the current value. The values cannot be negative. + * + *

    The value added is associated with the current {@code Context} and with empty labels. + * + * @param increment the value to add. + */ + void add(double increment); + + @Override + BoundDoubleCounter bind(Labels labels); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleCounterBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleCounterBuilder.java new file mode 100644 index 000000000..d7bf2dd36 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleCounterBuilder.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +/** Builder class for {@link DoubleCounter}. */ +public interface DoubleCounterBuilder extends SynchronousInstrumentBuilder { + @Override + DoubleCounterBuilder setDescription(String description); + + @Override + DoubleCounterBuilder setUnit(String unit); + + @Override + DoubleCounter build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleSumObserver.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleSumObserver.java new file mode 100644 index 000000000..2b2dd27d5 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleSumObserver.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * {@code SumObserver} is the asynchronous instrument corresponding to Counter, used to capture a + * monotonic sum with Observe(sum). + * + *

    "Sum" appears in the name to remind that it is used to capture sums directly. Use a + * SumObserver to capture any value that starts at zero and rises throughout the process lifetime + * and never falls. + * + *

    A {@code SumObserver} is a good choice in situations where a measurement is expensive to + * compute, such that it would be wasteful to compute on every request. + * + *

    Example: + * + *

    {@code
    + * // class YourClass {
    + * //
    + * //   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + * //   private static final DoubleSumObserver cpuObserver =
    + * //       meter.
    + * //           .doubleSumObserverBuilder("cpu_time")
    + * //           .setDescription("System CPU usage")
    + * //           .setUnit("ms")
    + * //           .build();
    + * //
    + * //   void init() {
    + * //     cpuObserver.setUpdater(
    + * //         new DoubleSumObserver.Callback() {
    + * //          @Override
    + * //           public void update(DoubleResult result) {
    + * //             // Get system cpu usage
    + * //             result.observe(cpuIdle, "state", "idle");
    + * //             result.observe(cpuUser, "state", "user");
    + * //           }
    + * //         });
    + * //   }
    + * // }
    + * }
    + */ +@ThreadSafe +public interface DoubleSumObserver extends AsynchronousInstrument {} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleSumObserverBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleSumObserverBuilder.java new file mode 100644 index 000000000..c0e205417 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleSumObserverBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import java.util.function.Consumer; + +/** Builder class for {@link DoubleSumObserver}. */ +public interface DoubleSumObserverBuilder + extends AsynchronousInstrumentBuilder { + @Override + DoubleSumObserverBuilder setDescription(String description); + + @Override + DoubleSumObserverBuilder setUnit(String unit); + + @Override + DoubleSumObserverBuilder setUpdater(Consumer updater); + + @Override + DoubleSumObserver build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownCounter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownCounter.java new file mode 100644 index 000000000..cc521224d --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownCounter.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.ThreadSafe; + +/** + * UpDownCounter is a synchronous instrument and very similar to Counter except that Add(increment) + * supports negative increments. This makes UpDownCounter not useful for computing a rate + * aggregation. The default aggregation is `Sum`, only the sum is non-monotonic. It is generally + * useful for capturing changes in an amount of resources used, or any quantity that rises and falls + * during a request. + * + *

    Example: + * + *

    {@code
    + * class YourClass {
    + *   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + *   private static final DoubleUpDownCounter upDownCounter =
    + *       meter.
    + *           .doubleUpDownCounterBuilder("resource_usage")
    + *           .setDescription("Current resource usage")
    + *           .setUnit("1")
    + *           .build();
    + *
    + *   // It is recommended that the API user keep references to a Bound Counters.
    + *   private static final BoundDoubleUpDownCounter someWorkBound =
    + *       upDownCounter.bind("work_name", "some_work");
    + *
    + *   void doSomeWork() {
    + *      someWorkBound.add(10.2);  // Resources needed for this task.
    + *      // Your code here.
    + *      someWorkBound.add(-10.0);
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface DoubleUpDownCounter extends SynchronousInstrument { + + /** + * Adds the given {@code increment} to the current value. + * + *

    The value added is associated with the current {@code Context} and provided set of labels. + * + * @param increment the value to add. + * @param labels the labels to be associated to this recording. + */ + void add(double increment, Labels labels); + + /** + * Adds the given {@code increment} to the current value. + * + *

    The value added is associated with the current {@code Context} and empty labels. + * + * @param increment the value to add. + */ + void add(double increment); + + @Override + BoundDoubleUpDownCounter bind(Labels labels); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownCounterBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownCounterBuilder.java new file mode 100644 index 000000000..953da7a86 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownCounterBuilder.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +/** Builder class for {@link DoubleUpDownCounter}. */ +public interface DoubleUpDownCounterBuilder extends SynchronousInstrumentBuilder { + @Override + DoubleUpDownCounterBuilder setDescription(String description); + + @Override + DoubleUpDownCounterBuilder setUnit(String unit); + + @Override + DoubleUpDownCounter build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserver.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserver.java new file mode 100644 index 000000000..05f115756 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserver.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * UpDownSumObserver is the asynchronous instrument corresponding to UpDownCounter, used to capture + * a non-monotonic count with Observe(sum). + * + *

    "Sum" appears in the name to remind that it is used to capture sums directly. Use a + * UpDownSumObserver to capture any value that starts at zero and rises or falls throughout the + * process lifetime. + * + *

    A {@code UpDownSumObserver} is a good choice in situations where a measurement is expensive to + * compute, such that it would be wasteful to compute on every request. + * + *

    Example: + * + *

    {@code
    + * // class YourClass {
    + * //
    + * //   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + * //   private static final DoubleUpDownSumObserver memoryObserver =
    + * //       meter.
    + * //           .doubleUpDownSumObserverBuilder("memory_usage")
    + * //           .setDescription("System memory usage")
    + * //           .setUnit("by")
    + * //           .build();
    + * //
    + * //   void init() {
    + * //     memoryObserver.setUpdater(
    + * //         new DoubleUpDownSumObserver.Callback() {
    + * //          @Override
    + * //           public void update(DoubleResult result) {
    + * //             // Get system memory usage
    + * //             result.observe(memoryUsed, "state", "used");
    + * //             result.observe(memoryFree, "state", "free");
    + * //           }
    + * //         });
    + * //   }
    + * // }
    + * }
    + */ +@ThreadSafe +public interface DoubleUpDownSumObserver extends AsynchronousInstrument {} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserverBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserverBuilder.java new file mode 100644 index 000000000..e824091e1 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserverBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import java.util.function.Consumer; + +/** Builder class for {@link DoubleUpDownSumObserver}. */ +public interface DoubleUpDownSumObserverBuilder + extends AsynchronousInstrumentBuilder { + @Override + DoubleUpDownSumObserverBuilder setDescription(String description); + + @Override + DoubleUpDownSumObserverBuilder setUnit(String unit); + + @Override + DoubleUpDownSumObserverBuilder setUpdater(Consumer updater); + + @Override + DoubleUpDownSumObserver build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueObserver.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueObserver.java new file mode 100644 index 000000000..018806bea --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueObserver.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * {@code ValueObserver} is the asynchronous instrument corresponding to ValueRecorder, used to + * capture values that are treated as individual observations, recorded with the observe(value) + * method. + * + *

    A {@code ValueObserver} is a good choice in situations where a measurement is expensive to + * compute, such that it would be wasteful to compute on every request. + * + *

    Example: + * + *

    {@code
    + * // class YourClass {
    + * //
    + * //   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + * //   private static final DoubleValueObserver cpuObserver =
    + * //       meter.
    + * //           .doubleValueObserverBuilder("cpu_temperature")
    + * //           .setDescription("System CPU temperature")
    + * //           .setUnit("ms")
    + * //           .build();
    + * //
    + * //   void init() {
    + * //     cpuObserver.setUpdater(
    + * //         new DoubleValueObserver.Callback() {
    + * //          @Override
    + * //           public void update(DoubleResult result) {
    + * //             // Get system cpu temperature
    + * //             result.observe(cpuTemperature);
    + * //           }
    + * //         });
    + * //   }
    + * // }
    + * }
    + */ +@ThreadSafe +public interface DoubleValueObserver extends AsynchronousInstrument {} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueObserverBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueObserverBuilder.java new file mode 100644 index 000000000..eafa8f0dc --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueObserverBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import java.util.function.Consumer; + +/** Builder class for {@link DoubleValueObserver}. */ +public interface DoubleValueObserverBuilder + extends AsynchronousInstrumentBuilder { + @Override + DoubleValueObserverBuilder setDescription(String description); + + @Override + DoubleValueObserverBuilder setUnit(String unit); + + @Override + DoubleValueObserverBuilder setUpdater(Consumer updater); + + @Override + DoubleValueObserver build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueRecorder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueRecorder.java new file mode 100644 index 000000000..80abefe08 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueRecorder.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.ThreadSafe; + +/** + * ValueRecorder is a synchronous instrument useful for recording any number, positive or negative. + * Values captured by a Record(value) are treated as individual events belonging to a distribution + * that is being summarized. + * + *

    ValueRecorder should be chosen either when capturing measurements that do not contribute + * meaningfully to a sum, or when capturing numbers that are additive in nature, but where the + * distribution of individual increments is considered interesting. + * + *

    One of the most common uses for ValueRecorder is to capture latency measurements. Latency + * measurements are not additive in the sense that there is little need to know the latency-sum of + * all processed requests. We use a ValueRecorder instrument to capture latency measurements + * typically because we are interested in knowing mean, median, and other summary statistics about + * individual events. + * + *

    Example: + * + *

    {@code
    + * class YourClass {
    + *
    + *   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + *   private static final DoubleValueRecorder valueRecorder =
    + *       meter.
    + *           .doubleValueRecorderBuilder("doWork_latency")
    + *           .setDescription("gRPC Latency")
    + *           .setUnit("ms")
    + *           .build();
    + *
    + *   // It is recommended that the API user keep references to a Bound Counters.
    + *   private static final BoundDoubleValueRecorder someWorkBound =
    + *       valueRecorder.bind("work_name", "some_work");
    + *
    + *   void doWork() {
    + *      long startTime = System.nanoTime();
    + *      // Your code here.
    + *      someWorkBound.record((System.nanoTime() - startTime) / 1e6);
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface DoubleValueRecorder extends SynchronousInstrument { + + /** + * Records the given measurement, associated with the current {@code Context} and provided set of + * labels. + * + * @param value the measurement to record. + * @param labels the set of labels to be associated to this recording + * @throws IllegalArgumentException if value is negative. + */ + void record(double value, Labels labels); + + /** + * Records the given measurement, associated with the current {@code Context} and empty labels. + * + * @param value the measurement to record. + * @throws IllegalArgumentException if value is negative. + */ + void record(double value); + + @Override + BoundDoubleValueRecorder bind(Labels labels); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueRecorderBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueRecorderBuilder.java new file mode 100644 index 000000000..17b787ea7 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/DoubleValueRecorderBuilder.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +/** Builder class for {@link DoubleValueRecorder}. */ +public interface DoubleValueRecorderBuilder extends SynchronousInstrumentBuilder { + @Override + DoubleValueRecorderBuilder setDescription(String description); + + @Override + DoubleValueRecorderBuilder setUnit(String unit); + + @Override + DoubleValueRecorder build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/GlobalMeterProvider.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/GlobalMeterProvider.java new file mode 100644 index 000000000..3a6b04ec9 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/GlobalMeterProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * IMPORTANT: This is a temporary class, and solution for the metrics package until it will be + * marked as stable. + */ +public final class GlobalMeterProvider { + private static final Object mutex = new Object(); + private static final AtomicReference globalMeterProvider = new AtomicReference<>(); + + private GlobalMeterProvider() {} + + /** Returns the globally registered {@link MeterProvider}. */ + public static MeterProvider get() { + MeterProvider meterProvider = globalMeterProvider.get(); + if (meterProvider == null) { + synchronized (mutex) { + if (globalMeterProvider.get() == null) { + return MeterProvider.noop(); + } + } + } + return meterProvider; + } + + /** + * Sets the {@link MeterProvider} that should be the global instance. Future calls to {@link + * #get()} will return the provided {@link MeterProvider} instance. This should be called once as + * early as possible in your application initialization logic, often in a {@code static} block in + * your main class. + */ + public static void set(MeterProvider meterProvider) { + globalMeterProvider.set(meterProvider); + } + + /** + * Gets or creates a named meter instance from the globally registered {@link MeterProvider}. + * + *

    This is a shortcut method for {@code getGlobalMeterProvider().get(instrumentationName)} + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library. + * @return a tracer instance. + */ + public static Meter getMeter(String instrumentationName) { + return get().get(instrumentationName); + } + + /** + * Gets or creates a named and versioned meter instance from the globally registered {@link + * MeterProvider}. + * + *

    This is a shortcut method for {@code getGlobalMeterProvider().get(instrumentationName, + * instrumentationVersion)} + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library. + * @param instrumentationVersion The version of the instrumentation library. + * @return a tracer instance. + */ + public static Meter getMeter(String instrumentationName, String instrumentationVersion) { + return get().get(instrumentationName, instrumentationVersion); + } +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/Instrument.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/Instrument.java new file mode 100644 index 000000000..d9e6228fe --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/Instrument.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** Base interface for all metrics defined in this package. */ +@ThreadSafe +@SuppressWarnings("InterfaceWithOnlyStatics") +public interface Instrument {} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/InstrumentBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/InstrumentBuilder.java new file mode 100644 index 000000000..ef146ed4d --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/InstrumentBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +/** The {@code Builder} class for the {@code Instrument}. */ +public interface InstrumentBuilder { + /** + * Sets the description of the {@code Instrument}. + * + *

    Default value is {@code ""}. + * + * @param description the description of the Instrument. + * @return this. + */ + InstrumentBuilder setDescription(String description); + + /** + * Sets the unit of the {@code Instrument}. + * + *

    Default value is {@code "1"}. + * + * @param unit the unit of the Instrument. + * @return this. + */ + InstrumentBuilder setUnit(String unit); + + /** + * Builds and returns a {@code Instrument} with the desired options. + * + * @return a {@code Instrument} with the desired options. + */ + Instrument build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongCounter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongCounter.java new file mode 100644 index 000000000..3cf662c38 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongCounter.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Counter is the most common synchronous instrument. This instrument supports an {@link #add(long, + * Labels)}` function for reporting an increment, and is restricted to non-negative increments. The + * default aggregation is `Sum`. + * + *

    Example: + * + *

    {@code
    + * class YourClass {
    + *   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + *   private static final LongCounter counter =
    + *       meter.
    + *           .longCounterBuilder("processed_jobs")
    + *           .setDescription("Processed jobs")
    + *           .setUnit("1")
    + *           .build();
    + *
    + *   // It is recommended that the API user keep a reference to a Bound Counter.
    + *   private static final BoundLongCounter someWorkBound =
    + *       counter.bind("work_name", "some_work");
    + *
    + *   void doSomeWork() {
    + *      // Your code here.
    + *      someWorkBound.add(10);
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface LongCounter extends SynchronousInstrument { + + /** + * Adds the given {@code increment} to the current value. The values cannot be negative. + * + *

    The value added is associated with the current {@code Context} and provided set of labels. + * + * @param increment the value to add. + * @param labels the set of labels to be associated to this recording. + */ + void add(long increment, Labels labels); + + /** + * Adds the given {@code increment} to the current value. The values cannot be negative. + * + *

    The value added is associated with the current {@code Context} and empty labels. + * + * @param increment the value to add. + */ + void add(long increment); + + @Override + BoundLongCounter bind(Labels labels); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongCounterBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongCounterBuilder.java new file mode 100644 index 000000000..fd0811b43 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongCounterBuilder.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +/** Builder class for {@link LongCounter}. */ +public interface LongCounterBuilder extends SynchronousInstrumentBuilder { + @Override + LongCounterBuilder setDescription(String description); + + @Override + LongCounterBuilder setUnit(String unit); + + @Override + LongCounter build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongSumObserver.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongSumObserver.java new file mode 100644 index 000000000..11af6f5ed --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongSumObserver.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * {@code SumObserver} is the asynchronous instrument corresponding to Counter, used to capture a + * monotonic sum with Observe(sum). + * + *

    "Sum" appears in the name to remind that it is used to capture sums directly. Use a + * SumObserver to capture any value that starts at zero and rises throughout the process lifetime + * and never falls. + * + *

    A {@code SumObserver} is a good choice in situations where a measurement is expensive to + * compute, such that it would be wasteful to compute on every request. + * + *

    Example: + * + *

    {@code
    + * // class YourClass {
    + * //
    + * //   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + * //   private static final LongSumObserver cpuObserver =
    + * //       meter.
    + * //           .longSumObserverBuilder("cpu_time")
    + * //           .setDescription("System CPU usage")
    + * //           .setUnit("ms")
    + * //           .build();
    + * //
    + * //   void init() {
    + * //     cpuObserver.setUpdater(
    + * //         new LongSumObserver.Callback() {
    + * //          @Override
    + * //           public void update(LongResult result) {
    + * //             // Get system cpu usage
    + * //             result.observe(cpuIdle, "state", "idle");
    + * //             result.observe(cpuUser, "state", "user");
    + * //           }
    + * //         });
    + * //   }
    + * // }
    + * }
    + */ +@ThreadSafe +public interface LongSumObserver extends AsynchronousInstrument {} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongSumObserverBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongSumObserverBuilder.java new file mode 100644 index 000000000..0d80e50cf --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongSumObserverBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import java.util.function.Consumer; + +/** Builder class for {@link LongSumObserver}. */ +public interface LongSumObserverBuilder + extends AsynchronousInstrumentBuilder { + @Override + LongSumObserverBuilder setDescription(String description); + + @Override + LongSumObserverBuilder setUnit(String unit); + + @Override + LongSumObserverBuilder setUpdater(Consumer updater); + + @Override + LongSumObserver build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownCounter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownCounter.java new file mode 100644 index 000000000..2789e26bc --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownCounter.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.ThreadSafe; + +/** + * UpDownCounter is a synchronous instrument and very similar to Counter except that Add(increment) + * supports negative increments. This makes UpDownCounter not useful for computing a rate + * aggregation. The default aggregation is `Sum`, only the sum is non-monotonic. It is generally + * useful for capturing changes in an amount of resources used, or any quantity that rises and falls + * during a request. + * + *

    Example: + * + *

    {@code
    + * class YourClass {
    + *   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + *   private static final LongUpDownCounter upDownCounter =
    + *       meter.
    + *           .longUpDownCounterBuilder("active_tasks")
    + *           .setDescription("Number of active tasks")
    + *           .setUnit("1")
    + *           .build();
    + *
    + *   // It is recommended that the API user keep a reference to a Bound Counter.
    + *   private static final BoundLongUpDownCounter someWorkBound =
    + *       upDownCounter.bind("work_name", "some_work");
    + *
    + *   void doSomeWork() {
    + *      someWorkBound.add(1);
    + *      // Your code here.
    + *      someWorkBound.add(-1);
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface LongUpDownCounter extends SynchronousInstrument { + + /** + * Adds the given {@code increment} to the current value. + * + *

    The value added is associated with the current {@code Context} and provided set of labels. + * + * @param increment the value to add. + * @param labels the set of labels to be associated to this recording. + */ + void add(long increment, Labels labels); + + /** + * Adds the given {@code increment} to the current value. + * + *

    The value added is associated with the current {@code Context} and empty labels. + * + * @param increment the value to add. + */ + void add(long increment); + + @Override + BoundLongUpDownCounter bind(Labels labels); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownCounterBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownCounterBuilder.java new file mode 100644 index 000000000..94c203099 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownCounterBuilder.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +/** Builder class for {@link LongUpDownCounter}. */ +public interface LongUpDownCounterBuilder extends SynchronousInstrumentBuilder { + @Override + LongUpDownCounterBuilder setDescription(String description); + + @Override + LongUpDownCounterBuilder setUnit(String unit); + + @Override + LongUpDownCounter build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownSumObserver.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownSumObserver.java new file mode 100644 index 000000000..57828ce68 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownSumObserver.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * UpDownSumObserver is the asynchronous instrument corresponding to UpDownCounter, used to capture + * a non-monotonic count with Observe(sum). + * + *

    "Sum" appears in the name to remind that it is used to capture sums directly. Use a + * UpDownSumObserver to capture any value that starts at zero and rises or falls throughout the + * process lifetime. + * + *

    A {@code UpDownSumObserver} is a good choice in situations where a measurement is expensive to + * compute, such that it would be wasteful to compute on every request. + * + *

    Example: + * + *

    {@code
    + * // class YourClass {
    + * //
    + * //   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + * //   private static final LongUpDownSumObserver memoryObserver =
    + * //       meter.
    + * //           .longUpDownSumObserverBuilder("memory_usage")
    + * //           .setDescription("System memory usage")
    + * //           .setUnit("by")
    + * //           .build();
    + * //
    + * //   void init() {
    + * //     memoryObserver.setUpdater(
    + * //         new LongUpDownSumObserver.Callback() {
    + * //          @Override
    + * //           public void update(LongResult result) {
    + * //             // Get system memory usage
    + * //             result.observe(memoryUsed, "state", "used");
    + * //             result.observe(memoryFree, "state", "free");
    + * //           }
    + * //         });
    + * //   }
    + * // }
    + * }
    + */ +@ThreadSafe +public interface LongUpDownSumObserver extends AsynchronousInstrument {} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownSumObserverBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownSumObserverBuilder.java new file mode 100644 index 000000000..547f07e15 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongUpDownSumObserverBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import java.util.function.Consumer; + +/** Builder class for {@link LongUpDownSumObserver}. */ +public interface LongUpDownSumObserverBuilder + extends AsynchronousInstrumentBuilder { + @Override + LongUpDownSumObserverBuilder setDescription(String description); + + @Override + LongUpDownSumObserverBuilder setUnit(String unit); + + @Override + LongUpDownSumObserverBuilder setUpdater(Consumer updater); + + @Override + LongUpDownSumObserver build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueObserver.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueObserver.java new file mode 100644 index 000000000..035dac16d --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueObserver.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * {@code ValueObserver} is the asynchronous instrument corresponding to ValueRecorder, used to + * capture values that are treated as individual observations, recorded with the observe(value) + * method. + * + *

    A {@code ValueObserver} is a good choice in situations where a measurement is expensive to + * compute, such that it would be wasteful to compute on every request. + * + *

    Example: + * + *

    {@code
    + * // class YourClass {
    + * //
    + * //   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + * //   private static final LongValueObserver cpuObserver =
    + * //       meter.
    + * //           .longValueObserverBuilder("cpu_fan_speed")
    + * //           .setDescription("System CPU fan speed")
    + * //           .setUnit("ms")
    + * //           .build();
    + * //
    + * //   void init() {
    + * //     cpuObserver.setUpdater(
    + * //         new LongValueObserver.Callback() {
    + * //          @Override
    + * //           public void update(LongResult result) {
    + * //             // Get system cpu fan speed
    + * //             result.observe(cpuFanSpeed);
    + * //           }
    + * //         });
    + * //   }
    + * // }
    + * }
    + */ +@ThreadSafe +public interface LongValueObserver extends AsynchronousInstrument {} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueObserverBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueObserverBuilder.java new file mode 100644 index 000000000..2757bea6c --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueObserverBuilder.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import java.util.function.Consumer; + +/** Builder class for {@link LongValueObserver}. */ +public interface LongValueObserverBuilder + extends AsynchronousInstrumentBuilder { + @Override + LongValueObserverBuilder setDescription(String description); + + @Override + LongValueObserverBuilder setUnit(String unit); + + @Override + LongValueObserverBuilder setUpdater(Consumer updater); + + @Override + LongValueObserver build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueRecorder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueRecorder.java new file mode 100644 index 000000000..9ddf39c30 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueRecorder.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.ThreadSafe; + +/** + * ValueRecorder is a synchronous instrument useful for recording any number, positive or negative. + * Values captured by a Record(value) are treated as individual events belonging to a distribution + * that is being summarized. + * + *

    ValueRecorder should be chosen either when capturing measurements that do not contribute + * meaningfully to a sum, or when capturing numbers that are additive in nature, but where the + * distribution of individual increments is considered interesting. + * + *

    One of the most common uses for ValueRecorder is to capture latency measurements. Latency + * measurements are not additive in the sense that there is little need to know the latency-sum of + * all processed requests. We use a ValueRecorder instrument to capture latency measurements + * typically because we are interested in knowing mean, median, and other summary statistics about + * individual events. + * + *

    Example: + * + *

    {@code
    + * class YourClass {
    + *
    + *   private static final Meter meter = OpenTelemetry.getMeterProvider().get("my_library_name");
    + *   private static final LongValueRecorder valueRecorder =
    + *       meter.
    + *           .longValueRecorderBuilder("doWork_latency")
    + *           .setDescription("gRPC Latency")
    + *           .setUnit("ns")
    + *           .build();
    + *
    + *   // It is recommended that the API user keep a reference to a Bound Counter.
    + *   private static final BoundLongValueRecorder someWorkBound =
    + *       valueRecorder.bind("work_name", "some_work");
    + *
    + *   void doWork() {
    + *      long startTime = System.nanoTime();
    + *      // Your code here.
    + *      someWorkBound.record(System.nanoTime() - startTime);
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface LongValueRecorder extends SynchronousInstrument { + + /** + * Records the given measurement, associated with the current {@code Context} and provided set of + * labels. + * + * @param value the measurement to record. + * @param labels the set of labels to be associated to this recording + * @throws IllegalArgumentException if value is negative. + */ + void record(long value, Labels labels); + + /** + * Records the given measurement, associated with the current {@code Context} and empty labels. + * + * @param value the measurement to record. + * @throws IllegalArgumentException if value is negative. + */ + void record(long value); + + @Override + BoundLongValueRecorder bind(Labels labels); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueRecorderBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueRecorderBuilder.java new file mode 100644 index 000000000..05bbc94af --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/LongValueRecorderBuilder.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +/** Builder class for {@link LongValueRecorder}. */ +public interface LongValueRecorderBuilder extends SynchronousInstrumentBuilder { + @Override + LongValueRecorderBuilder setDescription(String description); + + @Override + LongValueRecorderBuilder setUnit(String unit); + + @Override + LongValueRecorder build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/Meter.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/Meter.java new file mode 100644 index 000000000..96a81e2e0 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/Meter.java @@ -0,0 +1,181 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * Meter is a simple, interface that allows users to record measurements (metrics). + * + *

    There are two ways to record measurements: + * + *

      + *
    • Record raw measurements, and defer defining the aggregation and the labels for the exported + * Instrument. This should be used in libraries like gRPC to record measurements like + * "server_latency" or "received_bytes". + *
    • Record pre-defined aggregation data (or already aggregated data). This should be used to + * report cpu/memory usage, or simple metrics like "queue_length". + *
    + * + *

    TODO: Update comment. + */ +@ThreadSafe +public interface Meter { + + /** + * Returns a builder for a {@link DoubleCounter}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a {@code DoubleCounter.Builder}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + DoubleCounterBuilder doubleCounterBuilder(String name); + + /** + * Returns a builder for a {@link LongCounter}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a {@code LongCounter.Builder}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + LongCounterBuilder longCounterBuilder(String name); + + /** + * Returns a builder for a {@link DoubleUpDownCounter}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a {@code DoubleCounter.Builder}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + DoubleUpDownCounterBuilder doubleUpDownCounterBuilder(String name); + + /** + * Returns a builder for a {@link LongUpDownCounter}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a {@code LongCounter.Builder}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + LongUpDownCounterBuilder longUpDownCounterBuilder(String name); + + /** + * Returns a new builder for a {@link DoubleValueRecorder}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a new builder for a {@code DoubleValueRecorder}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + DoubleValueRecorderBuilder doubleValueRecorderBuilder(String name); + + /** + * Returns a new builder for a {@link LongValueRecorder}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a new builder for a {@code LongValueRecorder}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + LongValueRecorderBuilder longValueRecorderBuilder(String name); + + /** + * Returns a new builder for a {@link DoubleSumObserver}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a new builder for a {@code DoubleSumObserver}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + DoubleSumObserverBuilder doubleSumObserverBuilder(String name); + + /** + * Returns a new builder for a {@link LongSumObserver}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a new builder for a {@code LongSumObserver}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + LongSumObserverBuilder longSumObserverBuilder(String name); + + /** + * Returns a new builder for a {@link DoubleUpDownSumObserver}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a new builder for a {@code DoubleUpDownObserver}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + DoubleUpDownSumObserverBuilder doubleUpDownSumObserverBuilder(String name); + + /** + * Returns a new builder for a {@link LongUpDownSumObserver}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a new builder for a {@code LongUpDownSumObserver}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + LongUpDownSumObserverBuilder longUpDownSumObserverBuilder(String name); + + /** + * Returns a new builder for a {@link DoubleValueObserver}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a new builder for a {@code DoubleValueObserver}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + DoubleValueObserverBuilder doubleValueObserverBuilder(String name); + + /** + * Returns a new builder for a {@link LongValueObserver}. + * + * @param name the name of the instrument. Should be a ASCII string with a length no greater than + * 255 characters. + * @return a new builder for a {@code LongValueObserver}. + * @throws NullPointerException if {@code name} is null. + * @throws IllegalArgumentException if different metric with the same name already registered. + * @throws IllegalArgumentException if the {@code name} does not match the requirements. + */ + LongValueObserverBuilder longValueObserverBuilder(String name); + + /** + * Utility method that allows users to atomically record measurements to a set of Instruments with + * a common set of labels. + * + * @param keyValuePairs The set of labels to associate with this recorder and all it's recordings. + * @return a {@code MeasureBatchRecorder} that can be use to atomically record a set of + * measurements associated with different Measures. + */ + BatchRecorder newBatchRecorder(String... keyValuePairs); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/MeterProvider.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/MeterProvider.java new file mode 100644 index 000000000..a919069c8 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/MeterProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * A registry for creating named {@link Meter}s. The name Provider is for consistency with + * other languages and it is NOT loaded using reflection. + * + * @see io.opentelemetry.api.metrics.Meter + */ +@ThreadSafe +public interface MeterProvider { + + /** + * Returns a {@link MeterProvider} that only creates no-op {@link Instrument}s that neither record + * nor are emitted. + */ + static MeterProvider noop() { + return DefaultMeterProvider.getInstance(); + } + + /** + * Gets or creates a named meter instance. + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library. + * @return a tracer instance. + */ + Meter get(String instrumentationName); + + /** + * Gets or creates a named and versioned meter instance. + * + * @param instrumentationName The name of the instrumentation library, not the name of the + * instrument*ed* library. + * @param instrumentationVersion The version of the instrumentation library. + * @return a tracer instance. + */ + Meter get(String instrumentationName, String instrumentationVersion); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/SynchronousInstrument.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/SynchronousInstrument.java new file mode 100644 index 000000000..d68de6785 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/SynchronousInstrument.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.ThreadSafe; + +/** + * SynchronousInstrument is an interface that defines a type of instruments that are used to report + * measurements synchronously. That is, when the user reports individual measurements as they occur. + * + *

    Synchronous instrument events additionally have a Context associated with them, describing + * properties of the associated trace and distributed correlation values. + * + * @param the specific type of Bound Instrument this instrument can provide. + */ +@ThreadSafe +public interface SynchronousInstrument extends Instrument { + /** + * Returns a {@code Bound Instrument} associated with the specified labels. Multiples requests + * with the same set of labels may return the same {@code Bound Instrument} instance. + * + *

    It is recommended that callers keep a reference to the Bound Instrument instead of always + * calling this method for every operation. + * + * @param labels the set of labels, as key-value pairs. + * @return a {@code Bound Instrument} + * @throws NullPointerException if {@code labelValues} is null. + */ + B bind(Labels labels); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/SynchronousInstrumentBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/SynchronousInstrumentBuilder.java new file mode 100644 index 000000000..40961b7a6 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/SynchronousInstrumentBuilder.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +/** Builder class for {@link SynchronousInstrument}. */ +public interface SynchronousInstrumentBuilder extends InstrumentBuilder { + @Override + SynchronousInstrument build(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/ArrayBackedLabels.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/ArrayBackedLabels.java new file mode 100644 index 000000000..99ea6bba2 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/ArrayBackedLabels.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics.common; + +import io.opentelemetry.api.internal.ImmutableKeyValuePairs; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class ArrayBackedLabels extends ImmutableKeyValuePairs implements Labels { + private static final Labels EMPTY = Labels.builder().build(); + + static Labels empty() { + return EMPTY; + } + + private ArrayBackedLabels(Object[] data) { + super(data); + } + + static Labels sortAndFilterToLabels(Object... data) { + return new ArrayBackedLabels(data); + } + + @Override + public LabelsBuilder toBuilder() { + return new ArrayBackedLabelsBuilder(data()); + } +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/ArrayBackedLabelsBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/ArrayBackedLabelsBuilder.java new file mode 100644 index 000000000..44ca9aaf9 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/ArrayBackedLabelsBuilder.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics.common; + +import java.util.ArrayList; +import java.util.List; + +class ArrayBackedLabelsBuilder implements LabelsBuilder { + private final List data; + + ArrayBackedLabelsBuilder() { + data = new ArrayList<>(); + } + + ArrayBackedLabelsBuilder(List data) { + this.data = new ArrayList<>(data); + } + + @Override + public Labels build() { + return ArrayBackedLabels.sortAndFilterToLabels(data.toArray()); + } + + @Override + public LabelsBuilder put(String key, String value) { + data.add(key); + data.add(value); + return this; + } +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/Labels.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/Labels.java new file mode 100644 index 000000000..8821618ff --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/Labels.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics.common; + +import static io.opentelemetry.api.metrics.common.ArrayBackedLabels.sortAndFilterToLabels; + +import java.util.Map; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * An immutable container for labels, which are key-value pairs of {@link String}s. + * + *

    Implementations of this interface *must* be immutable and have well-defined value-based + * equals/hashCode implementations. If an implementation does not strictly conform to these + * requirements, behavior of the OpenTelemetry APIs and default SDK cannot be guaranteed. + * + *

    For this reason, it is strongly suggested that you use the implementation that is provided + * here via the factory methods and the {@link ArrayBackedLabelsBuilder}. + */ +@Immutable +public interface Labels { + + /** Returns a {@link Labels} instance with no attributes. */ + static Labels empty() { + return ArrayBackedLabels.empty(); + } + + /** Creates a new {@link LabelsBuilder} instance for creating arbitrary {@link Labels}. */ + static LabelsBuilder builder() { + return new ArrayBackedLabelsBuilder(); + } + + /** Returns a {@link Labels} instance with a single key-value pair. */ + static Labels of(String key, String value) { + return sortAndFilterToLabels(key, value); + } + + /** + * Returns a {@link Labels} instance with two key-value pairs. Order of the keys is not preserved. + * Duplicate keys will be removed. + */ + static Labels of(String key1, String value1, String key2, String value2) { + return sortAndFilterToLabels(key1, value1, key2, value2); + } + + /** + * Returns a {@link Labels} instance with three key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + static Labels of( + String key1, String value1, String key2, String value2, String key3, String value3) { + return sortAndFilterToLabels(key1, value1, key2, value2, key3, value3); + } + + /** + * Returns a {@link Labels} instance with four key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + static Labels of( + String key1, + String value1, + String key2, + String value2, + String key3, + String value3, + String key4, + String value4) { + return sortAndFilterToLabels(key1, value1, key2, value2, key3, value3, key4, value4); + } + + /** + * Returns a {@link Labels} instance with five key-value pairs. Order of the keys is not + * preserved. Duplicate keys will be removed. + */ + static Labels of( + String key1, + String value1, + String key2, + String value2, + String key3, + String value3, + String key4, + String value4, + String key5, + String value5) { + return sortAndFilterToLabels( + key1, value1, + key2, value2, + key3, value3, + key4, value4, + key5, value5); + } + + /** Returns a {@link Labels} instance with the provided {@code keyValueLabelPairs}. */ + static Labels of(String... keyValueLabelPairs) { + return sortAndFilterToLabels((Object[]) keyValueLabelPairs); + } + + /** Iterates over all the key-value pairs of labels contained by this instance. */ + void forEach(BiConsumer consumer); + + /** The number of key-value pairs of labels in this instance. */ + int size(); + + /** Returns the value for the given {@code key}, or {@code null} if the key is not present. */ + @Nullable + String get(String key); + + /** Returns whether this instance is empty (contains no labels). */ + boolean isEmpty(); + + /** Returns a read-only view of these {@link Labels} as a {@link Map}. */ + Map asMap(); + + /** Create a {@link LabelsBuilder} pre-populated with the contents of this Labels instance. */ + LabelsBuilder toBuilder(); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/LabelsBuilder.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/LabelsBuilder.java new file mode 100644 index 000000000..99da0e85f --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/common/LabelsBuilder.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics.common; + +/** A builder of {@link Labels} supporting an arbitrary number of key-value pairs. */ +public interface LabelsBuilder { + /** Create the {@link Labels} from this. */ + Labels build(); + + /** + * Puts a single label into this Builder. + * + * @return this Builder + */ + LabelsBuilder put(String key, String value); +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/internal/MetricsStringUtils.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/internal/MetricsStringUtils.java new file mode 100644 index 000000000..c52c4bfff --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/internal/MetricsStringUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics.internal; + +import javax.annotation.concurrent.Immutable; + +/** Internal utility methods for working with attribute keys, attribute values, and metric names. */ +@Immutable +public final class MetricsStringUtils { + + public static final int METRIC_NAME_MAX_LENGTH = 255; + + /** + * Determines whether the metric name contains a valid metric name. + * + * @param metricName the metric name to be validated. + * @return whether the metricName contains a valid name. + */ + public static boolean isValidMetricName(String metricName) { + if (metricName.isEmpty() || metricName.length() > METRIC_NAME_MAX_LENGTH) { + return false; + } + String pattern = "[aA-zZ][aA-zZ0-9_\\-.]*"; + return metricName.matches(pattern); + } + + private MetricsStringUtils() {} +} diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/internal/package-info.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/internal/package-info.java new file mode 100644 index 000000000..8108f8886 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/internal/package-info.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Interfaces and implementations that are internal to OpenTelemetry. + * + *

    All the content under this package and its subpackages are considered not part of the public + * API, and must not be used by users of the OpenTelemetry library. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.api.metrics.internal; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/package-info.java b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/package-info.java new file mode 100644 index 000000000..5d27ed340 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/main/java/io/opentelemetry/api/metrics/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** This package describes the Metrics API that can be used to record application Metrics. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.api.metrics; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/BatchRecorderTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/BatchRecorderTest.java new file mode 100644 index 000000000..1819f8a47 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/BatchRecorderTest.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class BatchRecorderTest { + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void testNewBatchRecorder_WrongNumberOfLabels() { + assertThatThrownBy(() -> meter.newBatchRecorder("key")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("key/value"); + } + + @Test + void testNewBatchRecorder_NullLabelKey() { + assertThatThrownBy(() -> meter.newBatchRecorder(null, "value")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("null keys"); + } + + @Test + void preventNull_MeasureLong() { + assertThatThrownBy(() -> meter.newBatchRecorder().put((LongValueRecorder) null, 5L).record()) + .isInstanceOf(NullPointerException.class) + .hasMessage("valueRecorder"); + } + + @Test + void preventNull_MeasureDouble() { + assertThatThrownBy(() -> meter.newBatchRecorder().put((DoubleValueRecorder) null, 5L).record()) + .isInstanceOf(NullPointerException.class) + .hasMessage("valueRecorder"); + } + + @Test + void preventNull_LongCounter() { + assertThatThrownBy(() -> meter.newBatchRecorder().put((LongCounter) null, 5L).record()) + .isInstanceOf(NullPointerException.class) + .hasMessage("counter"); + } + + @Test + void preventNull_DoubleCounter() { + assertThatThrownBy(() -> meter.newBatchRecorder().put((DoubleCounter) null, 5L).record()) + .isInstanceOf(NullPointerException.class) + .hasMessage("counter"); + } + + @Test + void preventNull_LongUpDownCounter() { + assertThatThrownBy(() -> meter.newBatchRecorder().put((LongUpDownCounter) null, 5L).record()) + .isInstanceOf(NullPointerException.class) + .hasMessage("upDownCounter"); + } + + @Test + void preventNull_DoubleUpDownCounter() { + assertThatThrownBy(() -> meter.newBatchRecorder().put((DoubleUpDownCounter) null, 5L).record()) + .isInstanceOf(NullPointerException.class) + .hasMessage("upDownCounter"); + } + + @Test + void doesNotThrow() { + BatchRecorder batchRecorder = meter.newBatchRecorder(); + batchRecorder.put(meter.longValueRecorderBuilder("longValueRecorder").build(), 44L); + batchRecorder.put(meter.longValueRecorderBuilder("negativeLongValueRecorder").build(), -44L); + batchRecorder.put(meter.doubleValueRecorderBuilder("doubleValueRecorder").build(), 77.556d); + batchRecorder.put( + meter.doubleValueRecorderBuilder("negativeDoubleValueRecorder").build(), -77.556d); + batchRecorder.put(meter.longCounterBuilder("longCounter").build(), 44L); + batchRecorder.put(meter.doubleCounterBuilder("doubleCounter").build(), 77.556d); + batchRecorder.put(meter.longUpDownCounterBuilder("longUpDownCounter").build(), -44L); + batchRecorder.put(meter.doubleUpDownCounterBuilder("doubleUpDownCounter").build(), -77.556d); + batchRecorder.record(); + } + + @Test + void negativeValue_DoubleCounter() { + BatchRecorder batchRecorder = meter.newBatchRecorder(); + assertThatThrownBy( + () -> batchRecorder.put(meter.doubleCounterBuilder("doubleCounter").build(), -77.556d)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Counters can only increase"); + } + + @Test + void negativeValue_LongCounter() { + BatchRecorder batchRecorder = meter.newBatchRecorder(); + assertThatThrownBy( + () -> batchRecorder.put(meter.longCounterBuilder("longCounter").build(), -44L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Counters can only increase"); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DefaultMeterTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DefaultMeterTest.java new file mode 100644 index 000000000..b307d09c3 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DefaultMeterTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class DefaultMeterTest { + @Test + void expectDefaultMeter() { + assertThat(MeterProvider.noop().get("test")).isInstanceOf(DefaultMeter.class); + assertThat(MeterProvider.noop().get("test")).isSameAs(DefaultMeter.getInstance()); + assertThat(MeterProvider.noop().get("test", "0.1.0")).isSameAs(DefaultMeter.getInstance()); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleCounterTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleCounterTest.java new file mode 100644 index 000000000..af197ab88 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleCounterTest.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class DoubleCounterTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.doubleCounterBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.doubleCounterBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.doubleCounterBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.doubleCounterBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy(() -> meter.doubleCounterBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.doubleCounterBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void add_preventNullLabels() { + assertThatThrownBy(() -> meter.doubleCounterBuilder("metric").build().add(1.0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void add_DoesNotThrow() { + DoubleCounter doubleCounter = + meter.doubleCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + doubleCounter.add(1.0, Labels.empty()); + doubleCounter.add(1.0); + } + + @Test + void add_PreventNegativeValue() { + DoubleCounter doubleCounter = + meter.doubleCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + assertThatThrownBy(() -> doubleCounter.add(-1.0, Labels.empty())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Counters can only increase"); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> meter.doubleCounterBuilder("metric").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_DoesNotThrow() { + DoubleCounter doubleCounter = + meter.doubleCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + BoundDoubleCounter bound = doubleCounter.bind(Labels.empty()); + bound.add(1.0); + bound.unbind(); + } + + @Test + void bound_PreventNegativeValue() { + DoubleCounter doubleCounter = + meter.doubleCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + BoundDoubleCounter bound = doubleCounter.bind(Labels.empty()); + try { + assertThatThrownBy(() -> bound.add(-1.0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Counters can only increase"); + } finally { + bound.unbind(); + } + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleSumObserverTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleSumObserverTest.java new file mode 100644 index 000000000..1063c340e --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleSumObserverTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class DoubleSumObserverTest { + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.doubleSumObserverBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.doubleSumObserverBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.doubleSumObserverBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.doubleSumObserverBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy(() -> meter.doubleSumObserverBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.doubleSumObserverBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void preventNull_Callback() { + assertThatThrownBy(() -> meter.doubleSumObserverBuilder("metric").setUpdater(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("callback"); + } + + @Test + void doesNotThrow() { + meter + .doubleSumObserverBuilder(NAME) + .setDescription(DESCRIPTION) + .setUnit(UNIT) + .setUpdater(result -> {}) + .build(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleUpDownCounterTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleUpDownCounterTest.java new file mode 100644 index 000000000..00b00ddf0 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleUpDownCounterTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class DoubleUpDownCounterTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.doubleUpDownCounterBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.doubleUpDownCounterBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.doubleUpDownCounterBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.doubleUpDownCounterBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy( + () -> meter.doubleUpDownCounterBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.doubleUpDownCounterBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void add_preventNullLabels() { + assertThatThrownBy(() -> meter.doubleUpDownCounterBuilder("metric").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void add_DoesNotThrow() { + DoubleUpDownCounter doubleUpDownCounter = + meter.doubleUpDownCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + doubleUpDownCounter.add(1.0, Labels.empty()); + doubleUpDownCounter.add(-1.0, Labels.empty()); + doubleUpDownCounter.add(1.0); + doubleUpDownCounter.add(-1.0); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> meter.doubleUpDownCounterBuilder("metric").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_DoesNotThrow() { + DoubleUpDownCounter doubleUpDownCounter = + meter.doubleUpDownCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + BoundDoubleUpDownCounter bound = doubleUpDownCounter.bind(Labels.empty()); + bound.add(1.0); + bound.add(-1.0); + bound.unbind(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserverTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserverTest.java new file mode 100644 index 000000000..d2d23a720 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleUpDownSumObserverTest.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class DoubleUpDownSumObserverTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.doubleUpDownSumObserverBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.doubleUpDownSumObserverBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.doubleUpDownSumObserverBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.doubleUpDownSumObserverBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy( + () -> meter.doubleUpDownSumObserverBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.doubleUpDownSumObserverBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void preventNull_Callback() { + assertThatThrownBy( + () -> meter.doubleUpDownSumObserverBuilder("metric").setUpdater(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("callback"); + } + + @Test + void doesNotThrow() { + meter + .doubleUpDownSumObserverBuilder(NAME) + .setDescription(DESCRIPTION) + .setUnit(UNIT) + .setUpdater(result -> {}) + .build(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleValueObserverTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleValueObserverTest.java new file mode 100644 index 000000000..c74a5db9d --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleValueObserverTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class DoubleValueObserverTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.doubleValueObserverBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.doubleValueObserverBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.doubleValueObserverBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.doubleValueObserverBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy( + () -> meter.doubleValueObserverBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.doubleValueObserverBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void preventNull_Callback() { + assertThatThrownBy(() -> meter.doubleValueObserverBuilder("metric").setUpdater(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("callback"); + } + + @Test + void doesNotThrow() { + meter + .doubleValueObserverBuilder(NAME) + .setDescription(DESCRIPTION) + .setUnit(UNIT) + .setUpdater(result -> {}) + .build(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleValueRecorderTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleValueRecorderTest.java new file mode 100644 index 000000000..90b1aeeb7 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/DoubleValueRecorderTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class DoubleValueRecorderTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.doubleValueRecorderBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.doubleValueRecorderBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.doubleValueRecorderBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.doubleValueRecorderBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy( + () -> meter.doubleValueRecorderBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.doubleValueRecorderBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void record_PreventNullLabels() { + assertThatThrownBy(() -> meter.doubleValueRecorderBuilder("metric").build().record(1.0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void record_DoesNotThrow() { + DoubleValueRecorder doubleValueRecorder = + meter.doubleValueRecorderBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + doubleValueRecorder.record(5.0, Labels.empty()); + doubleValueRecorder.record(-5.0, Labels.empty()); + doubleValueRecorder.record(5.0); + doubleValueRecorder.record(-5.0); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> meter.doubleValueRecorderBuilder("metric").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_DoesNotThrow() { + DoubleValueRecorder doubleValueRecorder = + meter.doubleValueRecorderBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + BoundDoubleValueRecorder bound = doubleValueRecorder.bind(Labels.empty()); + bound.record(5.0); + bound.record(-5.0); + bound.unbind(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongCounterTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongCounterTest.java new file mode 100644 index 000000000..df151ce20 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongCounterTest.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class LongCounterTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.longCounterBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.longCounterBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.longCounterBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.longCounterBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy(() -> meter.longCounterBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.longCounterBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void add_PreventNullLabels() { + assertThatThrownBy(() -> meter.longCounterBuilder("metric").build().add(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void add_DoesNotThrow() { + LongCounter longCounter = + meter.longCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + longCounter.add(1, Labels.empty()); + longCounter.add(1); + } + + @Test + void add_PreventNegativeValue() { + LongCounter longCounter = + meter.longCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + assertThatThrownBy(() -> longCounter.add(-1, Labels.empty())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Counters can only increase"); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> meter.longCounterBuilder("metric").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_DoesNotThrow() { + LongCounter longCounter = + meter.longCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + BoundLongCounter bound = longCounter.bind(Labels.empty()); + bound.add(1); + bound.unbind(); + } + + @Test + void bound_PreventNegativeValue() { + LongCounter longCounter = + meter.longCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + BoundLongCounter bound = longCounter.bind(Labels.empty()); + try { + assertThatThrownBy(() -> bound.add(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Counters can only increase"); + } finally { + bound.unbind(); + } + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongSumObserverTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongSumObserverTest.java new file mode 100644 index 000000000..2f48fcbc6 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongSumObserverTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class LongSumObserverTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.longSumObserverBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.longSumObserverBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.longSumObserverBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.longSumObserverBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy(() -> meter.longSumObserverBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.longSumObserverBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void preventNull_Callback() { + assertThatThrownBy(() -> meter.longSumObserverBuilder("metric").setUpdater(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("callback"); + } + + @Test + void doesNotThrow() { + meter + .longSumObserverBuilder(NAME) + .setDescription(DESCRIPTION) + .setUnit(UNIT) + .setUpdater(result -> {}) + .build(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongUpDownCounterTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongUpDownCounterTest.java new file mode 100644 index 000000000..148f4014d --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongUpDownCounterTest.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class LongUpDownCounterTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.longUpDownCounterBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.longUpDownCounterBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.longUpDownCounterBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.longUpDownCounterBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy(() -> meter.longUpDownCounterBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.longUpDownCounterBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void add_PreventNullLabels() { + assertThatThrownBy(() -> meter.longUpDownCounterBuilder("metric").build().add(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void add_DoesNotThrow() { + LongUpDownCounter longUpDownCounter = + meter.longUpDownCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + longUpDownCounter.add(1, Labels.empty()); + longUpDownCounter.add(-1, Labels.empty()); + longUpDownCounter.add(1); + longUpDownCounter.add(-1); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> meter.longUpDownCounterBuilder("metric").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_DoesNotThrow() { + LongUpDownCounter longUpDownCounter = + meter.longUpDownCounterBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + BoundLongUpDownCounter bound = longUpDownCounter.bind(Labels.empty()); + bound.add(1); + bound.add(-1); + bound.unbind(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongUpDownSumObserverTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongUpDownSumObserverTest.java new file mode 100644 index 000000000..1ecc7a19f --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongUpDownSumObserverTest.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class LongUpDownSumObserverTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.longUpDownSumObserverBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.longUpDownSumObserverBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.longUpDownSumObserverBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.longUpDownSumObserverBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(DefaultMeter.ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy( + () -> meter.longUpDownSumObserverBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.longUpDownSumObserverBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void preventNull_Callback() { + assertThatThrownBy(() -> meter.longUpDownSumObserverBuilder("metric").setUpdater(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("callback"); + } + + @Test + void doesNotThrow() { + meter + .longUpDownSumObserverBuilder(NAME) + .setDescription(DESCRIPTION) + .setUnit(UNIT) + .setUpdater(result -> {}) + .build(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongValueObserverTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongValueObserverTest.java new file mode 100644 index 000000000..0f1f9c09e --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongValueObserverTest.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static io.opentelemetry.api.metrics.DefaultMeter.ERROR_MESSAGE_INVALID_NAME; +import static java.util.Arrays.fill; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongValueObserver}. */ +class LongValueObserverTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.longValueObserverBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.longValueObserverBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> meter.longValueObserverBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.longValueObserverBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy(() -> meter.longValueObserverBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.longValueObserverBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void preventNull_Callback() { + assertThatThrownBy(() -> meter.longValueObserverBuilder("metric").setUpdater(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("callback"); + } + + @Test + void doesNotThrow() { + meter + .longValueObserverBuilder(NAME) + .setDescription(DESCRIPTION) + .setUnit(UNIT) + .setUpdater(result -> {}) + .build(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongValueRecorderTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongValueRecorderTest.java new file mode 100644 index 000000000..1b2da147a --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/LongValueRecorderTest.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics; + +import static io.opentelemetry.api.metrics.DefaultMeter.ERROR_MESSAGE_INVALID_NAME; +import static java.util.Arrays.fill; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import org.junit.jupiter.api.Test; + +/** Tests for {@link LongValueRecorder}. */ +public final class LongValueRecorderTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + private static final Meter meter = DefaultMeter.getInstance(); + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> meter.longValueRecorderBuilder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> meter.longValueRecorderBuilder("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNonPrintableMeasureName() { + assertThatThrownBy(() -> meter.longValueRecorderBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> meter.longValueRecorderBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy(() -> meter.longValueRecorderBuilder("metric").setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> meter.longValueRecorderBuilder("metric").setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void record_PreventNullLabels() { + assertThatThrownBy(() -> meter.longValueRecorderBuilder("metric").build().record(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void record_DoesNotThrow() { + LongValueRecorder longValueRecorder = + meter.longValueRecorderBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + longValueRecorder.record(5, Labels.empty()); + longValueRecorder.record(-5, Labels.empty()); + longValueRecorder.record(5); + longValueRecorder.record(-5); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> meter.longValueRecorderBuilder("metric").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_DoesNotThrow() { + LongValueRecorder longValueRecorder = + meter.longValueRecorderBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT).build(); + BoundLongValueRecorder bound = longValueRecorder.bind(Labels.empty()); + bound.record(5); + bound.record(-5); + bound.unbind(); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/common/LabelsTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/common/LabelsTest.java new file mode 100644 index 000000000..bd3c44003 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/common/LabelsTest.java @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class LabelsTest { + + @Test + void forEach() { + final Map entriesSeen = new LinkedHashMap<>(); + + Labels labels = + Labels.of( + "key1", "value1", + "key2", "value2"); + + labels.forEach(entriesSeen::put); + + assertThat(entriesSeen).containsExactly(entry("key1", "value1"), entry("key2", "value2")); + } + + @Test + void asMap() { + Labels labels = + Labels.of( + "key1", "value1", + "key2", "value2"); + + assertThat(labels.asMap()).containsExactly(entry("key1", "value1"), entry("key2", "value2")); + } + + @Test + void forEach_singleAttribute() { + final Map entriesSeen = new HashMap<>(); + + Labels labels = Labels.of("key", "value"); + labels.forEach(entriesSeen::put); + + assertThat(entriesSeen).containsExactly(entry("key", "value")); + } + + @Test + void forEach_empty() { + final AtomicBoolean sawSomething = new AtomicBoolean(false); + Labels emptyLabels = Labels.empty(); + emptyLabels.forEach((key, value) -> sawSomething.set(true)); + assertThat(sawSomething.get()).isFalse(); + } + + @Test + void orderIndependentEquality() { + Labels one = + Labels.of( + "key3", "value3", + "key1", "value1", + "key2", "value2"); + Labels two = + Labels.of( + "key2", "value2", + "key3", "value3", + "key1", "value1"); + + assertThat(one).isEqualTo(two); + } + + @Test + void nullValueEquivalentWithMissing() { + Labels one = + Labels.of( + "key3", "value3", + "key4", null, + "key1", "value1", + "key2", "value2"); + Labels two = + Labels.of( + "key2", "value2", + "key3", "value3", + "key1", "value1"); + + assertThat(one).isEqualTo(two); + } + + @Test + void deduplication() { + Labels one = + Labels.of( + "key1", "valueX", + "key1", "value1"); + Labels two = Labels.of("key1", "value1"); + + assertThat(one).isEqualTo(two); + } + + @Test + void threeLabels() { + Labels one = + Labels.of( + "key1", "value1", + "key3", "value3", + "key2", "value2"); + assertThat(one).isNotNull(); + } + + @Test + void fourLabels() { + Labels one = + Labels.of( + "key1", "value1", + "key2", "value2", + "key3", "value3", + "key4", "value4"); + assertThat(one).isNotNull(); + } + + @Test + void builder() { + Labels labels = + Labels.builder() + .put("key1", "duplicateShouldBeIgnored") + .put("key1", "value1") + .put("key2", "value2") + .build(); + + assertThat(labels) + .isEqualTo( + Labels.of( + "key1", "value1", + "key2", "value2")); + } + + @Test + void toBuilder() { + Labels initial = Labels.of("one", "a"); + Labels second = initial.toBuilder().put("two", "b").build(); + assertThat(initial.size()).isEqualTo(1); + assertThat(second.size()).isEqualTo(2); + assertThat(initial).isEqualTo(Labels.of("one", "a")); + assertThat(second).isEqualTo(Labels.of("one", "a", "two", "b")); + } +} diff --git a/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/internal/MetricsStringUtilsTest.java b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/internal/MetricsStringUtilsTest.java new file mode 100644 index 000000000..5be4d9252 --- /dev/null +++ b/opentelemetry-java/api/metrics/src/test/java/io/opentelemetry/api/metrics/internal/MetricsStringUtilsTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.metrics.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class MetricsStringUtilsTest { + + @Test + void isValidMetricName() { + assertThat(MetricsStringUtils.isValidMetricName("")).isFalse(); + assertThat( + MetricsStringUtils.isValidMetricName( + String.valueOf(new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]))) + .isFalse(); + assertThat(MetricsStringUtils.isValidMetricName("abcd")).isTrue(); + assertThat(MetricsStringUtils.isValidMetricName("ab.cd")).isTrue(); + assertThat(MetricsStringUtils.isValidMetricName("ab12cd")).isTrue(); + assertThat(MetricsStringUtils.isValidMetricName("1abcd")).isFalse(); + assertThat(MetricsStringUtils.isValidMetricName("ab*cd")).isFalse(); + } +} diff --git a/opentelemetry-java/bom-alpha/build.gradle.kts b/opentelemetry-java/bom-alpha/build.gradle.kts new file mode 100644 index 000000000..47db866d9 --- /dev/null +++ b/opentelemetry-java/bom-alpha/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("java-platform") + id("maven-publish") +} + +description = "OpenTelemetry Bill of Materials (Alpha)" +group = "io.opentelemetry" +base.archivesBaseName = "opentelemetry-bom-alpha" + +rootProject.subprojects.forEach { subproject -> + if (!project.name.startsWith("bom")) { + evaluationDependsOn(subproject.path) + } +} + +afterEvaluate { + dependencies { + constraints { + rootProject.subprojects + .sortedBy { it.findProperty("archivesBaseName") as String? } + .filter { !it.name.startsWith("bom") } + .filter { it.findProperty("otel.release") == "alpha" } + .forEach { project -> + project.plugins.withId("maven-publish") { + api(project) + } + } + } + } +} diff --git a/opentelemetry-java/bom-alpha/gradle.properties b/opentelemetry-java/bom-alpha/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/bom-alpha/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/bom/build.gradle.kts b/opentelemetry-java/bom/build.gradle.kts new file mode 100644 index 000000000..42126eb77 --- /dev/null +++ b/opentelemetry-java/bom/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("java-platform") + id("maven-publish") +} + +description = "OpenTelemetry Bill of Materials" +group = "io.opentelemetry" +base.archivesBaseName = "opentelemetry-bom" + +rootProject.subprojects.forEach { subproject -> + if (project != subproject) { + evaluationDependsOn(subproject.path) + } +} + +afterEvaluate { + dependencies { + constraints { + rootProject.subprojects + .sortedBy { it.findProperty("archivesBaseName") as String? } + .filter { !it.name.startsWith("bom")} + .filter { !it.hasProperty("otel.release") } + .forEach { project -> + project.plugins.withId("maven-publish") { + api(project) + } + } + } + } +} diff --git a/opentelemetry-java/build.gradle.kts b/opentelemetry-java/build.gradle.kts new file mode 100644 index 000000000..fd45f7c22 --- /dev/null +++ b/opentelemetry-java/build.gradle.kts @@ -0,0 +1,680 @@ +import com.diffplug.gradle.spotless.SpotlessExtension +import com.google.protobuf.gradle.* +import de.marcphilipp.gradle.nexus.NexusPublishExtension +import io.morethan.jmhreport.gradle.JmhReportExtension +import me.champeau.jmh.JmhParameters +import me.champeau.gradle.japicmp.JapicmpTask +import net.ltgt.gradle.errorprone.CheckSeverity +import net.ltgt.gradle.errorprone.ErrorProneOptions +import net.ltgt.gradle.errorprone.ErrorPronePlugin +import net.ltgt.gradle.nullaway.NullAwayOptions +import org.gradle.api.plugins.JavaPlugin.* +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import ru.vyarus.gradle.plugin.animalsniffer.AnimalSnifferExtension +import ru.vyarus.gradle.plugin.animalsniffer.AnimalSnifferPlugin +import java.time.Duration + +plugins { + id("com.diffplug.spotless") + id("com.github.ben-manes.versions") + id("io.codearte.nexus-staging") + id("nebula.release") + id ("com.google.osdetector") version "1.7.3" + + id("com.google.protobuf") apply false + id("de.marcphilipp.nexus-publish") apply false + id("io.morethan.jmhreport") apply false + id("me.champeau.jmh") apply false + id("net.ltgt.errorprone") apply false + id("net.ltgt.nullaway") apply false + id("ru.vyarus.animalsniffer") apply false + id("me.champeau.gradle.japicmp") apply false +} + +allprojects { + repositories { + mavenLocal() + maven { url = uri("https://maven.aliyun.com/nexus/content/repositories/central/") } + mavenCentral() + } +} + +/** + * Locate the project's artifact of a particular version. + */ +fun Project.findArtifact(version: String) : File { + val existingGroup = this.group + try { + // Temporarily change the group name because we want to fetch an artifact with the same + // Maven coordinates as the project, which Gradle would not allow otherwise. + this.group = "virtual_group" + val depModule = "io.opentelemetry:${base.archivesBaseName}:$version@jar" + val depJar = "${base.archivesBaseName}-${version}.jar" + val configuration: Configuration = configurations.detachedConfiguration( + dependencies.create(depModule) + ) + return files(configuration.files).filter { + it.name.equals(depJar) + }.singleFile + } finally { + this.group = existingGroup + } +} + +/** + * The latest *released* version of the project. Evaluated lazily so the work is only done if necessary. + */ +val latestReleasedVersion : String by lazy { + // hack to find the current released version of the project + val temp: Configuration = project.configurations.create("tempConfig") + // pick the api, since it's always there. + dependencies.add("tempConfig", "io.opentelemetry:opentelemetry-api:latest.release") + val moduleVersion = project.configurations["tempConfig"].resolvedConfiguration.firstLevelModuleDependencies.elementAt(0).moduleVersion + project.configurations.remove(temp) + println("Discovered latest release version: " + moduleVersion) + moduleVersion +} + +if (!JavaVersion.current().isJava11Compatible()) { + throw GradleException("JDK 11 or higher is required to build. " + + "One option is to download it from https://adoptopenjdk.net/. If you believe you already " + + "have it, please check that the JAVA_HOME environment variable is pointing at the " + + "JDK 11 installation.") +} + +// Nebula plugin will not configure if .git doesn't exist, let's allow building on it by stubbing it +// out. This supports building from the zip archive downloaded from GitHub. +//var releaseTask: TaskProvider +//if (file(".git").exists()) { +//// release { +//// defaultVersionStrategy = Strategies.getSNAPSHOT() +//// } +// +// nebulaRelease { +// addReleaseBranchPattern("""v\d+\.\d+\.x""") +// } +// +// releaseTask = tasks.named("release") +// releaseTask.configure { +// mustRunAfter("snapshotSetup", "finalSetup") +// } +//} else { +// releaseTask = tasks.register("release") +//} + +nexusStaging { + packageGroup = "io.opentelemetry" + username = System.getenv("SONATYPE_USER") + password = System.getenv("SONATYPE_KEY") + + // We have many artifacts so Maven Central takes a long time on its compliance checks. This sets + // the timeout for waiting for the repository to close to a comfortable 50 minutes. + numberOfRetries = 300 + delayBetweenRetriesInMillis = 10000 +} + +val enableNullaway: String? by project + +subprojects { + group = "io.opentelemetry" + + plugins.withId("java") { + plugins.apply("checkstyle") + plugins.apply("eclipse") + plugins.apply("idea") + plugins.apply("jacoco") + + plugins.apply("com.diffplug.spotless") + plugins.apply("net.ltgt.errorprone") + plugins.apply("net.ltgt.nullaway") + + configure { + archivesBaseName = "opentelemetry-${name}" + } + + configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } + + withJavadocJar() + withSourcesJar() + } + + configure { + configDirectory.set(file("$rootDir/buildscripts/")) + toolVersion = "8.12" + isIgnoreFailures = false + configProperties["rootDir"] = rootDir + } + + configure { + toolVersion = "0.8.7" + } + + val javaToolchains = the() + + tasks { + val testJava8 by registering(Test::class) { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(8)) + }) + + configure { + enabled = false + } + } + + val testAdditionalJavaVersions: String? by rootProject + if (testAdditionalJavaVersions == "true") { + named("check") { + dependsOn(testJava8) + } + } + + withType(JavaCompile::class) { + options.release.set(8) + + if (name != "jmhCompileGeneratedClasses") { + options.compilerArgs.addAll(listOf( + "-Xlint:all", + // 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. + "-Xlint:-try", + // We suppress the "processing" warning as suggested in + // https://groups.google.com/forum/#!topic/bazel-discuss/_R3A9TJSoPM + "-Xlint:-processing", + // We suppress the "options" warning because it prevents compilation on modern JDKs + "-Xlint:-options", + + // Fail build on any warning + "-Werror" + )) + } + //disable deprecation warnings for the protobuf module + if (project.name == "proto") { + options.compilerArgs.add("-Xlint:-deprecation") + } + + options.encoding = "UTF-8" + + if (name.contains("Test")) { + // serialVersionUID is basically guaranteed to be useless in tests + options.compilerArgs.add("-Xlint:-serial") + } + + (options as ExtensionAware).extensions.configure { + disableWarningsInGeneratedCode.set(true) + allDisabledChecksAsWarnings.set(true) + + (this as ExtensionAware).extensions.configure { + // Enable nullaway on main sources. + // TODO(anuraaga): Remove enableNullaway flag when all errors fixed + if (!name.contains("Test") && !name.contains("Jmh") && enableNullaway == "true") { + severity.set(CheckSeverity.ERROR) + } else { + severity.set(CheckSeverity.OFF) + } + annotatedPackages.add("io.opentelemetry") + } + + // Doesn't currently use Var annotations. + disable("Var") // "-Xep:Var:OFF" + + // ImmutableRefactoring suggests using com.google.errorprone.annotations.Immutable, + // but currently uses javax.annotation.concurrent.Immutable + disable("ImmutableRefactoring") // "-Xep:ImmutableRefactoring:OFF" + + // AutoValueImmutableFields suggests returning Guava types from API methods + disable("AutoValueImmutableFields") + // Suggests using Guava types for fields but we don't use Guava + disable("ImmutableMemberCollection") + // "-Xep:AutoValueImmutableFields:OFF" + + // Fully qualified names may be necessary when deprecating a class to avoid + // deprecation warning. + disable("UnnecessarilyFullyQualified") + + // Ignore warnings for protobuf and jmh generated files. + excludedPaths.set(".*generated.*|.*internal.shaded.*") + // "-XepExcludedPaths:.*/build/generated/source/proto/.*" + + disable("Java7ApiChecker") + disable("AndroidJdkLibsChecker") + //apparently disabling android doesn't disable this + disable("StaticOrDefaultInterfaceMethod") + + //until we have everything converted, we need these + disable("JdkObsolete") + disable("UnnecessaryAnonymousClass") + + // Limits APIs + disable("NoFunctionalReturnType") + + // We don't depend on Guava so use normal splitting + disable("StringSplitter") + + // Prevents lazy initialization + disable("InitializeInline") + + if (name.contains("Jmh") || name.contains("Test")) { + // Allow underscore in test-type method names + disable("MemberName") + } + } + } + + withType(Test::class) { + useJUnitPlatform() + + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showExceptions = true + showCauses = true + showStackTraces = true + } + maxHeapSize = "1500m" + } + + withType(Javadoc::class) { + exclude("io/opentelemetry/**/internal/**") + + with(options as StandardJavadocDocletOptions) { + source = "8" + encoding = "UTF-8" + docEncoding = "UTF-8" + breakIterator(true) + + addBooleanOption("html5", true) + + links("https://docs.oracle.com/javase/8/docs/api/") + addBooleanOption("Xdoclint:all,-missing", true) + + afterEvaluate { + val title = "${project.description}" + docTitle = title + windowTitle = title + } + } + } + + afterEvaluate { + withType(Jar::class) { + val moduleName: String by project + inputs.property("moduleName", moduleName) + + manifest { + attributes( + "Automatic-Module-Name" to moduleName, + "Built-By" to System.getProperty("user.name"), + "Built-JDK" to System.getProperty("java.version"), + "Implementation-Title" to project.name, + "Implementation-Version" to project.version) + } + } + } + } + + // https://docs.gradle.org/current/samples/sample_jvm_multi_project_with_code_coverage.html + + // Do not generate reports for individual projects + tasks.named("jacocoTestReport") { + enabled = false + } + + configurations { + val implementation by getting + + create("transitiveSourceElements") { + isVisible = false + isCanBeResolved = false + isCanBeConsumed = true + extendsFrom(implementation) + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION)) + attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("source-folders")) + } + val mainSources = the().sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME) + mainSources.java.srcDirs.forEach { + outgoing.artifact(it) + } + } + + create("coverageDataElements") { + isVisible = false + isCanBeResolved = false + isCanBeConsumed = true + extendsFrom(implementation) + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION)) + attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("jacoco-coverage-data")) + } + // This will cause the test task to run if the coverage data is requested by the aggregation task + tasks.withType(Test::class) { + outgoing.artifact(extensions.getByType().destinationFile!!) + } + } + + configureEach { + resolutionStrategy { + failOnVersionConflict() + preferProjectModules() + } + } + } + + configure { + java { + googleJavaFormat("1.9") + licenseHeaderFile(rootProject.file("buildscripts/spotless.license.java"), "(package|import|class|// Includes work from:)") + } + } + + val dependencyManagement by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = false + isVisible = false + } + + dependencies { + add(dependencyManagement.name, platform(project(":dependencyManagement"))) + afterEvaluate { + configurations.configureEach { + if (isCanBeResolved && !isCanBeConsumed) { + extendsFrom(dependencyManagement) + } + } + } + + add(COMPILE_ONLY_CONFIGURATION_NAME, "com.google.auto.value:auto-value-annotations") + add(COMPILE_ONLY_CONFIGURATION_NAME, "com.google.code.findbugs:jsr305") + + add(TEST_COMPILE_ONLY_CONFIGURATION_NAME, "com.google.auto.value:auto-value-annotations") + add(TEST_COMPILE_ONLY_CONFIGURATION_NAME, "com.google.errorprone:error_prone_annotations") + add(TEST_COMPILE_ONLY_CONFIGURATION_NAME, "com.google.code.findbugs:jsr305") + + add(TEST_IMPLEMENTATION_CONFIGURATION_NAME, "org.junit.jupiter:junit-jupiter-api") + add(TEST_IMPLEMENTATION_CONFIGURATION_NAME, "org.junit.jupiter:junit-jupiter-params") + add(TEST_IMPLEMENTATION_CONFIGURATION_NAME, "nl.jqno.equalsverifier:equalsverifier") + add(TEST_IMPLEMENTATION_CONFIGURATION_NAME, "org.mockito:mockito-core") + add(TEST_IMPLEMENTATION_CONFIGURATION_NAME, "org.mockito:mockito-junit-jupiter") + add(TEST_IMPLEMENTATION_CONFIGURATION_NAME, "org.assertj:assertj-core") + add(TEST_IMPLEMENTATION_CONFIGURATION_NAME, "org.awaitility:awaitility") + add(TEST_IMPLEMENTATION_CONFIGURATION_NAME, "io.github.netmikey.logunit:logunit-jul") + + add(TEST_RUNTIME_ONLY_CONFIGURATION_NAME, "org.junit.jupiter:junit-jupiter-engine") + add(TEST_RUNTIME_ONLY_CONFIGURATION_NAME, "org.junit.vintage:junit-vintage-engine") + + add(ErrorPronePlugin.CONFIGURATION_NAME, "com.google.errorprone:error_prone_core") + add(ErrorPronePlugin.CONFIGURATION_NAME, "com.uber.nullaway:nullaway") + + add(ANNOTATION_PROCESSOR_CONFIGURATION_NAME, "com.google.guava:guava-beta-checker") + + // Workaround for @javax.annotation.Generated + // see: https://github.com/grpc/grpc-java/issues/3633 + add(COMPILE_ONLY_CONFIGURATION_NAME, "javax.annotation:javax.annotation-api") + } + + plugins.withId("com.google.protobuf") { + protobuf { + val versions: Map by project + protoc { + // The artifact spec for the Protobuf Compiler + artifact = "com.google.protobuf:protoc:${versions["com.google.protobuf"]}" + if (osdetector.os == "osx") { + // Always use x86_64 version as ARM binary is not available + artifact += ":osx-x86_64" + } + } + plugins { + id("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:${versions["io.grpc"]}" + if (osdetector.os == "osx") { + // Always use x86_64 version as ARM binary is not available + artifact += ":osx-x86_64" + } + } + } + generateProtoTasks { + all().configureEach { + plugins { + id("grpc") + } + } + } + } + + afterEvaluate { + // Classpath when compiling protos, we add dependency management directly + // since it doesn't follow Gradle conventions of naming / properties. + dependencies { + add("compileProtoPath", platform(project(":dependencyManagement"))) + add("testCompileProtoPath", platform(project(":dependencyManagement"))) + } + } + } + + plugins.withId("ru.vyarus.animalsniffer") { + dependencies { + add(AnimalSnifferPlugin.SIGNATURE_CONF, "com.toasttab.android:gummy-bears-api-21:0.3.0:coreLib@signature") + } + + configure { + sourceSets = listOf(the().sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)) + } + } + + plugins.withId("me.champeau.jmh") { + // Always include the jmhreport plugin and run it after jmh task. + plugins.apply("io.morethan.jmhreport") + dependencies { + add("jmh", platform(project(":dependencyManagement"))) + add("jmh", "org.openjdk.jmh:jmh-core") + add("jmh", "org.openjdk.jmh:jmh-generator-bytecode") + } + + // invoke jmh on a single benchmark class like so: + // ./gradlew -PjmhIncludeSingleClass=StatsTraceContextBenchmark clean :grpc-core:jmh + configure { + failOnError.set(true) + resultFormat.set("JSON") + // Otherwise an error will happen: + // Could not expand ZIP 'byte-buddy-agent-1.9.7.jar'. + includeTests.set(false) + profilers.add("gc") + val jmhIncludeSingleClass: String? by project + if (jmhIncludeSingleClass != null) { + includes.add(jmhIncludeSingleClass as String) + } + } + + configure { + jmhResultPath = file("${buildDir}/results/jmh/results.json").absolutePath + jmhReportOutput = file("${buildDir}/results/jmh").absolutePath + } + + tasks { + // TODO(anuraaga): Unclear why this is triggering even though there don't seem to + // be duplicates, possibly a bug in JMH plugin. + named("processJmhResources") { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + + named("jmh") { + finalizedBy(named("jmhReport")) + } + } + } + } + + plugins.withId("me.champeau.gradle.japicmp") { + afterEvaluate { +// tasks { +// val jApiCmp by registering(JapicmpTask::class) { +// dependsOn("jar") +// // the japicmp "old" version is either the user-specified one, or the latest release. +// val userRequestedBase = project.properties["apiBaseVersion"] as String? +// val baselineVersion: String = userRequestedBase ?: latestReleasedVersion +// val baselineArtifact: File = project.findArtifact(baselineVersion) +// oldClasspath = files(baselineArtifact) +// +// // the japicmp "new" version is either the user-specified one, or the locally built jar. +// val newVersion : String? = project.properties["apiNewVersion"] as String? +// val newArtifact: File = if (newVersion == null) { +// val jar = getByName("jar") as Jar +// file(jar.archiveFile) +// } else { +// project.findArtifact(newVersion) +// } +// newClasspath = files(newArtifact) +// +// //only output changes, not everything +// isOnlyModified = true +// //this is needed so that we only consider the current artifact, and not dependencies +// isIgnoreMissingClasses = true +// // double wildcards don't seem to work here (*.internal.*) +// packageExcludes = listOf("*.internal", "io.opentelemetry.internal.shaded.jctools.*") +// if (newVersion == null) { +// val baseVersionString = if (userRequestedBase == null) "latest" else baselineVersion +// txtOutputFile = file("$rootDir/docs/apidiffs/current_vs_${baseVersionString}/${project.base.archivesBaseName}.txt") +// } else { +// txtOutputFile = file("$rootDir/docs/apidiffs/${newVersion}_vs_${baselineVersion}/${project.base.archivesBaseName}.txt") +// } +// } +// // have the check task depend on the api comparison task, to make it more likely it will get used. +// named("check") { +// dependsOn(jApiCmp) +// } +// } + } + } + + plugins.withId("maven-publish") { + // generate the api diff report for any module that is stable and publishes a jar. + if (!project.hasProperty("otel.release") && !project.name.startsWith("bom")) { + plugins.apply("me.champeau.gradle.japicmp") + } + plugins.apply("signing") + plugins.apply("de.marcphilipp.nexus-publish") + + configure { + publications { + register("mavenPublication") { + val release = findProperty("otel.release") + if (release != null) { + val versionParts = version.split('-').toMutableList() + versionParts[0] += "-${release}" + version = versionParts.joinToString("-") + } + groupId = "run.mone" + version = "0.5.0-opensource-SNAPSHOT" + afterEvaluate { + // not available until evaluated. + artifactId = the().archivesBaseName + pom.description.set(project.description) + } + + plugins.withId("java-platform") { + from(components["javaPlatform"]) + } + plugins.withId("java-library") { + from(components["java"]) + } + + versionMapping { + allVariants { + fromResolutionResult() + } + } + // set your private maven repository + repositories { + maven { + url = uri("https://xxx.xxx.xxx:443/artifactory/maven-snapshot-virtual") + credentials { + username = "" + password = "" + } + } + } + pom { + name.set("OpenTelemetry Java") + url.set("https://github.com/open-telemetry/opentelemetry-java") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + + developers { + developer { + id.set("opentelemetry") + name.set("OpenTelemetry") + url.set("https://github.com/open-telemetry/community") + } + } + + scm { + connection.set("scm:git:git@github.com:open-telemetry/opentelemetry-java.git") + developerConnection.set("scm:git:git@github.com:open-telemetry/opentelemetry-java.git") + url.set("git@github.com:open-telemetry/opentelemetry-java.git") + } + } + } + } + } + + configure { + repositories { + sonatype() + } + + connectTimeout.set(Duration.ofMinutes(5)) + clientTimeout.set(Duration.ofMinutes(5)) + } + + val publishToSonatype by tasks.getting +// releaseTask.configure { +// finalizedBy(publishToSonatype) +// } + rootProject.tasks.named("closeAndReleaseRepository") { + mustRunAfter(publishToSonatype) + } + + tasks.withType(Sign::class) { + onlyIf { System.getenv("CI") != null } + } + + configure { + useInMemoryPgpKeys(System.getenv("GPG_PRIVATE_KEY"), System.getenv("GPG_PASSWORD")) + sign(the().publications["mavenPublication"]) + } + } +} + +allprojects { + tasks.register("updateVersionInDocs") { + group = "documentation" + doLast { + val versionParts = version.toString().split('.') + val minorVersionNumber = Integer.parseInt(versionParts[1]) + val nextSnapshot = "${versionParts[0]}.${minorVersionNumber + 1}.0-SNAPSHOT" + + val readme = file("README.md") + if (readme.exists()) { + val readmeText = readme.readText() + val updatedText = readmeText + .replace("""\d+\.\d+\.\d+""".toRegex(), "${version}") + .replace("""\d+\.\d+\.\d+-SNAPSHOT""".toRegex(), "${nextSnapshot}") + .replace("""(implementation.*io\.opentelemetry:.*:)(\d+\.\d+\.\d+)(?!-SNAPSHOT)(.*)""".toRegex(), "\$1${version}\$3") + .replace("""(implementation.*io\.opentelemetry:.*:)(\d+\.\d+\.\d+-SNAPSHOT)(.*)""".toRegex(), "\$1${nextSnapshot}\$3") + .replace(""".*""".toRegex(), "${version}") + .replace(""".*""".toRegex(), "${version}-alpha") + readme.writeText(updatedText) + } + } + } +} diff --git a/opentelemetry-java/buildscripts/checkstyle-suppressions.xml b/opentelemetry-java/buildscripts/checkstyle-suppressions.xml new file mode 100644 index 000000000..1d7dcbe31 --- /dev/null +++ b/opentelemetry-java/buildscripts/checkstyle-suppressions.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/opentelemetry-java/buildscripts/checkstyle.xml b/opentelemetry-java/buildscripts/checkstyle.xml new file mode 100644 index 000000000..b7f26f158 --- /dev/null +++ b/opentelemetry-java/buildscripts/checkstyle.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-java/buildscripts/pre-commit b/opentelemetry-java/buildscripts/pre-commit new file mode 100755 index 000000000..c9fafa447 --- /dev/null +++ b/opentelemetry-java/buildscripts/pre-commit @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +./gradlew spotlessApply \ No newline at end of file diff --git a/opentelemetry-java/buildscripts/semantic-convention/.gitignore b/opentelemetry-java/buildscripts/semantic-convention/.gitignore new file mode 100644 index 000000000..a93b221be --- /dev/null +++ b/opentelemetry-java/buildscripts/semantic-convention/.gitignore @@ -0,0 +1 @@ +opentelemetry-specification/ diff --git a/opentelemetry-java/buildscripts/semantic-convention/generate.sh b/opentelemetry-java/buildscripts/semantic-convention/generate.sh new file mode 100755 index 000000000..30a352895 --- /dev/null +++ b/opentelemetry-java/buildscripts/semantic-convention/generate.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +ROOT_DIR="${SCRIPT_DIR}/../../" + +# freeze the spec & generator tools versions to make SemanticAttributes generation reproducible +SPEC_VERSION=v1.3.0 +GENERATOR_VERSION=0.3.1 + +cd ${SCRIPT_DIR} + +rm -rf opentelemetry-specification || true +mkdir opentelemetry-specification +cd opentelemetry-specification + +git init +git remote add origin https://github.com/open-telemetry/opentelemetry-specification.git +git fetch origin "$SPEC_VERSION" +git reset --hard FETCH_HEAD +cd ${SCRIPT_DIR} + +docker run --rm \ + -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/trace:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/semconv/src/main/java/io/opentelemetry/semconv/trace/attributes/:/output \ + otel/semconvgen:$GENERATOR_VERSION \ + -f /source code \ + --template /templates/SemanticAttributes.java.j2 \ + --output /output/SemanticAttributes.java \ + -Dclass=SemanticAttributes \ + -Dpkg=io.opentelemetry.semconv.trace.attributes + +docker run --rm \ + -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/resource:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/semconv/src/main/java/io/opentelemetry/semconv/resource/attributes/:/output \ + otel/semconvgen:$GENERATOR_VERSION \ + -f /source code \ + --template /templates/SemanticAttributes.java.j2 \ + --output /output/ResourceAttributes.java \ + -Dclass=ResourceAttributes \ + -Dpkg=io.opentelemetry.semconv.resource.attributes + +cd "$ROOT_DIR" +./gradlew spotlessApply diff --git a/opentelemetry-java/buildscripts/semantic-convention/templates/SemanticAttributes.java.j2 b/opentelemetry-java/buildscripts/semantic-convention/templates/SemanticAttributes.java.j2 new file mode 100644 index 000000000..e2834f391 --- /dev/null +++ b/opentelemetry-java/buildscripts/semantic-convention/templates/SemanticAttributes.java.j2 @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + + +{%- macro to_java_return_type(type) -%} + {%- if type == "string" -%} + String + {%- elif type == "string[]" -%} + List + {%- elif type == "boolean" -%} + boolean + {%- elif type == "int" -%} + long + {%- elif type == "double" -%} + double + {%- else -%} + {{type}} + {%- endif -%} +{%- endmacro %} +{%- macro to_java_key_type(type) -%} + {%- if type == "string" -%} + stringKey + {%- elif type == "string[]" -%} + stringArrayKey + {%- elif type == "boolean" -%} + booleanKey + {%- elif type == "int" -%} + longKey + {%- elif type == "double" -%} + doubleKey + {%- else -%} + {{lowerFirst(type)}}Key + {%- endif -%} +{%- endmacro %} +{%- macro print_value(type, value) -%} + {{ "\"" if type == "String"}}{{value}}{{ "\"" if type == "String"}} +{%- endmacro %} +{%- macro upFirst(text) -%} + {{ text[0]|upper}}{{text[1:] }} +{%- endmacro %} +{%- macro lowerFirst(text) -%} + {{ text[0]|lower}}{{text[1:] }} +{%- endmacro %} + +package {{pkg | trim}}; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; + +import io.opentelemetry.api.common.AttributeKey; +import java.util.List; + +// DO NOT EDIT, this is an Auto-generated file from buildscripts/semantic-convention{{template}} +public final class {{class}} { + {%- for attribute in attributes if attribute.is_local and not attribute.ref %} + + /** + * {% filter escape %}{{attribute.brief | to_doc_brief}}.{% endfilter %} + {%- if attribute.note %} + * + * Note: {% filter escape %}{{attribute.note | to_doc_brief}}.{% endfilter %} + {%- endif %} + {%- if attribute.deprecated %} + * + * @deprecated {{attribute.deprecated | to_doc_brief}}. + {%- endif %} + */ + {%- if attribute.deprecated %} + @Deprecated + {%- endif %} + public static final AttributeKey<{{upFirst(to_java_return_type(attribute.attr_type | string))}}> {{attribute.fqn | to_const_name}} = {{to_java_key_type(attribute.attr_type | string)}}("{{attribute.fqn}}"); + {%- endfor %} + + // Enum definitions + {%- for attribute in attributes if attribute.is_local and not attribute.ref %} + {%- if attribute.is_enum %} + {%- set class_name = attribute.fqn | to_camelcase(True) ~ "Values" %} + {%- set type = to_java_return_type(attribute.attr_type.enum_type) %} + public static final class {{class_name}} { + {%- for member in attribute.attr_type.members %} + /** {% filter escape %}{{member.brief | to_doc_brief}}.{% endfilter %} */ + public static final {{ type }} {{ member.member_id | to_const_name }} = {{ print_value(type, member.value) }}; + {%- endfor %} + private {{ class_name }}() {} + } + + {% endif %} + {%- endfor %} + + {%- if class == "SemanticAttributes" %} + // Manually defined and not YET in the YAML + /** + * The name of an event describing an exception. + * + *

    Typically an event with that name should not be manually created. Instead {@link + * io.opentelemetry.api.trace.Span#recordException(Throwable)} should be used. + */ + public static final String EXCEPTION_EVENT_NAME = "exception"; + {% endif %} + + private {{class}}() {} +} diff --git a/opentelemetry-java/buildscripts/spotless.license.java b/opentelemetry-java/buildscripts/spotless.license.java new file mode 100644 index 000000000..b504712ee --- /dev/null +++ b/opentelemetry-java/buildscripts/spotless.license.java @@ -0,0 +1,5 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + diff --git a/opentelemetry-java/code-of-conduct.md b/opentelemetry-java/code-of-conduct.md new file mode 100644 index 000000000..0099566bf --- /dev/null +++ b/opentelemetry-java/code-of-conduct.md @@ -0,0 +1,3 @@ +# OpenTelemetry Community Code of Conduct + +Please refer to our [OpenTelemetry Community Code of Conduct](https://github.com/open-telemetry/community/blob/main/code-of-conduct.md) diff --git a/opentelemetry-java/context/build.gradle.kts b/opentelemetry-java/context/build.gradle.kts new file mode 100644 index 000000000..9f66f5743 --- /dev/null +++ b/opentelemetry-java/context/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id("java-library") + id("maven-publish") + + id("me.champeau.jmh") + id("org.unbroken-dome.test-sets") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Context (Incubator)" +extra["moduleName"] = "io.opentelemetry.context" + +testSets { + create("grpcInOtelTest") + create("otelInGrpcTest") + + create("braveInOtelTest") + create("otelInBraveTest") + create("otelAsBraveTest") + + create("storageWrappersTest") + + create("strictContextEnabledTest") +} + +dependencies { + add("grpcInOtelTestImplementation", "io.grpc:grpc-context") + add("otelInGrpcTestImplementation", "io.grpc:grpc-context") + + add("braveInOtelTestImplementation", "io.zipkin.brave:brave") + add("otelAsBraveTestImplementation", "io.zipkin.brave:brave") + add("otelInBraveTestImplementation", "io.zipkin.brave:brave") + + add("strictContextEnabledTestImplementation", project(":api:all")) + + // MustBeClosed + compileOnly("com.google.errorprone:error_prone_annotations") + + testImplementation("org.awaitility:awaitility") + testImplementation("com.google.guava:guava") + testImplementation("org.junit-pioneer:junit-pioneer") +} + +tasks { + named("strictContextEnabledTest") { + jvmArgs("-Dio.opentelemetry.context.enableStrictContext=true") + } + + named("check") { + dependsOn("grpcInOtelTest", "otelInGrpcTest", "braveInOtelTest", "otelInBraveTest", + "otelAsBraveTest", "storageWrappersTest", "strictContextEnabledTest") + } +} + diff --git a/opentelemetry-java/context/src/braveInOtelTest/java/io/opentelemetry/context/BraveInOtelTest.java b/opentelemetry-java/context/src/braveInOtelTest/java/io/opentelemetry/context/BraveInOtelTest.java new file mode 100644 index 000000000..efaf20562 --- /dev/null +++ b/opentelemetry-java/context/src/braveInOtelTest/java/io/opentelemetry/context/BraveInOtelTest.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import brave.Tracing; +import brave.propagation.CurrentTraceContext; +import brave.propagation.TraceContext; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class BraveInOtelTest { + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + + private static final Tracing TRACING = + Tracing.newBuilder().currentTraceContext(new OpenTelemetryCurrentTraceContext()).build(); + + private static final TraceContext TRACE_CONTEXT = + TraceContext.newBuilder().traceId(1).spanId(1).addExtra("japan").build(); + + private static ExecutorService otherThread; + + @BeforeAll + static void setUp() { + otherThread = Executors.newSingleThreadExecutor(); + } + + @AfterAll + static void tearDown() { + otherThread.shutdown(); + } + + @Test + void braveOtelMix() { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + TraceContext context2 = + Tracing.current().currentTraceContext().get().toBuilder().addExtra("cheese").build(); + try (CurrentTraceContext.Scope ignored3 = + TRACING.currentTraceContext().newScope(context2)) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("cheese"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } + } + } + } + + @Test + void braveWrap() throws Exception { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + AtomicReference braveContainsJapan = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + TraceContext traceContext = Tracing.current().currentTraceContext().get(); + if (traceContext != null && traceContext.extra().contains("japan")) { + braveContainsJapan.set(true); + } else { + braveContainsJapan.set(false); + } + otelValue.set(Context.current().get(ANIMAL)); + }; + otherThread.submit(runnable).get(); + assertThat(braveContainsJapan).hasValue(false); + assertThat(otelValue).hasValue(null); + + otherThread.submit(TRACING.currentTraceContext().wrap(runnable)).get(); + assertThat(braveContainsJapan).hasValue(true); + + // Since Brave context is inside the OTel context, propagating the Brave context does not + // propagate the OTel context. + assertThat(otelValue).hasValue(null); + } + } + } + + @Test + void otelWrap() throws Exception { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + AtomicReference braveContainsJapan = new AtomicReference<>(false); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + TraceContext traceContext = Tracing.current().currentTraceContext().get(); + if (traceContext != null && traceContext.extra().contains("japan")) { + braveContainsJapan.set(true); + } else { + braveContainsJapan.set(false); + } + otelValue.set(Context.current().get(ANIMAL)); + }; + otherThread.submit(runnable).get(); + assertThat(braveContainsJapan).hasValue(false); + assertThat(otelValue).hasValue(null); + + otherThread.submit(Context.current().wrap(runnable)).get(); + assertThat(braveContainsJapan).hasValue(true); + assertThat(otelValue).hasValue("cat"); + } + } + } +} diff --git a/opentelemetry-java/context/src/braveInOtelTest/java/io/opentelemetry/context/OpenTelemetryCurrentTraceContext.java b/opentelemetry-java/context/src/braveInOtelTest/java/io/opentelemetry/context/OpenTelemetryCurrentTraceContext.java new file mode 100644 index 000000000..a476ea691 --- /dev/null +++ b/opentelemetry-java/context/src/braveInOtelTest/java/io/opentelemetry/context/OpenTelemetryCurrentTraceContext.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import brave.propagation.CurrentTraceContext; +import brave.propagation.TraceContext; +import com.google.errorprone.annotations.MustBeClosed; + +public class OpenTelemetryCurrentTraceContext extends CurrentTraceContext { + + private static final ContextKey TRACE_CONTEXT_KEY = + ContextKey.named("brave-tracecontext"); + + @Override + public TraceContext get() { + return Context.current().get(TRACE_CONTEXT_KEY); + } + + @SuppressWarnings({"ReferenceEquality", "MustBeClosedChecker"}) + @Override + @MustBeClosed + public Scope newScope(TraceContext context) { + Context currentOtel = Context.current(); + TraceContext currentBrave = currentOtel.get(TRACE_CONTEXT_KEY); + if (currentBrave == context) { + return Scope.NOOP; + } + + Context newOtel = currentOtel.with(TRACE_CONTEXT_KEY, context); + io.opentelemetry.context.Scope otelScope = newOtel.makeCurrent(); + return otelScope::close; + } +} diff --git a/opentelemetry-java/context/src/grpcInOtelTest/java/io/grpc/override/ContextStorageOverride.java b/opentelemetry-java/context/src/grpcInOtelTest/java/io/grpc/override/ContextStorageOverride.java new file mode 100644 index 000000000..b30df462e --- /dev/null +++ b/opentelemetry-java/context/src/grpcInOtelTest/java/io/grpc/override/ContextStorageOverride.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.grpc.override; + +import io.grpc.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; +import java.util.logging.Level; +import java.util.logging.Logger; + +// This exact package / class name indicates to gRPC to use this override. +public class ContextStorageOverride extends Context.Storage { + + private static final Logger log = Logger.getLogger(ContextStorageOverride.class.getName()); + + private static final ContextKey GRPC_CONTEXT = ContextKey.named("grpc-context"); + private static final Context.Key OTEL_SCOPE = Context.key("otel-scope"); + + @SuppressWarnings("MustBeClosedChecker") + @Override + public Context doAttach(Context toAttach) { + io.opentelemetry.context.Context otelContext = io.opentelemetry.context.Context.current(); + Context current = otelContext.get(GRPC_CONTEXT); + + if (current == toAttach) { + return toAttach; + } + + if (current == null) { + current = Context.ROOT; + } + + io.opentelemetry.context.Context newOtelContext = otelContext.with(GRPC_CONTEXT, toAttach); + Scope scope = newOtelContext.makeCurrent(); + return current.withValue(OTEL_SCOPE, scope); + } + + @Override + public void detach(Context toDetach, Context toRestore) { + if (current() != toDetach) { + // Log a severe message instead of throwing an exception as the context to attach is assumed + // to be the correct one and the unbalanced state represents a coding mistake in a lower + // layer in the stack that cannot be recovered from here. + log.log( + Level.SEVERE, + "Context was not attached when detaching", + new Throwable().fillInStackTrace()); + } + + Scope otelScope = OTEL_SCOPE.get(toRestore); + otelScope.close(); + } + + @Override + public Context current() { + return io.opentelemetry.context.Context.current().get(GRPC_CONTEXT); + } +} diff --git a/opentelemetry-java/context/src/grpcInOtelTest/java/io/opentelemetry/context/GrpcInOtelTest.java b/opentelemetry-java/context/src/grpcInOtelTest/java/io/opentelemetry/context/GrpcInOtelTest.java new file mode 100644 index 000000000..265c623ed --- /dev/null +++ b/opentelemetry-java/context/src/grpcInOtelTest/java/io/opentelemetry/context/GrpcInOtelTest.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class GrpcInOtelTest { + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + + private static final io.grpc.Context.Key FOOD = io.grpc.Context.key("food"); + private static final io.grpc.Context.Key COUNTRY = io.grpc.Context.key("country"); + + private static ExecutorService otherThread; + + @BeforeAll + static void setUp() { + otherThread = Executors.newSingleThreadExecutor(); + } + + @AfterAll + static void tearDown() { + otherThread.shutdown(); + } + + @Test + void grpcOtelMix() { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + assertThat(COUNTRY.get()).isNull(); + io.grpc.Context root = grpcContext.attach(); + try { + assertThat(COUNTRY.get()).isEqualTo("japan"); + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + assertThat(COUNTRY.get()).isEqualTo("japan"); + + io.grpc.Context context2 = io.grpc.Context.current().withValue(FOOD, "cheese"); + assertThat(FOOD.get()).isNull(); + io.grpc.Context toRestore = context2.attach(); + try { + assertThat(FOOD.get()).isEqualTo("cheese"); + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } finally { + context2.detach(toRestore); + } + } + } finally { + grpcContext.detach(root); + } + } + + @Test + void grpcWrap() throws Exception { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + io.grpc.Context root = grpcContext.attach(); + try { + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + AtomicReference grpcValue = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + grpcValue.set(COUNTRY.get()); + otelValue.set(Context.current().get(ANIMAL)); + }; + otherThread.submit(runnable).get(); + assertThat(grpcValue).hasValue(null); + assertThat(otelValue).hasValue(null); + + otherThread.submit(io.grpc.Context.current().wrap(runnable)).get(); + assertThat(grpcValue).hasValue("japan"); + + // Since gRPC context is inside the OTel context, propagating gRPC context does not + // propagate the OTel context. + assertThat(otelValue).hasValue(null); + } + } finally { + grpcContext.detach(root); + } + } + + @Test + void otelWrap() throws Exception { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + io.grpc.Context root = grpcContext.attach(); + try { + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + AtomicReference grpcValue = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + grpcValue.set(COUNTRY.get()); + otelValue.set(Context.current().get(ANIMAL)); + }; + otherThread.submit(runnable).get(); + assertThat(grpcValue).hasValue(null); + assertThat(otelValue).hasValue(null); + + otherThread.submit(Context.current().wrap(runnable)).get(); + assertThat(grpcValue).hasValue("japan"); + assertThat(otelValue).hasValue("cat"); + } + } finally { + grpcContext.detach(root); + } + } +} diff --git a/opentelemetry-java/context/src/jmh/java/io/opentelemetry/context/ContextBenchmark.java b/opentelemetry-java/context/src/jmh/java/io/opentelemetry/context/ContextBenchmark.java new file mode 100644 index 000000000..28547f1e1 --- /dev/null +++ b/opentelemetry-java/context/src/jmh/java/io/opentelemetry/context/ContextBenchmark.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +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.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@Threads(value = 1) +@Fork(3) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 5, time = 1) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +public class ContextBenchmark { + + @Param({"2", "3", "4", "5", "10", "20", "40"}) + private int size; + + private int middle; + + private List> keys; + private Context context = Context.root(); + + @Setup + public void setup() { + keys = new ArrayList<>(); + for (int i = 0; i < size; i++) { + ContextKey key = ContextKey.named(Integer.toString(i)); + context = context.with(key, "value"); + keys.add(key); + } + middle = size / 2; + } + + @Benchmark + public String readFirst() { + return context.get(keys.get(0)); + } + + @Benchmark + public String readLast() { + return context.get(keys.get(size - 1)); + } + + @Benchmark + public String readMiddle() { + return context.get(keys.get(middle)); + } + + @Benchmark + public void readAll(Blackhole bh) { + for (int i = 0; i < size; i++) { + bh.consume(context.get(keys.get(i))); + } + } + + @Benchmark + public Context writeOne() { + return Context.root().with(keys.get(0), "value"); + } + + @Benchmark + public Context writeAll() { + Context context = Context.root(); + for (int i = 0; i < size; i++) { + context = context.with(keys.get(i), "value"); + } + return context; + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ArrayBasedContext.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ArrayBasedContext.java new file mode 100644 index 000000000..ba3f22559 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ArrayBasedContext.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2015 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.context; + +import java.util.Arrays; +import javax.annotation.Nullable; + +final class ArrayBasedContext implements Context { + + private static final Context ROOT = new ArrayBasedContext(new Object[0]); + + // Used by auto-instrumentation agent. Check with auto-instrumentation before making changes to + // this method. + // + // In particular, do not change this return type to DefaultContext because auto-instrumentation + // hijacks this method and returns a bridged implementation of Context. + // + // Ideally auto-instrumentation would hijack the public Context.root() instead of this + // method, but auto-instrumentation also needs to inject its own implementation of Context + // into the class loader at the same time, which causes a problem because injecting a class into + // the class loader automatically resolves its super classes (interfaces), which in this case is + // Context, which would be the same class (interface) being instrumented at that time, + // which would lead to the JVM throwing a LinkageError "attempted duplicate interface definition" + static Context root() { + return ROOT; + } + + private final Object[] entries; + + private ArrayBasedContext(Object[] entries) { + this.entries = entries; + } + + @Override + @Nullable + public V get(ContextKey key) { + for (int i = 0; i < entries.length; i += 2) { + if (entries[i] == key) { + @SuppressWarnings("unchecked") + V result = (V) entries[i + 1]; + return result; + } + } + return null; + } + + @Override + public Context with(ContextKey key, V value) { + for (int i = 0; i < entries.length; i += 2) { + if (entries[i] == key) { + if (entries[i + 1] == value) { + return this; + } + Object[] newEntries = entries.clone(); + newEntries[i + 1] = value; + return new ArrayBasedContext(newEntries); + } + } + Object[] newEntries = Arrays.copyOf(entries, entries.length + 2); + newEntries[newEntries.length - 2] = key; + newEntries[newEntries.length - 1] = value; + return new ArrayBasedContext(newEntries); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("{"); + for (int i = 0; i < entries.length; i += 2) { + sb.append(entries[i]).append('=').append(entries[i + 1]).append(", "); + } + // get rid of that last pesky comma + if (sb.length() > 1) { + sb.setLength(sb.length() - 2); + } + sb.append('}'); + return sb.toString(); + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/Context.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/Context.java new file mode 100644 index 000000000..ffbd52cdf --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/Context.java @@ -0,0 +1,248 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2015 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.context; + +import com.google.errorprone.annotations.MustBeClosed; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.Nullable; + +/** + * A context propagation mechanism which can carry scoped-values across API boundaries and between + * threads. + * + *

    A Context object can be {@linkplain #makeCurrent set} to the {@link ContextStorage}, which + * effectively forms a scope for the context. The scope is bound to the current thread. + * Within a scope, its Context is accessible even across API boundaries, through {@link #current}. + * The scope is later exited by {@link Scope#close()} closing} the scope. + * + *

    Context objects are immutable and inherit state from their parent. To add or overwrite the + * current state a new context object must be created and then attached, replacing the previously + * bound context. For example: + * + *

    {@code
    + * Context withCredential = Context.current().with(CRED_KEY, cred);
    + * withCredential.wrap(new Runnable() {
    + *   public void run() {
    + *      readUserRecords(userId, CRED_KEY.get());
    + *   }
    + * }).run();
    + * }
    + * + *

    Notes and cautions on use: + * + *

      + *
    • Every {@link #makeCurrent()} must be followed by a {@link Scope#close()}. Breaking these + * rules may lead to memory leaks and incorrect scoping. + *
    • While Context objects are immutable they do not place such a restriction on the state they + * store. + *
    • Context is not intended for passing optional parameters to an API and developers should + * take care to avoid excessive dependence on context when designing an API. + *
    • Attaching Context from a different ancestor will cause information in the current Context + * to be lost. This should generally be avoided. + *
    + * + *

    Context propagation is not trivial, and when done incorrectly can lead to broken traces or + * even mixed traces. We provide a debug mechanism for context propagation, which can be enabled by + * setting {@code -Dio.opentelemetry.context.enableStrictContext=true} in your JVM args. This will + * enable a strict checker that makes sure that {@link Scope}s are closed on the correct thread and + * that they are not garbage collected before being closed. This is done with some relatively + * expensive stack trace walking. It is highly recommended to enable this in unit tests and staging + * environments, and you may consider enabling it in production if you have the CPU budget or have + * very strict requirements on context being propagated correctly (i.e., because you use context in + * a multi-tenant system). For kotlin coroutine users, this will also detect invalid usage of {@link + * #makeCurrent()} from coroutines and suspending functions. This detection relies on internal APIs + * of kotlin coroutines and may not function across all versions - let us know if you find a version + * of kotlin coroutines where this mechanism does not function. + * + * @see StrictContextStorage + */ +public interface Context { + + /** Return the context associated with the current {@link Scope}. */ + static Context current() { + Context current = ContextStorage.get().current(); + return current != null ? current : root(); + } + + /** + * Returns the root {@link Context} which all other {@link Context} are derived from. + * + *

    It should generally not be required to use the root {@link Context} directly - instead, use + * {@link Context#current()} to operate on the current {@link Context}. Only use this method if + * you are absolutely sure you need to disregard the current {@link Context} - this almost always + * is only a workaround hiding an underlying context propagation issue. + */ + static Context root() { + return ArrayBasedContext.root(); + } + + /** + * Returns an {@link Executor} which delegates to the provided {@code executor}, wrapping all + * invocations of {@link Executor#execute(Runnable)} with the {@linkplain Context#current() + * current context} at the time of invocation. + * + *

    This is generally used to create an {@link Executor} which will forward the {@link Context} + * during an invocation to another thread. For example, you may use something like {@code Executor + * dbExecutor = Context.wrapTasks(threadPool)} to ensure calls like {@code dbExecutor.execute(() + * -> database.query())} have {@link Context} available on the thread executing database queries. + * + * @since 1.1.0 + */ + static Executor taskWrapping(Executor executor) { + return command -> executor.execute(Context.current().wrap(command)); + } + + /** + * Returns an {@link ExecutorService} which delegates to the provided {@code executorService}, + * wrapping all invocations of {@link ExecutorService} methods such as {@link + * ExecutorService#execute(Runnable)} or {@link ExecutorService#submit(Runnable)} with the + * {@linkplain Context#current() current context} at the time of invocation. + * + *

    This is generally used to create an {@link ExecutorService} which will forward the {@link + * Context} during an invocation to another thread. For example, you may use something like {@code + * ExecutorService dbExecutor = Context.wrapTasks(threadPool)} to ensure calls like {@code + * dbExecutor.execute(() -> database.query())} have {@link Context} available on the thread + * executing database queries. + * + * @since 1.1.0 + */ + static ExecutorService taskWrapping(ExecutorService executorService) { + return new CurrentContextExecutorService(executorService); + } + + /** + * Returns the value stored in this {@link Context} for the given {@link ContextKey}, or {@code + * null} if there is no value for the key in this context. + */ + @Nullable + V get(ContextKey key); + + /** + * Returns a new context with the given key value set. + * + *

    {@code
    +   * Context withCredential = Context.current().with(CRED_KEY, cred);
    +   * withCredential.wrap(new Runnable() {
    +   *   public void run() {
    +   *      readUserRecords(userId, CRED_KEY.get());
    +   *   }
    +   * }).run();
    +   * }
    + * + *

    Note that multiple calls to {@link #with(ContextKey, Object)} can be chained together. + * + *

    {@code
    +   * context.with(K1, V1).with(K2, V2);
    +   * }
    + * + *

    Nonetheless, {@link Context} should not be treated like a general purpose map with a large + * number of keys and values — combine multiple related items together into a single key instead + * of separating them. But if the items are unrelated, have separate keys for them. + */ + Context with(ContextKey k1, V v1); + + /** Returns a new {@link Context} with the given {@link ImplicitContextKeyed} set. */ + default Context with(ImplicitContextKeyed value) { + return value.storeInContext(this); + } + + /** + * Makes this the {@linkplain Context#current() current context} and returns a {@link Scope} which + * corresponds to the scope of execution this context is current for. {@link Context#current()} + * will return this {@link Context} until {@link Scope#close()} is called. {@link Scope#close()} + * must be called to properly restore the previous context from before this scope of execution or + * context will not work correctly. It is recommended to use try-with-resources to call {@link + * Scope#close()} automatically. + * + *

    The default implementation of this method will store the {@link Context} in a {@link + * ThreadLocal}. Kotlin coroutine users SHOULD NOT use this method as the {@link ThreadLocal} will + * not be properly synced across coroutine suspension and resumption. Instead, use {@code + * withContext(context.asContextElement())} provided by the {@code opentelemetry-extension-kotlin} + * library. + * + *

    {@code
    +   * Context prevCtx = Context.current();
    +   * try (Scope ignored = ctx.makeCurrent()) {
    +   *   assert Context.current() == ctx;
    +   *   ...
    +   * }
    +   * assert Context.current() == prevCtx;
    +   * }
    + */ + @MustBeClosed + default Scope makeCurrent() { + return ContextStorage.get().attach(this); + } + + /** + * Returns a {@link Runnable} that makes this the {@linkplain Context#current() current context} + * and then invokes the input {@link Runnable}. + */ + default Runnable wrap(Runnable runnable) { + return () -> { + try (Scope ignored = makeCurrent()) { + runnable.run(); + } + }; + } + + /** + * Returns a {@link Runnable} that makes this the {@linkplain Context#current() current context} + * and then invokes the input {@link Runnable}. + */ + default Callable wrap(Callable callable) { + return () -> { + try (Scope ignored = makeCurrent()) { + return callable.call(); + } + }; + } + + /** + * Returns an {@link Executor} that will execute callbacks in the given {@code executor}, making + * this the {@linkplain Context#current() current context} before each execution. + */ + default Executor wrap(Executor executor) { + return command -> executor.execute(wrap(command)); + } + + /** + * Returns an {@link ExecutorService} that will execute callbacks in the given {@code executor}, + * making this the {@linkplain Context#current() current context} before each execution. + */ + default ExecutorService wrap(ExecutorService executor) { + return new ContextExecutorService(this, executor); + } + + /** + * Returns an {@link ScheduledExecutorService} that will execute callbacks in the given {@code + * executor}, making this the {@linkplain Context#current() current context} before each + * execution. + */ + default ScheduledExecutorService wrap(ScheduledExecutorService executor) { + return new ContextScheduledExecutorService(this, executor); + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextExecutorService.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextExecutorService.java new file mode 100644 index 000000000..7b9153d33 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextExecutorService.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class ContextExecutorService extends ForwardingExecutorService { + + private final Context context; + + ContextExecutorService(Context context, ExecutorService delegate) { + super(delegate); + this.context = context; + } + + final Context context() { + return context; + } + + @Override + public Future submit(Callable task) { + return delegate().submit(context.wrap(task)); + } + + @Override + public Future submit(Runnable task, T result) { + return delegate().submit(context.wrap(task), result); + } + + @Override + public Future submit(Runnable task) { + return delegate().submit(context.wrap(task)); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return delegate().invokeAll(wrap(context, tasks)); + } + + @Override + public List> invokeAll( + Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return delegate().invokeAll(wrap(context, tasks), timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + return delegate().invokeAny(wrap(context, tasks)); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return delegate().invokeAny(wrap(context, tasks), timeout, unit); + } + + @Override + public void execute(Runnable command) { + delegate().execute(context.wrap(command)); + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextKey.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextKey.java new file mode 100644 index 000000000..0b7aaf462 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextKey.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +/** + * Key for indexing values of type {@link T} stored in a {@link Context}. {@link ContextKey} are + * compared by reference, so it is expected that only one {@link ContextKey} is created for a + * particular type of context value. + * + *
    {@code
    + * public class ContextUser {
    + *
    + *   private static final ContextKey KEY = ContextKey.named("MyState");
    + *
    + *   public Context startWork() {
    + *     return Context.withValues(KEY, new MyState());
    + *   }
    + *
    + *   public void continueWork(Context context) {
    + *     MyState state = context.get(KEY);
    + *     // Keys are compared by reference only.
    + *     assert state != Context.current().get(ContextKey.named("MyState"));
    + *     ...
    + *   }
    + * }
    + *
    + * }
    + */ +// ErrorProne false positive, this is used for its type constraint, not only as a bag of statics. +@SuppressWarnings("InterfaceWithOnlyStatics") +public interface ContextKey { + + /** + * Returns a new {@link ContextKey} with the given debug name. The name does not impact behavior + * and is only for debugging purposes. Multiple different keys with the same name will be separate + * keys. + */ + static ContextKey named(String name) { + return new DefaultContextKey<>(name); + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextScheduledExecutorService.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextScheduledExecutorService.java new file mode 100644 index 000000000..a17ed8973 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextScheduledExecutorService.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +class ContextScheduledExecutorService extends ContextExecutorService + implements ScheduledExecutorService { + + ContextScheduledExecutorService(Context context, ScheduledExecutorService delegate) { + super(context, delegate); + } + + @Override + ScheduledExecutorService delegate() { + return (ScheduledExecutorService) super.delegate(); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + return delegate().schedule(context().wrap(command), delay, unit); + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + return delegate().schedule(context().wrap(callable), delay, unit); + } + + @Override + public ScheduledFuture scheduleAtFixedRate( + Runnable command, long initialDelay, long period, TimeUnit unit) { + return delegate().scheduleAtFixedRate(context().wrap(command), initialDelay, period, unit); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay( + Runnable command, long initialDelay, long delay, TimeUnit unit) { + return delegate().scheduleWithFixedDelay(context().wrap(command), initialDelay, delay, unit); + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorage.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorage.java new file mode 100644 index 000000000..f9e94d2d7 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorage.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2020 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.context; + +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * The storage for storing and retrieving the current {@link Context}. + * + *

    If you want to implement your own storage or add some hooks when a {@link Context} is attached + * and restored, you should use {@link ContextStorageProvider}. Here's an example that sets MDC + * before {@link Context} is attached: + * + *

    {@code
    + * > public class MyStorage implements ContextStorageProvider {
    + * >
    + * >   @Override
    + * >   public ContextStorage get() {
    + * >     ContextStorage threadLocalStorage = ContextStorage.defaultStorage();
    + * >     return new RequestContextStorage() {
    + * >       @Override
    + * >       public Scope T attach(Context toAttach) {
    + * >         Context current = current();
    + * >         setMdc(toAttach);
    + * >         Scope scope = threadLocalStorage.attach(toAttach);
    + * >         return () -> {
    + * >           clearMdc();
    + * >           setMdc(current);
    + * >           scope.close();
    + * >         }
    + * >       }
    + * >
    + * >       @Override
    + * >       public Context current() {
    + * >         return threadLocalStorage.current();
    + * >       }
    + * >     }
    + * >   }
    + * > }
    + * }
    + */ +public interface ContextStorage { + + /** + * Returns the {@link ContextStorage} being used by this application. This is only for use when + * integrating with other context propagation mechanisms and not meant for direct use. To attach + * or detach a {@link Context} in an application, use {@link Context#makeCurrent()} and {@link + * Scope#close()}. + */ + static ContextStorage get() { + return LazyStorage.get(); + } + + /** + * Returns the default {@link ContextStorage} which stores {@link Context} using a threadlocal. + */ + static ContextStorage defaultStorage() { + return ThreadLocalContextStorage.INSTANCE; + } + + /** + * Adds the {@code wrapper}, which will be executed with the {@link ContextStorage} is first used, + * i.e., by calling {@link Context#makeCurrent()}. This must be called as early in your + * application as possible to have effect, often as part of a static initialization block in your + * main class. + */ + static void addWrapper(Function wrapper) { + ContextStorageWrappers.addWrapper(wrapper); + } + + /** + * Sets the specified {@link Context} as the current {@link Context} and returns a {@link Scope} + * representing the scope of execution. {@link Scope#close()} must be called when the current + * {@link Context} should be restored to what it was before attaching {@code toAttach}. + */ + Scope attach(Context toAttach); + + /** + * Returns the current {@link Context}. If no {@link Context} has been attached yet, this will + * return {@code null}. + */ + @Nullable + Context current(); +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorageProvider.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorageProvider.java new file mode 100644 index 000000000..1c77bae0c --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorageProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import java.util.concurrent.Executor; + +/** + * A Java SPI (Service Provider Interface) to allow replacing the default {@link ContextStorage}. + * This can be useful if, for example, you want to store OpenTelemetry {@link Context} in another + * context propagation system. For example, the returned {@link ContextStorage} could delegate to + * methods in + * + *

    {@code + * com.linecorp.armeria.common.RequestContext}, {@code + * io.grpc.context.Context}, or {@code + * org.eclipse.microprofile.context.ThreadContext} + * + *

    if you are already using one of those systems in your application. Then you would not have to + * use methods like {@link Context#wrap(Executor)} and can use your current system instead. + */ +public interface ContextStorageProvider { + + /** Returns the {@link ContextStorage} to use to store {@link Context}. */ + ContextStorage get(); +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorageWrappers.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorageWrappers.java new file mode 100644 index 000000000..9f8eb3276 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ContextStorageWrappers.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Holder of functions to wrap the used {@link ContextStorage}. Separate class from {@link + * LazyStorage} to allow registering wrappers before initializing storage. + */ +final class ContextStorageWrappers { + + private static final Logger log = Logger.getLogger(ContextStorageWrappers.class.getName()); + + private static boolean storageInitialized; + + private static final List> wrappers = + new ArrayList<>(); + + private static final Object mutex = new Object(); + + static void addWrapper(Function wrapper) { + synchronized (mutex) { + if (storageInitialized) { + log.log( + Level.FINE, + "ContextStorage has already been initialized, ignoring call to add wrapper.", + new Throwable()); + return; + } + wrappers.add(wrapper); + } + } + + static List> getWrappers() { + synchronized (mutex) { + return wrappers; + } + } + + static void setStorageInitialized() { + synchronized (mutex) { + storageInitialized = true; + } + } + + private ContextStorageWrappers() {} +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/CurrentContextExecutorService.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/CurrentContextExecutorService.java new file mode 100644 index 000000000..346b84987 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/CurrentContextExecutorService.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +final class CurrentContextExecutorService extends ForwardingExecutorService { + + CurrentContextExecutorService(ExecutorService delegate) { + super(delegate); + } + + @Override + public Future submit(Callable task) { + return delegate().submit(Context.current().wrap(task)); + } + + @Override + public Future submit(Runnable task, T result) { + return delegate().submit(Context.current().wrap(task), result); + } + + @Override + public Future submit(Runnable task) { + return delegate().submit(Context.current().wrap(task)); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return delegate().invokeAll(wrap(Context.current(), tasks)); + } + + @Override + public List> invokeAll( + Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return delegate().invokeAll(wrap(Context.current(), tasks), timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + return delegate().invokeAny(wrap(Context.current(), tasks)); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return delegate().invokeAny(wrap(Context.current(), tasks), timeout, unit); + } + + @Override + public void execute(Runnable command) { + delegate().execute(Context.current().wrap(command)); + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/DefaultContextKey.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/DefaultContextKey.java new file mode 100644 index 000000000..d5e84cb56 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/DefaultContextKey.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +final class DefaultContextKey implements ContextKey { + + private final String name; + + DefaultContextKey(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ForwardingExecutorService.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ForwardingExecutorService.java new file mode 100644 index 000000000..b035a1bf5 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ForwardingExecutorService.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +/** A {@link ExecutorService} that implements methods that don't need {@link Context}. */ +abstract class ForwardingExecutorService implements ExecutorService { + + private final ExecutorService delegate; + + protected ForwardingExecutorService(ExecutorService delegate) { + this.delegate = delegate; + } + + ExecutorService delegate() { + return delegate; + } + + @Override + public final void shutdown() { + delegate.shutdown(); + } + + @Override + public final List shutdownNow() { + return delegate.shutdownNow(); + } + + @Override + public final boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public final boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public final boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + protected static Collection> wrap( + Context context, Collection> tasks) { + List> wrapped = new ArrayList<>(); + for (Callable task : tasks) { + wrapped.add(context.wrap(task)); + } + return wrapped; + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ImplicitContextKeyed.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ImplicitContextKeyed.java new file mode 100644 index 000000000..48eccfd77 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ImplicitContextKeyed.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import com.google.errorprone.annotations.MustBeClosed; + +/** + * A value that can be stored inside {@link Context}. Types will generally use this interface to + * allow storing themselves in {@link Context} without exposing a {@link ContextKey}. + */ +public interface ImplicitContextKeyed { + + /** + * Adds this {@link ImplicitContextKeyed} value to the {@link Context#current() current context} + * and makes the new {@link Context} the current context. {@link Scope#close()} must be called to + * properly restore the previous context from before this scope of execution or context will not + * work correctly. It is recommended to use try-with-resources to call {@link Scope#close()} + * automatically. + * + *

    This method is equivalent to {@code Context.current().with(value).makeCurrent()}. + * + *

    The default implementation of this method will store the {@link ImplicitContextKeyed} in a + * {@link ThreadLocal}. Kotlin coroutine users SHOULD NOT use this method as the {@link + * ThreadLocal} will not be properly synced across coroutine suspension and resumption. Instead, + * use {@code withContext(value.asContextElement())} provided by the {@code + * opentelemetry-extension-kotlin} library. + */ + @MustBeClosed + default Scope makeCurrent() { + return Context.current().with(this).makeCurrent(); + } + + /** + * Returns a new {@link Context} created by setting {@code this} into the provided {@link + * Context}. It is generally recommended to call {@link Context#with(ImplicitContextKeyed)} + * instead of this method. The following are equivalent. + * + *

      + *
    • {@code context.with(myContextValue)} + *
    • {@code myContextValue.storeInContext(context)} + *
    + */ + Context storeInContext(Context context); +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/LazyStorage.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/LazyStorage.java new file mode 100644 index 000000000..fa5e8e822 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/LazyStorage.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2015 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Copyright 2020 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.context; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +// Lazy-loaded storage. Delaying storage initialization until after class initialization makes it +// much easier to avoid circular loading since there can still be references to Context as long as +// they don't depend on storage, like key() and currentContextExecutor(). It also makes it easier +// to handle exceptions. +final class LazyStorage { + + // Used by auto-instrumentation agent. Check with auto-instrumentation before making changes to + // this method. + // + // Ideally auto-instrumentation would hijack the public ContextStorage.get() instead of this + // method, but auto-instrumentation also needs to inject its own implementation of ContextStorage + // into the class loader at the same time, which causes a problem because injecting a class into + // the class loader automatically resolves its super classes (interfaces), which in this case is + // ContextStorage, which would be the same class (interface) being instrumented at that time, + // which would lead to the JVM throwing a LinkageError "attempted duplicate interface definition" + static ContextStorage get() { + return storage; + } + + private static final String CONTEXT_STORAGE_PROVIDER_PROPERTY = + "io.opentelemetry.context.contextStorageProvider"; + private static final String ENFORCE_DEFAULT_STORAGE_VALUE = "default"; + + private static final String ENABLE_STRICT_CONTEXT_PROVIDER_PROPERTY = + "io.opentelemetry.context.enableStrictContext"; + + private static final Logger logger = Logger.getLogger(LazyStorage.class.getName()); + + private static final ContextStorage storage; + + static { + AtomicReference deferredStorageFailure = new AtomicReference<>(); + ContextStorage created = createStorage(deferredStorageFailure); + if (Boolean.getBoolean(ENABLE_STRICT_CONTEXT_PROVIDER_PROPERTY)) { + created = StrictContextStorage.create(created); + } + for (Function wrapper : + ContextStorageWrappers.getWrappers()) { + created = wrapper.apply(created); + } + storage = created; + ContextStorageWrappers.setStorageInitialized(); + Throwable failure = deferredStorageFailure.get(); + // Logging must happen after storage has been set, as loggers may use Context. + if (failure != null) { + logger.log( + Level.WARNING, "ContextStorageProvider initialized failed. Using default", failure); + } + } + + static ContextStorage createStorage(AtomicReference deferredStorageFailure) { + // Get the specified SPI implementation first here + final String providerClassName = System.getProperty(CONTEXT_STORAGE_PROVIDER_PROPERTY, ""); + // Allow user to enforce default ThreadLocalContextStorage + if (ENFORCE_DEFAULT_STORAGE_VALUE.equals(providerClassName)) { + return ContextStorage.defaultStorage(); + } + + List providers = new ArrayList<>(); + for (ContextStorageProvider provider : ServiceLoader.load(ContextStorageProvider.class)) { + if (provider + .getClass() + .getName() + .equals("io.opentelemetry.sdk.testing.context.SettableContextStorageProvider")) { + // Always use our testing helper context storage provider if it is on the classpath. + return provider.get(); + } + providers.add(provider); + } + + if (providers.isEmpty()) { + return ContextStorage.defaultStorage(); + } + + if (providerClassName.isEmpty()) { + if (providers.size() == 1) { + return providers.get(0).get(); + } + + deferredStorageFailure.set( + new IllegalStateException( + "Found multiple ContextStorageProvider. Set the " + + "io.opentelemetry.context.ContextStorageProvider property to the fully " + + "qualified class name of the provider to use. Falling back to default " + + "ContextStorage. Found providers: " + + providers)); + return ContextStorage.defaultStorage(); + } + + for (ContextStorageProvider provider : providers) { + if (provider.getClass().getName().equals(providerClassName)) { + return provider.get(); + } + } + + deferredStorageFailure.set( + new IllegalStateException( + "io.opentelemetry.context.ContextStorageProvider property set but no matching class " + + "could be found, requested: " + + providerClassName + + " but found providers: " + + providers)); + return ContextStorage.defaultStorage(); + } + + private LazyStorage() {} +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/Scope.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/Scope.java new file mode 100644 index 000000000..ca9ca673e --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/Scope.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import io.opentelemetry.context.ThreadLocalContextStorage.NoopScope; + +/** + * An {@link AutoCloseable} that represents a mounted context for a block of code. A failure to call + * {@link Scope#close()} will generally break tracing or cause memory leaks. It is recommended that + * you use this class with a {@code try-with-resources} block: + * + *
    {@code
    + * try (Scope ignored = span.makeCurrent()) {
    + *   ...
    + * }
    + * }
    + */ +public interface Scope extends AutoCloseable { + + /** + * Returns a {@link Scope} that does nothing. Represents attaching a {@link Context} when it is + * already attached. + */ + static Scope noop() { + return NoopScope.INSTANCE; + } + + @Override + void close(); +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/StrictContextStorage.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/StrictContextStorage.java new file mode 100644 index 000000000..87080f35a --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/StrictContextStorage.java @@ -0,0 +1,290 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2013-2020 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package io.opentelemetry.context; + +import static java.lang.Thread.currentThread; + +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * A {@link ContextStorage} which keeps track of opened and closed {@link Scope}s, reporting caller + * information if a {@link Scope} is closed incorrectly or not at all. + * + *

    Calling {@link StrictContextStorage#close()} will check at the moment it's called whether + * there are any scopes that have been opened but not closed yet. This could be called at the end of + * a unit test to ensure the tested code cleaned up scopes correctly. + */ +final class StrictContextStorage implements ContextStorage, AutoCloseable { + + /** + * Returns a new {@link StrictContextStorage} which delegates to the provided {@link + * ContextStorage}, wrapping created scopes to track their usage. + */ + static StrictContextStorage create(ContextStorage delegate) { + return new StrictContextStorage(delegate); + } + + private static final Logger logger = Logger.getLogger(StrictContextStorage.class.getName()); + + private final ContextStorage delegate; + private final PendingScopes pendingScopes; + + private StrictContextStorage(ContextStorage delegate) { + this.delegate = delegate; + pendingScopes = PendingScopes.create(); + } + + @Override + public Scope attach(Context context) { + Scope scope = delegate.attach(context); + + CallerStackTrace caller = new CallerStackTrace(context); + StackTraceElement[] stackTrace = caller.getStackTrace(); + + // Detect invalid use from top-level kotlin coroutine. The stacktrace will have the order + // makeCurrent -> invokeSuspend -> resumeWith + for (int i = 0; i < stackTrace.length; i++) { + StackTraceElement element = stackTrace[i]; + if (element.getClassName().equals(Context.class.getName()) + && element.getMethodName().equals("makeCurrent")) { + if (i + 2 < stackTrace.length) { + StackTraceElement maybeResumptionElement = stackTrace[i + 2]; + if (maybeResumptionElement + .getClassName() + .equals("kotlin.coroutines.jvm.internal.BaseContinuationImpl") + && maybeResumptionElement.getMethodName().equals("resumeWith")) { + throw new AssertionError( + "Attempting to call Context.makeCurrent from inside a Kotlin coroutine. " + + "This is not allowed. Use Context.asContextElement provided by " + + "opentelemetry-extension-kotlin instead of makeCurrent."); + } + } + } + } + + // "new CallerStackTrace(context)" isn't the line we want to start the caller stack trace with + int i = 1; + + // This skips OpenTelemetry API and Context packages which will be at the top of the stack + // trace above the business logic call. + while (i < stackTrace.length) { + String className = stackTrace[i].getClassName(); + if (className.startsWith("io.opentelemetry.api.") + || className.startsWith( + "io.opentelemetry.sdk.testing.context.SettableContextStorageProvider") + || className.startsWith("io.opentelemetry.context.")) { + i++; + } else { + break; + } + } + int from = i; + + stackTrace = Arrays.copyOfRange(stackTrace, from, stackTrace.length); + caller.setStackTrace(stackTrace); + + return new StrictScope(scope, caller); + } + + @Override + @Nullable + public Context current() { + return delegate.current(); + } + + /** + * Ensures all scopes that have been created by this storage have been closed. This can be useful + * to call at the end of a test to make sure everything has been cleaned up. + * + *

    Note: It is important to close all resources prior to calling this, so that + * in-flight operations are not mistaken as scope leaks. If this raises an error, consider if a + * {@linkplain Context#wrap(Executor)} wrapped executor} is still running. + * + * @throws AssertionError if any scopes were left unclosed. + */ + // AssertionError to ensure test runners render the stack trace + @Override + public void close() { + pendingScopes.expungeStaleEntries(); + List leaked = pendingScopes.drainPendingCallers(); + if (!leaked.isEmpty()) { + if (leaked.size() > 1) { + logger.log(Level.SEVERE, "Multiple scopes leaked - first will be thrown as an error."); + for (CallerStackTrace caller : leaked) { + logger.log(Level.SEVERE, "Scope leaked", callerError(caller)); + } + } + throw callerError(leaked.get(0)); + } + } + + final class StrictScope implements Scope { + final Scope delegate; + final CallerStackTrace caller; + + StrictScope(Scope delegate, CallerStackTrace caller) { + this.delegate = delegate; + this.caller = caller; + pendingScopes.put(this, caller); + } + + @Override + public void close() { + caller.closed = true; + pendingScopes.remove(this); + + // Detect invalid use from Kotlin suspending function. For non top-level coroutines, we can + // only detect illegal usage on close, which will happen after the suspending function + // resumes and is decoupled from the caller. + // Illegal usage is close -> (optional closeFinally) -> "suspending function name" -> + // resumeWith. + StackTraceElement[] stackTrace = new Throwable().getStackTrace(); + for (int i = 0; i < stackTrace.length; i++) { + StackTraceElement element = stackTrace[i]; + if (element.getClassName().equals(StrictScope.class.getName()) + && element.getMethodName().equals("close")) { + int maybeResumeWithFrameIndex = i + 2; + if (i + 1 < stackTrace.length) { + StackTraceElement nextElement = stackTrace[i + 1]; + if (nextElement.getClassName().equals("kotlin.jdk7.AutoCloseableKt") + && nextElement.getMethodName().equals("closeFinally") + && i + 2 < stackTrace.length) { + // Skip extension method for AutoCloseable.use + maybeResumeWithFrameIndex = i + 3; + } + } + if (stackTrace[maybeResumeWithFrameIndex].getMethodName().equals("invokeSuspend")) { + // Skip synthetic invokeSuspend function. + // NB: The stacktrace showed in an IntelliJ debug pane does not show this. + maybeResumeWithFrameIndex++; + } + if (maybeResumeWithFrameIndex < stackTrace.length) { + StackTraceElement maybeResumptionElement = stackTrace[maybeResumeWithFrameIndex]; + if (maybeResumptionElement + .getClassName() + .equals("kotlin.coroutines.jvm.internal.BaseContinuationImpl") + && maybeResumptionElement.getMethodName().equals("resumeWith")) { + throw new AssertionError( + "Attempting to close a Scope created by Context.makeCurrent from inside a Kotlin " + + "coroutine. This is not allowed. Use Context.asContextElement provided by " + + "opentelemetry-extension-kotlin instead of makeCurrent."); + } + } + } + } + + if (currentThread().getId() != caller.threadId) { + throw new IllegalStateException( + String.format( + "Thread [%s] opened scope, but thread [%s] closed it", + caller.threadName, currentThread().getName()), + caller); + } + delegate.close(); + } + + @Override + public String toString() { + String message = caller.getMessage(); + return message != null ? message : super.toString(); + } + } + + static class CallerStackTrace extends Throwable { + + private static final long serialVersionUID = 783294061323215387L; + + final String threadName = currentThread().getName(); + final long threadId = currentThread().getId(); + final Context context; + + volatile boolean closed; + + CallerStackTrace(Context context) { + super("Thread [" + currentThread().getName() + "] opened scope for " + context + " here:"); + this.context = context; + } + } + + static class PendingScopes extends WeakConcurrentMap { + + static PendingScopes create() { + return new PendingScopes(new ConcurrentHashMap<>()); + } + + // We need to explicitly pass a map to the constructor because we otherwise cannot remove from + // it. https://github.com/raphw/weak-lock-free/pull/12 + private final ConcurrentHashMap, CallerStackTrace> map; + + @SuppressWarnings("ThreadPriorityCheck") + PendingScopes(ConcurrentHashMap, CallerStackTrace> map) { + super(/* cleanerThread= */ false, /* reuseKeys= */ false, map); + this.map = map; + // Start cleaner thread ourselves to make sure it runs after initializing our fields. + Thread thread = new Thread(this); + thread.setName("weak-ref-cleaner-strictcontextstorage"); + thread.setPriority(Thread.MIN_PRIORITY); + thread.setDaemon(true); + thread.start(); + } + + List drainPendingCallers() { + List pendingCallers = + map.values().stream().filter(caller -> !caller.closed).collect(Collectors.toList()); + map.clear(); + return pendingCallers; + } + + // Called by cleaner thread. + @Override + public void run() { + try { + while (!Thread.interrupted()) { + CallerStackTrace caller = map.remove(remove()); + if (caller != null && !caller.closed) { + logger.log( + Level.SEVERE, "Scope garbage collected before being closed.", callerError(caller)); + } + } + } catch (InterruptedException ignored) { + // do nothing + } + } + } + + static AssertionError callerError(CallerStackTrace caller) { + // Sometimes unit test runners truncate the cause of the exception. + // This flattens the exception as the caller of close() isn't important vs the one that leaked + AssertionError toThrow = + new AssertionError( + "Thread [" + caller.threadName + "] opened a scope of " + caller.context + " here:"); + toThrow.setStackTrace(caller.getStackTrace()); + return toThrow; + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ThreadLocalContextStorage.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ThreadLocalContextStorage.java new file mode 100644 index 000000000..92f24849a --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/ThreadLocalContextStorage.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +enum ThreadLocalContextStorage implements ContextStorage { + INSTANCE; + + private static final Logger logger = Logger.getLogger(ThreadLocalContextStorage.class.getName()); + + private static final ThreadLocal THREAD_LOCAL_STORAGE = new ThreadLocal<>(); + + @Override + public Scope attach(Context toAttach) { + if (toAttach == null) { + // Null context not allowed so ignore it. + return NoopScope.INSTANCE; + } + + Context beforeAttach = current(); + if (toAttach == beforeAttach) { + return NoopScope.INSTANCE; + } + + THREAD_LOCAL_STORAGE.set(toAttach); + + return () -> { + if (current() != toAttach) { + logger.log( + Level.FINE, + "Context in storage not the expected context, Scope.close was not called correctly"); + } + THREAD_LOCAL_STORAGE.set(beforeAttach); + }; + } + + @Override + @Nullable + public Context current() { + return THREAD_LOCAL_STORAGE.get(); + } + + enum NoopScope implements Scope { + INSTANCE; + + @Override + public void close() {} + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/AbstractWeakConcurrentMap.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/AbstractWeakConcurrentMap.java new file mode 100644 index 000000000..e7a2963f4 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/AbstractWeakConcurrentMap.java @@ -0,0 +1,371 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Rafael Winterhalter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Suppress warnings since this is vendored as-is. +// CHECKSTYLE:OFF + +package io.opentelemetry.context.internal.shaded; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A thread-safe map with weak keys. Entries are based on a key's system hash code and keys are + * considered equal only by reference equality. This class offers an abstract-base implementation + * that allows to override methods. This class does not implement the {@link Map} interface because + * this implementation is incompatible with the map contract. While iterating over a map's entries, + * any key that has not passed iteration is referenced non-weakly. + * + *

    This class has been copied as is from + * https://github.com/raphw/weak-lock-free/blob/ad0e5e0c04d4a31f9485bf12b89afbc9d75473b3/src/main/java/com/blogspot/mydailyjava/weaklockfree/WeakConcurrentMap.java + */ +// Suppress warnings since this is vendored as-is. +@SuppressWarnings({"MissingSummary", "EqualsBrokenForNull", "FieldMissingNullable"}) +abstract class AbstractWeakConcurrentMap extends ReferenceQueue + implements Runnable, Iterable> { + + final ConcurrentMap, V> target; + + protected AbstractWeakConcurrentMap() { + this(new ConcurrentHashMap, V>()); + } + + /** @param target ConcurrentMap implementation that this class wraps. */ + protected AbstractWeakConcurrentMap(ConcurrentMap, V> target) { + this.target = target; + } + + /** + * Override with care as it can cause lookup failures if done incorrectly. The result must have + * the same {@link Object#hashCode()} as the input and be {@link Object#equals(Object) equal to} a + * weak reference of the key. When overriding this, also override {@link #resetLookupKey}. + */ + protected abstract L getLookupKey(K key); + + /** Resets any reusable state in the {@linkplain #getLookupKey lookup key}. */ + protected abstract void resetLookupKey(L lookupKey); + + /** + * @param key The key of the entry. + * @return The value of the entry or the default value if it did not exist. + */ + public V get(K key) { + if (key == null) throw new NullPointerException(); + V value; + L lookupKey = getLookupKey(key); + try { + value = target.get(lookupKey); + } finally { + resetLookupKey(lookupKey); + } + if (value == null) { + value = defaultValue(key); + if (value != null) { + V previousValue = target.putIfAbsent(new WeakKey(key, this), value); + if (previousValue != null) { + value = previousValue; + } + } + } + return value; + } + + /** + * @param key The key of the entry. + * @return The value of the entry or null if it did not exist. + */ + public V getIfPresent(K key) { + if (key == null) throw new NullPointerException(); + L lookupKey = getLookupKey(key); + try { + return target.get(lookupKey); + } finally { + resetLookupKey(lookupKey); + } + } + + /** + * @param key The key of the entry. + * @return {@code true} if the key already defines a value. + */ + public boolean containsKey(K key) { + if (key == null) throw new NullPointerException(); + L lookupKey = getLookupKey(key); + try { + return target.containsKey(lookupKey); + } finally { + resetLookupKey(lookupKey); + } + } + + /** + * @param key The key of the entry. + * @param value The value of the entry. + * @return The previous entry or {@code null} if it does not exist. + */ + public V put(K key, V value) { + if (key == null || value == null) throw new NullPointerException(); + return target.put(new WeakKey(key, this), value); + } + + /** + * @param key The key of the entry. + * @param value The value of the entry. + * @return The previous entry or {@code null} if it does not exist. + */ + public V putIfAbsent(K key, V value) { + if (key == null || value == null) throw new NullPointerException(); + V previous; + L lookupKey = getLookupKey(key); + try { + previous = target.get(lookupKey); + } finally { + resetLookupKey(lookupKey); + } + return previous == null ? target.putIfAbsent(new WeakKey(key, this), value) : previous; + } + + /** + * @param key The key of the entry. + * @param value The value of the entry. + * @return The previous entry or {@code null} if it does not exist. + */ + public V putIfProbablyAbsent(K key, V value) { + if (key == null || value == null) throw new NullPointerException(); + return target.putIfAbsent(new WeakKey(key, this), value); + } + + /** + * @param key The key of the entry. + * @return The removed entry or {@code null} if it does not exist. + */ + public V remove(K key) { + if (key == null) throw new NullPointerException(); + L lookupKey = getLookupKey(key); + try { + return target.remove(lookupKey); + } finally { + resetLookupKey(lookupKey); + } + } + + /** Clears the entire map. */ + public void clear() { + target.clear(); + } + + /** + * Creates a default value. There is no guarantee that the requested value will be set as a once + * it is created in case that another thread requests a value for a key concurrently. + * + * @param key The key for which to create a default value. + * @return The default value for a key without value or {@code null} for not defining a default + * value. + */ + protected V defaultValue(K key) { + return null; + } + + /** Cleans all unused references. */ + public void expungeStaleEntries() { + Reference reference; + while ((reference = poll()) != null) { + target.remove(reference); + } + } + + /** + * Returns the approximate size of this map where the returned number is at least as big as the + * actual number of entries. + * + * @return The minimum size of this map. + */ + public int approximateSize() { + return target.size(); + } + + @Override + public void run() { + try { + while (!Thread.interrupted()) { + target.remove(remove()); + } + } catch (InterruptedException ignored) { + // do nothing + } + } + + @Override + public Iterator> iterator() { + return new EntryIterator(target.entrySet().iterator()); + } + + @Override + public String toString() { + return target.toString(); + } + + /* + * Why this works: + * --------------- + * + * Note that this map only supports reference equality for keys and uses system hash codes. Also, for the + * WeakKey instances to function correctly, we are voluntarily breaking the Java API contract for + * hashCode/equals of these instances. + * + * System hash codes are immutable and can therefore be computed prematurely and are stored explicitly + * within the WeakKey instances. This way, we always know the correct hash code of a key and always + * end up in the correct bucket of our target map. This remains true even after the weakly referenced + * key is collected. + * + * If we are looking up the value of the current key via WeakConcurrentMap::get or any other public + * API method, we know that any value associated with this key must still be in the map as the mere + * existence of this key makes it ineligible for garbage collection. Therefore, looking up a value + * using another WeakKey wrapper guarantees a correct result. + * + * If we are looking up the map entry of a WeakKey after polling it from the reference queue, we know + * that the actual key was already collected and calling WeakKey::get returns null for both the polled + * instance and the instance within the map. Since we explicitly stored the identity hash code for the + * referenced value, it is however trivial to identify the correct bucket. From this bucket, the first + * weak key with a null reference is removed. Due to hash collision, we do not know if this entry + * represents the weak key. However, we do know that the reference queue polls at least as many weak + * keys as there are stale map entries within the target map. If no key is ever removed from the map + * explicitly, the reference queue eventually polls exactly as many weak keys as there are stale entries. + * + * Therefore, we can guarantee that there is no memory leak. + * + * It is the responsibility of the actual map implementation to implement a lookup key that is used for + * lookups. The lookup key must supply the same semantics as the weak key with regards to hash code. + * The weak key invokes the latent key's equality method upon evaluation. + */ + + public static final class WeakKey extends WeakReference { + + private final int hashCode; + + WeakKey(K key, ReferenceQueue queue) { + super(key, queue); + hashCode = System.identityHashCode(key); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object other) { + if (other instanceof WeakKey) { + return ((WeakKey) other).get() == get(); + } else { + return other.equals(this); + } + } + + @Override + public String toString() { + return String.valueOf(get()); + } + } + + private class EntryIterator implements Iterator> { + + private final Iterator, V>> iterator; + + private Map.Entry, V> nextEntry; + + private K nextKey; + + private EntryIterator(Iterator, V>> iterator) { + this.iterator = iterator; + findNext(); + } + + private void findNext() { + while (iterator.hasNext()) { + nextEntry = iterator.next(); + nextKey = nextEntry.getKey().get(); + if (nextKey != null) { + return; + } + } + nextEntry = null; + nextKey = null; + } + + @Override + public boolean hasNext() { + return nextKey != null; + } + + @Override + public Map.Entry next() { + if (nextKey == null) { + throw new NoSuchElementException(); + } + try { + return new SimpleEntry(nextKey, nextEntry); + } finally { + findNext(); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private class SimpleEntry implements Map.Entry { + + private final K key; + + final Map.Entry, V> entry; + + private SimpleEntry(K key, Map.Entry, V> entry) { + this.key = key; + this.entry = entry; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return entry.getValue(); + } + + @Override + public V setValue(V value) { + if (value == null) throw new NullPointerException(); + return entry.setValue(value); + } + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/WeakConcurrentMap.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/WeakConcurrentMap.java new file mode 100644 index 000000000..cae64112e --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/WeakConcurrentMap.java @@ -0,0 +1,239 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Rafael Winterhalter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Suppress warnings since this is vendored as-is. +// CHECKSTYLE:OFF + +package io.opentelemetry.context.internal.shaded; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A thread-safe map with weak keys. Entries are based on a key's system hash code and keys are + * considered equal only by reference equality. This class does not implement the {@link + * java.util.Map} interface because this implementation is incompatible with the map contract. While + * iterating over a map's entries, any key that has not passed iteration is referenced non-weakly. + * + *

    This class has been copied as is from + * https://github.com/raphw/weak-lock-free/blob/ad0e5e0c04d4a31f9485bf12b89afbc9d75473b3/src/main/java/com/blogspot/mydailyjava/weaklockfree/WeakConcurrentMap.java + */ +// Suppress warnings since this is copied as-is. +@SuppressWarnings({ + "HashCodeToString", + "MissingSummary", + "UngroupedOverloads", + "ThreadPriorityCheck", + "FieldMissingNullable" +}) +public class WeakConcurrentMap + extends AbstractWeakConcurrentMap> { + + /** + * Lookup keys are cached thread-locally to avoid allocations on lookups. This is beneficial as + * the JIT unfortunately can't reliably replace the {@link LookupKey} allocation with stack + * allocations, even though the {@link LookupKey} does not escape. + */ + private static final ThreadLocal> LOOKUP_KEY_CACHE = + new ThreadLocal>() { + @Override + protected LookupKey initialValue() { + return new LookupKey(); + } + }; + + private static final AtomicLong ID = new AtomicLong(); + + private final Thread thread; + + private final boolean reuseKeys; + + /** @param cleanerThread {@code true} if a thread should be started that removes stale entries. */ + public WeakConcurrentMap(boolean cleanerThread) { + this(cleanerThread, isPersistentClassLoader(LookupKey.class.getClassLoader())); + } + + /** + * Checks whether the provided {@link ClassLoader} may be unloaded like a web application class + * loader, for example. + * + *

    If the class loader can't be unloaded, it is safe to use {@link ThreadLocal}s and to reuse + * the {@link LookupKey}. Otherwise, the use of {@link ThreadLocal}s may lead to class loader + * leaks as it prevents the class loader this class is loaded by to unload. + * + * @param classLoader The class loader to check. + * @return {@code true} if the provided class loader can be unloaded. + */ + private static boolean isPersistentClassLoader(ClassLoader classLoader) { + try { + return classLoader == null // bootstrap class loader + || classLoader == ClassLoader.getSystemClassLoader() + || classLoader + == ClassLoader.getSystemClassLoader().getParent(); // ext/platfrom class loader; + } catch (Throwable ignored) { + return false; + } + } + + /** + * @param cleanerThread {@code true} if a thread should be started that removes stale entries. + * @param reuseKeys {@code true} if the lookup keys should be reused via a {@link ThreadLocal}. + * Note that setting this to {@code true} may result in class loader leaks. See {@link + * #isPersistentClassLoader(ClassLoader)} for more details. + */ + public WeakConcurrentMap(boolean cleanerThread, boolean reuseKeys) { + this(cleanerThread, reuseKeys, new ConcurrentHashMap, V>()); + } + + /** + * @param cleanerThread {@code true} if a thread should be started that removes stale entries. + * @param reuseKeys {@code true} if the lookup keys should be reused via a {@link ThreadLocal}. + * Note that setting this to {@code true} may result in class loader leaks. See {@link + * #isPersistentClassLoader(ClassLoader)} for more details. + * @param target ConcurrentMap implementation that this class wraps. + */ + public WeakConcurrentMap( + boolean cleanerThread, boolean reuseKeys, ConcurrentMap, V> target) { + super(target); + this.reuseKeys = reuseKeys; + if (cleanerThread) { + thread = new Thread(this); + thread.setName("weak-ref-cleaner-" + ID.getAndIncrement()); + thread.setPriority(Thread.MIN_PRIORITY); + thread.setDaemon(true); + thread.start(); + } else { + thread = null; + } + } + + @Override + @SuppressWarnings("unchecked") + protected LookupKey getLookupKey(K key) { + LookupKey lookupKey; + if (reuseKeys) { + lookupKey = (LookupKey) LOOKUP_KEY_CACHE.get(); + } else { + lookupKey = new LookupKey(); + } + return lookupKey.withValue(key); + } + + @Override + protected void resetLookupKey(LookupKey lookupKey) { + lookupKey.reset(); + } + + /** @return The cleaner thread or {@code null} if no such thread was set. */ + public Thread getCleanerThread() { + return thread; + } + + /* + * A lookup key must only be used for looking up instances within a map. For this to work, it implements an identical contract for + * hash code and equals as the WeakKey implementation. At the same time, the lookup key implementation does not extend WeakReference + * and avoids the overhead that a weak reference implies. + */ + + // can't use AutoClosable/try-with-resources as this project still supports Java 6 + static final class LookupKey { + + private K key; + private int hashCode; + + LookupKey withValue(K key) { + this.key = key; + hashCode = System.identityHashCode(key); + return this; + } + + /** Failing to reset a lookup key can lead to memory leaks as the key is strongly referenced. */ + void reset() { + key = null; + hashCode = 0; + } + + @Override + public boolean equals(Object other) { + if (other instanceof WeakConcurrentMap.LookupKey) { + return ((LookupKey) other).key == key; + } else { + return ((WeakKey) other).get() == key; + } + } + + @Override + public int hashCode() { + return hashCode; + } + } + + /** + * A {@link WeakConcurrentMap} where stale entries are removed as a side effect of interacting + * with this map. + */ + public static class WithInlinedExpunction extends WeakConcurrentMap { + + public WithInlinedExpunction() { + super(false); + } + + @Override + public V get(K key) { + expungeStaleEntries(); + return super.get(key); + } + + @Override + public boolean containsKey(K key) { + expungeStaleEntries(); + return super.containsKey(key); + } + + @Override + public V put(K key, V value) { + expungeStaleEntries(); + return super.put(key, value); + } + + @Override + public V remove(K key) { + expungeStaleEntries(); + return super.remove(key); + } + + @Override + public Iterator> iterator() { + expungeStaleEntries(); + return super.iterator(); + } + + @Override + public int approximateSize() { + expungeStaleEntries(); + return super.approximateSize(); + } + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/package-info.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/package-info.java new file mode 100644 index 000000000..d6dd9ae3c --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/internal/shaded/package-info.java @@ -0,0 +1,7 @@ +/** + * Interfaces and implementations that are internal to OpenTelemetry. + * + *

    All the content under this package and its subpackages are considered not part of the public + * API, and must not be used by users of the OpenTelemetry library. + */ +package io.opentelemetry.context.internal.shaded; diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/package-info.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/package-info.java new file mode 100644 index 000000000..ab3ec8695 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/package-info.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * A context propagation mechanism which can carry scoped-values across API boundaries and between + * threads. + * + * @see io.opentelemetry.context.Context + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.context; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/ContextPropagators.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/ContextPropagators.java new file mode 100644 index 000000000..0e5de0717 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/ContextPropagators.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import static java.util.Objects.requireNonNull; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * A container of the registered propagators for every supported format. + * + *

    This container can be used to access a single, composite propagator for each supported format, + * which will be responsible for injecting and extracting data for each registered concern (traces, + * correlations, etc). Propagation will happen through {@link io.opentelemetry.context.Context}, + * from which values will be read upon injection, and which will store values from the extraction + * step. The resulting {@code Context} can then be used implicitly or explicitly by the + * OpenTelemetry API. + * + *

    Example of usage on the client: + * + *

    {@code
    + * private static final Tracer tracer = OpenTelemetry.getTracer();
    + * void onSendRequest() {
    + *   try (Scope ignored = span.makeCurrent()) {
    + *     ContextPropagators propagators = OpenTelemetry.getPropagators();
    + *     TextMapPropagator textMapPropagator = propagators.getTextMapPropagator();
    + *
    + *     // Inject the span's SpanContext and other available concerns (such as correlations)
    + *     // contained in the specified Context.
    + *     Map map = new HashMap<>();
    + *     textMapPropagator.inject(Context.current(), map, new Setter() {
    + *       public void put(Map map, String key, String value) {
    + *         map.put(key, value);
    + *       }
    + *     });
    + *     // Send the request including the text map and wait for the response.
    + *   }
    + * }
    + * }
    + * + *

    Example of usage in the server: + * + *

    {@code
    + * private static final Tracer tracer = OpenTelemetry.getTracer();
    + * void onRequestReceived() {
    + *   ContextPropagators propagators = OpenTelemetry.getPropagators();
    + *   TextMapPropagator textMapPropagator = propagators.getTextMapPropagator();
    + *
    + *   // Extract and store the propagated span's SpanContext and other available concerns
    + *   // in the specified Context.
    + *   Context context = textMapPropagator.extract(Context.current(), request,
    + *     new Getter() {
    + *       public String get(Object request, String key) {
    + *         // Return the value associated to the key, if available.
    + *       }
    + *     }
    + *   );
    + *   Span span = tracer.spanBuilder("MyRequest")
    + *       .setParent(context)
    + *       .setSpanKind(SpanKind.SERVER).startSpan();
    + *   try (Scope ignored = span.makeCurrent()) {
    + *     // Handle request and send response back.
    + *   } finally {
    + *     span.end();
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface ContextPropagators { + + /** + * Returns a {@link ContextPropagators} which can be used to extract and inject context in text + * payloads with the given {@link TextMapPropagator}. Use {@link + * TextMapPropagator#composite(TextMapPropagator...)} to register multiple propagators, which will + * all be executed when extracting or injecting. + * + *
    {@code
    +   * ContextPropagators propagators = ContextPropagators.create(
    +   *   TextMapPropagator.composite(
    +   *     HttpTraceContext.getInstance(),
    +   *     W3CBaggagePropagator.getInstance(),
    +   *     new MyCustomContextPropagator()));
    +   * }
    + */ + static ContextPropagators create(TextMapPropagator textPropagator) { + requireNonNull(textPropagator, "textPropagator"); + return new DefaultContextPropagators(textPropagator); + } + + /** Returns a {@link ContextPropagators} which performs no injection or extraction. */ + static ContextPropagators noop() { + return DefaultContextPropagators.noop(); + } + + /** + * Returns a {@link TextMapPropagator} propagator. + * + *

    The returned value will be a composite instance containing all the registered {@link + * TextMapPropagator} propagators. If none is registered, the returned value will be a no-op + * instance. + * + * @return the {@link TextMapPropagator} propagator to inject and extract data. + */ + TextMapPropagator getTextMapPropagator(); +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/DefaultContextPropagators.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/DefaultContextPropagators.java new file mode 100644 index 000000000..28e41ed98 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/DefaultContextPropagators.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +/** + * {@code DefaultContextPropagators} is the default, built-in implementation of {@link + * ContextPropagators}. + * + *

    All the registered propagators are stored internally as a simple list, and are invoked + * synchronically upon injection and extraction. + * + *

    The propagation fields retrieved from all registered propagators are de-duplicated. + */ +final class DefaultContextPropagators implements ContextPropagators { + + private static final ContextPropagators NOOP = + new DefaultContextPropagators(NoopTextMapPropagator.getInstance()); + + static ContextPropagators noop() { + return NOOP; + } + + private final TextMapPropagator textMapPropagator; + + @Override + public TextMapPropagator getTextMapPropagator() { + return textMapPropagator; + } + + DefaultContextPropagators(TextMapPropagator textMapPropagator) { + this.textMapPropagator = textMapPropagator; + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/MultiTextMapPropagator.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/MultiTextMapPropagator.java new file mode 100644 index 000000000..ce2cecb16 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/MultiTextMapPropagator.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import io.opentelemetry.context.Context; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +final class MultiTextMapPropagator implements TextMapPropagator { + private final TextMapPropagator[] textPropagators; + private final Collection allFields; + + MultiTextMapPropagator(TextMapPropagator... textPropagators) { + this(Arrays.asList(textPropagators)); + } + + MultiTextMapPropagator(List textPropagators) { + this.textPropagators = new TextMapPropagator[textPropagators.size()]; + textPropagators.toArray(this.textPropagators); + this.allFields = Collections.unmodifiableList(getAllFields(this.textPropagators)); + } + + @Override + public Collection fields() { + return allFields; + } + + private static List getAllFields(TextMapPropagator[] textPropagators) { + Set fields = new LinkedHashSet<>(); + for (TextMapPropagator textPropagator : textPropagators) { + fields.addAll(textPropagator.fields()); + } + + return new ArrayList<>(fields); + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null || setter == null) { + return; + } + for (TextMapPropagator textPropagator : textPropagators) { + textPropagator.inject(context, carrier, setter); + } + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Context.root(); + } + if (getter == null) { + return context; + } + for (TextMapPropagator textPropagator : textPropagators) { + context = textPropagator.extract(context, carrier, getter); + } + return context; + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/NoopTextMapPropagator.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/NoopTextMapPropagator.java new file mode 100644 index 000000000..42117345f --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/NoopTextMapPropagator.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import io.opentelemetry.context.Context; +import java.util.Collection; +import java.util.Collections; +import javax.annotation.Nullable; + +final class NoopTextMapPropagator implements TextMapPropagator { + private static final NoopTextMapPropagator INSTANCE = new NoopTextMapPropagator(); + + static TextMapPropagator getInstance() { + return INSTANCE; + } + + @Override + public Collection fields() { + return Collections.emptyList(); + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) {} + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Context.root(); + } + return context; + } +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapGetter.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapGetter.java new file mode 100644 index 000000000..f160b7857 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapGetter.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import javax.annotation.Nullable; + +/** + * Interface that allows a {@code TextMapPropagator} to read propagated fields from a carrier. + * + *

    {@code Getter} is stateless and allows to be saved as a constant to avoid runtime allocations. + * + * @param carrier of propagation fields, such as an http request. + */ +public interface TextMapGetter { + + /** + * Returns all the keys in the given carrier. + * + * @param carrier carrier of propagation fields, such as an http request. + * @since 0.10.0 + */ + Iterable keys(C carrier); + + /** + * 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}. + */ + @Nullable + String get(@Nullable C carrier, String key); +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapPropagator.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapPropagator.java new file mode 100644 index 000000000..8505d9a49 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapPropagator.java @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import io.opentelemetry.context.Context; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Injects and extracts a value as text into carriers that travel in-band across process boundaries. + * Encoding is expected to conform to the HTTP Header Field semantics. Values are often encoded as + * RPC/HTTP request headers. + * + *

    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 values and the server-side extracts them. + * + *

    Specific concern values (traces, correlations, etc) will be read from the specified {@code + * Context}, and resulting values will be stored in a new {@code Context} upon extraction. It is + * recommended to use a single {@code Context.Key} to store the entire concern data: + * + *

    {@code
    + * public static final Context.Key CONCERN_KEY = Context.key("my-concern-key");
    + * public MyConcernPropagator implements TextMapPropagator {
    + *   public  void inject(Context context, C carrier, Setter setter) {
    + *     Object concern = CONCERN_KEY.get(context);
    + *     // Use concern in the specified context to propagate data.
    + *   }
    + *   public  Context extract(Context context, C carrier, Getter getter) {
    + *     // Use getter to get the data from the carrier.
    + *     return context.withValue(CONCERN_KEY, concern);
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public interface TextMapPropagator { + + /** + * Returns a {@link TextMapPropagator} which simply delegates injection and extraction to the + * provided propagators. + * + *

    Invocation order of {@code TextMapPropagator#inject()} and {@code + * TextMapPropagator#extract()} for registered trace propagators is undefined. + */ + static TextMapPropagator composite(TextMapPropagator... propagators) { + return composite(Arrays.asList(propagators)); + } + + /** + * Returns a {@link TextMapPropagator} which simply delegates injection and extraction to the + * provided propagators. + * + *

    Invocation order of {@code TextMapPropagator#inject()} and {@code + * TextMapPropagator#extract()} for registered trace propagators is undefined. + */ + static TextMapPropagator composite(Iterable propagators) { + List propagatorsList = new ArrayList<>(); + for (TextMapPropagator propagator : propagators) { + propagatorsList.add(propagator); + } + if (propagatorsList.isEmpty()) { + return NoopTextMapPropagator.getInstance(); + } + if (propagatorsList.size() == 1) { + return propagatorsList.get(0); + } + return new MultiTextMapPropagator(propagatorsList); + } + + /** Returns a {@link TextMapPropagator} which does no injection or extraction. */ + static TextMapPropagator noop() { + return NoopTextMapPropagator.getInstance(); + } + + /** + * The propagation fields defined. If your carrier is reused, you should delete the fields here + * before calling {@link #inject(Context, Object, TextMapSetter)} )}. + * + *

    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. + * + *

    Some use cases for this are: + * + *

      + *
    • Allow pre-allocation of fields, especially in systems like gRPC Metadata + *
    • Allow a single-pass over an iterator + *
    + * + * @return the fields that will be used by this formatter. + */ + Collection fields(); + + /** + * Injects data for downstream consumers, for example as HTTP headers. The carrier may be null to + * facilitate calling this method with a lambda for the {@link TextMapSetter}, in which case that + * null will be passed to the {@link TextMapSetter} implementation. + * + * @param context the {@code Context} containing the value to be injected. + * @param carrier holds propagation fields. For example, an outgoing message or http request. + * @param setter invoked for each propagation key to add or remove. + * @param carrier of propagation fields, such as an http request + */ + void inject(Context context, @Nullable C carrier, TextMapSetter setter); + + /** + * Extracts data from upstream. For example, from incoming http headers. The returned Context + * should contain the extracted data, if any, merged with the data from the passed-in Context. + * + *

    If the incoming information could not be parsed, implementations MUST return the original + * Context, unaltered. + * + * @param context the {@code Context} used to store the extracted value. + * @param carrier holds propagation fields. For example, an outgoing message or http request. + * @param getter invoked for each propagation key to get data from the carrier. + * @param the type of carrier of the propagation fields, such as an http request. + * @return the {@code Context} containing the extracted data. + */ + Context extract(Context context, @Nullable C carrier, TextMapGetter getter); +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapSetter.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapSetter.java new file mode 100644 index 000000000..28168c959 --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/TextMapSetter.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import javax.annotation.Nullable; + +/** + * Class that allows a {@code TextMapPropagator} to set propagated fields into a carrier. + * + *

    {@code Setter} is stateless and allows to be saved as a constant to avoid runtime allocations. + * + * @param carrier of propagation fields, such as an http request + */ +public interface TextMapSetter { + + /** + * Replaces a propagated field with the given value. + * + *

    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. To + * facilitate implementations as java lambdas, this parameter may be null. + * @param key the key of the field. + * @param value the value of the field. + */ + void set(@Nullable C carrier, String key, String value); +} diff --git a/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/package-info.java b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/package-info.java new file mode 100644 index 000000000..70ba05efa --- /dev/null +++ b/opentelemetry-java/context/src/main/java/io/opentelemetry/context/propagation/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Interfaces for defining {@link io.opentelemetry.context.propagation.ContextPropagators} for + * allowing context propagation across process boundaries, for example when sending context to a + * remote server. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.context.propagation; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/context/src/otelAsBraveTest/java/io/opentelemetry/context/BraveContextStorageProvider.java b/opentelemetry-java/context/src/otelAsBraveTest/java/io/opentelemetry/context/BraveContextStorageProvider.java new file mode 100644 index 000000000..5f3dc54b8 --- /dev/null +++ b/opentelemetry-java/context/src/otelAsBraveTest/java/io/opentelemetry/context/BraveContextStorageProvider.java @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import brave.Tracing; +import brave.propagation.CurrentTraceContext; +import brave.propagation.TraceContext; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class BraveContextStorageProvider implements ContextStorageProvider { + + @Override + public ContextStorage get() { + return BraveContextStorage.INSTANCE; + } + + @SuppressWarnings("ReferenceEquality") + private enum BraveContextStorage implements ContextStorage { + INSTANCE; + + @Override + public Scope attach(Context toAttach) { + TraceContext braveContextToAttach = ((BraveContextWrapper) toAttach).braveContext; + + CurrentTraceContext currentTraceContext = Tracing.current().currentTraceContext(); + TraceContext currentBraveContext = currentTraceContext.get(); + if (currentBraveContext == braveContextToAttach) { + return Scope.noop(); + } + + CurrentTraceContext.Scope braveScope = currentTraceContext.newScope(braveContextToAttach); + return braveScope::close; + } + + @Override + public Context current() { + TraceContext current = Tracing.current().currentTraceContext().get(); + if (current != null) { + return new BraveContextWrapper(current); + } + return new BraveContextWrapper(TraceContext.newBuilder().traceId(1).spanId(1).build()); + } + } + + private static class BraveContextValues { + private final Object[] values; + + BraveContextValues(Object key, Object value) { + this.values = new Object[] {key, value}; + } + + BraveContextValues(Object[] values) { + this.values = values; + } + + Object getValue(Object key) { + for (int i = 0; i < values.length; i += 2) { + if (values[i] == key) { + return values[i + 1]; + } + } + return null; + } + + BraveContextValues with(Object key, Object value) { + final Object[] copy; + for (int i = 0; i < values.length; i += 2) { + if (values[i] == key) { + copy = values.clone(); + copy[i + 1] = value; + return new BraveContextValues(copy); + } + } + + copy = Arrays.copyOf(values, values.length + 2); + copy[values.length - 2] = key; + copy[values.length - 1] = value; + return new BraveContextValues(copy); + } + } + + private static class BraveContextWrapper implements Context { + + private final TraceContext braveContext; + + private BraveContextWrapper(TraceContext braveContext) { + this.braveContext = braveContext; + } + + @Override + public V get(ContextKey key) { + BraveContextValues values = braveContext.findExtra(BraveContextValues.class); + if (values == null) { + return null; + } + @SuppressWarnings("unchecked") + V value = (V) values.getValue(key); + return value; + } + + @Override + public Context with(ContextKey k1, V v1) { + List extras = braveContext.extra(); + BraveContextValues values = null; + int existingValuesIndex = -1; + for (int i = 0; i < extras.size(); i++) { + Object extra = extras.get(i); + if (extra instanceof BraveContextValues) { + values = (BraveContextValues) extra; + existingValuesIndex = i; + break; + } + } + final List newExtras; + if (values == null) { + values = new BraveContextValues(k1, v1); + newExtras = new ArrayList<>(extras.size() + 1); + newExtras.addAll(extras); + newExtras.add(values); + } else { + newExtras = new ArrayList<>(extras); + newExtras.set(existingValuesIndex, values.with(k1, v1)); + } + + TraceContext.Builder builder = braveContext.toBuilder(); + builder.clearExtra(); + newExtras.forEach(builder::addExtra); + return new BraveContextWrapper(builder.build()); + } + } +} diff --git a/opentelemetry-java/context/src/otelAsBraveTest/java/io/opentelemetry/context/OtelAsBraveTest.java b/opentelemetry-java/context/src/otelAsBraveTest/java/io/opentelemetry/context/OtelAsBraveTest.java new file mode 100644 index 000000000..6acad3d0e --- /dev/null +++ b/opentelemetry-java/context/src/otelAsBraveTest/java/io/opentelemetry/context/OtelAsBraveTest.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import brave.Tracing; +import brave.propagation.CurrentTraceContext; +import brave.propagation.TraceContext; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class OtelAsBraveTest { + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + + private static final Tracing TRACING = + Tracing.newBuilder().currentTraceContext(CurrentTraceContext.Default.create()).build(); + + private static final TraceContext TRACE_CONTEXT = + TraceContext.newBuilder().traceId(1).spanId(1).addExtra("japan").build(); + + private static ExecutorService otherThread; + + @BeforeAll + static void setUp() { + otherThread = Executors.newSingleThreadExecutor(); + } + + @AfterAll + static void tearDown() { + otherThread.shutdown(); + } + + @Test + void braveOtelMix() { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + TraceContext context2 = + Tracing.current().currentTraceContext().get().toBuilder().addExtra("cheese").build(); + try (CurrentTraceContext.Scope ignored3 = + TRACING.currentTraceContext().newScope(context2)) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("cheese"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } + } + } + } + + @Test + void braveWrap() throws Exception { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + AtomicReference braveContainsJapan = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + TraceContext traceContext = Tracing.current().currentTraceContext().get(); + if (traceContext != null && traceContext.extra().contains("japan")) { + braveContainsJapan.set(true); + } else { + braveContainsJapan.set(false); + } + otelValue.set(Context.current().get(ANIMAL)); + }; + otherThread.submit(runnable).get(); + assertThat(braveContainsJapan).hasValue(false); + assertThat(otelValue).hasValue(null); + + otherThread.submit(TRACING.currentTraceContext().wrap(runnable)).get(); + assertThat(braveContainsJapan).hasValue(true); + assertThat(otelValue).hasValue("cat"); + } + } + } + + @Test + void otelWrap() throws Exception { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + AtomicReference braveContainsJapan = new AtomicReference<>(false); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + TraceContext traceContext = Tracing.current().currentTraceContext().get(); + if (traceContext != null && traceContext.extra().contains("japan")) { + braveContainsJapan.set(true); + } else { + braveContainsJapan.set(false); + } + otelValue.set(Context.current().get(ANIMAL)); + }; + otherThread.submit(runnable).get(); + assertThat(braveContainsJapan).hasValue(false); + assertThat(otelValue).hasValue(null); + + otherThread.submit(Context.current().wrap(runnable)).get(); + assertThat(braveContainsJapan).hasValue(true); + assertThat(otelValue).hasValue("cat"); + } + } + } +} diff --git a/opentelemetry-java/context/src/otelAsBraveTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider b/opentelemetry-java/context/src/otelAsBraveTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider new file mode 100644 index 000000000..923d2cc2d --- /dev/null +++ b/opentelemetry-java/context/src/otelAsBraveTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider @@ -0,0 +1 @@ +io.opentelemetry.context.BraveContextStorageProvider \ No newline at end of file diff --git a/opentelemetry-java/context/src/otelInBraveTest/java/io/opentelemetry/context/BraveContextStorageProvider.java b/opentelemetry-java/context/src/otelInBraveTest/java/io/opentelemetry/context/BraveContextStorageProvider.java new file mode 100644 index 000000000..541875635 --- /dev/null +++ b/opentelemetry-java/context/src/otelInBraveTest/java/io/opentelemetry/context/BraveContextStorageProvider.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import brave.Tracing; +import brave.propagation.CurrentTraceContext; +import brave.propagation.TraceContext; +import java.util.List; +import javax.annotation.Nullable; + +public class BraveContextStorageProvider implements ContextStorageProvider { + + @Override + public ContextStorage get() { + return BraveContextStorage.INSTANCE; + } + + @SuppressWarnings("ReferenceEquality") + private enum BraveContextStorage implements ContextStorage { + INSTANCE; + + @Override + public Scope attach(Context toAttach) { + CurrentTraceContext currentTraceContext = Tracing.current().currentTraceContext(); + TraceContext currentBraveContext = currentTraceContext.get(); + + Context currentContext = fromBraveContext(currentBraveContext); + if (currentContext == toAttach) { + return Scope.noop(); + } + + TraceContext newBraveContext; + if (toAttach instanceof BraveContextWrapper) { + newBraveContext = ((BraveContextWrapper) toAttach).toBraveContext(); + } else { + newBraveContext = toBraveContext(currentBraveContext, toAttach); + } + + if (currentBraveContext == newBraveContext) { + return Scope.noop(); + } + CurrentTraceContext.Scope braveScope = currentTraceContext.newScope(newBraveContext); + return braveScope::close; + } + + @Override + public Context current() { + return new BraveContextWrapper(Tracing.current().currentTraceContext().get()); + } + } + + // Need to wrap the Context because brave findExtra searches for perfect match of the class. + static final class BraveContextWrapper implements Context { + @Nullable private final TraceContext baseBraveContext; + private final Context delegate; + + BraveContextWrapper(@Nullable TraceContext baseBraveContext) { + this(baseBraveContext, fromBraveContext(baseBraveContext)); + } + + BraveContextWrapper(@Nullable TraceContext baseBraveContext, Context delegate) { + this.baseBraveContext = baseBraveContext; + this.delegate = delegate; + } + + TraceContext toBraveContext() { + if (fromBraveContext(baseBraveContext) == delegate) { + return baseBraveContext; + } + return BraveContextStorageProvider.toBraveContext(baseBraveContext, delegate); + } + + @Nullable + @Override + public V get(ContextKey key) { + return delegate.get(key); + } + + @Override + public Context with(ContextKey k1, V v1) { + return new BraveContextWrapper(baseBraveContext, delegate.with(k1, v1)); + } + } + + static TraceContext toBraveContext(@Nullable TraceContext braveContext, Context context) { + TraceContext.Builder builder = + braveContext == null ? TraceContext.newBuilder() : braveContext.toBuilder(); + return builder.addExtra(new ContextWrapper(context)).build(); + } + + private static Context fromBraveContext(@Nullable TraceContext braveContext) { + if (braveContext == null) { + return Context.root(); + } + List extra = braveContext.extra(); + for (int i = extra.size() - 1; i >= 0; i--) { + Object nextExtra = extra.get(i); + if (nextExtra.getClass() == ContextWrapper.class) { + return ((ContextWrapper) nextExtra).context; + } + } + return Context.root(); + } + + private static final class ContextWrapper { + private final Context context; + + private ContextWrapper(Context context) { + this.context = context; + } + } +} diff --git a/opentelemetry-java/context/src/otelInBraveTest/java/io/opentelemetry/context/OtelInBraveTest.java b/opentelemetry-java/context/src/otelInBraveTest/java/io/opentelemetry/context/OtelInBraveTest.java new file mode 100644 index 000000000..72a8b402a --- /dev/null +++ b/opentelemetry-java/context/src/otelInBraveTest/java/io/opentelemetry/context/OtelInBraveTest.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import brave.Tracing; +import brave.propagation.CurrentTraceContext; +import brave.propagation.TraceContext; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class OtelInBraveTest { + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + private static final Context CONTEXT_WITH_ANIMAL = Context.root().with(ANIMAL, "japan"); + + private static final Tracing TRACING = + Tracing.newBuilder().currentTraceContext(CurrentTraceContext.Default.create()).build(); + private static final TraceContext TRACE_CONTEXT = + BraveContextStorageProvider.toBraveContext( + TraceContext.newBuilder().traceId(1).spanId(1).build(), CONTEXT_WITH_ANIMAL); + + private static ExecutorService otherThread; + + @BeforeAll + static void setUp() { + otherThread = Executors.newSingleThreadExecutor(); + } + + @AfterAll + static void tearDown() { + otherThread.shutdown(); + } + + @Test + void braveOtelMix() { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("japan"); + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + TraceContext context2 = + Tracing.current().currentTraceContext().get().toBuilder().addExtra("cheese").build(); + try (CurrentTraceContext.Scope ignored3 = + TRACING.currentTraceContext().newScope(context2)) { + assertThat(Tracing.current().currentTraceContext().get().extra()).contains("cheese"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } + } + } + } + + @Test + void braveWrap() throws Exception { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("japan"); + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + TraceContext context2 = + Tracing.current().currentTraceContext().get().toBuilder().addExtra("cheese").build(); + try (CurrentTraceContext.Scope ignored3 = + TRACING.currentTraceContext().newScope(context2)) { + AtomicReference braveContainsCheese = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + TraceContext traceContext = Tracing.current().currentTraceContext().get(); + if (traceContext != null && traceContext.extra().contains("cheese")) { + braveContainsCheese.set(true); + } else { + braveContainsCheese.set(false); + } + otelValue.set(Context.current().get(ANIMAL)); + }; + + otherThread.submit(runnable).get(); + assertThat(braveContainsCheese).hasValue(false); + assertThat(otelValue).hasValue(null); + + otherThread.submit(TRACING.currentTraceContext().wrap(runnable)).get(); + assertThat(braveContainsCheese).hasValue(true); + assertThat(otelValue).hasValue("cat"); + } + } + } + } + + @Test + void otelWrap() throws Exception { + try (CurrentTraceContext.Scope ignored = + TRACING.currentTraceContext().newScope(TRACE_CONTEXT)) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("japan"); + try (Scope ignored2 = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + TraceContext context2 = + Tracing.current().currentTraceContext().get().toBuilder().addExtra("cheese").build(); + try (CurrentTraceContext.Scope ignored3 = + TRACING.currentTraceContext().newScope(context2)) { + AtomicReference braveContainsCheese = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + TraceContext traceContext = Tracing.current().currentTraceContext().get(); + if (traceContext != null && traceContext.extra().contains("cheese")) { + braveContainsCheese.set(true); + } else { + braveContainsCheese.set(false); + } + otelValue.set(Context.current().get(ANIMAL)); + }; + + otherThread.submit(runnable).get(); + assertThat(braveContainsCheese).hasValue(false); + assertThat(otelValue).hasValue(null); + + Runnable task = Context.current().wrap(runnable); + otherThread.submit(task).get(); + assertThat(braveContainsCheese).hasValue(true); + assertThat(otelValue).hasValue("cat"); + } + } + } + } +} diff --git a/opentelemetry-java/context/src/otelInBraveTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider b/opentelemetry-java/context/src/otelInBraveTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider new file mode 100644 index 000000000..923d2cc2d --- /dev/null +++ b/opentelemetry-java/context/src/otelInBraveTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider @@ -0,0 +1 @@ +io.opentelemetry.context.BraveContextStorageProvider \ No newline at end of file diff --git a/opentelemetry-java/context/src/otelInGrpcTest/java/io/opentelemetry/context/GrpcContextStorageProvider.java b/opentelemetry-java/context/src/otelInGrpcTest/java/io/opentelemetry/context/GrpcContextStorageProvider.java new file mode 100644 index 000000000..bceada955 --- /dev/null +++ b/opentelemetry-java/context/src/otelInGrpcTest/java/io/opentelemetry/context/GrpcContextStorageProvider.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +public class GrpcContextStorageProvider implements ContextStorageProvider { + private static final io.grpc.Context.Key OTEL_CONTEXT = + io.grpc.Context.keyWithDefault("otel-context", Context.root()); + + @Override + public ContextStorage get() { + return GrpcContextStorage.INSTANCE; + } + + private enum GrpcContextStorage implements ContextStorage { + INSTANCE; + + @Override + public Scope attach(Context toAttach) { + io.grpc.Context grpcContext = io.grpc.Context.current(); + Context current = OTEL_CONTEXT.get(grpcContext); + + if (current == toAttach) { + return Scope.noop(); + } + + io.grpc.Context newGrpcContext; + if (toAttach instanceof GrpcContextWrapper) { + // This was already constructed with an embedded grpc Context. + newGrpcContext = ((GrpcContextWrapper) toAttach).toGrpcContext(); + } else { + newGrpcContext = grpcContext.withValue(OTEL_CONTEXT, toAttach); + } + + io.grpc.Context toRestore = newGrpcContext.attach(); + return () -> newGrpcContext.detach(toRestore); + } + + @Override + public Context current() { + // We return an object that embeds both the + io.grpc.Context grpcContext = io.grpc.Context.current(); + return GrpcContextWrapper.wrapperFromGrpc(grpcContext); + } + } + + private static class GrpcContextWrapper implements Context { + // If otel context changes the grpc Context may be out of sync. + // There are 2 options here: 1. always update the grpc Context, 2. update only when needed. + // Currently the second one is implemented. + private final io.grpc.Context baseGrpcContext; + private final Context context; + + private GrpcContextWrapper(io.grpc.Context grpcContext, Context context) { + this.baseGrpcContext = grpcContext; + this.context = context; + } + + private static GrpcContextWrapper wrapperFromGrpc(io.grpc.Context grpcContext) { + return new GrpcContextWrapper(grpcContext, OTEL_CONTEXT.get(grpcContext)); + } + + private io.grpc.Context toGrpcContext() { + if (OTEL_CONTEXT.get(baseGrpcContext) == context) { + // No changes to the wrapper + return baseGrpcContext; + } + return baseGrpcContext.withValue(OTEL_CONTEXT, context); + } + + @Override + public V get(ContextKey key) { + return context.get(key); + } + + @Override + public Context with(ContextKey k1, V v1) { + return new GrpcContextWrapper(baseGrpcContext, context.with(k1, v1)); + } + } +} diff --git a/opentelemetry-java/context/src/otelInGrpcTest/java/io/opentelemetry/context/OtelInGrpcTest.java b/opentelemetry-java/context/src/otelInGrpcTest/java/io/opentelemetry/context/OtelInGrpcTest.java new file mode 100644 index 000000000..6b5cf16da --- /dev/null +++ b/opentelemetry-java/context/src/otelInGrpcTest/java/io/opentelemetry/context/OtelInGrpcTest.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class OtelInGrpcTest { + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + + private static final io.grpc.Context.Key FOOD = io.grpc.Context.key("food"); + private static final io.grpc.Context.Key COUNTRY = io.grpc.Context.key("country"); + + private static ExecutorService otherThread; + + @BeforeAll + static void setUp() { + otherThread = Executors.newSingleThreadExecutor(); + } + + @AfterAll + static void tearDown() { + otherThread.shutdown(); + } + + @Test + void grpcOtelMix() { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + assertThat(COUNTRY.get()).isNull(); + io.grpc.Context root = grpcContext.attach(); + try { + assertThat(COUNTRY.get()).isEqualTo("japan"); + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + assertThat(COUNTRY.get()).isEqualTo("japan"); + + io.grpc.Context context2 = io.grpc.Context.current().withValue(FOOD, "cheese"); + assertThat(FOOD.get()).isNull(); + io.grpc.Context toRestore = context2.attach(); + try { + assertThat(FOOD.get()).isEqualTo("cheese"); + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } finally { + context2.detach(toRestore); + } + } + } finally { + grpcContext.detach(root); + } + } + + @Test + void grpcWrap() throws Exception { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + io.grpc.Context root = grpcContext.attach(); + try { + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + AtomicReference grpcValue = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + grpcValue.set(COUNTRY.get()); + otelValue.set(Context.current().get(ANIMAL)); + }; + + otherThread.submit(runnable).get(); + assertThat(grpcValue).hasValue(null); + assertThat(otelValue).hasValue(null); + + otherThread.submit(io.grpc.Context.current().wrap(runnable)).get(); + assertThat(grpcValue).hasValue("japan"); + assertThat(otelValue).hasValue("cat"); + } + } finally { + grpcContext.detach(root); + } + } + + @Test + void otelWrap() throws Exception { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(COUNTRY, "japan"); + io.grpc.Context root = grpcContext.attach(); + try { + try (Scope ignored = Context.current().with(ANIMAL, "cat").makeCurrent()) { + assertThat(COUNTRY.get()).isEqualTo("japan"); + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + AtomicReference grpcValue = new AtomicReference<>(); + AtomicReference otelValue = new AtomicReference<>(); + Runnable runnable = + () -> { + grpcValue.set(COUNTRY.get()); + otelValue.set(Context.current().get(ANIMAL)); + }; + + otherThread.submit(runnable).get(); + assertThat(grpcValue).hasValue(null); + assertThat(otelValue).hasValue(null); + + otherThread.submit(Context.current().wrap(runnable)).get(); + + assertThat(grpcValue).hasValue("japan"); + assertThat(otelValue).hasValue("cat"); + } + } finally { + grpcContext.detach(root); + } + } +} diff --git a/opentelemetry-java/context/src/otelInGrpcTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider b/opentelemetry-java/context/src/otelInGrpcTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider new file mode 100644 index 000000000..7e81d07a3 --- /dev/null +++ b/opentelemetry-java/context/src/otelInGrpcTest/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider @@ -0,0 +1 @@ +io.opentelemetry.context.GrpcContextStorageProvider \ No newline at end of file diff --git a/opentelemetry-java/context/src/storageWrappersTest/java/io/opentelemetry/context/StorageWrappersTest.java b/opentelemetry-java/context/src/storageWrappersTest/java/io/opentelemetry/context/StorageWrappersTest.java new file mode 100644 index 000000000..7be7b99f8 --- /dev/null +++ b/opentelemetry-java/context/src/storageWrappersTest/java/io/opentelemetry/context/StorageWrappersTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; + +class StorageWrappersTest { + + private static final ContextKey ANIMAL = ContextKey.named("key"); + + private static final AtomicInteger scopeOpenedCount = new AtomicInteger(); + private static final AtomicInteger scopeClosedCount = new AtomicInteger(); + + @SuppressWarnings("UnnecessaryLambda") + private static final Function wrapper = + delegate -> + new ContextStorage() { + @Override + public Scope attach(Context toAttach) { + Scope scope = delegate.attach(toAttach); + scopeOpenedCount.incrementAndGet(); + return () -> { + scope.close(); + scopeClosedCount.incrementAndGet(); + }; + } + + @Override + public Context current() { + return delegate.current(); + } + }; + + @BeforeEach + void resetCounts() { + scopeOpenedCount.set(0); + scopeClosedCount.set(0); + } + + // Run twice to ensure second wrapping has no effect. + @RepeatedTest(2) + void wrapAndInitialize() { + ContextStorage.addWrapper(wrapper); + + assertThat(scopeOpenedCount).hasValue(0); + assertThat(scopeClosedCount).hasValue(0); + + try (Scope ignored = Context.current().with(ANIMAL, "koala").makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("koala"); + } + + assertThat(scopeOpenedCount).hasValue(1); + assertThat(scopeClosedCount).hasValue(1); + } +} diff --git a/opentelemetry-java/context/src/strictContextEnabledTest/java/io/opentelemetry/context/StrictContextEnabledTest.java b/opentelemetry-java/context/src/strictContextEnabledTest/java/io/opentelemetry/context/StrictContextEnabledTest.java new file mode 100644 index 000000000..990496603 --- /dev/null +++ b/opentelemetry-java/context/src/strictContextEnabledTest/java/io/opentelemetry/context/StrictContextEnabledTest.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.github.netmikey.logunit.api.LogCapturer; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; + +@SuppressWarnings("MustBeClosedChecker") +class StrictContextEnabledTest { + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(StrictContextStorage.class); + + @Test + void garbageCollectedScope() { + Context.current().with(ANIMAL, "cat").makeCurrent(); + + await() + .atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + System.gc(); + LoggingEvent log = + logs.assertContains("Scope garbage collected before being closed."); + assertThat(log.getLevel()).isEqualTo(Level.ERROR); + assertThat(log.getThrowable().getMessage()) + .matches("Thread \\[Test worker\\] opened a scope of .* here:"); + }); + } +} diff --git a/opentelemetry-java/context/src/strictContextEnabledTest/java/io/opentelemetry/context/StrictContextStorageTest.java b/opentelemetry-java/context/src/strictContextEnabledTest/java/io/opentelemetry/context/StrictContextStorageTest.java new file mode 100644 index 000000000..0ad903a74 --- /dev/null +++ b/opentelemetry-java/context/src/strictContextEnabledTest/java/io/opentelemetry/context/StrictContextStorageTest.java @@ -0,0 +1,186 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2013-2020 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; + +@SuppressWarnings("MustBeClosedChecker") +class StrictContextStorageTest { + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + private static final String TRACE_ID = "7b2e170db4df2d593ddb4ddf2ddf2d59"; + private static final String SPAN_ID = "b2e170db4df2d593"; + + private static final Span SPAN = + Span.wrap( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(StrictContextStorage.class); + + // In this test we intentionally leak context so need to restore it ourselves, bypassing the + // strict storage. + @AfterEach + void resetContext() { + ThreadLocalContextStorage.INSTANCE.attach(Context.root()); + } + + // TODO(anuraaga): These rules conflict with error prone so one or the other needs to be + // disabled. + @SuppressWarnings({"checkstyle:EmptyBlock", "checkstyle:WhitespaceAround"}) + @Test + void decorator_close_afterCorrectUsage() { + try (Scope ws = Context.current().with(ANIMAL, "cat").makeCurrent()) { + try (Scope ws2 = Context.current().with(ANIMAL, "dog").makeCurrent()) {} + } + + ((StrictContextStorage) ContextStorage.get()).close(); // doesn't error + } + + static final class BusinessClass { + + static Scope businessMethodMakeContextCurrent() { + return Context.current().with(ANIMAL, "cat").makeCurrent(); + } + + static Scope businessMethodMakeSpanCurrent() { + return SPAN.makeCurrent(); + } + + private BusinessClass() {} + } + + @Test + public void scope_close_onWrongThread_newScope() throws Exception { + scope_close_onWrongThread( + BusinessClass::businessMethodMakeContextCurrent, "businessMethodMakeContextCurrent"); + } + + @Test + public void decorator_close_withLeakedScope_onWrongThread_newScope() throws Exception { + decorator_close_withLeakedScope( + BusinessClass::businessMethodMakeContextCurrent, "businessMethodMakeContextCurrent"); + } + + @Test + public void scope_close_onWrongThread_withSpanInScope() throws Exception { + scope_close_onWrongThread( + BusinessClass::businessMethodMakeSpanCurrent, "businessMethodMakeSpanCurrent"); + } + + @Test + public void decorator_close_withLeakedScope_withSpanInScope() throws Exception { + decorator_close_withLeakedScope( + BusinessClass::businessMethodMakeSpanCurrent, "businessMethodMakeSpanCurrent"); + } + + void scope_close_onWrongThread(Supplier method, String methodName) throws Exception { + AtomicReference closeable = new AtomicReference<>(); + Thread t1 = new Thread(() -> closeable.set(method.get())); + t1.setName("t1"); + t1.start(); + t1.join(); + + AtomicReference errorCatcher = new AtomicReference<>(); + + Thread t2 = + new Thread( + () -> { + try { + closeable.get().close(); + } catch (Throwable t) { + errorCatcher.set(t); + } + }); + t2.setName("t2"); + t2.start(); + t2.join(); + + assertThat(errorCatcher.get()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Thread [t1] opened scope, but thread [t2] closed it") + .satisfies( + e -> + assertThat(e.getCause().getMessage()) + .matches("Thread \\[t1\\] opened scope for .* here:")); + } + + @SuppressWarnings("ReturnValueIgnored") + void decorator_close_withLeakedScope(Supplier method, String methodName) throws Exception { + AtomicReference scope = new AtomicReference<>(); + Thread thread = new Thread(() -> scope.set(method.get())); + thread.setName("t1"); + thread.start(); + thread.join(); + + assertThatThrownBy(() -> ((StrictContextStorage) ContextStorage.get()).close()) + .isInstanceOf(AssertionError.class) + .satisfies( + t -> assertThat(t.getMessage()).matches("Thread \\[t1\\] opened a scope of .* here:")) + .hasNoCause(); + } + + static void assertStackTraceStartsWithMethod(Throwable throwable, String methodName) { + assertThat(throwable.getStackTrace()[0].getMethodName()).isEqualTo(methodName); + } + + @Test + @SuppressWarnings("UnusedVariable") + void multipleLeaks() { + Scope scope1 = Context.current().with(ANIMAL, "cat").makeCurrent(); + Scope scope2 = Context.current().with(ANIMAL, "dog").makeCurrent(); + assertThatThrownBy(() -> ((StrictContextStorage) ContextStorage.get()).close()) + .isInstanceOf(AssertionError.class); + } + + @Test + void garbageCollectedScope() { + Context.current().with(ANIMAL, "cat").makeCurrent(); + + await() + .atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + System.gc(); + LoggingEvent log = + logs.assertContains("Scope garbage collected before being closed."); + assertThat(log.getLevel()).isEqualTo(Level.ERROR); + assertThat(log.getThrowable().getMessage()) + .matches("Thread \\[Test worker\\] opened a scope of .* here:"); + }); + } +} diff --git a/opentelemetry-java/context/src/test/java/io/opentelemetry/context/ContextTest.java b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/ContextTest.java new file mode 100644 index 000000000..f42c830f7 --- /dev/null +++ b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/ContextTest.java @@ -0,0 +1,522 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import io.github.netmikey.logunit.api.LogCapturer; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; + +@SuppressWarnings("ClassCanBeStatic") +@ExtendWith(MockitoExtension.class) +class ContextTest { + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + private static final ContextKey BAG = ContextKey.named("bag"); + + private static final Context CAT = Context.current().with(ANIMAL, "cat"); + + @RegisterExtension + LogCapturer logs = + LogCapturer.create().captureForType(ThreadLocalContextStorage.class, Level.DEBUG); + + // Make sure all tests clean up + @AfterEach + void tearDown() { + assertThat(Context.current()).isEqualTo(Context.root()); + } + + @Test + void startsWithRoot() { + assertThat(Context.current()).isEqualTo(Context.root()); + } + + @Test + void canBeAttached() { + Context context = Context.current().with(ANIMAL, "cat"); + assertThat(Context.current().get(ANIMAL)).isNull(); + try (Scope ignored = context.makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + try (Scope ignored2 = Context.root().makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isNull(); + } + + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } + assertThat(Context.current().get(ANIMAL)).isNull(); + } + + @Test + void attachSameTwice() { + Context context = Context.current().with(ANIMAL, "cat"); + assertThat(Context.current().get(ANIMAL)).isNull(); + try (Scope ignored = context.makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + + try (Scope ignored2 = context.makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } + + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + } + assertThat(Context.current().get(ANIMAL)).isNull(); + } + + @Test + void newThreadStartsWithRoot() throws Exception { + Context context = Context.current().with(ANIMAL, "cat"); + try (Scope ignored = context.makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat"); + AtomicReference current = new AtomicReference<>(); + Thread thread = new Thread(() -> current.set(Context.current())); + thread.start(); + thread.join(); + assertThat(current.get()).isEqualTo(Context.root()); + } + } + + @Test + public void closingScopeWhenNotActiveIsLogged() { + Context initial = Context.current(); + Context context = initial.with(ANIMAL, "cat"); + try (Scope scope = context.makeCurrent()) { + Context context2 = context.with(ANIMAL, "dog"); + try (Scope ignored = context2.makeCurrent()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("dog"); + scope.close(); + } + } + assertThat(Context.current()).isEqualTo(initial); + LoggingEvent log = logs.assertContains("Context in storage not the expected context"); + assertThat(log.getLevel()).isEqualTo(Level.DEBUG); + } + + @Test + void withValues() { + Context context1 = Context.current().with(ANIMAL, "cat"); + assertThat(context1.get(ANIMAL)).isEqualTo("cat"); + + Context context2 = context1.with(BAG, 100); + // Old unaffected + assertThat(context1.get(ANIMAL)).isEqualTo("cat"); + assertThat(context1.get(BAG)).isNull(); + + assertThat(context2.get(ANIMAL)).isEqualTo("cat"); + assertThat(context2.get(BAG)).isEqualTo(100); + + Context context3 = context2.with(ANIMAL, "dog"); + // Old unaffected + assertThat(context2.get(ANIMAL)).isEqualTo("cat"); + assertThat(context2.get(BAG)).isEqualTo(100); + + assertThat(context3.get(ANIMAL)).isEqualTo("dog"); + assertThat(context3.get(BAG)).isEqualTo(100); + + Context context4 = context3.with(BAG, null); + // Old unaffected + assertThat(context3.get(ANIMAL)).isEqualTo("dog"); + assertThat(context3.get(BAG)).isEqualTo(100); + + assertThat(context4.get(ANIMAL)).isEqualTo("dog"); + assertThat(context4.get(BAG)).isNull(); + + Context context5 = context4.with(ANIMAL, "dog"); + assertThat(context5.get(ANIMAL)).isEqualTo("dog"); + assertThat(context5).isSameAs(context4); + + String dog = new String("dog"); + assertThat(dog).isEqualTo("dog"); + assertThat(dog).isNotSameAs("dog"); + Context context6 = context5.with(ANIMAL, dog); + assertThat(context6.get(ANIMAL)).isEqualTo("dog"); + // We reuse context object when values match by reference, not value. + assertThat(context6).isNotSameAs(context5); + } + + @Test + void wrapRunnable() { + AtomicReference value = new AtomicReference<>(); + Runnable callback = () -> value.set(Context.current().get(ANIMAL)); + + callback.run(); + assertThat(value).hasValue(null); + + CAT.wrap(callback).run(); + assertThat(value).hasValue("cat"); + + callback.run(); + assertThat(value).hasValue(null); + } + + @Test + void wrapCallable() throws Exception { + AtomicReference value = new AtomicReference<>(); + Callable callback = + () -> { + value.set(Context.current().get(ANIMAL)); + return "foo"; + }; + + assertThat(callback.call()).isEqualTo("foo"); + assertThat(value).hasValue(null); + + assertThat(CAT.wrap(callback).call()).isEqualTo("foo"); + assertThat(value).hasValue("cat"); + + assertThat(callback.call()).isEqualTo("foo"); + assertThat(value).hasValue(null); + } + + @Test + void wrapExecutor() { + AtomicReference value = new AtomicReference<>(); + Executor executor = MoreExecutors.directExecutor(); + Runnable callback = () -> value.set(Context.current().get(ANIMAL)); + + executor.execute(callback); + assertThat(value).hasValue(null); + + CAT.wrap(executor).execute(callback); + assertThat(value).hasValue("cat"); + + executor.execute(callback); + assertThat(value).hasValue(null); + + try (Scope ignored = CAT.makeCurrent()) { + Context.taskWrapping(executor).execute(callback); + assertThat(value).hasValue("cat"); + } + } + + @Nested + @TestInstance(Lifecycle.PER_CLASS) + class WrapExecutorService { + + protected ScheduledExecutorService executor; + protected ExecutorService wrapped; + protected AtomicReference value; + + protected ExecutorService wrap(ExecutorService executorService) { + return CAT.wrap(executorService); + } + + @BeforeAll + void initExecutor() { + executor = Executors.newSingleThreadScheduledExecutor(); + wrapped = wrap(executor); + } + + @AfterAll + void stopExecutor() { + executor.shutdown(); + } + + @BeforeEach + void setUp() { + value = new AtomicReference<>(); + } + + @Test + void execute() { + Runnable runnable = () -> value.set(Context.current().get(ANIMAL)); + wrapped.execute(runnable); + await().untilAsserted(() -> assertThat(value).hasValue("cat")); + } + + @Test + void submitRunnable() { + Runnable runnable = () -> value.set(Context.current().get(ANIMAL)); + Futures.getUnchecked(wrapped.submit(runnable)); + assertThat(value).hasValue("cat"); + } + + @Test + void submitRunnableResult() { + Runnable runnable = () -> value.set(Context.current().get(ANIMAL)); + assertThat(Futures.getUnchecked(wrapped.submit(runnable, "foo"))).isEqualTo("foo"); + assertThat(value).hasValue("cat"); + } + + @Test + void submitCallable() { + Callable callable = + () -> { + value.set(Context.current().get(ANIMAL)); + return "foo"; + }; + assertThat(Futures.getUnchecked(wrapped.submit(callable))).isEqualTo("foo"); + assertThat(value).hasValue("cat"); + } + + @Test + void invokeAll() throws Exception { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + Callable callable1 = + () -> { + value1.set(Context.current().get(ANIMAL)); + return "foo"; + }; + Callable callable2 = + () -> { + value2.set(Context.current().get(ANIMAL)); + return "bar"; + }; + List> futures = wrapped.invokeAll(Arrays.asList(callable1, callable2)); + assertThat(futures.get(0).get()).isEqualTo("foo"); + assertThat(futures.get(1).get()).isEqualTo("bar"); + assertThat(value1).hasValue("cat"); + assertThat(value2).hasValue("cat"); + } + + @Test + void invokeAllTimeout() throws Exception { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + Callable callable1 = + () -> { + value1.set(Context.current().get(ANIMAL)); + return "foo"; + }; + Callable callable2 = + () -> { + value2.set(Context.current().get(ANIMAL)); + return "bar"; + }; + List> futures = + wrapped.invokeAll(Arrays.asList(callable1, callable2), 10, TimeUnit.SECONDS); + assertThat(futures.get(0).get()).isEqualTo("foo"); + assertThat(futures.get(1).get()).isEqualTo("bar"); + assertThat(value1).hasValue("cat"); + assertThat(value2).hasValue("cat"); + } + + @Test + void invokeAny() throws Exception { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + Callable callable1 = + () -> { + value1.set(Context.current().get(ANIMAL)); + throw new IllegalStateException("callable2 wins"); + }; + Callable callable2 = + () -> { + value2.set(Context.current().get(ANIMAL)); + return "bar"; + }; + assertThat(wrapped.invokeAny(Arrays.asList(callable1, callable2))).isEqualTo("bar"); + assertThat(value1).hasValue("cat"); + assertThat(value2).hasValue("cat"); + } + + @Test + void invokeAnyTimeout() throws Exception { + AtomicReference value1 = new AtomicReference<>(); + AtomicReference value2 = new AtomicReference<>(); + Callable callable1 = + () -> { + value1.set(Context.current().get(ANIMAL)); + throw new IllegalStateException("callable2 wins"); + }; + Callable callable2 = + () -> { + value2.set(Context.current().get(ANIMAL)); + return "bar"; + }; + assertThat(wrapped.invokeAny(Arrays.asList(callable1, callable2), 10, TimeUnit.SECONDS)) + .isEqualTo("bar"); + assertThat(value1).hasValue("cat"); + assertThat(value2).hasValue("cat"); + } + } + + @Nested + @TestInstance(Lifecycle.PER_CLASS) + class CurrentContextWrappingExecutorService extends WrapExecutorService { + @Override + protected ExecutorService wrap(ExecutorService executorService) { + return Context.taskWrapping(executorService); + } + + private Scope scope; + + @BeforeEach + // Closed in AfterEach + @SuppressWarnings("MustBeClosedChecker") + void makeCurrent() { + scope = CAT.makeCurrent(); + } + + @AfterEach + void close() { + scope.close(); + scope = null; + } + } + + @Test + void keyToString() { + assertThat(ANIMAL.toString()).isEqualTo("animal"); + } + + @Test + void attachSameContext() { + Context context = Context.current().with(ANIMAL, "cat"); + try (Scope scope1 = context.makeCurrent()) { + assertThat(scope1).isNotSameAs(Scope.noop()); + try (Scope scope2 = context.makeCurrent()) { + assertThat(scope2).isSameAs(Scope.noop()); + } + } + } + + // We test real context-related above but should test cleanup gets delegated, which is best with + // a mock. + @Nested + @TestInstance(Lifecycle.PER_CLASS) + class DelegatesToExecutorService { + + @Mock private ExecutorService executor; + + @Test + void delegatesCleanupMethods() throws Exception { + ExecutorService wrapped = CAT.wrap(executor); + wrapped.shutdown(); + verify(executor).shutdown(); + verifyNoMoreInteractions(executor); + wrapped.shutdownNow(); + verify(executor).shutdownNow(); + verifyNoMoreInteractions(executor); + when(executor.isShutdown()).thenReturn(true); + assertThat(wrapped.isShutdown()).isTrue(); + verify(executor).isShutdown(); + verifyNoMoreInteractions(executor); + when(wrapped.isTerminated()).thenReturn(true); + assertThat(wrapped.isTerminated()).isTrue(); + verify(executor).isTerminated(); + verifyNoMoreInteractions(executor); + when(executor.awaitTermination(anyLong(), any())).thenReturn(true); + assertThat(wrapped.awaitTermination(1, TimeUnit.SECONDS)).isTrue(); + verify(executor).awaitTermination(1, TimeUnit.SECONDS); + verifyNoMoreInteractions(executor); + } + } + + @Nested + @TestInstance(Lifecycle.PER_CLASS) + class WrapScheduledExecutorService extends WrapExecutorService { + + private ScheduledExecutorService wrapScheduled; + + @BeforeEach + void wrapScheduled() { + wrapScheduled = CAT.wrap(executor); + } + + @Test + void scheduleRunnable() throws Exception { + Runnable runnable = () -> value.set(Context.current().get(ANIMAL)); + wrapScheduled.schedule(runnable, 0, TimeUnit.SECONDS).get(); + assertThat(value).hasValue("cat"); + } + + @Test + void scheduleCallable() throws Exception { + Callable callable = + () -> { + value.set(Context.current().get(ANIMAL)); + return "foo"; + }; + assertThat(wrapScheduled.schedule(callable, 0, TimeUnit.SECONDS).get()).isEqualTo("foo"); + assertThat(value).hasValue("cat"); + } + + @Test + void scheduleAtFixedRate() { + Runnable runnable = () -> value.set(Context.current().get(ANIMAL)); + ScheduledFuture future = + wrapScheduled.scheduleAtFixedRate(runnable, 0, 10, TimeUnit.SECONDS); + await().untilAsserted(() -> assertThat(value).hasValue("cat")); + future.cancel(true); + } + + @Test + void scheduleWithFixedDelay() { + Runnable runnable = () -> value.set(Context.current().get(ANIMAL)); + ScheduledFuture future = + wrapScheduled.scheduleWithFixedDelay(runnable, 0, 10, TimeUnit.SECONDS); + await().untilAsserted(() -> assertThat(value).hasValue("cat")); + future.cancel(true); + } + } + + @Test + void emptyContext() { + assertThat(Context.root().get(new HashCollidingKey())).isEqualTo(null); + } + + @Test + void string() { + assertThat(Context.root()).hasToString("{}"); + assertThat(Context.root().with(ANIMAL, "cat")).hasToString("{animal=cat}"); + assertThat(Context.root().with(ANIMAL, "cat").with(BAG, 10)) + .hasToString("{animal=cat, bag=10}"); + } + + @Test + void hashcodeCollidingKeys() { + Context context = Context.root(); + HashCollidingKey cheese = new HashCollidingKey(); + HashCollidingKey wine = new HashCollidingKey(); + + Context twoKeys = context.with(cheese, "whiz").with(wine, "boone's farm"); + + assertThat(twoKeys.get(wine)).isEqualTo("boone's farm"); + assertThat(twoKeys.get(cheese)).isEqualTo("whiz"); + } + + @SuppressWarnings("HashCodeToString") + private static class HashCollidingKey implements ContextKey { + @Override + public int hashCode() { + return 1; + } + } +} diff --git a/opentelemetry-java/context/src/test/java/io/opentelemetry/context/LazyStorageTest.java b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/LazyStorageTest.java new file mode 100644 index 000000000..7d52cd834 --- /dev/null +++ b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/LazyStorageTest.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.net.URL; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearSystemProperty; +import org.junitpioneer.jupiter.SetSystemProperty; + +class LazyStorageTest { + + private static final String CONTEXT_STORAGE_PROVIDER_PROPERTY = + "io.opentelemetry.context.contextStorageProvider"; + private static final String MOCK_CONTEXT_STORAGE_PROVIDER = + "io.opentelemetry.context.LazyStorageTest$MockContextStorageProvider"; + private static final AtomicReference DEFERRED_STORAGE_FAILURE = + new AtomicReference<>(); + + @Test + @ClearSystemProperty(key = CONTEXT_STORAGE_PROVIDER_PROPERTY) + void empty_providers() { + assertThat(LazyStorage.createStorage(DEFERRED_STORAGE_FAILURE)) + .isEqualTo(ContextStorage.defaultStorage()); + } + + @Test + @SetSystemProperty(key = CONTEXT_STORAGE_PROVIDER_PROPERTY, value = MOCK_CONTEXT_STORAGE_PROVIDER) + void set_storage_provider_property_and_empty_providers() { + assertThat(LazyStorage.createStorage(DEFERRED_STORAGE_FAILURE)) + .isEqualTo(ContextStorage.defaultStorage()); + } + + @Test + @ClearSystemProperty(key = CONTEXT_STORAGE_PROVIDER_PROPERTY) + void unset_storage_provider_property_and_one_providers() throws Exception { + File serviceFile = createContextStorageProvider(); + try { + assertThat(LazyStorage.createStorage(DEFERRED_STORAGE_FAILURE)).isEqualTo(mockContextStorage); + } finally { + assertThat(serviceFile.delete()).isTrue(); + } + } + + @Test + @SetSystemProperty( + key = CONTEXT_STORAGE_PROVIDER_PROPERTY, + value = "not.match.provider.class.name") + void set_storage_provider_property_not_matches_one_providers() throws Exception { + File serviceFile = createContextStorageProvider(); + try { + assertThat(LazyStorage.createStorage(DEFERRED_STORAGE_FAILURE)) + .isEqualTo(ContextStorage.defaultStorage()); + } finally { + assertThat(serviceFile.delete()).isTrue(); + } + } + + @Test + @SetSystemProperty(key = CONTEXT_STORAGE_PROVIDER_PROPERTY, value = MOCK_CONTEXT_STORAGE_PROVIDER) + void set_storage_provider_property_matches_one_providers() throws Exception { + File serviceFile = createContextStorageProvider(); + try { + assertThat(LazyStorage.createStorage(DEFERRED_STORAGE_FAILURE)).isEqualTo(mockContextStorage); + } finally { + assertThat(serviceFile.delete()).isTrue(); + } + } + + @Test + @SetSystemProperty(key = CONTEXT_STORAGE_PROVIDER_PROPERTY, value = "default") + void enforce_default_and_empty_providers() { + assertThat(LazyStorage.createStorage(DEFERRED_STORAGE_FAILURE)) + .isEqualTo(ContextStorage.defaultStorage()); + } + + @Test + @SetSystemProperty(key = CONTEXT_STORAGE_PROVIDER_PROPERTY, value = "default") + void enforce_default_and_one_providers() throws IOException { + File serviceFile = createContextStorageProvider(); + try { + assertThat(LazyStorage.createStorage(DEFERRED_STORAGE_FAILURE)) + .isEqualTo(ContextStorage.defaultStorage()); + } finally { + assertThat(serviceFile.delete()).isTrue(); + } + } + + private static File createContextStorageProvider() throws IOException { + URL location = + MockContextStorageProvider.class.getProtectionDomain().getCodeSource().getLocation(); + File file = + new File( + location.getPath() + "META-INF/services/" + ContextStorageProvider.class.getName()); + file.getParentFile().mkdirs(); + + @SuppressWarnings("DefaultCharset") + Writer output = new FileWriter(file); + output.write(MockContextStorageProvider.class.getName()); + output.close(); + + return file; + } + + private static final ContextStorage mockContextStorage = + new ContextStorage() { + @Override + public Scope attach(Context toAttach) { + return null; + } + + @Override + public Context current() { + return null; + } + }; + + public static final class MockContextStorageProvider implements ContextStorageProvider { + @Override + public ContextStorage get() { + return mockContextStorage; + } + } +} diff --git a/opentelemetry-java/context/src/test/java/io/opentelemetry/context/internal/shaded/WeakConcurrentMapTest.java b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/internal/shaded/WeakConcurrentMapTest.java new file mode 100644 index 000000000..3d635be77 --- /dev/null +++ b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/internal/shaded/WeakConcurrentMapTest.java @@ -0,0 +1,178 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Rafael Winterhalter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Suppress warnings since this is vendored as-is. +// CHECKSTYLE:OFF + +package io.opentelemetry.context.internal.shaded; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +// Suppress warnings since this is copied as-is. +@SuppressWarnings({ + "overrides", + "UnusedVariable", + "EqualsHashCode", + "MultiVariableDeclaration", +}) +class WeakConcurrentMapTest { + + @Test + void testLocalExpunction() throws Exception { + final WeakConcurrentMap.WithInlinedExpunction map = + new WeakConcurrentMap.WithInlinedExpunction(); + assertThat(map.getCleanerThread(), nullValue(Thread.class)); + new MapTestCase(map) { + @Override + protected void triggerClean() { + map.expungeStaleEntries(); + } + }.doTest(); + } + + @Test + void testExternalThread() throws Exception { + WeakConcurrentMap map = new WeakConcurrentMap(false); + assertThat(map.getCleanerThread(), nullValue(Thread.class)); + Thread thread = new Thread(map); + thread.start(); + new MapTestCase(map).doTest(); + thread.interrupt(); + Thread.sleep(200L); + assertThat(thread.isAlive(), is(false)); + } + + @Test + void testInternalThread() throws Exception { + WeakConcurrentMap map = new WeakConcurrentMap(true); + assertThat(map.getCleanerThread(), not(nullValue(Thread.class))); + new MapTestCase(map).doTest(); + map.getCleanerThread().interrupt(); + Thread.sleep(200L); + assertThat(map.getCleanerThread().isAlive(), is(false)); + } + + static class KeyEqualToWeakRefOfItself { + + @Override + public boolean equals(Object obj) { + if (obj instanceof WeakReference) { + return equals(((WeakReference) obj).get()); + } + return super.equals(obj); + } + } + + static class CheapUnloadableWeakConcurrentMap + extends AbstractWeakConcurrentMap { + + @Override + protected Object getLookupKey(KeyEqualToWeakRefOfItself key) { + return key; + } + + @Override + protected void resetLookupKey(Object lookupKey) {} + } + + @Test + void testKeyWithWeakRefEquals() { + CheapUnloadableWeakConcurrentMap map = new CheapUnloadableWeakConcurrentMap(); + + KeyEqualToWeakRefOfItself key = new KeyEqualToWeakRefOfItself(); + Object value = new Object(); + map.put(key, value); + assertThat(map.containsKey(key), is(true)); + assertThat(map.get(key), is(value)); + assertThat(map.putIfAbsent(key, new Object()), is(value)); + assertThat(map.remove(key), is(value)); + assertThat(map.containsKey(key), is(false)); + } + + private static class MapTestCase { + + private final WeakConcurrentMap map; + + public MapTestCase(WeakConcurrentMap map) { + this.map = map; + } + + void doTest() throws Exception { + Object key1 = new Object(), + value1 = new Object(), + key2 = new Object(), + value2 = new Object(), + key3 = new Object(), + value3 = new Object(), + key4 = new Object(), + value4 = new Object(); + map.put(key1, value1); + map.put(key2, value2); + map.put(key3, value3); + map.put(key4, value4); + assertThat(map.get(key1), is(value1)); + assertThat(map.get(key2), is(value2)); + assertThat(map.get(key3), is(value3)); + assertThat(map.get(key4), is(value4)); + Map values = new HashMap(); + values.put(key1, value1); + values.put(key2, value2); + values.put(key3, value3); + values.put(key4, value4); + for (Map.Entry entry : map) { + assertThat(values.remove(entry.getKey()), is(entry.getValue())); + } + assertThat(values.isEmpty(), is(true)); + key1 = key2 = null; // Make eligible for GC + System.gc(); + Thread.sleep(200L); + triggerClean(); + assertThat(map.get(key3), is(value3)); + assertThat(map.getIfPresent(key3), is(value3)); + assertThat(map.get(key4), is(value4)); + assertThat(map.approximateSize(), is(2)); + assertThat(map.target.size(), is(2)); + assertThat(map.remove(key3), is(value3)); + assertThat(map.get(key3), nullValue()); + assertThat(map.getIfPresent(key3), nullValue()); + assertThat(map.get(key4), is(value4)); + assertThat(map.approximateSize(), is(1)); + assertThat(map.target.size(), is(1)); + map.clear(); + assertThat(map.get(key3), nullValue()); + assertThat(map.get(key4), nullValue()); + assertThat(map.approximateSize(), is(0)); + assertThat(map.target.size(), is(0)); + assertThat(map.iterator().hasNext(), is(false)); + } + + protected void triggerClean() {} + } +} diff --git a/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/DefaultPropagatorsTest.java b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/DefaultPropagatorsTest.java new file mode 100644 index 000000000..6bbb9e40d --- /dev/null +++ b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/DefaultPropagatorsTest.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DefaultContextPropagators}. */ +class DefaultPropagatorsTest { + + @Test + void addTextMapPropagatorNull() { + assertThatThrownBy(() -> ContextPropagators.create(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void testInject() { + CustomTextMapPropagator propagator1 = new CustomTextMapPropagator("prop1"); + CustomTextMapPropagator propagator2 = new CustomTextMapPropagator("prop2"); + ContextPropagators propagators = + ContextPropagators.create(TextMapPropagator.composite(propagator1, propagator2)); + + Context context = Context.current(); + context = context.with(propagator1.getKey(), "value1"); + context = context.with(propagator2.getKey(), "value2"); + + Map map = new HashMap<>(); + propagators.getTextMapPropagator().inject(context, map, MapSetter.INSTANCE); + assertThat(map.get(propagator1.getKeyName())).isEqualTo("value1"); + assertThat(map.get(propagator2.getKeyName())).isEqualTo("value2"); + } + + @Test + void testExtract() { + CustomTextMapPropagator propagator1 = new CustomTextMapPropagator("prop1"); + CustomTextMapPropagator propagator2 = new CustomTextMapPropagator("prop2"); + CustomTextMapPropagator propagator3 = new CustomTextMapPropagator("prop3"); + ContextPropagators propagators = + ContextPropagators.create(TextMapPropagator.composite(propagator1, propagator2)); + + // Put values for propagators 1 and 2 only. + Map map = new HashMap<>(); + map.put(propagator1.getKeyName(), "value1"); + map.put(propagator2.getKeyName(), "value2"); + + Context context = + propagators.getTextMapPropagator().extract(Context.current(), map, MapGetter.INSTANCE); + assertThat(context.get(propagator1.getKey())).isEqualTo("value1"); + assertThat(context.get(propagator2.getKey())).isEqualTo("value2"); + assertThat(context.get(propagator3.getKey())).isNull(); // Handle missing value. + } + + @Test + public void testDuplicatedFields() { + CustomTextMapPropagator propagator1 = new CustomTextMapPropagator("prop1"); + CustomTextMapPropagator propagator2 = new CustomTextMapPropagator("prop2"); + CustomTextMapPropagator propagator3 = new CustomTextMapPropagator("prop1"); + CustomTextMapPropagator propagator4 = new CustomTextMapPropagator("prop2"); + ContextPropagators propagators = + ContextPropagators.create( + TextMapPropagator.composite(propagator1, propagator2, propagator3, propagator4)); + + Collection fields = propagators.getTextMapPropagator().fields(); + assertThat(fields).containsExactly("prop1", "prop2"); + } + + @Test + void noopPropagator() { + ContextPropagators propagators = ContextPropagators.noop(); + + Context context = Context.current(); + Map map = new HashMap<>(); + propagators.getTextMapPropagator().inject(context, map, MapSetter.INSTANCE); + assertThat(map).isEmpty(); + + assertThat(propagators.getTextMapPropagator().extract(context, map, MapGetter.INSTANCE)) + .isSameAs(context); + } + + private static class CustomTextMapPropagator implements TextMapPropagator { + private final String name; + private final ContextKey key; + + CustomTextMapPropagator(String name) { + this.name = name; + this.key = ContextKey.named(name); + } + + ContextKey getKey() { + return key; + } + + String getKeyName() { + return name; + } + + @Override + public Collection fields() { + return Collections.singletonList(name); + } + + @Override + public void inject(Context context, C carrier, TextMapSetter setter) { + Object payload = context.get(key); + if (payload != null) { + setter.set(carrier, name, payload.toString()); + } + } + + @Override + public Context extract(Context context, C carrier, TextMapGetter getter) { + String payload = getter.get(carrier, name); + if (payload != null) { + context = context.with(key, payload); + } + + return context; + } + } + + private static final class MapSetter implements TextMapSetter> { + private static final MapSetter INSTANCE = new MapSetter(); + + @Override + public void set(Map map, String key, String value) { + map.put(key, value); + } + + private MapSetter() {} + } + + private static final class MapGetter implements TextMapGetter> { + private static final MapGetter INSTANCE = new MapGetter(); + + @Override + public Iterable keys(Map map) { + return map.keySet(); + } + + @Override + public String get(Map map, String key) { + return map.get(key); + } + + private MapGetter() {} + } +} diff --git a/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/MultiTextMapPropagatorTest.java b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/MultiTextMapPropagatorTest.java new file mode 100644 index 000000000..66e80bc95 --- /dev/null +++ b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/MultiTextMapPropagatorTest.java @@ -0,0 +1,171 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MultiTextMapPropagatorTest { + + private static final ContextKey KEY = ContextKey.named("key"); + + @Mock private TextMapPropagator propagator1; + @Mock private TextMapPropagator propagator2; + @Mock private TextMapPropagator propagator3; + + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + @Test + void addPropagator_null() { + assertThrows( + NullPointerException.class, + () -> new MultiTextMapPropagator((List) null)); + } + + @Test + void fields() { + when(propagator1.fields()).thenReturn(Arrays.asList("foo", "bar")); + when(propagator2.fields()).thenReturn(Arrays.asList("hello", "world")); + TextMapPropagator prop = new MultiTextMapPropagator(propagator1, propagator2); + + Collection fields = prop.fields(); + assertThat(fields).containsExactly("foo", "bar", "hello", "world"); + } + + @Test + void fields_duplicates() { + when(propagator1.fields()).thenReturn(Arrays.asList("foo", "bar", "foo")); + when(propagator2.fields()).thenReturn(Arrays.asList("hello", "world", "world", "bar")); + TextMapPropagator prop = new MultiTextMapPropagator(propagator1, propagator2); + + Collection fields = prop.fields(); + assertThat(fields).containsExactly("foo", "bar", "hello", "world"); + } + + @Test + void fields_readOnly() { + when(propagator1.fields()).thenReturn(Arrays.asList("rubber", "baby")); + when(propagator2.fields()).thenReturn(Arrays.asList("buggy", "bumpers")); + TextMapPropagator prop = new MultiTextMapPropagator(propagator1, propagator2); + Collection fields = prop.fields(); + assertThrows(UnsupportedOperationException.class, () -> fields.add("hi")); + } + + @Test + void inject_allDelegated() { + Map carrier = new HashMap<>(); + Context context = mock(Context.class); + TextMapSetter> setter = Map::put; + + TextMapPropagator prop = new MultiTextMapPropagator(propagator1, propagator2, propagator3); + prop.inject(context, carrier, setter); + verify(propagator1).inject(context, carrier, setter); + verify(propagator2).inject(context, carrier, setter); + verify(propagator3).inject(context, carrier, setter); + } + + @Test + void extract_noPropagators() { + Map carrier = new HashMap<>(); + Context context = mock(Context.class); + + TextMapPropagator prop = new MultiTextMapPropagator(); + Context resContext = prop.extract(context, carrier, getter); + assertThat(context).isSameAs(resContext); + } + + @Test + void extract_found_all() { + Map carrier = new HashMap<>(); + TextMapPropagator prop = new MultiTextMapPropagator(propagator1, propagator2, propagator3); + Context context1 = mock(Context.class); + Context context2 = mock(Context.class); + Context context3 = mock(Context.class); + Context expectedContext = mock(Context.class); + + when(propagator1.extract(context1, carrier, getter)).thenReturn(context2); + when(propagator2.extract(context2, carrier, getter)).thenReturn(context3); + when(propagator3.extract(context3, carrier, getter)).thenReturn(expectedContext); + + assertThat(prop.extract(context1, carrier, getter)).isEqualTo(expectedContext); + } + + @Test + void extract_notFound() { + Map carrier = new HashMap<>(); + Context context = mock(Context.class); + when(propagator1.extract(context, carrier, getter)).thenReturn(context); + when(propagator2.extract(context, carrier, getter)).thenReturn(context); + + TextMapPropagator prop = new MultiTextMapPropagator(propagator1, propagator2); + Context result = prop.extract(context, carrier, getter); + + assertThat(result).isSameAs(context); + } + + @Test + void extract_nullContext() { + assertThat( + new MultiTextMapPropagator(propagator1, propagator2) + .extract(null, Collections.emptyMap(), getter)) + .isSameAs(Context.root()); + } + + @Test + void extract_nullGetter() { + Context context = Context.current().with(KEY, "treasure"); + assertThat( + new MultiTextMapPropagator(propagator1, propagator2) + .extract(context, Collections.emptyMap(), null)) + .isSameAs(context); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + new MultiTextMapPropagator(propagator1, propagator2).inject(null, carrier, Map::put); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = Context.current().with(KEY, "treasure"); + new MultiTextMapPropagator(propagator1, propagator2).inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } +} diff --git a/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/NoopTextMapPropagatorTest.java b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/NoopTextMapPropagatorTest.java new file mode 100644 index 000000000..4b5965df1 --- /dev/null +++ b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/NoopTextMapPropagatorTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class NoopTextMapPropagatorTest { + + private static final ContextKey KEY = ContextKey.named("key"); + + @Test + void noopFields() { + assertThat(TextMapPropagator.noop().fields()).isEmpty(); + } + + @Test + void extract_contextUnchanged() { + Context input = Context.current(); + Context result = + TextMapPropagator.noop().extract(input, new HashMap<>(), MapTextMapGetter.INSTANCE); + assertThat(result).isSameAs(input); + } + + @Test + void extract_nullContext() { + assertThat( + TextMapPropagator.noop() + .extract(null, Collections.emptyMap(), MapTextMapGetter.INSTANCE)) + .isSameAs(Context.root()); + } + + @Test + void extract_nullGetter() { + Context context = Context.current().with(KEY, "treasure"); + assertThat(TextMapPropagator.noop().extract(context, Collections.emptyMap(), null)) + .isSameAs(context); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + TextMapPropagator.noop().inject(null, carrier, Map::put); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = Context.current().with(KEY, "treasure"); + TextMapPropagator.noop().inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } + + enum MapTextMapGetter implements TextMapGetter> { + INSTANCE; + + @Override + public Iterable keys(Map carrier) { + return null; + } + + @Nullable + @Override + public String get(@Nullable Map carrier, String key) { + return null; + } + } +} diff --git a/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/TextMapPropagatorTest.java b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/TextMapPropagatorTest.java new file mode 100644 index 000000000..39927d6d6 --- /dev/null +++ b/opentelemetry-java/context/src/test/java/io/opentelemetry/context/propagation/TextMapPropagatorTest.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.context.propagation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TextMapPropagatorTest { + + @Mock private TextMapPropagator propagator; + + @Test + void compositeNonMulti() { + assertThat(TextMapPropagator.composite()).isSameAs(TextMapPropagator.noop()); + assertThat(TextMapPropagator.composite(propagator)).isSameAs(propagator); + } +} diff --git a/opentelemetry-java/dependencyManagement/build.gradle.kts b/opentelemetry-java/dependencyManagement/build.gradle.kts new file mode 100644 index 000000000..9e5e502e8 --- /dev/null +++ b/opentelemetry-java/dependencyManagement/build.gradle.kts @@ -0,0 +1,144 @@ +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask + +plugins { + `java-platform` + + id("com.github.ben-manes.versions") +} + +data class DependencySet(val group: String, val version: String, val modules: List) + +val dependencyVersions = hashMapOf() +rootProject.extra["versions"] = dependencyVersions + +val DEPENDENCY_BOMS = listOf( + "com.linecorp.armeria:armeria-bom:1.8.0", + "io.grpc:grpc-bom:1.38.0", + "io.zipkin.brave:brave-bom:5.13.3", + "com.google.guava:guava-bom:30.1.1-jre", + "com.google.protobuf:protobuf-bom:3.17.2", + "com.fasterxml.jackson:jackson-bom:2.12.3", + "org.junit:junit-bom:5.7.2", + "io.zipkin.reporter2:zipkin-reporter-bom:2.16.3" +) + +val DEPENDENCY_SETS = listOf( + DependencySet( + "com.google.auto.value", + "1.8.1", + listOf("auto-value", "auto-value-annotations") + ), + DependencySet( + "com.google.errorprone", + "2.7.1", + listOf("error_prone_annotations", "error_prone_core") + ), + DependencySet( + "io.opencensus", + "0.28.3", + listOf( + "opencensus-api", + "opencensus-impl-core", + "opencensus-impl", + "opencensus-exporter-metrics-util" + ) + ), + DependencySet( + "io.prometheus", + "0.11.0", + listOf("simpleclient", "simpleclient_common", "simpleclient_httpserver") + ), + DependencySet( + "javax.annotation", + "1.3.2", + listOf("javax.annotation-api") + ), + DependencySet( + "org.openjdk.jmh", + "1.32", + listOf("jmh-core", "jmh-generator-bytecode") + ), + DependencySet( + "org.mockito", + "3.10.0", + listOf("mockito-core", "mockito-junit-jupiter") + ), + DependencySet( + "org.testcontainers", + "1.15.3", + listOf("testcontainers", "junit-jupiter") + ) +) + +val DEPENDENCIES = listOf( + "com.github.stefanbirkner:system-rules:1.19.0", + "com.google.code.findbugs:jsr305:3.0.2", + "com.google.code.gson:gson:2.8.7", + "com.google.guava:guava-beta-checker:1.0", + "com.lmax:disruptor:3.4.4", + "com.sparkjava:spark-core:2.9.3", + "com.squareup.okhttp3:okhttp:4.9.1", + "com.sun.net.httpserver:http:20070405", + "com.tngtech.archunit:archunit-junit4:0.19.0", + "com.uber.nullaway:nullaway:0.9.1", + "edu.berkeley.cs.jqf:jqf-fuzz:1.7", + "eu.rekawek.toxiproxy:toxiproxy-java:2.1.4", + "io.github.netmikey.logunit:logunit-jul:1.1.0", + "io.jaegertracing:jaeger-client:1.6.0", + "io.opentracing:opentracing-api:0.33.0", + "io.zipkin.zipkin2:zipkin-junit:2.23.2", + "junit:junit:4.13.2", + "nl.jqno.equalsverifier:equalsverifier:3.6.1", + "org.assertj:assertj-core:3.19.0", + "org.awaitility:awaitility:4.1.0", + "org.codehaus.mojo:animal-sniffer-annotations:1.20", + "org.curioswitch.curiostack:protobuf-jackson:1.2.0", + "org.jctools:jctools-core:3.3.0", + "org.junit-pioneer:junit-pioneer:1.4.2", + "org.skyscreamer:jsonassert:1.5.0", + "org.slf4j:slf4j-simple:1.7.30" +) + +javaPlatform { + allowDependencies() +} + +dependencies { + for (bom in DEPENDENCY_BOMS) { + api(enforcedPlatform(bom)) + val split = bom.split(':') + dependencyVersions[split[0]] = split[2] + } + constraints { + for (set in DEPENDENCY_SETS) { + for (module in set.modules) { + api("${set.group}:${module}:${set.version}") + dependencyVersions[set.group] = set.version + } + } + for (dependency in DEPENDENCIES) { + api(dependency) + val split = dependency.split(':') + dependencyVersions[split[0]] = split[2] + } + } +} + +fun isNonStable(version: String): Boolean { + val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) } + val regex = "^[0-9,.v-]+(-r)?$".toRegex() + val isGuava = version.endsWith("-jre") + val isStable = stableKeyword || regex.matches(version) || isGuava + return isStable.not() +} + +tasks { + named("dependencyUpdates") { + revision = "release" + checkConstraints = true + + rejectVersionIf { + isNonStable(candidate.version) + } + } +} diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-api.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-api.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-api.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-context.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-context.txt new file mode 100644 index 000000000..9ddf5535a --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-context.txt @@ -0,0 +1,5 @@ +Comparing source compatibility of against +***! MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.context.Context (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++! NEW METHOD: PUBLIC(+) STATIC(+) java.util.concurrent.Executor taskWrapping(java.util.concurrent.Executor) + +++! NEW METHOD: PUBLIC(+) STATIC(+) java.util.concurrent.ExecutorService taskWrapping(java.util.concurrent.ExecutorService) diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-jaeger-thrift.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-jaeger-thrift.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-jaeger-thrift.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-jaeger.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-jaeger.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-jaeger.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-logging-otlp.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-logging-otlp.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-logging-otlp.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-logging.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-logging.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-logging.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp-common.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp-common.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp-common.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp-trace.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp-trace.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp-trace.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-otlp.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-zipkin.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-zipkin.txt new file mode 100644 index 000000000..95fffe4a6 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-exporter-zipkin.txt @@ -0,0 +1,4 @@ +Comparing source compatibility of against +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.exporter.zipkin.ZipkinSpanExporter (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.util.logging.Logger baseLogger diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-annotations.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-annotations.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-annotations.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-aws.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-aws.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-aws.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-kotlin.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-kotlin.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-kotlin.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-trace-propagators.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-trace-propagators.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-extension-trace-propagators.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-common.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-common.txt new file mode 100644 index 000000000..8f9ecdb7b --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-common.txt @@ -0,0 +1,22 @@ +Comparing source compatibility of against +*** MODIFIED CLASS: PUBLIC ABSTRACT io.opentelemetry.sdk.resources.Resource (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.resources.ResourceBuilder builder() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder toBuilder() ++++ NEW CLASS: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW CONSTRUCTOR: PUBLIC(+) ResourceBuilder() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.Resource build() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(java.lang.String, java.lang.String) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(java.lang.String, long) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(java.lang.String, double) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(java.lang.String, boolean) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(java.lang.String, java.lang.String[]) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(java.lang.String, long[]) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(java.lang.String, double[]) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(java.lang.String, boolean[]) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(io.opentelemetry.api.common.AttributeKey, java.lang.Object) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder put(io.opentelemetry.api.common.AttributeKey, int) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder putAll(io.opentelemetry.api.common.Attributes) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.ResourceBuilder putAll(io.opentelemetry.sdk.resources.Resource) diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-aws.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-aws.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-aws.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-jaeger-remote-sampler.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-jaeger-remote-sampler.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-jaeger-remote-sampler.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-resources.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-resources.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-extension-resources.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-testing.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-testing.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-testing.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-trace.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-trace.txt new file mode 100644 index 000000000..1f2056fe1 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk-trace.txt @@ -0,0 +1,4 @@ +Comparing source compatibility of against +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.trace.export.SimpleSpanProcessor (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.common.CompletableResultCode forceFlush() diff --git a/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk.txt b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.1.0_vs_1.0.0/opentelemetry-sdk.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-api.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-api.txt new file mode 100644 index 000000000..88faf9b5f --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-api.txt @@ -0,0 +1,7 @@ +Comparing source compatibility of against +***! MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.trace.Span (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++! NEW METHOD: PUBLIC(+) io.opentelemetry.api.trace.Span setAllAttributes(io.opentelemetry.api.common.Attributes) +***! MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.trace.SpanBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++! NEW METHOD: PUBLIC(+) io.opentelemetry.api.trace.SpanBuilder setAllAttributes(io.opentelemetry.api.common.Attributes) diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-context.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-context.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-context.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-jaeger-thrift.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-jaeger-thrift.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-jaeger-thrift.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-jaeger.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-jaeger.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-jaeger.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-logging-otlp.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-logging-otlp.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-logging-otlp.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-logging.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-logging.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-logging.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp-common.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp-common.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp-common.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp-trace.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp-trace.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp-trace.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-otlp.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-zipkin.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-zipkin.txt new file mode 100644 index 000000000..3f99bae9f --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-exporter-zipkin.txt @@ -0,0 +1,5 @@ +Comparing source compatibility of against +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder setReadTimeout(long, java.util.concurrent.TimeUnit) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder setReadTimeout(java.time.Duration) diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-annotations.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-annotations.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-annotations.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-aws.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-aws.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-aws.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-kotlin.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-kotlin.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-kotlin.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-trace-propagators.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-trace-propagators.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-extension-trace-propagators.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-common.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-common.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-common.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-aws.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-aws.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-aws.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-jaeger-remote-sampler.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-jaeger-remote-sampler.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-jaeger-remote-sampler.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-resources.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-resources.txt new file mode 100644 index 000000000..411b6633b --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-extension-resources.txt @@ -0,0 +1,10 @@ +Comparing source compatibility of against ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.extension.resources.HostResource (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.resources.Resource get() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.extension.resources.HostResourceProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW CONSTRUCTOR: PUBLIC(+) HostResourceProvider() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.Resource createResource(io.opentelemetry.sdk.autoconfigure.ConfigProperties) diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-testing.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-testing.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-testing.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-trace.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-trace.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk-trace.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk.txt b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/1.2.0_vs_1.1.0/opentelemetry-sdk.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-api.txt new file mode 100644 index 000000000..031b54b48 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -0,0 +1,5 @@ +Comparing source compatibility of against +=== UNCHANGED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.common.Attributes (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + === UNCHANGED METHOD: PUBLIC ABSTRACT java.lang.Object get(io.opentelemetry.api.common.AttributeKey) + +++ NEW ANNOTATION: javax.annotation.Nullable diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-context.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-context.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-context.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-jaeger-thrift.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-jaeger-thrift.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-jaeger-thrift.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-jaeger.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-jaeger.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-jaeger.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp-common.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp-common.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp-common.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp-trace.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp-trace.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp-trace.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-otlp.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-annotations.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-annotations.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-annotations.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-aws.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-aws.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-aws.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt new file mode 100644 index 000000000..094355074 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt @@ -0,0 +1,10 @@ +Comparing source compatibility of against +=== UNCHANGED CLASS: PUBLIC FINAL io.opentelemetry.extension.kotlin.ContextExtensionsKt (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + *** MODIFIED ANNOTATION: kotlin.Metadata + --- REMOVED ELEMENT: bv=1,0,3 (-) + *** MODIFIED ELEMENT: mv=1,5,1 (<- 1,4,1) + === UNCHANGED ELEMENT: k=2 + === UNCHANGED ELEMENT: d1=�� � ��� ��� ��� ���� ����0�*�0�� ����0�*�0�� ����0�*�0�¨�� + === UNCHANGED ELEMENT: d2=asContextElement,Lkotlin/coroutines/CoroutineContext;,Lio/opentelemetry/context/Context;,Lio/opentelemetry/context/ImplicitContextKeyed;,getOpenTelemetryContext,opentelemetry-extension-kotlin + +++ NEW ELEMENT: xi=48 (+) diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt new file mode 100644 index 000000000..ba69ce5f2 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt @@ -0,0 +1,25 @@ +Comparing source compatibility of against ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.extension.trace.propagation.B3ConfigurablePropagator (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW CONSTRUCTOR: PUBLIC(+) B3ConfigurablePropagator() + +++ NEW METHOD: PUBLIC(+) java.lang.String getName() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.context.propagation.TextMapPropagator getPropagator() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.extension.trace.propagation.B3MultiConfigurablePropagator (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW CONSTRUCTOR: PUBLIC(+) B3MultiConfigurablePropagator() + +++ NEW METHOD: PUBLIC(+) java.lang.String getName() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.context.propagation.TextMapPropagator getPropagator() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.extension.trace.propagation.JaegerConfigurablePropagator (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW CONSTRUCTOR: PUBLIC(+) JaegerConfigurablePropagator() + +++ NEW METHOD: PUBLIC(+) java.lang.String getName() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.context.propagation.TextMapPropagator getPropagator() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.extension.trace.propagation.OtTraceConfigurablePropagator (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW CONSTRUCTOR: PUBLIC(+) OtTraceConfigurablePropagator() + +++ NEW METHOD: PUBLIC(+) java.lang.String getName() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.context.propagation.TextMapPropagator getPropagator() diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-aws.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-aws.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-aws.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-jaeger-remote-sampler.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-jaeger-remote-sampler.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-jaeger-remote-sampler.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-resources.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-resources.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-resources.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt new file mode 100644 index 000000000..f80002c11 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt @@ -0,0 +1,5 @@ +Comparing source compatibility of against +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.AttributesAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.AttributesAssert hasSize(int) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.AttributesAssert isEmpty() diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt new file mode 100644 index 000000000..df2614649 --- /dev/null +++ b/opentelemetry-java/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/opentelemetry-java/docs/jmh.md b/opentelemetry-java/docs/jmh.md new file mode 100644 index 000000000..fed02923d --- /dev/null +++ b/opentelemetry-java/docs/jmh.md @@ -0,0 +1,21 @@ + +# how to jmh + +[jmh] (Java Benchmark Harness) is a tool for running benchmarks and reporting results. + +opentelemetry-java has a lot of micro benchmarks. They live inside +`jmh` directories in the appropriate module. + +The benchmarks are run with a gradle plugin. + +To run an entire suite for a module, you can run the jmh gradle task. +As an example, here's how you can run the benchmarks for all of +the sdk trace module. + +``` +`./gradlew :sdk:trace:jmh` +``` + +If you just want to run a single benchmark and not the entire suite: + +`./gradlew -PjmhIncludeSingleClass=BatchSpanProcessorBenchmark :sdk:trace:jmh` \ No newline at end of file diff --git a/opentelemetry-java/docs/rationale.md b/opentelemetry-java/docs/rationale.md new file mode 100644 index 000000000..f0fa1d821 --- /dev/null +++ b/opentelemetry-java/docs/rationale.md @@ -0,0 +1,95 @@ +# OpenTelemetry Rationale + +When creating a library, often times designs and decisions are made that get lost over time. This +document tries to collect information on design decisions to answer common questions that may come +up when you explore the SDK. + +## Span not `Closeable` + +Because a `Span` has a lifecycle, where it is started and MUST be ended, it seems intuitive that a +`Span` should implement `Closeable` or `AutoCloseable` to allow usage with Java try-with-resources +construct. However, `Span`s are unique in that they must still be alive when handling exceptions, +which try-with-resources does not allow. Take this example: + +```java +Span span = tracer.spanBuilder("someWork").startSpan(); +try (Scope scope = TracingContextUtils.currentContextWith(span)) { + // Do things. +} catch (Exception ex) { + span.recordException(ex); +} finally { + span.end(); +} +``` + +It would not be possible to call `recordException` if `span` was also using try-with-resources. +Because this is a common usage for spans, we do not support try-with-resources. + + +## Versioning and Releases + +### Assumptions + +- This project uses semver v2, as does the rest of OpenTelemetry. + +### Goals + +- API Stability: + - Once the API for a given signal (spans, logs, metrics, baggage) has been officially released, code instrumented with that API module will +function, *with no recompilation required*, with any API+SDK that has the same major version, and equal or greater minor or patch version. + - For example, libraries that are instrumented with `opentelemetry-api-trace:1.0.1` will function, at runtime with +SDK library `opentelemetry-sdk-trace:1.11.33` plus `opentelemetry-api-trace:1.11.33` (or whatever specific versions are specified by + the bom version `1.11.33`, if the individual versions have diverged). + - We call this requirement the "ABI" compatibility requirement for "Application Binary Interface" compatibility. +- SDK Stability: + - Public portions of the SDK (constructors, configuration, end-user interfaces) must remain backwards compatible. + - Precisely what this includes has yet to be delineated. +- Internal implementation details of both the API and SDK are allowed to be changed, + as long as the public APIs are not changed in an ABI-incompatible manner. + +### Methods + +- Mature signals + - API modules for mature (i.e. released) signals will be transitive dependencies of the `opentelemetry-api` module. + - Methods for accessing mature APIs will be added, as appropriate to the `OpenTelemetry` interface. + - SDK modules for mature (i.e. released) signals will be transitive dependencies of the `opentelemetry-sdk` module. + - Configuration options for the SDK modules for mature signals will be exposed, as appropriate, on the `OpenTelemetrySdk` class. + - Modules for these mature signals will be included in the opentelemetry-bom to ensure that users runtime dependencies are kept in sync. + - Mixing and matching runtime API and SDK versions, eg. by avoiding use of the BOM, will not be supported by this project. + - Once a public API (either in the official API or in the SDK) has been released, we will endeavor to support that API in perpetuity. + +- Immature or experimental signals + - API modules for immature signals will not be transitive dependencies of the `opentelemetry-api` module. + - API modules will be versioned with an "-alpha" suffix to make it abundantly clear that depending on them is at your own risk. + - API modules for immature signals will be co-versioned along with mature API modules, with the added suffix. + - The java packages for immature APIs will be used as if they were mature signals. This will enable users to easily transition from immature to + mature usage, without having to change imports. + - SDK modules for immature signals will also be versioned with an "-alpha" suffix, in parallel to their API modules. + +### Examples + +Purely for illustration purposes, not intended to represent actual releases: + +- `v1.0.0` release: + - `io.opentelemetry:opentelemetry-api:1.0.0` + - Includes APIs for tracing, baggage, context, propagators (via the context dependency) + - `io.opentelemetry:opentelemetry-api-metrics:1.0.0-alpha` + - Note: packages here are the final package structure: `io.opentelemetry.api.metrics.*` + - `io.opentelemetry:opentelemetry-sdk-trace:1.0.0` + - `io.opentelemetry:opentelemetry-sdk-common:1.0.0` + - Shared code for metrics/trace implementations (clocks, etc) + - `io.opentelemetry:opentelemetry-sdk-metrics:1.0.0-alpha` + - Note: packages here are the final package structure: `io.opentelemetry.sdk.metrics.*` + - `io.opentelemetry:opentelemetry-sdk-all:1.0.0` + - The SDK side of `io.opentelemetry:opentelemetry-api:1.0.0` + - No mention of metrics in here! +- `v1.15.0` release (with metrics) + - `io.opentelemetry:opentelemetry-api:1.15.0` + - Contains APIs for tracing, baggage, propagators (via the context dependency), metrics + - `io.opentelemetry:opentelemetry-sdk-trace:1.15.0` + - `io.opentelemetry:opentelemetry-sdk-common:1.15.0` + - Shared code for metrics/trace implementations (clocks, etc) + - `io.opentelemetry:opentelemetry-sdk-metrics:1.15.0` + - Note: packages here have not changed from the experimental jar...just a jar rename happened. + - `io.opentelemetry:opentelemetry-sdk-all:1.15.0` + - The SDK side of io.opentelemetry:opentelemetry-api:1.15.0 diff --git a/opentelemetry-java/docs/sdk-configuration.md b/opentelemetry-java/docs/sdk-configuration.md new file mode 100644 index 000000000..e7cd1abd7 --- /dev/null +++ b/opentelemetry-java/docs/sdk-configuration.md @@ -0,0 +1,649 @@ +> This is a design document and is not necessarily updated with evolving APIs. For up-to-date +> examples of SDK configuration see the +> [documentation](https://opentelemetry.io/docs/java/manual_instrumentation/). + +# Design for configuring the SDK + +This document outlines some of the goals we have for user configuration of the SDK. It is a +continuation of discussion started with https://github.com/open-telemetry/opentelemetry-java/issues/2022. + +## Target audiences + +There are a few different target audiences that are related to our configuration story. + +- Application developers, aka end-users. Often have no knowledge of tracing but want to add +OpenTelemetry to their app and see traces show up in a console. Application developers will increase +in number forever, while the below are more constant. + +- Dev-ops / framework developers. Write components or frameworks to support providing tracing to +their application developers. May write custom SDK extensions such as exporters, samplers, to fit +with their internal infrastructure and as such have some familiarity with tracing at least as the +SDK presents it. + +- Telemetry extension authors. Write custom SDK extensions, often to support a particular backend. +Very familiar with telemetry. + +- OpenTelemetry maintainers. Write the SDK code. + +When making decisions, especially about complexity, we always prioritize the application developers, +then framework developers, then maintainers. This is because we expect those with less domain +knowledge about tracing to require a simpler experience than those with more. There is also more +bang-for-the-buck by making the end-user experience as streamlined as possible since we expect there +to be much more of them than other audiences. + +## Goals and non-goals + +### Goals + +- Provide a single entrypoint to configuring the SDK. For end-users, less familiar with the SDK, we +want to have everything together to provide discoverability and simpler end-user code. If there are +several, clear use cases which benefit from different entrypoints, we could have multiple +corresponding to each one. + +- Fit well with common Java idioms such as dependency injection, or common frameworks like Spring. + +- Reduce the chance of gotchas or configuration mishaps. + +- Aim for optimal performance of the configured SDK for the most common use case. + +### Non-goals + +- Provide the best possible experience for custom SDKs. Generally the burden of the experience for +custom SDKs can fall on their authors, we optimize for our standard usage, the full SDK. Any +reference to "the SDK" in this document refers to the full SDK with all signals. + +- Make sure everything is auto-configurable. This is out of the scope of the SDK, and instead is +left to auto-configuration layers, which are also described below but not as part of the core SDK. +The SDK provides an autoconfiguration extension as an option which is not internal to the main SDK +components. + +## Configuring an instance of the SDK + +The SDK exposes configuration options for all the signals it supports. Users all have different +requirements for how they use the SDK; for example they may use different exporters depending on +their backend. Because we cannot guess the configuration the user needs, we expect that the SDK must +be configured by the user before it can be used. + +Goals for configuring the SDK are + +- Discoverability of options +- Ease of use by end users, e.g., less complicated code required +- Avoid requiring duplicate configuration, which can lead to errors or confusion +- Provide good defaults where possible + +In Java, the builder pattern is common for configuring instances. Let's look at what that may look +like. The simplest configuration will be when a user wants to get a default experience, exporting +with a specific exporter to an endpoint. + +The SDK builder will simply accept its components as builder parameters. It only allows +setting SDK implementations and is not meant for use with partial SDKs. + +```java +class OpenTelemetrySdkBuilder { + public OpenTelemetrySdkBuilder setTracerProvider(SdkTracerProvider tracerProvider); + public OpenTelemetrySdkBuilder setPropagators(ContextPropagators propagators); + public OpenTelemetrySdk buildAndRegisterGlobal(); + public OpenTelemetrySdk build(); +} +``` + +Metrics are yet GA and must be configured separately. Eventually, metrics configuration will become +part of the `OpenTelemetrySdkBuilder`. + +A very simple configuration may look like this. + +```java +class HelloWorld { + public static void main(String[] args) { + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder( + OtlpGrpcSpanExporter.builder() + .setEndpoint("https://collector-service:4317") + .build()) + .build()) + .build(); + + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .setTracerProvider(tracerProvider) + .buildAndRegisterGlobal(); + + SdkMeterProvider meterProvider = SdkMeterProvider.builder().build(); + GlobalMeterProvider.set(meterProvider); + IntervalMetricReader.builder() + .setMetricProducers(Collections.singletonList(meterProvider)) + .setMetricExporter( + OtlpGrpcMetricExporter.builder().setEndpoint("https://collector-service:4317").build()) + .build() + .start(); + } +} +``` + +This + +- Exports spans using OTLP to `collector-service` + - Uses the BatchSpanProcessor. +- Exports metrics using OTLP to `collector-service` + - Uses the IntervalMetricReader +- Uses ParentBased(AlwaysOn) sampler +- Uses standard random IDs +- Uses a default Resource which simply consists of the SDK (or any other resources we decide to) +include in the core SDK, not extensions +- Uses the default Clock, which uses Java 8 / 9+ optimized APIs for getting time + - The only real reason a user would set this is for unit tests, not for production +- Enforces default numeric limits related to number of attribute, etc +- Enables single w3c propagator + +Because the exporting is often the only aspect an end user needs to configure, this is the simplest +possible API for configuring the SDK. + +Let's look at a more complicated example + +```java +class HelloWorld { + public static void main(String[] args) { + Resource resource = Resource.getDefault().merge(CoolResource.getDefault()); + Clock clock = AtomicClock.create(); + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setResource(resource) + .setClock(clock) + .addSpanProcessor(CustomAttributeAddingProcessor.create()) + .addSpanProcessor(CustomEventAddingProcessor.create()) + .addSpanProcessor( + BatchSpanProcessor.builder( + OtlpGrpcSpanExporter.builder() + .setEndpoint("https://collector-service:4317") + .setTimeout(Duration.ofSeconds(10)) + .build()) + .setMaxExportBatchSize(10000) + .build()) + .addSpanProcessor( + SimpleSpanProcessor.create( + ZipkinSpanExporter.builder().setEndpoint("https://zipkin-service:9411").build())) + .setSampler(Sampler.traceIdRatioBased(0.5)) + .setSpanLimits(SpanLimits.builder().setMaxNumberOfAttributes(10).build()) + .setIdGenerator(TimestampedIdGenerator.create()) + .build(); + + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder() + .setPropagators( + ContextPropagators.create( + TextMapPropagator.composite( + W3CTraceContextPropagator.getInstance(), + B3Propagator.injectingSingleHeader()))) + .setTracerProvider(tracerProvider) + .buildAndRegisterGlobal(); + + OpenTelemetrySdk openTelemetryForJaegerBackend = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(JaegerPropagator.getInstance())) + .setTracerProvider(tracerProvider) + .build(); + + SdkMeterProvider meterProvider = + SdkMeterProvider.builder() + .setResource(resource) + .setClock(clock) + .registerView( + InstrumentSelector.builder().setInstrumentType(InstrumentType.COUNTER).build(), + View.builder() + .setLabelsProcessorFactory(LabelsProcessorFactory.noop()) + .setAggregatorFactory(AggregatorFactory.count(AggregationTemporality.DELTA)) + .build()) + .build(); + GlobalMeterProvider.set(meterProvider); + IntervalMetricReader.builder() + .setMetricProducers(Collections.singletonList(meterProvider)) + .setMetricExporter( + OtlpGrpcMetricExporter.builder().setEndpoint("https://collector-service:4317").build()) + .build() + .start(); + } +} +``` + +This configures resource, clock, exporters, span processors, propagators, sampler, trace limits, and metrics. +It configures two SDKs with different propagators. We unfortunately cannot achieve our goal of only +setting a property once - `Resource` and `Clock` are shared among signals but configured repeatedly. +An alternative is to flatten all settings onto `OpenTelemetrySdkBuilder` - this has a downside that +it makes it very natural to create new tracer / meter providers for each configuration of the SDK, +which could result in a lot of duplication of resources like threads, workers, TCP connections. So +instead, this can make it clearer that those signals themselves are full-featured, and +`OpenTelemetry` is just a bag of signals. + +Keep in mind, reconfiguring `Clock` is expected to be an extremely uncommon operation. + +Another thing to keep in mind is that it will be less common for an application developer to go this far +and we can expect it is actually framework developers that use the full configuration capabilities +of the SDK, likely by tying it to a separate configuration system. + +### Why build instances + +We have found that even at such an early stage of user adoption, users want to build instances of +the SDK. + +- Integrates with dependency injection, e.g., Spring, in a similar way as many other libraries +- Can allow having multiple instances in the same app, in multi-concern single-classloader scenarios +- Allow managing lifecycle of SDK, e.g., shutting down and starting along with the lifecycle of a +serverless runtime + +### Configuring the SDK in a framework + +It is extremely common for Java apps to be written using a dependency injection framework like +Spring, Guice, Dagger, HK, and many more. They will all follow a very similar pattern though. + +```java +@Component +public class OpenTelemetryModule { + @Bean + public Resource resource() { + return Resource.getDefault().merge(CoolResource.getDefault()); + } + + @Bean + public Clock otelClock() { + return AtomicClock.create(); + } + + @Bean + public TracerSdkProvider tracerProvider(Resource resource, Clock clock, MonitoringConfig config) { + return SdkTracerProvider.builder() + .setResource(resource) + .setClock(clock) + .addSpanProcessor(CustomAttributeAddingProcessor.create()) + .addSpanProcessor(CustomEventAddingProcessor.create()) + .addSpanProcessor( + BatchSpanProcessor.builder( + OtlpGrpcSpanExporter.builder() + .setEndpoint(config.getOtlpExporter().getEndpoint()) + .setTimeout(config.getOtlpExporter().getTimeout()) + .build()) + .setBatchQueueSize(config.getOtlpExporter().getQueueSize()) + .setExporterTimeout(config.getOtlpExporter().getTimeout()) + .build()) + .addSpanProcessor( + SimpleSpanProcessor.create( + ZipkinSpanExporter.builder() + .setEndpoint(config.getZipkinExporter().getEndpoint()) + .build())) + .setSampler( + config.getSamplingRatio() != 0 + ? Sampler.traceIdRatioBased(config.getSamplingRatio()) + : Sampler.parentBased(Sampler.alwaysOn())) + .setSpanLimits( + SpanLimits.builder().setMaxNumberOfAttributes(config.getMaxSpanAttributes()).build()) + .setIdGenerator(TimestampedIdGenerator.create()) + .build(); + } + + @Bean + public MeterSdkProvider meterProvider(Resource resource, Clock clock) { + return SdkMeterProvider.builder() + .setResource(resource) + .setClock(clock) + .registerView( + InstrumentSelector.builder().setInstrumentType(InstrumentType.COUNTER).build(), + View.builder() + .setLabelsProcessorFactory(LabelsProcessorFactory.noop()) + .setAggregatorFactory(AggregatorFactory.count(AggregationTemporality.DELTA)) + .build()) + .build(); + } + + @Bean + public IntervalMetricReader metricReader(SdkMeterProvider meterProvider) { + return IntervalMetricReader.builder() + .setMetricProducers(List.of(meterProvider)) + .setMetricExporter( + OtlpGrpcMetricExporter.builder().setEndpoint("https://collector-service:4317").build()) + .build(); + } + + @Bean + public OpenTelemetry openTelemetry(SdkTracerProvider tracerProvider, SdkMeterProvider meterProvider) { + GlobalMeterProvider.set(meterProvider); + return OpenTelemetrySdk.builder() + .setPropagators( + ContextPropagators.create( + TextMapPropagator.composite( + W3CTraceContextPropagator.getInstance(), + B3Propagator.injectingSingleHeader()))) + .setTracerProvider(tracerProvider) + .buildAndRegisterGlobal(); + } + + @Bean + @ForJaeger + public OpenTelemetry openTelemetryJaeger(SdkTracerProvider tracerProvider, SdkMeterProvider meterProvider) { + return OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(JaegerPropagator.getInstance())) + .setTracerProvider(tracerProvider) + .build(); + } + + @Bean + public AuthServiceStub authService(@ForJaeger OpenTelemetry openTelemetry, AuthConfig config) { + return AuthServiceGrpc.newBlockingStub(ManagedChannelBuilder.forEndpoint(config.getEndpoint())) + .withInterceptor(TracingClientInterceptor.create(openTelemetry)); + } + + @Bean + public ServletFilter servletFilter(OpenTelemetry openTelemetry) { + return TracingServletFilter.create(openTelemetry); + } +} + +// Use some instrumented client +@Component +public class MyAuthInterceptor { + + private final AuthServiceStub authService; + + @Inject + public MyAuthInterceptor(AuthServiceStub authService) { + this.authService = authService; + } + + public void doAuth() { + if (!authService.getToken("credential").isAuthenticated()) { + throw new HackerException(); + } + } +} + +// Use tracer directly, not so common +@Component +public class MyService { + + private final Tracer tracer; + private final Meter meter; + + @Inject + public MyService(TracerProvider tracerProvider, MeterProvider meterProvider) { + tracer = tracerProvider.get("my-service"); + meter = meterProvider.get("my-service"); + } + + public void doLogic() { + Span span = tracer.spanBuilder("logic").startSpan(); + try (Scope ignored = span.makeCurrent()) { + Thread.sleep(1000); + } finally { + span.end(); + } + } +} +``` + +## The global instance of the SDK + +A built instance is convenient to use in most Java apps because of dependency injection. Because it +has a easy-to-reason initialization ordering, being tied into the dependency ordering (even if dependency +injection happened to be done manually through constructor invocation), we encourage application +developers to only use it. + +However, there are corner cases where an instance cannot be injected. The famous example is MySQL - +MySQL interceptors are initialized by calling a default constructor, and there is no way to pass a +built instance of a signal provider. For this case, we must store an SDK instance into a global +variable. It is expected that frameworks or end-users will set the SDK as the global to support +instrumentation that requires this. + +Before an SDK has been set, access to the global `OpenTelemetry` will return a no-op +`DefaultOpenTelemetry`. This is because it is possible library instrumentation is using the global, +and it may even use it during the processing of a request (rather than only at initialization time). +For this reason, we cannot throw an exception. Instead, if the SDK is detected on the classpath, we +will log a `SEVERE` warning once-only indicating the API has been accessed before the SDK configured +with directions on how a user could solve the problem. SDKs must be configured early +in an application to ensure it applies to all of the logic in the app, and this will generally be +ensured by the configuration framework such as Spring. For application developers, this restriction +should not have any effect one way or the other in the vast majority of cases. + +MySQL is the only known corner case that requires the global SDK instance. If such a corner case +didn't exist, we may not even support it in the first place. + +See special note about Java Agent below though. + +## Telemetry within SDK components + +SDK components, such as exporters or remote samplers, may want to emit telemetry for their own +processing. However, the SDK components must be initialized before the SDK can be fully built. We do +not support partially built SDK because one cannot reason about the behavior of it. Similarly we +do not support using the global instance of the SDK before it has been built. Therefore, SDK +components that require `OpenTelemetry` must accept it lazily. This is a restriction, but given +such components are rarely developed by application developers, and generally developed by either +framework authors or OpenTelemetry maintainers, this restriction is deemed reasonable. + +If this mechanism was built into the SDK, it may look like + +```java +interface OpenTelemetryComponent { + default void setOpenTelemetry(OpenTelemetry openTelemetry) {} +} +interface SpanExporter extends OpenTelemetryComponent { +} +public class BatchExporter implements SpanExporter { + + private volatile Tracer tracer; + + @Override + public void setOpenTelemetry(OpenTelemetry openTelemetry) { + tracer = openTelemetry.getTracerProvider().get("spanexporter"); + } + + @Override + public void export() { + Tracer tracer = this.tracer; + if (tracer != null) { + tracer.spanBuilder("export").startSpan(); + } + } +} +public class OpenTelemetrySdkBuilder { + public OpenTelemetrySdkBuilder addSpanExporter(SpanExporter exporter) { + tracerProvider.addSpanExporter(exporter); + components.add(exporter); + } + + public OpenTelemetrySdkBuilder setSampler(Sampler sampler) { + tracerProvider.setSampler(sampler); + components.add(sampler); + } + + public OpenTelemetry build() { + OpenTelemetrySdk sdk = new OpenTelemetrySdk(tracerProvider.build(), meterProvider.build()); + for (OpenTelemetryComponent component : components) { + component.setOpenTelemetry(sdk); + } + } +} +``` + +A framework author will have an even easier time since most dependency injection frameworks +natively support lazy injection. + +```java +@Component +public class MonitoringModule { + + @Bean + @ForSpanExporter + public Tracer tracer(TracerProvider tracerProvider) { + return tracerProvider.get("spanexporter"); + } +} + +@Component +public class MyExporter implements SpanExporter { + + private Lazy tracer; + + @Inject + public MyExporter(@ForSpanExporter Lazy tracer) { + this.tracer = tracer; + } + + @Override + public void export() { + tracer.get().spanBuilder("export").startSpan(); + } +} +``` + +## Immutability of OpenTelemetry configuration + +The above attempts to avoid allowing a built SDK to be mutated, e.g., it is shallow immutable. Allowing mutation can make code +harder to reason about (any component, even deep in business logic, could update the SDK without +hindrance), can reduce performance (require volatile read on most operations), and produce thread +safety issues if not well implemented. In particular, compared to the current state as of writing, + +- `TracerSdkManagement.addSpanProcessor` is not needed. We needed a mutable SDK to allow span +processors to use global APIs for telemetry, but because we instead push the complexity of handling +[telemetry within SDK components](#Telemetry within SDK components) to those components, where the +maintainers will have more domain knowledge. It allows this mutator method to be removed from the +end-user API. + +- `TracerSdkManagement.updateTraceConfig` - instead of allowing replacing of the config at the top level, we should consider making +`TraceConfig` an interface, and the SDK default implementation is a simple implementation that always returns +a constant configuration. It allows the above benefits of immutability to be in place for the common case where dynamic updates are not needed. +Where dynamic updates are needed, it can be replaced with a mutable implementation instead of making +the SDK configuration mutable. This keeps update methods out of the end-user APIs and will generally +give framework developers more control by handling dynamicism themselves without the chance of +end-users to affect it negatively. For example, spring-boot may wire trace config updates to +actuator, its admin interface, and does not have to worry about business logic side-stepping this +mechanism by calling `updateTraceConfig`. + +Some highly buggy code that could be enabled by mutability. + +```java +class SleuthUsingService { + + @Inject + private OpenTelemetry openTelemetry; + + public void doLogic() { + // My logic is important, so always sample it! + OpenTelemetrySdk.getTracerManagement().updateTraceConfig(config -> config.setSampler(ALWAYS_ON)); + // This service was able to affect other services, even though Sleuth intends to + // "manage the SDK". Unlike the javaagent, it can't block access to SDK methods we may provide. + doSampledLogicWhileOtherServicesAlsoGetSampled(); + } +} +``` + +## Library instrumentation + +As the configuration of observability is contained on `OpenTelemetry` instances, it is expected that +library instrumentation accept an `OpenTelemetry` instance, often as a builder for their e.g., +tracing interceptor, when configuring observability. + +## Auto-configuration + +The above presents programmatic configuration for the SDK and proposes that the core SDK has no +other mechanism for configuration. There is no SPI nor processing of environment variables or +system properties. There are many mechanisms for configuration, for example Spring Boot. +Integration with these systems becomes easier to reason about if we consider auto-configuration at +a layer above the core SDK. + +### Java Auto-Instrumentation Agent + +Java Auto-Instrumentation Agent is the primary means of automatically configuring the SDK. It +contains system properties, environment variables, and SPIs for allowing a user to have a fully +setup tracing configuration just by applying the agent. In fact, the agent does not even allow a +user to use the SDK directly, actively blocking it. Instead of having a situation where some +configuration is automatic in the core SDK and some in the agent, we can move it all to the agent. +The agent already exposes exporter SPIs - it can also expose SPIs for customization of the SDK +components that are manually configured above. + +- We could consider having a very similar autoconfiguration wrapper artifact as an SDK extension too. +But we would assume the core SDK is always manually configured. + +To allow users of the agent to apply tracing to their own code, the agent should attempt to +instrument dependency injection to provide an instance of `OpenTelemetry` using the agent-configured +SDK, for example it should add it to the Spring `ApplicationContext`. For cases where dependency +injection is not available, though, there is no option but to provide access to the SDK through a +global variable. We can expect such usage to still function correctly even if the agent is removed +and a different configuration mechanism is used, such as manual configuration as above, or Spring +Sleuth. + +### SDK Auto-Configuration Wrapper + +For non-agent users, we can still provide a non-programmatic solution for configuring the SDK - +it can be a different artifact which contains SPIs similar to what we have currently, supports +environment variables and other auto-configuration. A single entrypoint method, `initialize()` could +determine the configuration, initialize `OpenTelemetry`, and set it as the global. As this artifact +is in our control, it would be reasonable for `opentelemetry-api` to check the classpath for the +presence of the wrapper and invoke it automatically. + +### Spring Sleuth + +[Spring Sleuth](https://spring.io/projects/spring-cloud-sleuth) (or any similar observability-aware server framework such as +[curio-server-framework](https://github.com/curioswitch/curiostack/blob/master/common/server/framework/src/main/java/org/curioswitch/common/server/framework/monitoring/MonitoringModule.java) +or internal frameworks developed by devops teams at companies) is also a mechanism for automatically +configuring the SDK. In general, we would expect Sleuth users to not be using the java agent. + +Examples of how Sleuth could work are presented above in examples using `@Bean`. In particular, we +expect it to have its own set of configuration properties - by making sure we don't implement +configuration properties in the core SDK, only configuration layers like the agent or a possible +configuration wrapper, we avoid the possibility of confusion by having duplicate variables (in +practice, OpenTelemetry naming would likely be ignored and overwritten by Spring naming). + +## Partial SDKs + +We allow implementing particular signals of the OpenTelemetry API without using our SDK. For example, +a MeterProvider may be implemented with micrometer. For this reason, each signal must also present +all of its options in the form of, e.g., `TracerSdkProviderBuilder`. We expect the vast majority of +users to use `OpenTelemetrySdkBuilder` - while there is some duplication with the signal provider +builder, it is work maintainers can do to present the simplest interface for the most common use +case of using the whole SDK. + +Without SPI, the way to initialize a partial SDK would be to use `DefaultOpenTelemetry`. + +```java +@Bean +public OpenTelemetry openTelemetry() { + return DefaultOpenTelemetry.builder() + .setTraceProvider(TracerSdkProvider.builder().build()) + .setMeterProvider(MicrometerProvider.builder().build()) + .build(); +} +``` + +As this should be a fairly minor use case, and commonly handled by framework developers, this seems +reasonable. We can also hope that where it is important, it is the author of partial SDKs that +provide a one-stop-shop entrypoint. + +```java +@Bean +public OpenTelemetry openTelemetry() { + return OpenTelemetrySdkWithMicrometer.builder() + .addSpanExporter() + .setMeterRegistry() + .build(); +} +``` + +## Alternatives considered + +### Always allow using the global OpenTelemetry + +We discuss some [advantages](#Immutability of OpenTelemetry configuration) of `OpenTelemetry` not +being mutable. One of the main side effects of this decision is not allowing the global to be used +before it is configured. An alternative approach may start with a core that is mutated when +configured, and global usage would still be valid even if references are made before configuration. +The lifecycle becomes difficult to reason about i.e., when is `OpenTelemetry` actually ready for use? +Dependency injection makes it explicit, global doesn't. It also seems to have performance +implications for less common end-user use cases (dynamic config) or for reasons that non-end-users +can handle (telemetry within telemetry). + +### SPI loading of OpenTelemetry components + +We could detect OpenTelemetry components using SPI, but we don't expect partial SDKs to be so common. +Instead of an inside-out approach of initializing a partial SDK within our code, we can instead just +encourage an outside-in approach where a partial-SDK-specific wrapper is created. This reduces the +magic in configuring `OpenTelemetry`, it all happens through our single entry-point. diff --git a/opentelemetry-java/docs/zpages/TRACEZ_DESIGN.md b/opentelemetry-java/docs/zpages/TRACEZ_DESIGN.md new file mode 100644 index 000000000..70687ed20 --- /dev/null +++ b/opentelemetry-java/docs/zpages/TRACEZ_DESIGN.md @@ -0,0 +1,168 @@ +# OpenTelemetry SDK Contrib - /tracez and /traceconfigz Design Doc + +This file contains information about the design choices for the OpenTelemetry /tracez and +/traceconfigz zPages. + +## Introduction + +The OpenTelemetry zPages are a set of dynamically generated HTML pages that display trace and +metrics data for a running process: the /tracez zPage displays trace information about running +spans, sample span latencies, and sample error spans, while the /traceconfigz zPage is a web page +that allows the users to change tracing parameters, such as sampling probability and max number of +attributes. + +### /tracez zPage + +The /tracez zPage displays information on running spans, sample span latencies, and sample error +spans. The data is aggregated into a summary-level table: + +![tracez-table](img/tracez-table.png) + +You can click on each of the counts in the table cells to access the corresponding span +details. For example, here are the details of the `ChildSpan` latency sample (row 1, col 4): + +![tracez-details](img/tracez-details.png) + +### /traceconfigz zPage + +The /traceconfigz zPage displays information about the currently active tracing configuration and +provides an interface for users to modify relevant parameters: + +![traceconfigz](img/traceconfigz.png) + +## Motivation + +We are building the Java zPages in order to create a lightweight application performance monitoring +tool that allows users to troubleshoot OpenTelemetry instrumentation. + +## Design + +### Frontend + +#### HttpHandler + +The `HttpHandler` is responsible for rendering corresponding HTML content. An abstract base class, +`ZPageHandler` (OpenCensus implementation), is implemented to standardize handlers for different +zPages. Each page will implement their own `ZPageHandler`, extending the base class, to generate the +corresponding HTML content for that page. + +![httphandler](img/httphandler.png) + +##### TraceZ Handler + +For the `TracezZPageHandler` class, the span data from `TracezDataAggregator` will be passed in when +an instance of the class is created. It will later be used to retrieve span information and display +the data in a table. + +##### TraceConfigZ Handler + +For the `TraceConfigzZPageHandler` class, the `TraceConfig` class will be used to change sampling +rate and tracing parameters. + +#### HttpServer +The `HttpServer` is responsible for listening to incoming requests, obtaining requested data, and +rendering corresponding HTML content. The `HttpServer` class from `com.sun.net` will be used to +handle http requests and responses on different routes (users need to ensure that they are using a +version of JDK that includes the `HttpServer` class; this requirement will be added to the README). +Once a request is received by the `HttpServer`, it will invoke the handle function which in turn +invokes the `emitHtml` function to render the HTML content. + +The `HttpServer` class utilizes `com.sun.net.httpserver` to create servers. Users need to ensure +that they are using a version of JDK that comes with the package. + +##### Handling Requests + +![requests-flowchart](img/requests-flowchart.png) + +### Backend + +#### Overview + +The proposed structure is encompassed by the following diagram: + +![span-lifecycle](img/span-lifecycle.png) + +Spans, which are the units of tracing, are monitored by a `SpanProcessor`. The `SpanProcessor` is +exposed to a `DataAggregator`, which can retrieve information about the spans through API calls. +Lastly, the frontend calls functions in the `DataAggregator` to obtain information needed for the +web page. + +#### SpanProcessor + +A `SpanProcessor` watches the lifecycle of each span, and its functions are called every time a span +starts or ends. For the /tracez zPage, I have implemented a `TracezSpanProcessor`, which will +maintain two data structures: a running span cache and a completed span cache. + +##### Visual Diagram + +Below is a visual diagram of the `TracezSpanProcessor` class: + +![span-processor-flowchart](img/span-processor-flowchart.png) + +When a span starts, it is first added to the `runningSpanCache`. Once that span ends, it is removed +from the `runningSpanCache` and added to the `completedSpanCache` as either a latency sample if +there are no errors or an error sample if there are. + +##### Initial Design + +At first, we planned to have both the running span and completed span caches map span IDs to spans. +The problem with this setup was that the number of completed spans would grow without bound. Note +that the number of running spans is naturally limited by the SDK, so the size of the running span +cache will never grow too large at any given time. However, the same cannot be said of the completed +span cache. In order to limit the number of completed spans, we had to consider an alternative data +structure that could impose the necessary limits. + +##### Proposed Design + +To constrain the number of completed spans, we built a new class called `TracezSpanBuckets` and +reworked the completed span cache to map span names to `TracezSpanBuckets` instances. The +`TracezSpanBuckets` class uses FIFO evicting queues to limit the number of latency samples per +bucket to 16 and the number of error samples per bucket to 8. With a hard limit, this setup ensures +that the completed span cache does not grow too large too quickly. For reference, OpenCensus +restricted the number of latency samples per bucket to 10 and the number of error samples per bucket +to 5. + +#### DataAggregator + +The `DataAggregator` restructures the data from the `SpanProcessor` into an accessible format for +the frontend to display. For this, I have implemented a class called `TracezDataAggregator`. This +class is constructed with a `TracezSpanProcessor` instance, so that the `TracezDataAggregator` class +can access the spans collected by a specific `TracezSpanProcessor`. + +##### Proposed Design + +The `TracezDataAggregator`'s purpose is to simplify the frontend's job of displaying information. +Consequently, the class supports functions for retrieving spans names, span counts, along with the +spans themselves. The frontend can then directly use the data collections that are returned. + +##### Accessing the TracezSpanProcessor + +When a user instruments their code, they first create a `SpanProcessor` and then add that instance +to a `TracerSdkProvider` with `addSpanProcessor`. An example from the official quickstart docs is +shown below: + +![quickstart](img/quickstart.png) + +While implementing the `TracezDataAggregator`, we faced the issue of how the backend was supposed to +access a `TracezSpanProcessor` created by the user. At the moment, there is no way for developers to +access the span processor instances that are added to the `TracerSdkProvider`. Consequently, we +propose that the `HttpServer` class add the `TracezSpanProcessor` itself, rather than requiring the +user to create and add the instance. This can be done by replicating the code in the above example: +use the `getTracerProvider` function in the `OpenTelemetrySdk` class and then call addProcessor with +the returned object. Note that `getTracerProvider` in the `OpenTelemetrySdk` class calls +`getTracerProvider` in the `OpenTelemetry` class, which returns a Singleton instance. This means +that the `OpenTelemetrySdk` class should return a Singleton instance as well, so the backend will +get the same `TracerSdkProvider` that the user uses. + +#### TraceConfigZ + +The final component of this project is the /traceconfigz zPage, which allows the user to update the +config for the /tracez zPage in real-time. In OpenTelemetry, updates are already handled by the +TraceSdkProvider class, so we only needed to wire them up and write the corresponding HTML. Since +most of the infrastructure was already built, there was no real design aspect to this. + +### Sequence Diagram + +Below is a sequence diagram of how the frontend and backend components will communicate: + +![sequence-diagram](img/sequence-diagram.png) diff --git a/opentelemetry-java/docs/zpages/img/httphandler.png b/opentelemetry-java/docs/zpages/img/httphandler.png new file mode 100644 index 000000000..f42a36268 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/httphandler.png differ diff --git a/opentelemetry-java/docs/zpages/img/quickstart.png b/opentelemetry-java/docs/zpages/img/quickstart.png new file mode 100644 index 000000000..d4530f1d0 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/quickstart.png differ diff --git a/opentelemetry-java/docs/zpages/img/requests-flowchart.png b/opentelemetry-java/docs/zpages/img/requests-flowchart.png new file mode 100644 index 000000000..01b57a5f1 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/requests-flowchart.png differ diff --git a/opentelemetry-java/docs/zpages/img/sequence-diagram.png b/opentelemetry-java/docs/zpages/img/sequence-diagram.png new file mode 100644 index 000000000..aeb7b0210 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/sequence-diagram.png differ diff --git a/opentelemetry-java/docs/zpages/img/span-lifecycle.png b/opentelemetry-java/docs/zpages/img/span-lifecycle.png new file mode 100644 index 000000000..16827c263 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/span-lifecycle.png differ diff --git a/opentelemetry-java/docs/zpages/img/span-processor-flowchart.png b/opentelemetry-java/docs/zpages/img/span-processor-flowchart.png new file mode 100644 index 000000000..eea13c898 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/span-processor-flowchart.png differ diff --git a/opentelemetry-java/docs/zpages/img/traceconfigz.png b/opentelemetry-java/docs/zpages/img/traceconfigz.png new file mode 100644 index 000000000..0047e1ab8 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/traceconfigz.png differ diff --git a/opentelemetry-java/docs/zpages/img/tracez-details.png b/opentelemetry-java/docs/zpages/img/tracez-details.png new file mode 100644 index 000000000..d45a01391 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/tracez-details.png differ diff --git a/opentelemetry-java/docs/zpages/img/tracez-table.png b/opentelemetry-java/docs/zpages/img/tracez-table.png new file mode 100644 index 000000000..c9605fe38 Binary files /dev/null and b/opentelemetry-java/docs/zpages/img/tracez-table.png differ diff --git a/opentelemetry-java/examples/README.md b/opentelemetry-java/examples/README.md new file mode 100644 index 000000000..bce0efd53 --- /dev/null +++ b/opentelemetry-java/examples/README.md @@ -0,0 +1,40 @@ +# Java OpenTelemetry Examples + +This module contains a set of fully-functional, working examples of using the OpenTelemetry Java +APIs and SDK that should all be able to be run locally. Some of them assume you have docker +running on your local machine. + +## Example modules: + +- [Using the SDK AutoConfiguration module](autoconfigure) + - This module contains a fully-functional example of using the autoconfigure SDK extension module to + configure the SDK using only environment variables (or system properties). + - Note: the `opentelemetry-sdk-extension-autoconfigure` module is still experimental at this time. +- [Setting up OTLP exporters](otlp) + - OTLP is the OpenTelemetry Protocol. This module will demonstrate how to configure the OTLP exporters, + and send data to the OpenTelemetry collector using them. + - Note: this example requires having docker installed to run the example. +- [Configuring the Jaeger Exporter](jaeger) + - This module contains a fully-functional example of configuring the OpenTelemetry SDK to use a + Jaeger exporter, and send some spans to it using the OpenTelemetry API. + - Note: this example requires having docker installed to run the example. +- [Setting up the Zipkin exporter](zipkin) + - This module contains a fully-functional example of configuring the OpenTelemetry SDK to use a + Jaeger exporter, and send some spans to a zipkin backend using the OpenTelemetry API. + - Note: this example requires having docker installed to run the example. +- [Configuring the Logging Exporters](logging) + - This module contains a fully-functional example of configuring the OpenTelemetry SDK to use a + logging exporter. +- [Manually Configuring the SDK](sdk-usage) + - This module shows some concrete examples of manually configuring the Java OpenTelemetry SDK for Tracing. +- [Using the OpenTelemetry metrics API](metrics) + - This module contains examples of using the (still experimental) OpenTelemetry metrics APIs. +- [Setting up the Prometheus exporter](prometheus) + - The module shows how to configure the OpenTelemetry SDK to expose an endpoint that can be scraped + by Prometheus. + - Note: this example uses experimental metrics APIs and SDK. +- [Manual instrumentation of GRPC](grpc) + - This module provides an example of writing manual instrumentation for GRPC, both client and + server. + - Note that if you want to use more production-ready instrumentation for GRPC, this is provided + as a part of the [OpenTelemetry Java Instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation) project. diff --git a/opentelemetry-java/examples/autoconfigure/README.md b/opentelemetry-java/examples/autoconfigure/README.md new file mode 100644 index 000000000..db7c9bd7c --- /dev/null +++ b/opentelemetry-java/examples/autoconfigure/README.md @@ -0,0 +1,47 @@ +# SDK autoconfiguration example + +This is a simple example that demonstrates the usage of +the [OpenTelemetry SDK Autoconfigure](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure) +module. + +## Prerequisites + +* Java 1.8 + +## How to run + +First build this example application: + +```shell +../gradlew shadowJar +``` + +Then start the example application with the logging exporter configured: + +```shell +java -Dotel.traces.exporter=logging \ + -cp build/libs/opentelemetry-examples-autoconfigure-0.1.0-SNAPSHOT-all.jar \ + io.opentelemetry.example.autoconfigure.AutoConfigExample +``` + +Alternatively, instead of system properties you can use environment variables: + +```shell +export OTEL_TRACES_EXPORTER=logging + +java -cp build/libs/opentelemetry-examples-autoconfigure-0.1.0-SNAPSHOT-all.jar \ + io.opentelemetry.example.autoconfigure.AutoConfigExample +``` + +Full documentation of all supported properties can be found in +the [OpenTelemetry SDK Autoconfigure README](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure). + +After running the app you should see the trace printed out in the console: + +``` +... +INFO: 'important work' : ca3938a5793f6f9aba5c757f536a50cb b5e826c981112198 INTERNAL [tracer: io.opentelemetry.example.autoconfigure.AutoConfigExample:] AttributesMap{data={foo=42, bar=a string!}, capacity=128, totalAddedValues=2} +... +``` + +Congratulations! You are now collecting traces using OpenTelemetry. \ No newline at end of file diff --git a/opentelemetry-java/examples/autoconfigure/build.gradle b/opentelemetry-java/examples/autoconfigure/build.gradle new file mode 100644 index 000000000..3758b6671 --- /dev/null +++ b/opentelemetry-java/examples/autoconfigure/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' +} + +description = "OpenTelemetry Examples for SDK autoconfiguration" +ext.moduleName = "io.opentelemetry.examples.autoconfigure" + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-exporter-logging") + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") +} diff --git a/opentelemetry-java/examples/autoconfigure/src/main/java/io/opentelemetry/example/autoconfigure/AutoConfigExample.java b/opentelemetry-java/examples/autoconfigure/src/main/java/io/opentelemetry/example/autoconfigure/AutoConfigExample.java new file mode 100644 index 000000000..d35d47262 --- /dev/null +++ b/opentelemetry-java/examples/autoconfigure/src/main/java/io/opentelemetry/example/autoconfigure/AutoConfigExample.java @@ -0,0 +1,47 @@ +package io.opentelemetry.example.autoconfigure; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration; + +/** + * An example of using {@link io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration} + * and logging exporter: {@link io.opentelemetry.exporter.logging.LoggingSpanExporter}. + */ +public final class AutoConfigExample { + private static final String INSTRUMENTATION_NAME = AutoConfigExample.class.getName(); + + public static void main(String[] args) throws InterruptedException { + // Turn off metrics for this example + // TODO: this won't be needed after 1.1.0 release + System.setProperty("otel.metrics.exporter", "none"); + + // Let the SDK configure itself using environment variables and system properties + OpenTelemetry openTelemetry = OpenTelemetrySdkAutoConfiguration.initialize(); + + AutoConfigExample example = new AutoConfigExample(openTelemetry); + // Do some real work that'll emit telemetry + example.doWork(); + } + + private final Tracer tracer; + + public AutoConfigExample(OpenTelemetry openTelemetry) { + this.tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME); + } + + public void doWork() throws InterruptedException { + Span span = + tracer + .spanBuilder("important work") + .setAttribute("foo", 42) + .setAttribute("bar", "a string!") + .startSpan(); + try { + Thread.sleep(1000); + } finally { + span.end(); + } + } +} diff --git a/opentelemetry-java/examples/build.gradle b/opentelemetry-java/examples/build.gradle new file mode 100644 index 000000000..b02c8000f --- /dev/null +++ b/opentelemetry-java/examples/build.gradle @@ -0,0 +1,48 @@ +plugins { + id "com.diffplug.spotless" + id "com.github.johnrengelman.shadow" apply false +} + +println("Building against OpenTelemetry version: ${project.properties["io.opentelemetry.version"]}") + +subprojects { + apply plugin: 'eclipse' + apply plugin: 'java' + apply plugin: 'java-library' + apply plugin: 'idea' + apply plugin: 'com.diffplug.spotless' + apply plugin: 'com.github.johnrengelman.shadow' + + group = "io.opentelemetry" + version = "0.1.0-SNAPSHOT" + + ext { + openTelemetryVersion = "1.2.0" + openTelemetryAlphaVersion = "1.2.0-alpha" + + grpcVersion = '1.34.1' + protobufVersion = '3.11.4' + protocVersion = protobufVersion + } + + repositories { + mavenCentral() + maven { + // Add snapshot repository + url "https://oss.sonatype.org/content/repositories/snapshots" + } + } + + dependencies { + implementation platform("io.opentelemetry:opentelemetry-bom:${openTelemetryVersion}") + implementation platform("io.opentelemetry:opentelemetry-bom-alpha:${openTelemetryAlphaVersion}") + implementation platform("io.grpc:grpc-bom:${grpcVersion}") + } + + spotless { + java { + targetExclude '**/generated/**' + googleJavaFormat("1.9") + } + } +} diff --git a/opentelemetry-java/examples/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java/examples/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..c9d55ea1c --- /dev/null +++ b/opentelemetry-java/examples/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637 +size 59203 diff --git a/opentelemetry-java/examples/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java/examples/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..5d78686df --- /dev/null +++ b/opentelemetry-java/examples/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip +distributionSha256Sum=fd591a34af7385730970399f473afabdb8b28d57fd97d6625c388d090039d6fd +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/opentelemetry-java/examples/gradlew b/opentelemetry-java/examples/gradlew new file mode 100755 index 000000000..2fe81a7d9 --- /dev/null +++ b/opentelemetry-java/examples/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java/examples/gradlew.bat b/opentelemetry-java/examples/gradlew.bat new file mode 100644 index 000000000..9618d8d96 --- /dev/null +++ b/opentelemetry-java/examples/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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="-Xmx64m" "-Xms64m" + +@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/opentelemetry-java/examples/grpc/README.md b/opentelemetry-java/examples/grpc/README.md new file mode 100644 index 000000000..5e54b1ff4 --- /dev/null +++ b/opentelemetry-java/examples/grpc/README.md @@ -0,0 +1,33 @@ +# gRPC Example + +**Note:** This is an advanced scenario useful for people that want to *manually* instrument their own code. + +This example demonstrates how to use the OpenTelemetry API to instrument normal and streamed gRPC calls. +The example creates the **Root Span** on the client and sends the distributed context +over the gRPC request. On the server side, the example shows how to extract the context +and create a **Child Span**. + +# How to run + +## Prerequisites +* Java 1.8 + +## 1 - Compile +```shell script +../gradlew shadowJar +``` + +## 2 - Start the Server +```shell script +java -cp ./build/libs/opentelemetry-examples-grpc-0.1.0-SNAPSHOT-all.jar io.opentelemetry.example.grpc.HelloWorldServer +``` + +## 3 - Start the normal Client +```shell script +java -cp ./build/libs/opentelemetry-examples-grpc-0.1.0-SNAPSHOT-all.jar io.opentelemetry.example.grpc.HelloWorldClient +``` + +## 4 - Start the streamed Client +```shell script +java -cp ./build/libs/opentelemetry-examples-grpc-all-0.1.0-SNAPSHOT.jar io.opentelemetry.example.grpc.HelloWorldClientStream +``` \ No newline at end of file diff --git a/opentelemetry-java/examples/grpc/build.gradle b/opentelemetry-java/examples/grpc/build.gradle new file mode 100644 index 000000000..0d5405a43 --- /dev/null +++ b/opentelemetry-java/examples/grpc/build.gradle @@ -0,0 +1,45 @@ +plugins { + id "com.google.protobuf" +} + +description = 'OpenTelemetry Examples for gRPC' +ext.moduleName = "io.opentelemetry.examples.grpc" + +dependencies { + implementation "io.opentelemetry:opentelemetry-api" + implementation "io.opentelemetry:opentelemetry-sdk" + implementation "io.opentelemetry:opentelemetry-exporter-logging" + + //alpha module + implementation "io.opentelemetry:opentelemetry-semconv" + + implementation "io.grpc:grpc-protobuf" + implementation "io.grpc:grpc-stub" + implementation "io.grpc:grpc-netty-shaded" + + if (JavaVersion.current().isJava9Compatible()) { + // Workaround for @javax.annotation.Generated + // see: https://github.com/grpc/grpc-java/issues/3633 + compileOnly "javax.annotation:javax.annotation-api:1.3.2" + } +} + +protobuf { + protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" } + plugins { + grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } +} + +// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. +sourceSets { + main { + java { + srcDirs 'build/generated/source/proto/main/grpc' + srcDirs 'build/generated/source/proto/main/java' + } + } +} diff --git a/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/ExampleConfiguration.java b/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/ExampleConfiguration.java new file mode 100644 index 000000000..cfdd76e6f --- /dev/null +++ b/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/ExampleConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.grpc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; + +class ExampleConfiguration { + + static OpenTelemetry initOpenTelemetry() { + + // Set to process the spans with the LoggingSpanExporter + LoggingSpanExporter exporter = new LoggingSpanExporter(); + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + + OpenTelemetrySdk openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + // install the W3C Trace Context propagator + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build(); + + // it's always a good idea to shutdown the SDK when your process exits. + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + System.err.println( + "*** forcing the Span Exporter to shutdown and process the remaining spans"); + sdkTracerProvider.shutdown(); + System.err.println("*** Trace Exporter shut down"); + })); + + return openTelemetrySdk; + } +} diff --git a/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldClient.java b/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldClient.java new file mode 100644 index 000000000..fc05fd7d6 --- /dev/null +++ b/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldClient.java @@ -0,0 +1,139 @@ +/* + * Copyright 2015 The gRPC Authors + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.grpc; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.StatusRuntimeException; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class HelloWorldClient { + private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName()); + private final ManagedChannel channel; + private final String serverHostname; + private final Integer serverPort; + private final GreeterGrpc.GreeterBlockingStub blockingStub; + + // it is important to initialize the OpenTelemetry SDK as early as possible in your application's + // lifecycle. + private static final OpenTelemetry openTelemetry = ExampleConfiguration.initOpenTelemetry(); + + // OTel Tracing API + private final Tracer tracer = + openTelemetry.getTracer("io.opentelemetry.example.HelloWorldClient"); + // Share context via text headers + private final TextMapPropagator textFormat = + openTelemetry.getPropagators().getTextMapPropagator(); + // Inject context into the gRPC request metadata + private final TextMapSetter setter = + (carrier, key, value) -> + carrier.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value); + + /** Construct client connecting to HelloWorld server at {@code host:port}. */ + public HelloWorldClient(String host, int port) { + this.serverHostname = host; + this.serverPort = port; + this.channel = + ManagedChannelBuilder.forAddress(host, port) + // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid + // needing certificates. + .usePlaintext() + // Intercept the request to tag the span context + .intercept(new OpenTelemetryClientInterceptor()) + .build(); + blockingStub = GreeterGrpc.newBlockingStub(channel); + // Initialize the OTel tracer + } + + 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 + " ..."); + + // Start a span + Span span = + tracer.spanBuilder("helloworld.Greeter/SayHello").setSpanKind(SpanKind.CLIENT).startSpan(); + span.setAttribute("component", "grpc"); + span.setAttribute("rpc.service", "Greeter"); + span.setAttribute("net.peer.ip", this.serverHostname); + span.setAttribute("net.peer.port", this.serverPort); + + // Set the context with the current span + try (Scope scope = span.makeCurrent()) { + HelloRequest request = HelloRequest.newBuilder().setName(name).build(); + try { + HelloReply response = blockingStub.sayHello(request); + logger.info("Greeting: " + response.getMessage()); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); + span.setStatus(StatusCode.ERROR, "gRPC status: " + e.getStatus()); + } + } finally { + span.end(); + } + } + + public final class OpenTelemetryClientInterceptor implements ClientInterceptor { + + @Override + public ClientCall interceptCall( + MethodDescriptor methodDescriptor, CallOptions callOptions, Channel channel) { + return new ForwardingClientCall.SimpleForwardingClientCall<>( + channel.newCall(methodDescriptor, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + // Inject the request with the current context + textFormat.inject(Context.current(), headers, setter); + // Perform the gRPC request + super.start(responseListener, headers); + } + }; + } + } + + /** + * 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 Exception { + // Access a service running on the local machine on port 50051 + HelloWorldClient client = new HelloWorldClient("localhost", 50051); + try { + String user = "World"; + // Use the arg as the name to greet if provided + if (args.length > 0) { + user = args[0]; + } + for (int i = 0; i < 10; i++) { + client.greet(user + " " + i); + } + } finally { + client.shutdown(); + } + } +} diff --git a/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldClientStream.java b/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldClientStream.java new file mode 100644 index 000000000..b66dd95d7 --- /dev/null +++ b/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldClientStream.java @@ -0,0 +1,197 @@ +/* + * Copyright 2015 The gRPC Authors + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.grpc; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class HelloWorldClientStream { + private static final Logger logger = Logger.getLogger(HelloWorldClientStream.class.getName()); + private final ManagedChannel channel; + private final String serverHostname; + private final Integer serverPort; + private final GreeterGrpc.GreeterStub asyncStub; + + // Export spans as log entries + private static final LoggingSpanExporter exporter = new LoggingSpanExporter(); + // OTel API + private static final OpenTelemetry openTelemetry = initOpenTelemetry(exporter); + + private final Tracer tracer = + openTelemetry.getTracer("io.opentelemetry.example.HelloWorldClient"); + // Share context via text headers + private final TextMapPropagator textFormat = + openTelemetry.getPropagators().getTextMapPropagator(); + // Inject context into the gRPC request metadata + private final TextMapSetter setter = + (carrier, key, value) -> + carrier.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value); + + /** Construct client connecting to HelloWorld server at {@code host:port}. */ + public HelloWorldClientStream(String host, int port) { + this.serverHostname = host; + this.serverPort = port; + this.channel = + ManagedChannelBuilder.forAddress(host, port) + // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid + // needing certificates. + .usePlaintext() + // Intercept the request to tag the span context + .intercept(new OpenTelemetryClientInterceptor()) + .build(); + asyncStub = GreeterGrpc.newStub(channel); + // Initialize the OTel tracer + } + + public void shutdown() throws InterruptedException { + channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + /** Say hello to server. */ + public void greet(List names) { + logger.info("Will try to greet " + Arrays.toString(names.toArray()) + " ..."); + + // Start a span + Span span = + tracer.spanBuilder("helloworld.Greeter/SayHello").setSpanKind(SpanKind.CLIENT).startSpan(); + span.setAttribute("component", "grpc"); + span.setAttribute(SemanticAttributes.RPC_SERVICE, "Greeter"); + span.setAttribute(SemanticAttributes.NET_HOST_IP, this.serverHostname); + span.setAttribute(SemanticAttributes.NET_PEER_PORT, this.serverPort); + + StreamObserver requestObserver; + + // Set the context with the current span + try (Scope scope = span.makeCurrent()) { + HelloReplyStreamObserver replyObserver = new HelloReplyStreamObserver(); + requestObserver = asyncStub.sayHelloStream(replyObserver); + for (String name : names) { + try { + requestObserver.onNext(HelloRequest.newBuilder().setName(name).build()); + // Sleep for a bit before sending the next one. + Thread.sleep(500); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "RPC failed: {0}", e.getMessage()); + requestObserver.onError(e); + } + } + requestObserver.onCompleted(); + span.addEvent("Done sending"); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); + span.setStatus(StatusCode.ERROR, "gRPC status: " + e.getStatus()); + } finally { + span.end(); + } + } + + private static class HelloReplyStreamObserver implements StreamObserver { + + public HelloReplyStreamObserver() { + logger.info("Greeting: "); + } + + @Override + public void onNext(HelloReply value) { + Span span = Span.current(); + span.addEvent("Data received: " + value.getMessage()); + logger.info(value.getMessage()); + } + + @Override + public void onError(Throwable t) { + Span span = Span.current(); + logger.log(Level.WARNING, "RPC failed: {0}", t.getMessage()); + span.setStatus(StatusCode.ERROR, "gRPC status: " + t.getMessage()); + } + + @Override + public void onCompleted() { + // Since onCompleted is async and the span.end() is called in the main thread, + // it is recommended to set the span Status in the main thread. + } + } + + public final class OpenTelemetryClientInterceptor implements ClientInterceptor { + + @Override + public ClientCall interceptCall( + MethodDescriptor methodDescriptor, CallOptions callOptions, Channel channel) { + return new ForwardingClientCall.SimpleForwardingClientCall<>( + channel.newCall(methodDescriptor, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + // Inject the request with the current context + textFormat.inject(Context.current(), headers, setter); + // Perform the gRPC request + super.start(responseListener, headers); + } + }; + } + } + + private static OpenTelemetry initOpenTelemetry(LoggingSpanExporter exporter) { + // Set to process the the spans by the LogExporter + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + + // install the W3C Trace Context propagator + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build(); + } + + /** + * 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 Exception { + // Access a service running on the local machine on port 50051 + HelloWorldClientStream client = new HelloWorldClientStream("localhost", 50051); + try { + List users = Arrays.asList("world", "this", "is", "a", "list", "of", "names"); + // Use the arg as the name to greet if provided + if (args.length > 0) { + users = Arrays.asList(args); + } + client.greet(users); + } finally { + client.shutdown(); + } + } +} diff --git a/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldServer.java b/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldServer.java new file mode 100644 index 000000000..236771be7 --- /dev/null +++ b/opentelemetry-java/examples/grpc/src/main/java/io/opentelemetry/example/grpc/HelloWorldServer.java @@ -0,0 +1,169 @@ +/* + * Copyright 2015 The gRPC Authors + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.grpc; + +import io.grpc.Contexts; +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.logging.Logger; + +/** Server that manages startup/shutdown of a {@code Greeter} server. */ +public final class HelloWorldServer { + private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName()); + + private static final int PORT = 50051; + + // it is important to initialize the OpenTelemetry SDK as early as possible in your application's + // lifecycle. + private static final OpenTelemetry openTelemetry = ExampleConfiguration.initOpenTelemetry(); + + // Extract the Distributed Context from the gRPC metadata + private static final TextMapGetter getter = + new TextMapGetter<>() { + @Override + public Iterable keys(Metadata carrier) { + return carrier.keys(); + } + + @Override + public String get(Metadata carrier, String key) { + Metadata.Key k = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + if (carrier.containsKey(k)) { + return carrier.get(k); + } + return ""; + } + }; + + private Server server; + + private final Tracer tracer = + openTelemetry.getTracer("io.opentelemetry.example.HelloWorldServer"); + private final TextMapPropagator textFormat = + openTelemetry.getPropagators().getTextMapPropagator(); + + private void start() throws IOException { + /* The port on which the server should run */ + + server = + ServerBuilder.forPort(PORT) + .addService(new GreeterImpl()) + // Intercept gRPC calls + .intercept(new OpenTelemetryServerInterceptor()) + .build() + .start(); + logger.info("Server started, listening on " + PORT); + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + // 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(); + } + } + + static class GreeterImpl extends GreeterGrpc.GreeterImplBase { + + // We serve a normal gRPC call + @Override + public void sayHello(HelloRequest req, StreamObserver responseObserver) { + // Serve the request + HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + // We serve a stream gRPC call + @Override + public StreamObserver sayHelloStream( + final StreamObserver responseObserver) { + return new StreamObserver<>() { + @Override + public void onNext(HelloRequest value) { + responseObserver.onNext( + HelloReply.newBuilder().setMessage("Hello " + value.getName()).build()); + } + + @Override + public void onError(Throwable t) { + logger.info("[Error] " + t.getMessage()); + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + } + + private class OpenTelemetryServerInterceptor implements io.grpc.ServerInterceptor { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + // Extract the Span Context from the metadata of the gRPC request + Context extractedContext = textFormat.extract(Context.current(), headers, getter); + InetSocketAddress clientInfo = + (InetSocketAddress) call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + // Build a span based on the received context + Span span = + tracer + .spanBuilder("helloworld.Greeter/SayHello") + .setParent(extractedContext) + .setSpanKind(SpanKind.SERVER) + .startSpan(); + try (Scope innerScope = span.makeCurrent()) { + span.setAttribute("component", "grpc"); + span.setAttribute("rpc.service", "Greeter"); + span.setAttribute("net.peer.ip", clientInfo.getHostString()); + span.setAttribute("net.peer.port", clientInfo.getPort()); + // Process the gRPC call normally + return Contexts.interceptCall(io.grpc.Context.current(), call, headers, next); + } finally { + span.end(); + } + } + } + + /** Main launches the server from the command line. */ + public static void main(String[] args) throws IOException, InterruptedException { + final HelloWorldServer server = new HelloWorldServer(); + server.start(); + server.blockUntilShutdown(); + } +} diff --git a/opentelemetry-java/examples/grpc/src/main/proto/helloworld.proto b/opentelemetry-java/examples/grpc/src/main/proto/helloworld.proto new file mode 100644 index 000000000..a77520723 --- /dev/null +++ b/opentelemetry-java/examples/grpc/src/main/proto/helloworld.proto @@ -0,0 +1,38 @@ +// Copyright 2015 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.opentelemetry.example.grpc"; +option java_outer_classname = "HelloWorldProto"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc SayHelloStream (stream HelloRequest) returns (stream 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/opentelemetry-java/examples/http/README.md b/opentelemetry-java/examples/http/README.md new file mode 100644 index 000000000..329739c00 --- /dev/null +++ b/opentelemetry-java/examples/http/README.md @@ -0,0 +1,30 @@ +# HTTP Example + +**Note:** This is an advanced scenario useful for people that want to *manually* instrument their own code. + +This is a simple example that demonstrates how to use the OpenTelemetry SDK +to *manually* instrument a simple HTTP based Client/Server application. +The example creates the **Root Span** on the client and sends the context +over the HTTP request. On the server side, the example shows how to extract the context +and create a **Child Span** with attached a **Span Event**. + +# How to run + +## Prerequisites +* Java 1.8.231 +* Be on the project root folder + +## 1 - Compile +```shell script +../gradlew shadowJar +``` + +## 2 - Start the Server +```shell script +java -cp ./build/libs/opentelemetry-examples-http-0.1.0-SNAPSHOT-all.jar io.opentelemetry.example.http.HttpServer +``` + +## 3 - Start the Client +```shell script +java -cp ./build/libs/opentelemetry-examples-http-0.1.0-SNAPSHOT-all.jar io.opentelemetry.example.http.HttpClient +``` \ No newline at end of file diff --git a/opentelemetry-java/examples/http/build.gradle b/opentelemetry-java/examples/http/build.gradle new file mode 100644 index 000000000..8fb5124de --- /dev/null +++ b/opentelemetry-java/examples/http/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' +} + +description = 'OpenTelemetry Examples for HTTP' +ext.moduleName = "io.opentelemetry.examples.http" + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-exporter-logging") + + //alpha modules + implementation("io.opentelemetry:opentelemetry-semconv") + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") +} diff --git a/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/ExampleConfiguration.java b/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/ExampleConfiguration.java new file mode 100644 index 000000000..905a024e1 --- /dev/null +++ b/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/ExampleConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.http; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; + +/** + * All SDK management takes place here, away from the instrumentation code, which should only access + * the OpenTelemetry APIs. + */ +class ExampleConfiguration { + + /** + * Initializes the OpenTelemetry SDK with a logging span exporter and the W3C Trace Context + * propagator. + * + * @return A ready-to-use {@link OpenTelemetry} instance. + */ + static OpenTelemetry initOpenTelemetry() { + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .build(); + + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build(); + + Runtime.getRuntime().addShutdownHook(new Thread(sdkTracerProvider::close)); + return sdk; + } +} diff --git a/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/HttpClient.java b/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/HttpClient.java new file mode 100644 index 000000000..52f4ded37 --- /dev/null +++ b/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/HttpClient.java @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.http; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.Charset; + +public final class HttpClient { + + // it's important to initialize the OpenTelemetry SDK as early in your applications lifecycle as + // possible. + private static final OpenTelemetry openTelemetry = ExampleConfiguration.initOpenTelemetry(); + + private static final Tracer tracer = + openTelemetry.getTracer("io.opentelemetry.example.http.HttpClient"); + private static final TextMapPropagator textMapPropagator = + openTelemetry.getPropagators().getTextMapPropagator(); + + // Export traces to log + // Inject the span context into the request + private static final TextMapSetter setter = URLConnection::setRequestProperty; + + private void makeRequest() throws IOException, URISyntaxException { + int port = 8080; + URL url = new URL("http://127.0.0.1:" + port); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + + int status = 0; + StringBuilder content = new StringBuilder(); + + // Name convention for the Span is not yet defined. + // See: https://github.com/open-telemetry/opentelemetry-specification/issues/270 + Span span = tracer.spanBuilder("/").setSpanKind(SpanKind.CLIENT).startSpan(); + try (Scope scope = span.makeCurrent()) { + span.setAttribute(SemanticAttributes.HTTP_METHOD, "GET"); + span.setAttribute("component", "http"); + /* + Only one of the following is required + - http.url + - http.scheme, http.host, http.target + - http.scheme, peer.hostname, peer.port, http.target + - http.scheme, peer.ip, peer.port, http.target + */ + URI uri = url.toURI(); + url = + new URI( + uri.getScheme(), + null, + uri.getHost(), + uri.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment()) + .toURL(); + + span.setAttribute(SemanticAttributes.HTTP_URL, url.toString()); + + // Inject the request with the current Context/Span. + textMapPropagator.inject(Context.current(), con, setter); + + try { + // Process the request + con.setRequestMethod("GET"); + status = con.getResponseCode(); + BufferedReader in = + new BufferedReader( + new InputStreamReader(con.getInputStream(), Charset.defaultCharset())); + String inputLine; + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + in.close(); + } catch (Exception e) { + span.setStatus(StatusCode.ERROR, "HTTP Code: " + status); + } + } finally { + span.end(); + } + + // Output the result of the request + System.out.println("Response Code: " + status); + System.out.println("Response Msg: " + content); + } + + /** + * Main method to run the example. + * + * @param args It is not required. + */ + public static void main(String[] args) { + HttpClient httpClient = new HttpClient(); + + // Perform request every 5s + Thread t = + new Thread( + () -> { + while (true) { + try { + httpClient.makeRequest(); + Thread.sleep(5000); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + }); + t.start(); + } +} diff --git a/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/HttpServer.java b/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/HttpServer.java new file mode 100644 index 000000000..0b1bec0f0 --- /dev/null +++ b/opentelemetry-java/examples/http/src/main/java/io/opentelemetry/example/http/HttpServer.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.http; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.Charset; + +public final class HttpServer { + // It's important to initialize your OpenTelemetry SDK as early in your application's lifecycle as + // possible. + private static final OpenTelemetry openTelemetry = ExampleConfiguration.initOpenTelemetry(); + private static final Tracer tracer = + openTelemetry.getTracer("io.opentelemetry.example.http.HttpServer"); + + private static final int port = 8080; + private final com.sun.net.httpserver.HttpServer server; + + // Extract the context from http headers + private static final TextMapGetter getter = + new TextMapGetter<>() { + @Override + public Iterable keys(HttpExchange carrier) { + return carrier.getRequestHeaders().keySet(); + } + + @Override + public String get(HttpExchange carrier, String key) { + if (carrier.getRequestHeaders().containsKey(key)) { + return carrier.getRequestHeaders().get(key).get(0); + } + return ""; + } + }; + + private HttpServer() throws IOException { + this(port); + } + + private HttpServer(int port) throws IOException { + server = com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(port), 0); + // Test urls + server.createContext("/", new HelloHandler()); + server.start(); + System.out.println("Server ready on http://127.0.0.1:" + port); + } + + private static class HelloHandler implements HttpHandler { + + public static final TextMapPropagator TEXT_MAP_PROPAGATOR = + openTelemetry.getPropagators().getTextMapPropagator(); + + @Override + public void handle(HttpExchange exchange) throws IOException { + // Extract the context from the HTTP request + Context context = TEXT_MAP_PROPAGATOR.extract(Context.current(), exchange, getter); + + Span span = + tracer.spanBuilder("GET /").setParent(context).setSpanKind(SpanKind.SERVER).startSpan(); + + try (Scope scope = span.makeCurrent()) { + // Set the Semantic Convention + span.setAttribute("component", "http"); + span.setAttribute("http.method", "GET"); + /* + One of the following is required: + - http.scheme, http.host, http.target + - http.scheme, http.server_name, net.host.port, http.target + - http.scheme, net.host.name, net.host.port, http.target + - http.url + */ + span.setAttribute("http.scheme", "http"); + span.setAttribute("http.host", "localhost:" + HttpServer.port); + span.setAttribute("http.target", "/"); + // Process the request + answer(exchange, span); + } finally { + // Close the span + span.end(); + } + } + + private void answer(HttpExchange exchange, Span span) throws IOException { + // Generate an Event + span.addEvent("Start Processing"); + + // Process the request + String response = "Hello World!"; + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes(Charset.defaultCharset())); + os.close(); + System.out.println("Served Client: " + exchange.getRemoteAddress()); + + // Generate an Event with an attribute + Attributes eventAttributes = Attributes.of(stringKey("answer"), response); + span.addEvent("Finish Processing", eventAttributes); + } + } + + private void stop() { + server.stop(0); + } + + /** + * Main method to run the example. + * + * @param args It is not required. + * @throws Exception Something might go wrong. + */ + public static void main(String[] args) throws Exception { + final HttpServer s = new HttpServer(); + // Gracefully close the server + Runtime.getRuntime().addShutdownHook(new Thread(s::stop)); + } +} diff --git a/opentelemetry-java/examples/jaeger/README.md b/opentelemetry-java/examples/jaeger/README.md new file mode 100644 index 000000000..dc964ffca --- /dev/null +++ b/opentelemetry-java/examples/jaeger/README.md @@ -0,0 +1,36 @@ +# Jaeger Example + +This is a simple example that demonstrates how to use the OpenTelemetry SDK +to instrument a simple application using Jaeger as trace exporter. + +# How to run + +## Prerequisites +* Java 1.8+ +* Docker 19.03 +* Jaeger 1.16 - [Link][jaeger] + + +## 1 - Compile +```shell script +../gradlew shadowJar +``` +## 2 - Run Jaeger + +```shell script +docker run --rm -it --name jaeger\ + -p 16686:16686 \ + -p 14250:14250 \ + jaegertracing/all-in-one:1.16 +``` + + +## 3 - Start the Application +```shell script +java -cp build/libs/opentelemetry-examples-jaeger-0.1.0-SNAPSHOT-all.jar io.opentelemetry.example.jaeger.JaegerExample localhost 14250 +``` +## 4 - Open the Jaeger UI + +Navigate to http://localhost:16686 + +[jaeger]:[https://www.jaegertracing.io/docs/1.16/getting-started/ diff --git a/opentelemetry-java/examples/jaeger/build.gradle b/opentelemetry-java/examples/jaeger/build.gradle new file mode 100644 index 000000000..03a414b78 --- /dev/null +++ b/opentelemetry-java/examples/jaeger/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' +} + +description = 'OpenTelemetry Examples for Jaeger Exporter' +ext.moduleName = "io.opentelemetry.examples.jaeger" + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry.myself:opentelemetry-exporter-jaeger-myself:0.1.0-SNAPSHOT") + + //alpha module + implementation "io.opentelemetry:opentelemetry-semconv" + + implementation("io.grpc:grpc-protobuf") + implementation("io.grpc:grpc-netty-shaded") +} diff --git a/opentelemetry-java/examples/jaeger/src/main/java/io/opentelemetry/example/jaeger/ExampleConfiguration.java b/opentelemetry-java/examples/jaeger/src/main/java/io/opentelemetry/example/jaeger/ExampleConfiguration.java new file mode 100644 index 000000000..177ed8006 --- /dev/null +++ b/opentelemetry-java/examples/jaeger/src/main/java/io/opentelemetry/example/jaeger/ExampleConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.jaeger; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.concurrent.TimeUnit; + +/** + * All SDK management takes place here, away from the instrumentation code, which should only access + * the OpenTelemetry APIs. + */ +class ExampleConfiguration { + + /** + * Initialize an OpenTelemetry SDK with a Jaeger exporter and a SimpleSpanProcessor. + * + * @param jaegerHost The host of your Jaeger instance. + * @param jaegerPort the port of your Jaeger instance. + * @return A ready-to-use {@link OpenTelemetry} instance. + */ + static OpenTelemetry initOpenTelemetry(String jaegerHost, int jaegerPort) { + // Create a channel towards Jaeger end point + ManagedChannel jaegerChannel = + ManagedChannelBuilder.forAddress(jaegerHost, jaegerPort).usePlaintext().build(); + // Export traces to Jaeger + JaegerGrpcSpanExporter jaegerExporter = + JaegerGrpcSpanExporter.builder() + .setChannel(jaegerChannel) + .setTimeout(30, TimeUnit.SECONDS) + .build(); + + Resource serviceNameResource = + Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "otel-jaeger-example")); + + // Set to process the spans by the Jaeger Exporter + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(jaegerExporter)) + .setResource(Resource.getDefault().merge(serviceNameResource)) + .build(); + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + + // it's always a good idea to shut down the SDK cleanly at JVM exit. + Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close)); + + return openTelemetry; + } +} diff --git a/opentelemetry-java/examples/jaeger/src/main/java/io/opentelemetry/example/jaeger/JaegerExample.java b/opentelemetry-java/examples/jaeger/src/main/java/io/opentelemetry/example/jaeger/JaegerExample.java new file mode 100644 index 000000000..a728f32d7 --- /dev/null +++ b/opentelemetry-java/examples/jaeger/src/main/java/io/opentelemetry/example/jaeger/JaegerExample.java @@ -0,0 +1,55 @@ +package io.opentelemetry.example.jaeger; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; + +public final class JaegerExample { + + private final Tracer tracer; + + public JaegerExample(OpenTelemetry openTelemetry) { + tracer = openTelemetry.getTracer("io.opentelemetry.example.JaegerExample"); + } + + private void myWonderfulUseCase() { + // Generate a span + Span span = this.tracer.spanBuilder("Start my wonderful use case").startSpan(); + span.addEvent("Event 0"); + // execute my use case - here we simulate a wait + doWork(); + span.addEvent("Event 1"); + span.end(); + } + + private void doWork() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // do the right thing here + } + } + + public static void main(String[] args) { + // Parsing the input + if (args.length < 2) { + System.out.println("Missing [hostname] [port]"); + System.exit(1); + } + String jaegerHostName = args[0]; + int jaegerPort = Integer.parseInt(args[1]); + + // it is important to initialize your SDK as early as possible in your application's lifecycle + OpenTelemetry openTelemetry = + ExampleConfiguration.initOpenTelemetry(jaegerHostName, jaegerPort); + + // Start the example + JaegerExample example = new JaegerExample(openTelemetry); + // generate a few sample spans + for (int i = 0; i < 10; i++) { + example.myWonderfulUseCase(); + } + + System.out.println("Bye"); + } +} diff --git a/opentelemetry-java/examples/logging/build.gradle b/opentelemetry-java/examples/logging/build.gradle new file mode 100644 index 000000000..82c32679c --- /dev/null +++ b/opentelemetry-java/examples/logging/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' +} + +description = "OpenTelemetry Examples for logging exporters" +ext.moduleName = "io.opentelemetry.examples.logging" + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-exporter-logging") + + //alpha modules + implementation("io.opentelemetry:opentelemetry-api-metrics") +} diff --git a/opentelemetry-java/examples/logging/src/main/java/io/opentelemetry/example/logging/ExampleConfiguration.java b/opentelemetry-java/examples/logging/src/main/java/io/opentelemetry/example/logging/ExampleConfiguration.java new file mode 100644 index 000000000..a06546c49 --- /dev/null +++ b/opentelemetry-java/examples/logging/src/main/java/io/opentelemetry/example/logging/ExampleConfiguration.java @@ -0,0 +1,47 @@ +package io.opentelemetry.example.logging; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.exporter.logging.LoggingMetricExporter; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.Collections; + +/** + * All SDK management takes place here, away from the instrumentation code, which should only access + * the OpenTelemetry APIs. + */ +public final class ExampleConfiguration { + + /** The number of milliseconds between metric exports. */ + private static final long METRIC_EXPORT_INTERVAL_MS = 800L; + + /** + * Initializes an OpenTelemetry SDK with a logging exporter and a SimpleSpanProcessor. + * + * @return A ready-to-use {@link OpenTelemetry} instance. + */ + public static OpenTelemetry initOpenTelemetry() { + // This will be used to create instruments + SdkMeterProvider meterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + + // Create an instance of IntervalMetricReader and configure it + // to read metrics from the meterProvider and export them to the logging exporter + IntervalMetricReader.builder() + .setMetricExporter(new LoggingMetricExporter()) + .setMetricProducers(Collections.singleton(meterProvider)) + .setExportIntervalMillis(METRIC_EXPORT_INTERVAL_MS) + .build(); + + // Tracer provider configured to export spans with SimpleSpanProcessor using + // the logging exporter. + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .build(); + return OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + } +} diff --git a/opentelemetry-java/examples/logging/src/main/java/io/opentelemetry/example/logging/LoggingExporterExample.java b/opentelemetry-java/examples/logging/src/main/java/io/opentelemetry/example/logging/LoggingExporterExample.java new file mode 100644 index 000000000..9cd5f2ef6 --- /dev/null +++ b/opentelemetry-java/examples/logging/src/main/java/io/opentelemetry/example/logging/LoggingExporterExample.java @@ -0,0 +1,64 @@ +package io.opentelemetry.example.logging; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; + +/** + * An example of using {@link io.opentelemetry.exporter.logging.LoggingSpanExporter} and {@link + * io.opentelemetry.exporter.logging.LoggingMetricExporter}. + */ +public final class LoggingExporterExample { + private static final String INSTRUMENTATION_NAME = LoggingExporterExample.class.getName(); + + private final Tracer tracer; + private final LongCounter counter; + + public LoggingExporterExample(OpenTelemetry openTelemetry) { + tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME); + counter = + GlobalMeterProvider.getMeter(INSTRUMENTATION_NAME).longCounterBuilder("work_done").build(); + } + + public void myWonderfulUseCase() { + Span span = this.tracer.spanBuilder("start my wonderful use case").startSpan(); + span.addEvent("Event 0"); + doWork(); + span.addEvent("Event 1"); + span.end(); + } + + private void doWork() { + Span span = this.tracer.spanBuilder("doWork").startSpan(); + try { + Thread.sleep(1000); + counter.add(1); + } catch (InterruptedException e) { + // do the right thing here + } finally { + span.end(); + } + } + + public static void main(String[] args) { + // it is important to initialize your SDK as early as possible in your application's lifecycle + OpenTelemetry oTel = ExampleConfiguration.initOpenTelemetry(); + + // Start the example + LoggingExporterExample example = new LoggingExporterExample(oTel); + // Generate a few sample spans + for (int i = 0; i < 5; i++) { + example.myWonderfulUseCase(); + } + + try { + // Flush out the metrics that have not yet been exported + Thread.sleep(1000L); + } catch (InterruptedException e) { + } + + System.out.println("Bye"); + } +} diff --git a/opentelemetry-java/examples/metrics/build.gradle b/opentelemetry-java/examples/metrics/build.gradle new file mode 100644 index 000000000..917d8da5a --- /dev/null +++ b/opentelemetry-java/examples/metrics/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java' +} + +description = 'OpenTelemetry Examples for metrics' +ext.moduleName = "io.opentelemetry.examples.metrics" + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + + //alpha modules + implementation("io.opentelemetry:opentelemetry-api-metrics") +} diff --git a/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/DoubleCounterExample.java b/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/DoubleCounterExample.java new file mode 100644 index 000000000..69d9ad32b --- /dev/null +++ b/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/DoubleCounterExample.java @@ -0,0 +1,69 @@ +package io.opentelemetry.example.metrics; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import javax.swing.filechooser.FileSystemView; + +/** + * Example of using {@link DoubleCounter} to count disk space used by files with specific + * extensions. + */ +public final class DoubleCounterExample { + private static final OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + private static final Tracer tracer = + openTelemetry.getTracer("io.opentelemetry.example.metrics", "0.13.1"); + private static final Meter sampleMeter = + GlobalMeterProvider.get().get("io.opentelemetry.example.metrics", "0.13.1"); + private static final File directoryToCountIn = + FileSystemView.getFileSystemView().getHomeDirectory(); + private static final DoubleCounter diskSpaceCounter = + sampleMeter + .doubleCounterBuilder("calculated_used_space") + .setDescription("Counts disk space used by file extension.") + .setUnit("MB") + .build(); + + public static void main(String[] args) { + Span span = tracer.spanBuilder("calculate space").setSpanKind(SpanKind.INTERNAL).startSpan(); + DoubleCounterExample example = new DoubleCounterExample(); + try (Scope scope = span.makeCurrent()) { + List extensionsToFind = new ArrayList<>(); + extensionsToFind.add("dll"); + extensionsToFind.add("png"); + extensionsToFind.add("exe"); + example.calculateSpaceUsedByFilesWithExtension(extensionsToFind, directoryToCountIn); + } catch (Exception e) { + span.setStatus(StatusCode.ERROR, "Error while calculating used space"); + } finally { + span.end(); + } + } + + public void calculateSpaceUsedByFilesWithExtension(List extensions, File directory) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + for (String extension : extensions) { + if (file.getName().endsWith("." + extension)) { + // we can add values to the counter for specific labels + // the label key is "file_extension", its value is the name of the extension + diskSpaceCounter.add( + (double) file.length() / 1_000_000, Labels.of("file_extension", extension)); + } + } + } + } + } +} diff --git a/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/LongCounterExample.java b/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/LongCounterExample.java new file mode 100644 index 000000000..fbb668408 --- /dev/null +++ b/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/LongCounterExample.java @@ -0,0 +1,68 @@ +package io.opentelemetry.example.metrics; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import java.io.File; +import javax.swing.filechooser.FileSystemView; + +/** + * Example of using {@link LongCounter} and {@link BoundLongCounter} to count searched directories. + */ +public final class LongCounterExample { + private static final OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + private static final Tracer tracer = + openTelemetry.getTracer("io.opentelemetry.example.metrics", "0.13.1"); + + private static final Meter sampleMeter = + GlobalMeterProvider.getMeter("io.opentelemetry.example.metrics", "0.13.1"); + private static final LongCounter directoryCounter = + sampleMeter + .longCounterBuilder("directories_search_count") + .setDescription("Counts directories accessed while searching for files.") + .setUnit("unit") + .build(); + private static final File homeDirectory = FileSystemView.getFileSystemView().getHomeDirectory(); + // we can use BoundCounters to not specify labels each time + private static final BoundLongCounter homeDirectoryCounter = + directoryCounter.bind(Labels.of("root directory", homeDirectory.getName())); + + public static void main(String[] args) { + Span span = tracer.spanBuilder("workflow").setSpanKind(SpanKind.INTERNAL).startSpan(); + LongCounterExample example = new LongCounterExample(); + try (Scope scope = span.makeCurrent()) { + homeDirectoryCounter.add(1); // count root directory + example.findFile("file_to_find.txt", homeDirectory); + } catch (Exception e) { + span.setStatus(StatusCode.ERROR, "Error while finding file"); + } finally { + span.end(); + } + } + + public void findFile(String name, File directory) { + File[] files = directory.listFiles(); + System.out.println("Currently looking at " + directory.getAbsolutePath()); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + // we don't have to specify the value for the "root directory" label again + // since this is a BoundLongCounter with pre-set labels + homeDirectoryCounter.add(1); + findFile(name, file); + } else if (name.equalsIgnoreCase(file.getName())) { + System.out.println(file.getParentFile()); + } + } + } + } +} diff --git a/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/LongValueObserverExample.java b/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/LongValueObserverExample.java new file mode 100644 index 000000000..2afdbe728 --- /dev/null +++ b/opentelemetry-java/examples/metrics/src/main/java/io/opentelemetry/example/metrics/LongValueObserverExample.java @@ -0,0 +1,26 @@ +package io.opentelemetry.example.metrics; + +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.LongValueObserver; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; + +/** + * Example of using {@link LongValueObserver} to measure execution time of method. Setting the + * {@link LongValueObserver} updater sets a callback that gets executed every collection interval. + * Useful for expensive measurements that would be wastefully to calculate each request. + */ +public final class LongValueObserverExample { + + public static void main(String[] args) { + Meter sampleMeter = GlobalMeterProvider.getMeter("io.opentelemetry.example.metrics", "0.13.1"); + LongValueObserver observer = + sampleMeter + .longValueObserverBuilder("jvm.memory.total") + .setDescription("Reports JVM memory usage.") + .setUnit("byte") + .setUpdater( + result -> result.observe(Runtime.getRuntime().totalMemory(), Labels.empty())) + .build(); + } +} diff --git a/opentelemetry-java/examples/otlp/build.gradle b/opentelemetry-java/examples/otlp/build.gradle new file mode 100644 index 000000000..54b68bc8e --- /dev/null +++ b/opentelemetry-java/examples/otlp/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' +} + +description = 'OpenTelemetry Example for OTLP Exporters' +ext.moduleName = "io.opentelemetry.examples.otlp" + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-exporter-otlp") + + //pull in the autoconfigure extension so we parse the `otel.resource.attributes` system property used in the example. + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + implementation("io.opentelemetry:opentelemetry-api-metrics") + implementation("io.opentelemetry:opentelemetry-sdk-metrics") + implementation("io.opentelemetry:opentelemetry-exporter-otlp-metrics") + + //external dependency for the grpc transport implementation + implementation "io.grpc:grpc-netty-shaded" +} diff --git a/opentelemetry-java/examples/otlp/docker/.env b/opentelemetry-java/examples/otlp/docker/.env new file mode 100644 index 000000000..e25dbb814 --- /dev/null +++ b/opentelemetry-java/examples/otlp/docker/.env @@ -0,0 +1,2 @@ +OTELCOL_IMG=otel/opentelemetry-collector-dev:latest +OTELCOL_ARGS= diff --git a/opentelemetry-java/examples/otlp/docker/README.md b/opentelemetry-java/examples/otlp/docker/README.md new file mode 100644 index 000000000..4db17ee28 --- /dev/null +++ b/opentelemetry-java/examples/otlp/docker/README.md @@ -0,0 +1,25 @@ +# OpenTelemetry Collector Demo + +*IMPORTANT:* This uses a pre-released version of the OpenTelemetry Collector. + +This demo uses `docker-compose` and by default runs against the +`otel/opentelemetry-collector-dev:latest` image. To run the demo, switch +to this directory and run: + +```shell +docker-compose up -d +``` + +The demo exposes the following backends: + +- Jaeger at http://0.0.0.0:16686 +- Zipkin at http://0.0.0.0:9411 +- Prometheus at http://0.0.0.0:9090 + +Notes: + +- It may take some time for the application metrics to appear on the Prometheus + dashboard; + +To clean up any docker container from the demo run `docker-compose down` from +this directory. diff --git a/opentelemetry-java/examples/otlp/docker/docker-compose.yaml b/opentelemetry-java/examples/otlp/docker/docker-compose.yaml new file mode 100644 index 000000000..3e067e4fa --- /dev/null +++ b/opentelemetry-java/examples/otlp/docker/docker-compose.yaml @@ -0,0 +1,42 @@ +version: "2" +services: + + # Jaeger + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268" + - "14250" + + # Zipkin + zipkin-all-in-one: + image: openzipkin/zipkin:latest + ports: + - "9411:9411" + + # Collector + otel-collector: + image: ${OTELCOL_IMG} + command: ["--config=/etc/otel-collector-config-demo.yaml", "${OTELCOL_ARGS}"] + volumes: + - ./otel-collector-config-demo.yaml:/etc/otel-collector-config-demo.yaml + ports: + - "1888:1888" # pprof extension + - "8888:8888" # Prometheus metrics exposed by the collector + - "8889:8889" # Prometheus exporter metrics + - "13133:13133" # health_check extension + - "55678" # OpenCensus receiver + - "55681:55679" # zpages extension + - "4317:4317" # otlp receiver + depends_on: + - jaeger-all-in-one + - zipkin-all-in-one + + prometheus: + container_name: prometheus + image: prom/prometheus:latest + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" diff --git a/opentelemetry-java/examples/otlp/docker/otel-collector-config-demo.yaml b/opentelemetry-java/examples/otlp/docker/otel-collector-config-demo.yaml new file mode 100644 index 000000000..9e0dcf484 --- /dev/null +++ b/opentelemetry-java/examples/otlp/docker/otel-collector-config-demo.yaml @@ -0,0 +1,49 @@ +receivers: + otlp: + protocols: + grpc: + +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + namespace: promexample + const_labels: + label1: value1 + logging: + loglevel: debug + + zipkin: + endpoint: "http://zipkin-all-in-one:9411/api/v2/spans" + format: proto + + jaeger: + endpoint: jaeger-all-in-one:14250 + insecure: true + +# Alternatively, use jaeger_thrift_http with the settings below. In this case +# update the list of exporters on the traces pipeline. +# +# jaeger_thrift_http: +# url: http://jaeger-all-in-one:14268/api/traces + +processors: + batch: + +extensions: + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 + +service: + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging, zipkin, jaeger] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [logging, prometheus] diff --git a/opentelemetry-java/examples/otlp/docker/prometheus.yaml b/opentelemetry-java/examples/otlp/docker/prometheus.yaml new file mode 100644 index 000000000..ddea76205 --- /dev/null +++ b/opentelemetry-java/examples/otlp/docker/prometheus.yaml @@ -0,0 +1,6 @@ +scrape_configs: + - job_name: 'otel-collector' + scrape_interval: 2s + static_configs: + - targets: ['otel-collector:8889'] + - targets: ['otel-collector:8888'] diff --git a/opentelemetry-java/examples/otlp/src/main/java/io/opentelemetry/example/otlp/ExampleConfiguration.java b/opentelemetry-java/examples/otlp/src/main/java/io/opentelemetry/example/otlp/ExampleConfiguration.java new file mode 100644 index 000000000..da3677dc6 --- /dev/null +++ b/opentelemetry-java/examples/otlp/src/main/java/io/opentelemetry/example/otlp/ExampleConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.otlp; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + * All SDK management takes place here, away from the instrumentation code, which should only access + * the OpenTelemetry APIs. + */ +public final class ExampleConfiguration { + + /** + * Adds a BatchSpanProcessor initialized with OtlpGrpcSpanExporter to the TracerSdkProvider. + * + * @return a ready-to-use {@link OpenTelemetry} instance. + */ + static OpenTelemetry initOpenTelemetry() { + OtlpGrpcSpanExporter spanExporter = + OtlpGrpcSpanExporter.builder().setTimeout(2, TimeUnit.SECONDS).build(); + BatchSpanProcessor spanProcessor = + BatchSpanProcessor.builder(spanExporter) + .setScheduleDelay(100, TimeUnit.MILLISECONDS) + .build(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(spanProcessor) + .setResource(OpenTelemetrySdkAutoConfiguration.getResource()) + .build(); + OpenTelemetrySdk openTelemetrySdk = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + + Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close)); + + return openTelemetrySdk; + } + + /** + * Initializes a Metrics SDK with a OtlpGrpcMetricExporter and an IntervalMetricReader. + * + * @return a ready-to-use {@link MeterProvider} instance + */ + static MeterProvider initOpenTelemetryMetrics() { + // set up the metric exporter and wire it into the SDK and a timed reader. + OtlpGrpcMetricExporter metricExporter = OtlpGrpcMetricExporter.getDefault(); + + SdkMeterProvider meterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + IntervalMetricReader intervalMetricReader = + IntervalMetricReader.builder() + .setMetricExporter(metricExporter) + .setMetricProducers(Collections.singleton(meterProvider)) + .setExportIntervalMillis(1000) + .buildAndStart(); + + Runtime.getRuntime().addShutdownHook(new Thread(intervalMetricReader::shutdown)); + + return meterProvider; + } +} diff --git a/opentelemetry-java/examples/otlp/src/main/java/io/opentelemetry/example/otlp/OtlpExporterExample.java b/opentelemetry-java/examples/otlp/src/main/java/io/opentelemetry/example/otlp/OtlpExporterExample.java new file mode 100644 index 000000000..e6bda7ade --- /dev/null +++ b/opentelemetry-java/examples/otlp/src/main/java/io/opentelemetry/example/otlp/OtlpExporterExample.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.otlp; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongValueRecorder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; + +/** + * Example code for setting up the OTLP exporters. + * + *

    If you wish to use this code, you'll need to run a copy of the collector locally, on the + * default port. There is a docker-compose configuration for doing this in the docker subdirectory + * of this module. + */ +public final class OtlpExporterExample { + + public static void main(String[] args) throws InterruptedException { + // this will make sure that a proper service.name attribute is set on all the spans/metrics. + // note: this is not something you should generally do in code, but should be provided on the + // command-line. This is here to make the example more self-contained. + System.setProperty("otel.resource.attributes", "service.name=OtlpExporterExample"); + + // it is important to initialize your SDK as early as possible in your application's lifecycle + OpenTelemetry openTelemetry = ExampleConfiguration.initOpenTelemetry(); + // note: currently metrics is alpha and the configuration story is still unfolding. This will + // definitely change in the future. + MeterProvider meterProvider = ExampleConfiguration.initOpenTelemetryMetrics(); + + Tracer tracer = openTelemetry.getTracer("io.opentelemetry.example"); + Meter meter = meterProvider.get("io.opentelemetry.example"); + LongCounter counter = meter.longCounterBuilder("example_counter").build(); + LongValueRecorder recorder = + meter.longValueRecorderBuilder("super_timer").setUnit("ms").build(); + + for (int i = 0; i < 10; i++) { + long startTime = System.currentTimeMillis(); + Span exampleSpan = tracer.spanBuilder("exampleSpan").startSpan(); + try (Scope scope = exampleSpan.makeCurrent()) { + counter.add(1); + exampleSpan.setAttribute("good", "true"); + exampleSpan.setAttribute("exampleNumber", i); + Thread.sleep(100); + } finally { + recorder.record(System.currentTimeMillis() - startTime); + exampleSpan.end(); + } + } + + // sleep for a bit to let everything settle + Thread.sleep(2000); + } +} diff --git a/opentelemetry-java/examples/prometheus/README.md b/opentelemetry-java/examples/prometheus/README.md new file mode 100644 index 000000000..d48eb254a --- /dev/null +++ b/opentelemetry-java/examples/prometheus/README.md @@ -0,0 +1,41 @@ +# Prometheus Example + +This example demonstrates how to use the OpenTelemetry SDK +to instrument a simple application using Prometheus as the metric exporter and expose the metrics via HTTP. + +These are collected by a Prometheus instance which is configured to pull these metrics via HTTP. + +# How to run + +## Prerequisites +* Java 1.7 +* Docker 19.03 + +## 1 - Compile +```shell script +../gradlew shadowJar +``` +## 2 - Run Prometheus + +Start Prometheus instance with a configuration that sets up a HTTP collection job for ```127.0.0.1:19090``` + +See [prometheus.yml](prometheus.yml) + +```shell script +docker run --network="host" --rm -it \ + --name prometheus \ + -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \ + prom/prometheus + +``` + +## 3 - Start the Application +```shell script +java -cp build/libs/opentelemetry-examples-prometheus-0.1.0-SNAPSHOT-all.jar io.opentelemetry.example.prometheus.PrometheusExample 19090 +``` +## 4 - Open the Prometheus UI + +Navigate to: + +http://localhost:9090/graph?g0.range_input=15m&g0.expr=incoming_messages&g0.tab=0 + diff --git a/opentelemetry-java/examples/prometheus/build.gradle b/opentelemetry-java/examples/prometheus/build.gradle new file mode 100644 index 000000000..787341f9a --- /dev/null +++ b/opentelemetry-java/examples/prometheus/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' +} + +description = 'OpenTelemetry Example for Prometheus Exporter' +ext.moduleName = "io.opentelemetry.examples.prometheus" + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.prometheus:simpleclient:0.8.1") + implementation("io.prometheus:simpleclient_httpserver:0.8.1") + + //alpha modules + implementation("io.opentelemetry:opentelemetry-exporter-prometheus") +} diff --git a/opentelemetry-java/examples/prometheus/prometheus.yml b/opentelemetry-java/examples/prometheus/prometheus.yml new file mode 100644 index 000000000..5160b3dfa --- /dev/null +++ b/opentelemetry-java/examples/prometheus/prometheus.yml @@ -0,0 +1,30 @@ +global: + scrape_interval: 15s + scrape_timeout: 10s + evaluation_interval: 15s +alerting: + alertmanagers: + - static_configs: + - targets: [] + scheme: http + timeout: 10s + api_version: v1 +scrape_configs: +- job_name: prometheus + honor_timestamps: true + scrape_interval: 15s + scrape_timeout: 10s + metrics_path: /metrics + scheme: http + static_configs: + - targets: + - localhost:9090 +- job_name: otel_java_prometheus_example + honor_timestamps: true + scrape_interval: 15s + scrape_timeout: 10s + metrics_path: /metrics + scheme: http + static_configs: + - targets: + - 127.0.0.1:19090 diff --git a/opentelemetry-java/examples/prometheus/src/main/java/io/opentelemetry/example/prometheus/ExampleConfiguration.java b/opentelemetry-java/examples/prometheus/src/main/java/io/opentelemetry/example/prometheus/ExampleConfiguration.java new file mode 100644 index 000000000..62669a37f --- /dev/null +++ b/opentelemetry-java/examples/prometheus/src/main/java/io/opentelemetry/example/prometheus/ExampleConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.prometheus; + +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.exporter.prometheus.PrometheusCollector; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.prometheus.client.exporter.HTTPServer; +import java.io.IOException; + +public final class ExampleConfiguration { + private static HTTPServer server; + + /** + * Initializes the Meter SDK and configures the prometheus collector with all default settings. + * + * @param prometheusPort the port to open up for scraping. + * @return A MeterProvider for use in instrumentation. + */ + static MeterProvider initializeOpenTelemetry(int prometheusPort) throws IOException { + SdkMeterProvider meterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + + PrometheusCollector.builder().setMetricProducer(meterProvider).buildAndRegister(); + + server = new HTTPServer(prometheusPort); + + return meterProvider; + } + + static void shutdownPrometheusEndpoint() { + server.stop(); + } +} diff --git a/opentelemetry-java/examples/prometheus/src/main/java/io/opentelemetry/example/prometheus/PrometheusExample.java b/opentelemetry-java/examples/prometheus/src/main/java/io/opentelemetry/example/prometheus/PrometheusExample.java new file mode 100644 index 000000000..f8f8793ae --- /dev/null +++ b/opentelemetry-java/examples/prometheus/src/main/java/io/opentelemetry/example/prometheus/PrometheusExample.java @@ -0,0 +1,67 @@ +package io.opentelemetry.example.prometheus; + +import io.opentelemetry.api.metrics.LongValueObserver; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.exporter.prometheus.PrometheusCollector; +import io.prometheus.client.exporter.HTTPServer; +import java.io.IOException; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Example of using the {@link PrometheusCollector} to convert OTel metrics to Prometheus format and + * expose these to a Prometheus instance via a {@link HTTPServer} exporter. + * + *

    A {@link LongValueObserver} is used to periodically measure how many incoming messages are + * awaiting processing. The {@link LongValueObserver} Updater gets executed every collection + * interval. + */ +public final class PrometheusExample { + private long incomingMessageCount; + + public PrometheusExample(MeterProvider meterProvider) { + Meter meter = meterProvider.get("PrometheusExample", "0.13.1"); + meter + .longValueObserverBuilder("incoming.messages") + .setDescription("No of incoming messages awaiting processing") + .setUnit("message") + .setUpdater(result -> result.observe(incomingMessageCount, Labels.empty())) + .build(); + } + + void simulate() { + for (int i = 10; i > 0; i--) { + try { + System.out.println( + i + " Iterations to go, current incomingMessageCount is: " + incomingMessageCount); + incomingMessageCount = ThreadLocalRandom.current().nextLong(100); + Thread.sleep(1000); + } catch (InterruptedException e) { + // ignored here + } + } + } + + public static void main(String[] args) throws IOException { + int prometheusPort = 0; + try { + prometheusPort = Integer.parseInt(args[0]); + } catch (Exception e) { + System.out.println("Port not set, or is invalid. Exiting"); + System.exit(1); + } + + // it is important to initialize the OpenTelemetry SDK as early as possible in your process. + MeterProvider meterProvider = ExampleConfiguration.initializeOpenTelemetry(prometheusPort); + + PrometheusExample prometheusExample = new PrometheusExample(meterProvider); + + prometheusExample.simulate(); + + System.out.println("Exiting"); + + // clean up the prometheus endpoint + ExampleConfiguration.shutdownPrometheusEndpoint(); + } +} diff --git a/opentelemetry-java/examples/sdk-usage/README.md b/opentelemetry-java/examples/sdk-usage/README.md new file mode 100644 index 000000000..9f161f57b --- /dev/null +++ b/opentelemetry-java/examples/sdk-usage/README.md @@ -0,0 +1,23 @@ +# SDK Usage Examples + +This is a simple example that demonstrates how to use and configure the OpenTelemetry SDK. + +## Prerequisites +* Java 1.8 or higher + + +## Compile +Compile with +```shell script +../gradlew shadowJar +``` + +## Run + +The following commands are used to run the examples. +```shell script +java -cp build/libs/opentelemetry-examples-sdk-usage-0.1.0-SNAPSHOT-all.jar io.opentelemetry.sdk.example.ConfigureTraceExample +``` +```shell script +java -cp build/libs/opentelemetry-examples-sdk-usage-0.1.0-SNAPSHOT-all.jar io.opentelemetry.sdk.example.ConfigureSpanProcessorExample +``` \ No newline at end of file diff --git a/opentelemetry-java/examples/sdk-usage/build.gradle b/opentelemetry-java/examples/sdk-usage/build.gradle new file mode 100644 index 000000000..46176a32b --- /dev/null +++ b/opentelemetry-java/examples/sdk-usage/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' +} + +description = 'OpenTelemetry Examples for SDK Usage' +ext.moduleName = "io.opentelemetry.examples.sdk.usage" + +dependencies { + implementation "io.opentelemetry:opentelemetry-sdk" + implementation "io.opentelemetry:opentelemetry-exporter-logging" + implementation "io.grpc:grpc-context" +} diff --git a/opentelemetry-java/examples/sdk-usage/src/main/java/io/opentelemetry/sdk/example/ConfigureSpanProcessorExample.java b/opentelemetry-java/examples/sdk-usage/src/main/java/io/opentelemetry/sdk/example/ConfigureSpanProcessorExample.java new file mode 100644 index 000000000..63fe3c6ea --- /dev/null +++ b/opentelemetry-java/examples/sdk-usage/src/main/java/io/opentelemetry/sdk/example/ConfigureSpanProcessorExample.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.example; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.concurrent.TimeUnit; + +/** This example shows how to instantiate different Span Processors. */ +public final class ConfigureSpanProcessorExample { + + private static final LoggingSpanExporter exporter = new LoggingSpanExporter(); + private static final OpenTelemetrySdk openTelemetry = OpenTelemetrySdk.builder().build(); + // Get the Tracer Management interface + private static final SdkTracerProvider tracerManagement = openTelemetry.getSdkTracerProvider(); + // Acquire a tracer + private static final Tracer tracer = openTelemetry.getTracer("ConfigureSpanProcessorExample"); + + public static void main(String[] args) { + // Example how to configure the default SpanProcessors. + defaultSpanProcessors(); + // After this method, the following SpanProcessors are registered: + // - SimpleSpanProcessor + // - BatchSpanProcessor + // - MultiSpanProcessor <- this is a container for other SpanProcessors + // |-- SimpleSpanProcessor + // |-- BatchSpanProcessor + + // We generate a single Span so we can see some output on the console. + // Since there are 4 different SpanProcessor registered, this Span is exported 4 times. + tracer.spanBuilder("Span #1").startSpan().end(); + + // When exiting, it is recommended to call the shutdown method. This method calls `shutdown` on + // all configured SpanProcessors. This way, the configured exporters can release all resources + // and terminate their job sending the remaining traces to their back end. + tracerManagement.shutdown(); + } + + private static void defaultSpanProcessors() { + // OpenTelemetry offers 3 different default span processors: + // - SimpleSpanProcessor + // - BatchSpanProcessor + // - MultiSpanProcessor + // Default span processors require an exporter as parameter. In this example we use the + // LoggingSpanExporter which prints on the console output the spans. + + // Configure the simple spans processor. This span processor exports span immediately after they + // are ended. + SpanProcessor simpleSpansProcessor = SimpleSpanProcessor.create(exporter); + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder().addSpanProcessor(simpleSpansProcessor).build()) + .build(); + + // Configure the batch spans processor. This span processor exports span in batches. + BatchSpanProcessor batchSpansProcessor = + BatchSpanProcessor.builder(exporter) + .setMaxExportBatchSize(512) // set the maximum batch size to use + .setMaxQueueSize(2048) // set the queue size. This must be >= the export batch size + .setExporterTimeout( + 30, TimeUnit.SECONDS) // set the max amount of time an export can run before getting + // interrupted + .setScheduleDelay(5, TimeUnit.SECONDS) // set time between two different exports + .build(); + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder().addSpanProcessor(batchSpansProcessor).build()) + .build(); + + // Configure the composite span processor. A Composite SpanProcessor accepts a list of Span + // Processors. + SpanProcessor multiSpanProcessor = + SpanProcessor.composite(simpleSpansProcessor, batchSpansProcessor); + OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder().addSpanProcessor(multiSpanProcessor).build()) + .build(); + } +} diff --git a/opentelemetry-java/examples/sdk-usage/src/main/java/io/opentelemetry/sdk/example/ConfigureTraceExample.java b/opentelemetry-java/examples/sdk-usage/src/main/java/io/opentelemetry/sdk/example/ConfigureTraceExample.java new file mode 100644 index 000000000..c4c934198 --- /dev/null +++ b/opentelemetry-java/examples/sdk-usage/src/main/java/io/opentelemetry/sdk/example/ConfigureTraceExample.java @@ -0,0 +1,194 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.example; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; + +/** + * This example demonstrates various {@link SpanLimits} options and how to configure them into an + * SDK. + */ +class ConfigureTraceExample { + + public static void main(String[] args) { + // SpanLimits handles the tracing configuration + + OpenTelemetrySdk openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .build()) + .build(); + + printSpanLimits(openTelemetrySdk); + Tracer tracer = openTelemetrySdk.getTracer("ConfigureTraceExample"); + + // OpenTelemetry has a maximum of 32 Attributes by default for Spans, Links, and Events. + Span multiAttrSpan = tracer.spanBuilder("Example Span Attributes").startSpan(); + multiAttrSpan.setAttribute("Attribute 1", "first attribute value"); + multiAttrSpan.setAttribute("Attribute 2", "second attribute value"); + multiAttrSpan.end(); + + // The configuration can be changed in the trace provider. + // For example, we can change the maximum number of Attributes per span to 1. + SpanLimits newConf = SpanLimits.builder().setMaxNumberOfAttributes(1).build(); + + openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .setSpanLimits(newConf) + .build()) + .build(); + + printSpanLimits(openTelemetrySdk); + + // If more attributes than allowed by the configuration are set, they are dropped. + Span singleAttrSpan = tracer.spanBuilder("Example Span Attributes").startSpan(); + singleAttrSpan.setAttribute("Attribute 1", "first attribute value"); + singleAttrSpan.setAttribute("Attribute 2", "second attribute value"); + singleAttrSpan.end(); + + // OpenTelemetry offers three different default samplers: + // - alwaysOn: it samples all traces + // - alwaysOff: it rejects all traces + // - probability: it samples traces based on the probability passed in input + Sampler traceIdRatioBased = Sampler.traceIdRatioBased(0.5); + + // We build an SDK with the alwaysOff sampler. + openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .setSampler(Sampler.alwaysOff()) + .build()) + .build(); + + printSpanLimits(openTelemetrySdk); + + tracer = openTelemetrySdk.getTracer("ConfigureTraceExample"); + tracer.spanBuilder("Not forwarded to any processors").startSpan().end(); + tracer.spanBuilder("Not forwarded to any processors").startSpan().end(); + + // We build an SDK with the alwaysOn sampler. + openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .setSampler(Sampler.alwaysOn()) + .build()) + .build(); + printSpanLimits(openTelemetrySdk); + + tracer = openTelemetrySdk.getTracer("ConfigureTraceExample"); + tracer.spanBuilder("Forwarded to all processors").startSpan().end(); + tracer.spanBuilder("Forwarded to all processors").startSpan().end(); + + // We build an SDK with the configuration to use the probability sampler which was configured to + // sample + // only 50% of the spans. + openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .setSampler(traceIdRatioBased) + .build()) + .build(); + printSpanLimits(openTelemetrySdk); + + tracer = openTelemetrySdk.getTracer("ConfigureTraceExample"); + + for (int i = 0; i < 10; i++) { + tracer + .spanBuilder(String.format("Span %d might be forwarded to all processors", i)) + .startSpan() + .end(); + } + + // We can also implement our own sampler. We need to implement the + // io.opentelemetry.sdk.trace.Sampler interface. + class MySampler implements Sampler { + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return SamplingResult.create( + name.contains("SAMPLE") ? SamplingDecision.RECORD_AND_SAMPLE : SamplingDecision.DROP); + } + + @Override + public String getDescription() { + return "My Sampler Implementation!"; + } + } + + // Add MySampler to the Trace Configuration + openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .setSampler(new MySampler()) + .build()) + .build(); + printSpanLimits(openTelemetrySdk); + + tracer = openTelemetrySdk.getTracer("ConfigureTraceExample"); + + tracer.spanBuilder("#1 - SamPleD").startSpan().end(); + tracer + .spanBuilder("#2 - SAMPLE this trace will be the first to be printed in the console output") + .startSpan() + .end(); + tracer.spanBuilder("#3 - Smth").startSpan().end(); + tracer + .spanBuilder("#4 - SAMPLED this trace will be the second one shown in the console output") + .startSpan() + .end(); + tracer.spanBuilder("#5").startSpan().end(); + } + + private static void printSpanLimits(OpenTelemetrySdk sdk) { + SpanLimits config = sdk.getSdkTracerProvider().getSpanLimits(); + System.err.println("=================================="); + System.err.print("Max number of attributes: "); + System.err.println(config.getMaxNumberOfAttributes()); + System.err.print("Max number of attributes per event: "); + System.err.println(config.getMaxNumberOfAttributesPerEvent()); + System.err.print("Max number of attributes per link: "); + System.err.println(config.getMaxNumberOfAttributesPerLink()); + System.err.print("Max number of events: "); + System.err.println(config.getMaxNumberOfEvents()); + System.err.print("Max number of links: "); + System.err.println(config.getMaxNumberOfLinks()); + System.err.print("Sampler: "); + System.err.println(sdk.getSdkTracerProvider().getSampler().getDescription()); + } +} diff --git a/opentelemetry-java/examples/settings.gradle b/opentelemetry-java/examples/settings.gradle new file mode 100644 index 000000000..0e04ddea0 --- /dev/null +++ b/opentelemetry-java/examples/settings.gradle @@ -0,0 +1,24 @@ +pluginManagement { + plugins { + id "com.diffplug.spotless" version "5.6.1" + id "com.github.johnrengelman.shadow" version "6.1.0" + id 'com.google.protobuf' version '0.8.8' + } +} + +rootProject.name = "opentelemetry-java-examples" +include ":opentelemetry-examples-autoconfigure", + ":opentelemetry-examples-grpc", + ":opentelemetry-examples-http", + ":opentelemetry-examples-jaeger", + ":opentelemetry-examples-metrics", + ":opentelemetry-examples-prometheus", + ":opentelemetry-examples-otlp", + ":opentelemetry-examples-sdk-usage", + ":opentelemetry-examples-zipkin", + ":opentelemetry-examples-logging" + +rootProject.children.each { + it.projectDir = "$rootDir/" + it.name + .replace("opentelemetry-examples-", "") as File +} diff --git a/opentelemetry-java/examples/zipkin/README.md b/opentelemetry-java/examples/zipkin/README.md new file mode 100644 index 000000000..7b8609647 --- /dev/null +++ b/opentelemetry-java/examples/zipkin/README.md @@ -0,0 +1,32 @@ +# Zipkin Example + +This is a simple example that demonstrates how to use the OpenTelemetry SDK +to instrument a simple application using Zipkin as trace exporter. + +# How to run + +## Prerequisites +* Java 1.8.231 +* Docker 19.03 + +## 1 - Compile +```shell script +../gradlew shadowJar +``` +## 2 - Run Zipkin + +```shell script +docker run --rm -it --name zipkin \ + -p 9411:9411 \ + openzipkin/zipkin:2.21 +``` + +## 3 - Start the Application +```shell script +java -cp build/libs/opentelemetry-examples-zipkin-0.1.0-SNAPSHOT-all.jar io.opentelemetry.example.zipkin.ZipkinExample localhost 9411 +``` +## 4 - Open the Zipkin UI + +Navigate to http://localhost:9411/zipkin and click on search. + +[zipkin]:[https://zipkin.io/] diff --git a/opentelemetry-java/examples/zipkin/build.gradle b/opentelemetry-java/examples/zipkin/build.gradle new file mode 100644 index 000000000..2d86c3069 --- /dev/null +++ b/opentelemetry-java/examples/zipkin/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' +} + +description = 'OpenTelemetry Examples for Zipkin Exporter' +ext.moduleName = "io.opentelemetry.examples.zipkin" + +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-exporter-zipkin") + + //alpha module + implementation "io.opentelemetry:opentelemetry-semconv" +} diff --git a/opentelemetry-java/examples/zipkin/src/main/java/io/opentelemetry/example/zipkin/ExampleConfiguration.java b/opentelemetry-java/examples/zipkin/src/main/java/io/opentelemetry/example/zipkin/ExampleConfiguration.java new file mode 100644 index 000000000..048b2329e --- /dev/null +++ b/opentelemetry-java/examples/zipkin/src/main/java/io/opentelemetry/example/zipkin/ExampleConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.example.zipkin; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; + +/** + * All SDK management takes place here, away from the instrumentation code, which should only access + * the OpenTelemetry APIs. + */ +public final class ExampleConfiguration { + // Zipkin API Endpoints for uploading spans + private static final String ENDPOINT_V2_SPANS = "/api/v2/spans"; + + // Name of the service + private static final String SERVICE_NAME = "myExampleService"; + + /** Adds a SimpleSpanProcessor initialized with ZipkinSpanExporter to the TracerSdkProvider */ + static OpenTelemetry initializeOpenTelemetry(String ip, int port) { + String httpUrl = String.format("http://%s:%s", ip, port); + ZipkinSpanExporter zipkinExporter = + ZipkinSpanExporter.builder().setEndpoint(httpUrl + ENDPOINT_V2_SPANS).build(); + + Resource serviceNameResource = + Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, SERVICE_NAME)); + + // Set to process the spans by the Zipkin Exporter + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(zipkinExporter)) + .setResource(Resource.getDefault().merge(serviceNameResource)) + .build(); + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + + // add a shutdown hook to shut down the SDK + Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close)); + + // return the configured instance so it can be used for instrumentation. + return openTelemetry; + } +} diff --git a/opentelemetry-java/examples/zipkin/src/main/java/io/opentelemetry/example/zipkin/ZipkinExample.java b/opentelemetry-java/examples/zipkin/src/main/java/io/opentelemetry/example/zipkin/ZipkinExample.java new file mode 100644 index 000000000..d77beb67f --- /dev/null +++ b/opentelemetry-java/examples/zipkin/src/main/java/io/opentelemetry/example/zipkin/ZipkinExample.java @@ -0,0 +1,62 @@ +package io.opentelemetry.example.zipkin; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Scope; + +public final class ZipkinExample { + // The Tracer we'll use for the example + private final Tracer tracer; + + public ZipkinExample(TracerProvider tracerProvider) { + tracer = tracerProvider.get("io.opentelemetry.example.ZipkinExample"); + } + + // This method instruments doWork() method + public void myWonderfulUseCase() { + // Generate span + Span span = tracer.spanBuilder("Start my wonderful use case").startSpan(); + try (Scope scope = span.makeCurrent()) { + // Add some Event to the span + span.addEvent("Event 0"); + // execute my use case - here we simulate a wait + doWork(); + // Add some Event to the span + span.addEvent("Event 1"); + } finally { + span.end(); + } + } + + public void doWork() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // ignore in an example + } + } + + public static void main(String[] args) { + // Parsing the input + if (args.length < 2) { + System.out.println("Missing [hostname] [port]"); + System.exit(1); + } + + String ip = args[0]; + int port = Integer.parseInt(args[1]); + + // it is important to initialize the OpenTelemetry SDK as early as possible in your process. + OpenTelemetry openTelemetry = ExampleConfiguration.initializeOpenTelemetry(ip, port); + + TracerProvider tracerProvider = openTelemetry.getTracerProvider(); + + // start example + ZipkinExample example = new ZipkinExample(tracerProvider); + example.myWonderfulUseCase(); + + System.out.println("Bye"); + } +} diff --git a/opentelemetry-java/exporters/build.gradle.kts b/opentelemetry-java/exporters/build.gradle.kts new file mode 100644 index 000000000..38f6851f8 --- /dev/null +++ b/opentelemetry-java/exporters/build.gradle.kts @@ -0,0 +1,10 @@ +subprojects { + // https://github.com/gradle/gradle/issues/847 + group = "io.opentelemetry.exporters" + val proj = this + plugins.withId("java") { + configure { + archivesBaseName = "opentelemetry-exporter-${proj.name}" + } + } +} diff --git a/opentelemetry-java/exporters/jaeger-thrift/README.md b/opentelemetry-java/exporters/jaeger-thrift/README.md new file mode 100644 index 000000000..92f41ba1f --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/README.md @@ -0,0 +1,29 @@ +# OpenTelemetry - Jaeger Exporter - Thrift + +[![Javadocs][javadoc-image]][javadoc-url] + +This is the OpenTelemetry exporter, sending span data to Jaeger via Thrift over HTTP. + +## Configuration + +The Jaeger Thrift span exporter can be configured programmatically. + +An example of simple Jaeger Thrift exporter initialization. In this case +spans will be sent to a Jaeger Thrift endpoint running on `localhost`: + +```java +JaegerThriftSpanExporter exporter = + JaegerThriftSpanExporter.builder() + .setEndpoint("http://localhost:14268/api/traces") + .build(); +``` + +If you need configuration via environment variables and/or system properties, you will want to use +the [autoconfigure](../../sdk-extensions/autoconfigure) module. + +## Compatibility + +As with the OpenTelemetry SDK itself, this exporter is compatible with Java 8+ and Android API level 24+. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-exporters-jaeger-thrift.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-exporters-jaeger-thrift diff --git a/opentelemetry-java/exporters/jaeger-thrift/build.gradle.kts b/opentelemetry-java/exporters/jaeger-thrift/build.gradle.kts new file mode 100644 index 000000000..9fa4a00ee --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry - Jaeger Thrift Exporter" +extra["moduleName"] = "io.opentelemetry.exporter.jaeger.thrift" + +dependencies { + api(project(":sdk:all")) + + implementation(project(":sdk:all")) + implementation(project(":semconv")) + + implementation("io.jaegertracing:jaeger-client") + + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("com.squareup.okhttp3:okhttp") + testImplementation("com.google.guava:guava-testlib") + + testImplementation(project(":sdk:testing")) +} diff --git a/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/Adapter.java b/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/Adapter.java new file mode 100644 index 000000000..22a58a34b --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/Adapter.java @@ -0,0 +1,257 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger.thrift; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; + +import com.google.gson.Gson; +import io.jaegertracing.thriftjava.Log; +import io.jaegertracing.thriftjava.Span; +import io.jaegertracing.thriftjava.SpanRef; +import io.jaegertracing.thriftjava.SpanRefType; +import io.jaegertracing.thriftjava.Tag; +import io.jaegertracing.thriftjava.TagType; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.annotation.concurrent.ThreadSafe; + +/** Adapts OpenTelemetry objects to Jaeger objects. */ +@ThreadSafe +final class Adapter { + + static final AttributeKey KEY_ERROR = booleanKey("error"); + static final String KEY_LOG_EVENT = "event"; + static final String KEY_EVENT_DROPPED_ATTRIBUTES_COUNT = "otel.event.dropped_attributes_count"; + static final String KEY_DROPPED_ATTRIBUTES_COUNT = "otel.dropped_attributes_count"; + static final String KEY_DROPPED_EVENTS_COUNT = "otel.dropped_events_count"; + static final String KEY_SPAN_KIND = "span.kind"; + static final String KEY_SPAN_STATUS_MESSAGE = "otel.status_message"; + static final String KEY_SPAN_STATUS_CODE = "otel.status_code"; + static final String KEY_INSTRUMENTATION_LIBRARY_NAME = "otel.library.name"; + static final String KEY_INSTRUMENTATION_LIBRARY_VERSION = "otel.library.version"; + public static final Gson GSON = new Gson(); + + private Adapter() {} + + /** + * Converts a list of {@link SpanData} into a collection of Jaeger's {@link Span}. + * + * @param spans the list of spans to be converted + * @return the collection of Jaeger spans + * @see #toJaeger(SpanData) + */ + static List toJaeger(Collection spans) { + return spans.stream().map(Adapter::toJaeger).collect(Collectors.toList()); + } + + /** + * Converts a single {@link SpanData} into a Jaeger's {@link Span}. + * + * @param span the span to be converted + * @return the Jaeger span + */ + static Span toJaeger(SpanData span) { + Span target = new Span(); + + long traceIdHigh = traceIdAsLongHigh(span.getTraceId()); + long traceIdLow = traceIdAsLongLow(span.getTraceId()); + + target.setTraceIdHigh(traceIdHigh); + target.setTraceIdLow(traceIdLow); + target.setSpanId(spanIdAsLong(span.getSpanId())); + target.setOperationName(span.getName()); + target.setStartTime(TimeUnit.NANOSECONDS.toMicros(span.getStartEpochNanos())); + target.setDuration( + TimeUnit.NANOSECONDS.toMicros(span.getEndEpochNanos() - span.getStartEpochNanos())); + + List tags = toTags(span.getAttributes()); + int droppedAttributes = span.getTotalAttributeCount() - span.getAttributes().size(); + if (droppedAttributes > 0) { + tags.add(new Tag(KEY_DROPPED_ATTRIBUTES_COUNT, TagType.LONG).setVLong(droppedAttributes)); + } + + target.setLogs(toJaegerLogs(span.getEvents())); + int droppedEvents = span.getTotalRecordedEvents() - span.getEvents().size(); + if (droppedEvents > 0) { + tags.add(new Tag(KEY_DROPPED_EVENTS_COUNT, TagType.LONG).setVLong(droppedEvents)); + } + + List references = toSpanRefs(span.getLinks()); + + // add the parent span + if (span.getParentSpanContext().isValid()) { + long parentSpanId = spanIdAsLong(span.getParentSpanId()); + references.add(new SpanRef(SpanRefType.CHILD_OF, traceIdLow, traceIdHigh, parentSpanId)); + target.setParentSpanId(parentSpanId); + } + target.setReferences(references); + + if (span.getKind() != SpanKind.INTERNAL) { + tags.add( + new Tag(KEY_SPAN_KIND, TagType.STRING) + .setVStr(span.getKind().name().toLowerCase(Locale.ROOT))); + } + + if (!span.getStatus().getDescription().isEmpty()) { + tags.add( + new Tag(KEY_SPAN_STATUS_MESSAGE, TagType.STRING) + .setVStr(span.getStatus().getDescription())); + } + + if (span.getStatus().getStatusCode() != StatusCode.UNSET) { + tags.add( + new Tag(KEY_SPAN_STATUS_CODE, TagType.STRING) + .setVStr(span.getStatus().getStatusCode().name())); + } + + tags.add( + new Tag(KEY_INSTRUMENTATION_LIBRARY_NAME, TagType.STRING) + .setVStr(span.getInstrumentationLibraryInfo().getName())); + + if (span.getInstrumentationLibraryInfo().getVersion() != null) { + tags.add( + new Tag(KEY_INSTRUMENTATION_LIBRARY_VERSION, TagType.STRING) + .setVStr(span.getInstrumentationLibraryInfo().getVersion())); + } + + if (span.getStatus().getStatusCode() == StatusCode.ERROR) { + tags.add(toTag(KEY_ERROR, true)); + } + target.setTags(tags); + + return target; + } + + /** + * Converts {@link EventData}s into a collection of Jaeger's {@link Log}. + * + * @param timedEvents the timed events to be converted + * @return a collection of Jaeger logs + * @see #toJaegerLog(EventData) + */ + // VisibleForTesting + static List toJaegerLogs(List timedEvents) { + return timedEvents.stream().map(Adapter::toJaegerLog).collect(Collectors.toList()); + } + + /** + * Converts a {@link EventData} into Jaeger's {@link Log}. + * + * @param event the timed event to be converted + * @return a Jaeger log + */ + // VisibleForTesting + static Log toJaegerLog(EventData event) { + Log result = new Log(); + result.setTimestamp(TimeUnit.NANOSECONDS.toMicros(event.getEpochNanos())); + result.addToFields(new Tag(KEY_LOG_EVENT, TagType.STRING).setVStr(event.getName())); + + int droppedAttributesCount = event.getDroppedAttributesCount(); + if (droppedAttributesCount > 0) { + result.addToFields( + new Tag(KEY_EVENT_DROPPED_ATTRIBUTES_COUNT, TagType.LONG) + .setVLong(droppedAttributesCount)); + } + List attributeTags = toTags(event.getAttributes()); + for (Tag attributeTag : attributeTags) { + result.addToFields(attributeTag); + } + return result; + } + + /** + * Converts a map of attributes into a collection of Jaeger's {@link Tag}. + * + * @param attributes the span attributes + * @return a collection of Jaeger key values + * @see #toTag + */ + static List toTags(Attributes attributes) { + List results = new ArrayList<>(); + attributes.forEach((key, value) -> results.add(toTag(key, value))); + return results; + } + + /** + * Converts the given {@link AttributeKey} and value into Jaeger's {@link Tag}. + * + * @param key the entry key as string + * @param value the entry value + * @return a Jaeger key value + */ + // VisibleForTesting + static Tag toTag(AttributeKey key, Object value) { + switch (key.getType()) { + case STRING: + return new Tag(key.getKey(), TagType.STRING).setVStr((String) value); + case LONG: + return new Tag(key.getKey(), TagType.LONG).setVLong((long) value); + case BOOLEAN: + return new Tag(key.getKey(), TagType.BOOL).setVBool((boolean) value); + case DOUBLE: + return new Tag(key.getKey(), TagType.DOUBLE).setVDouble((double) value); + default: + return new Tag(key.getKey(), TagType.STRING).setVStr(GSON.toJson(value)); + } + } + + /** + * Converts {@link LinkData}s into a collection of Jaeger's {@link SpanRef}. + * + * @param links the span's links property to be converted + * @return a collection of Jaeger span references + */ + // VisibleForTesting + static List toSpanRefs(List links) { + List spanRefs = new ArrayList<>(links.size()); + for (LinkData link : links) { + spanRefs.add(toSpanRef(link)); + } + return spanRefs; + } + + /** + * Converts a single {@link LinkData} into a Jaeger's {@link SpanRef}. + * + * @param link the OpenTelemetry link to be converted + * @return the Jaeger span reference + */ + // VisibleForTesting + static SpanRef toSpanRef(LinkData link) { + // we can assume that all links are *follows from* + // https://github.com/open-telemetry/opentelemetry-java/issues/475 + // https://github.com/open-telemetry/opentelemetry-java/pull/481/files#r312577862 + return new SpanRef( + SpanRefType.FOLLOWS_FROM, + traceIdAsLongLow(link.getSpanContext().getTraceId()), + traceIdAsLongHigh(link.getSpanContext().getTraceId()), + spanIdAsLong(link.getSpanContext().getSpanId())); + } + + private static long traceIdAsLongHigh(String traceId) { + return new BigInteger(traceId.substring(0, 16), 16).longValue(); + } + + private static long traceIdAsLongLow(String traceId) { + return new BigInteger(traceId.substring(16, 32), 16).longValue(); + } + + private static long spanIdAsLong(String spanId) { + return new BigInteger(spanId, 16).longValue(); + } +} diff --git a/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporter.java b/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporter.java new file mode 100644 index 000000000..be761d975 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporter.java @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger.thrift; + +import io.jaegertracing.internal.exceptions.SenderException; +import io.jaegertracing.thrift.internal.senders.ThriftSender; +import io.jaegertracing.thriftjava.Process; +import io.jaegertracing.thriftjava.Span; +import io.jaegertracing.thriftjava.Tag; +import io.jaegertracing.thriftjava.TagType; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.concurrent.ThreadSafe; + +/** Exports spans to Jaeger via Thrift, using Jaeger's thrift model. */ +@ThreadSafe +public final class JaegerThriftSpanExporter implements SpanExporter { + + static final String DEFAULT_ENDPOINT = "http://localhost:14268/api/traces"; + + private static final String DEFAULT_HOST_NAME = "unknown"; + private static final String CLIENT_VERSION_KEY = "jaeger.version"; + private static final String CLIENT_VERSION_VALUE = "opentelemetry-java"; + private static final String HOSTNAME_KEY = "hostname"; + private static final String IP_KEY = "ip"; + private static final String IP_DEFAULT = "0.0.0.0"; + + private final ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(JaegerThriftSpanExporter.class.getName())); + private final ThriftSender thriftSender; + private final Process process; + + /** + * Creates a new Jaeger gRPC Span Reporter with the given name, using the given channel. + * + * @param thriftSender The sender used for sending the data. + */ + JaegerThriftSpanExporter(ThriftSender thriftSender) { + this.thriftSender = thriftSender; + String hostname; + String ipv4; + + try { + hostname = InetAddress.getLocalHost().getHostName(); + ipv4 = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + hostname = DEFAULT_HOST_NAME; + ipv4 = IP_DEFAULT; + } + + Tag clientTag = new Tag(CLIENT_VERSION_KEY, TagType.STRING).setVStr(CLIENT_VERSION_VALUE); + Tag ipv4Tag = new Tag(IP_KEY, TagType.STRING).setVStr(ipv4); + Tag hostnameTag = new Tag(HOSTNAME_KEY, TagType.STRING).setVStr(hostname); + + this.process = new Process(); + this.process.addToTags(clientTag); + this.process.addToTags(ipv4Tag); + this.process.addToTags(hostnameTag); + } + + /** + * Submits all the given spans in a single batch to the Jaeger collector. + * + * @param spans the list of sampled Spans to be exported. + * @return the result of the operation + */ + @Override + public CompletableResultCode export(Collection spans) { + Map> batches = + spans.stream().collect(Collectors.groupingBy(SpanData::getResource)).entrySet().stream() + .collect( + Collectors.toMap( + entry -> createProcess(entry.getKey()), + entry -> Adapter.toJaeger(entry.getValue()))); + + List batchResults = new ArrayList<>(batches.size()); + batches.forEach( + (process, jaegerSpans) -> { + CompletableResultCode batchResult = new CompletableResultCode(); + batchResults.add(batchResult); + try { + // todo: consider making truly async + thriftSender.send(process, jaegerSpans); + batchResult.succeed(); + } catch (SenderException e) { + logger.log(Level.WARNING, "Failed to export spans", e); + batchResult.fail(); + } + }); + return CompletableResultCode.ofAll(batchResults); + } + + private Process createProcess(Resource resource) { + Process result = new Process(this.process); + + String serviceName = resource.getAttributes().get(ResourceAttributes.SERVICE_NAME); + if (serviceName == null || serviceName.isEmpty()) { + serviceName = Resource.getDefault().getAttributes().get(ResourceAttributes.SERVICE_NAME); + } + result.setServiceName(serviceName); + + List tags = Adapter.toTags(resource.getAttributes()); + tags.forEach(result::addToTags); + return result; + } + + /** + * The Jaeger exporter does not batch spans, so this method will immediately return with success. + * + * @return always Success + */ + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + /** + * Returns a new builder instance for this exporter. + * + * @return a new builder instance for this exporter. + */ + public static JaegerThriftSpanExporterBuilder builder() { + return new JaegerThriftSpanExporterBuilder(); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately + * cancelled. + */ + @Override + public CompletableResultCode shutdown() { + final CompletableResultCode result = new CompletableResultCode(); + // todo + return result.succeed(); + } + + // Visible for testing + Process getProcess() { + return process; + } +} diff --git a/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporterBuilder.java b/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporterBuilder.java new file mode 100644 index 000000000..75a8471e8 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporterBuilder.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger.thrift; + +import io.jaegertracing.thrift.internal.senders.HttpSender; +import io.jaegertracing.thrift.internal.senders.ThriftSender; +import org.apache.thrift.transport.TTransportException; + +/** Builder utility for this exporter. */ +public final class JaegerThriftSpanExporterBuilder { + + private String endpoint = JaegerThriftSpanExporter.DEFAULT_ENDPOINT; + private ThriftSender thriftSender; + + /** + * Explicitly set the {@link ThriftSender} instance to use for this Exporter. Will override any + * endpoint that has been set. + * + * @param thriftSender The ThriftSender to use. + * @return this. + */ + public JaegerThriftSpanExporterBuilder setThriftSender(ThriftSender thriftSender) { + this.thriftSender = thriftSender; + return this; + } + + /** + * Sets the Jaeger endpoint to connect to. Needs to include the full API path for trace ingest. + * + *

    Optional, defaults to "http://localhost:14268/api/traces". + * + * @param endpoint The Jaeger endpoint URL, ex. "https://jaegerhost:14268/api/traces". + * @return this. + */ + public JaegerThriftSpanExporterBuilder setEndpoint(String endpoint) { + this.endpoint = endpoint; + return this; + } + + /** + * Constructs a new instance of the exporter based on the builder's values. + * + * @return a new exporter's instance. + */ + public JaegerThriftSpanExporter build() { + if (thriftSender == null) { + try { + thriftSender = new HttpSender.Builder(endpoint).build(); + } catch (TTransportException e) { + throw new IllegalStateException("Failed to construct a thrift HttpSender.", e); + } + } + return new JaegerThriftSpanExporter(thriftSender); + } + + JaegerThriftSpanExporterBuilder() {} +} diff --git a/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/package-info.java b/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/package-info.java new file mode 100644 index 000000000..1173e8d84 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/src/main/java/io/opentelemetry/exporter/jaeger/thrift/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.jaeger.thrift; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/AdapterTest.java b/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/AdapterTest.java new file mode 100644 index 000000000..561b935d7 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/AdapterTest.java @@ -0,0 +1,363 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger.thrift; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.io.BaseEncoding; +import io.jaegertracing.thriftjava.Log; +import io.jaegertracing.thriftjava.SpanRef; +import io.jaegertracing.thriftjava.SpanRefType; +import io.jaegertracing.thriftjava.Tag; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link Adapter}. */ +class AdapterTest { + private static final BaseEncoding hex = BaseEncoding.base16().lowerCase(); + private static final String LINK_TRACE_ID = "ff000000000000000000000000cba123"; + private static final String LINK_SPAN_ID = "0000000000fed456"; + private static final String TRACE_ID = "0000000000000000ff00000000abc123"; + private static final String SPAN_ID = "ff00000000def456"; + private static final String PARENT_SPAN_ID = "0000000000aef789"; + + @Test + void testThriftSpans() { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + + SpanData span = getSpanData(startMs, endMs, SpanKind.SERVER); + List spans = Collections.singletonList(span); + + List jaegerSpans = Adapter.toJaeger(spans); + + // the span contents are checked somewhere else + assertThat(jaegerSpans).hasSize(1); + } + + @Test + void testThriftSpan() { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + + SpanData span = getSpanData(startMs, endMs, SpanKind.SERVER, 2, 4); + + // test + io.jaegertracing.thriftjava.Span jaegerSpan = Adapter.toJaeger(span); + + String rebuildTraceId = + traceIdFromLongs(jaegerSpan.getTraceIdHigh(), jaegerSpan.getTraceIdLow()); + assertThat(rebuildTraceId).isEqualTo(span.getTraceId()); + assertThat(spanIdFromLong(jaegerSpan.getSpanId())).isEqualTo(span.getSpanId()); + assertThat(jaegerSpan.getOperationName()).isEqualTo("GET /api/endpoint"); + assertThat(jaegerSpan.getStartTime()).isEqualTo(MILLISECONDS.toMicros(startMs)); + assertThat(jaegerSpan.getDuration()).isEqualTo(MILLISECONDS.toMicros(duration)); + + assertThat(jaegerSpan.getTagsSize()).isEqualTo(7); + assertThat(getValue(jaegerSpan.getTags(), Adapter.KEY_SPAN_KIND).getVStr()).isEqualTo("server"); + assertThat(getValue(jaegerSpan.getTags(), Adapter.KEY_SPAN_STATUS_CODE).getVLong()) + .isEqualTo(0); + assertThat(getValue(jaegerSpan.getTags(), Adapter.KEY_SPAN_STATUS_MESSAGE).getVStr()) + .isEqualTo("ok!"); + assertThat(getValue(jaegerSpan.getTags(), Adapter.KEY_DROPPED_EVENTS_COUNT).getVLong()) + .isEqualTo(1); + assertThat(getValue(jaegerSpan.getTags(), Adapter.KEY_DROPPED_ATTRIBUTES_COUNT).getVLong()) + .isEqualTo(3); + + assertThat(jaegerSpan.getLogsSize()).isEqualTo(1); + Log log = jaegerSpan.getLogs().get(0); + assertThat(getValue(log.getFields(), Adapter.KEY_LOG_EVENT).getVStr()) + .isEqualTo("the log message"); + assertThat(getValue(log.getFields(), "foo").getVStr()).isEqualTo("bar"); + + assertThat(jaegerSpan.getReferencesSize()).isEqualTo(2); + + assertHasFollowsFrom(jaegerSpan); + assertHasParent(jaegerSpan); + } + + @Test + void testThriftSpan_internal() { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + + SpanData span = getSpanData(startMs, endMs, SpanKind.INTERNAL); + + // test + io.jaegertracing.thriftjava.Span jaegerSpan = Adapter.toJaeger(span); + + assertThat(jaegerSpan.getTagsSize()).isEqualTo(4); + assertThat(getValue(jaegerSpan.getTags(), Adapter.KEY_SPAN_KIND)).isNull(); + } + + @Test + void testJaegerLogs() { + // prepare + EventData eventsData = getTimedEvent(); + + // test + Collection logs = Adapter.toJaegerLogs(Collections.singletonList(eventsData)); + + // verify + assertThat(logs).hasSize(1); + } + + @Test + void testJaegerLog() { + // prepare + EventData event = getTimedEvent(); + + // test + Log log = Adapter.toJaegerLog(event); + + // verify + assertThat(log.getFieldsSize()).isEqualTo(2); + + assertThat(getValue(log.getFields(), Adapter.KEY_LOG_EVENT).getVStr()) + .isEqualTo("the log message"); + assertThat(getValue(log.getFields(), "foo").getVStr()).isEqualTo("bar"); + assertThat(getValue(log.getFields(), Adapter.KEY_EVENT_DROPPED_ATTRIBUTES_COUNT)).isNull(); + } + + @Test + void jaegerLog_droppedAttributes() { + EventData event = getTimedEvent(3); + + // test + Log log = Adapter.toJaegerLog(event); + + // verify + assertThat(getValue(log.getFields(), Adapter.KEY_EVENT_DROPPED_ATTRIBUTES_COUNT).getVLong()) + .isEqualTo(2); + } + + @Test + void testKeyValue() { + // test + Tag kvB = Adapter.toTag(booleanKey("valueB"), true); + Tag kvD = Adapter.toTag(doubleKey("valueD"), 1.); + Tag kvI = Adapter.toTag(longKey("valueI"), 2L); + Tag kvS = Adapter.toTag(stringKey("valueS"), "foobar"); + Tag kvArrayB = Adapter.toTag(booleanArrayKey("valueArrayB"), Arrays.asList(true, false)); + Tag kvArrayD = Adapter.toTag(doubleArrayKey("valueArrayD"), Arrays.asList(1.2345, 6.789)); + Tag kvArrayI = Adapter.toTag(longArrayKey("valueArrayI"), Arrays.asList(12345L, 67890L)); + Tag kvArrayS = Adapter.toTag(stringArrayKey("valueArrayS"), Arrays.asList("foobar", "barfoo")); + + // verify + assertThat(kvB.isVBool()).isTrue(); + + assertThat(kvD.getVDouble()).isEqualTo(1); + assertThat(kvI.getVLong()).isEqualTo(2); + assertThat(kvS.getVStr()).isEqualTo("foobar"); + assertThat(kvArrayB.getVStr()).isEqualTo("[true,false]"); + assertThat(kvArrayD.getVStr()).isEqualTo("[1.2345,6.789]"); + assertThat(kvArrayI.getVStr()).isEqualTo("[12345,67890]"); + assertThat(kvArrayS.getVStr()).isEqualTo("[\"foobar\",\"barfoo\"]"); + } + + @Test + void testSpanRefs() { + // prepare + LinkData link = + LinkData.create(createSpanContext("00000000000000000000000000cba123", "0000000000fed456")); + + // test + Collection spanRefs = Adapter.toSpanRefs(Collections.singletonList(link)); + + // verify + assertThat(spanRefs).hasSize(1); // the actual span ref is tested in another test + } + + @Test + void testSpanRef() { + // prepare + LinkData link = LinkData.create(createSpanContext(TRACE_ID, SPAN_ID)); + + // test + SpanRef spanRef = Adapter.toSpanRef(link); + + // verify + assertThat(spanIdFromLong(spanRef.getSpanId())).isEqualTo(SPAN_ID); + assertThat(traceIdFromLongs(spanRef.getTraceIdHigh(), spanRef.getTraceIdLow())) + .isEqualTo(TRACE_ID); + assertThat(spanRef.getRefType()).isEqualTo(SpanRefType.FOLLOWS_FROM); + } + + @Test + void testStatusNotUnset() { + long startMs = System.currentTimeMillis(); + long endMs = startMs + 900; + SpanData span = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(createSpanContext(TRACE_ID, SPAN_ID)) + .setName("GET /api/endpoint") + .setStartEpochNanos(MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(MILLISECONDS.toNanos(endMs)) + .setKind(SpanKind.SERVER) + .setStatus(StatusData.error()) + .setTotalRecordedEvents(0) + .setTotalRecordedLinks(0) + .build(); + + assertThat(Adapter.toJaeger(span)).isNotNull(); + } + + @Test + void testSpanError() { + Attributes attributes = + Attributes.of( + stringKey("error.type"), + this.getClass().getName(), + stringKey("error.message"), + "server error"); + long startMs = System.currentTimeMillis(); + long endMs = startMs + 900; + SpanData span = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(createSpanContext(TRACE_ID, SPAN_ID)) + .setName("GET /api/endpoint") + .setStartEpochNanos(MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(MILLISECONDS.toNanos(endMs)) + .setKind(SpanKind.SERVER) + .setStatus(StatusData.error()) + .setAttributes(attributes) + .setTotalRecordedEvents(0) + .setTotalRecordedLinks(0) + .build(); + + io.jaegertracing.thriftjava.Span jaegerSpan = Adapter.toJaeger(span); + assertThat(getValue(jaegerSpan.getTags(), "error.type").getVStr()) + .isEqualTo(this.getClass().getName()); + assertThat(getValue(jaegerSpan.getTags(), "error").isVBool()).isTrue(); + } + + private static EventData getTimedEvent() { + return getTimedEvent(-1); + } + + private static EventData getTimedEvent(int totalAttributeCount) { + long epochNanos = MILLISECONDS.toNanos(System.currentTimeMillis()); + Attributes attributes = Attributes.of(stringKey("foo"), "bar"); + if (totalAttributeCount <= 0) { + totalAttributeCount = attributes.size(); + } + return EventData.create(epochNanos, "the log message", attributes, totalAttributeCount); + } + + private static SpanData getSpanData(long startMs, long endMs, SpanKind kind) { + return getSpanData(startMs, endMs, kind, 1, 1); + } + + private static SpanData getSpanData( + long startMs, long endMs, SpanKind kind, int totalRecordedEvents, int totalAttributeCount) { + Attributes attributes = Attributes.of(booleanKey("valueB"), true); + + LinkData link = LinkData.create(createSpanContext(LINK_TRACE_ID, LINK_SPAN_ID), attributes); + + return TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(createSpanContext(TRACE_ID, SPAN_ID)) + .setParentSpanContext( + SpanContext.create( + TRACE_ID, PARENT_SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())) + .setName("GET /api/endpoint") + .setStartEpochNanos(MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(MILLISECONDS.toNanos(endMs)) + .setAttributes(Attributes.of(booleanKey("valueB"), true)) + .setTotalAttributeCount(totalAttributeCount) + .setEvents(Collections.singletonList(getTimedEvent())) + .setTotalRecordedEvents(totalRecordedEvents) + .setLinks(Collections.singletonList(link)) + .setTotalRecordedLinks(1) + .setKind(kind) + .setResource(Resource.create(Attributes.empty())) + .setStatus(StatusData.create(StatusCode.OK, "ok!")) + .build(); + } + + private static SpanContext createSpanContext(String traceId, String spanId) { + return SpanContext.create(traceId, spanId, TraceFlags.getDefault(), TraceState.getDefault()); + } + + @Nullable + private static Tag getValue(List tagsList, String s) { + for (Tag kv : tagsList) { + if (kv.getKey().equals(s)) { + return kv; + } + } + return null; + } + + private static void assertHasFollowsFrom(io.jaegertracing.thriftjava.Span jaegerSpan) { + boolean found = false; + for (SpanRef spanRef : jaegerSpan.getReferences()) { + + if (SpanRefType.FOLLOWS_FROM.equals(spanRef.getRefType())) { + assertThat(traceIdFromLongs(spanRef.getTraceIdHigh(), spanRef.getTraceIdLow())) + .isEqualTo(LINK_TRACE_ID); + assertThat(spanIdFromLong(spanRef.getSpanId())).isEqualTo(LINK_SPAN_ID); + found = true; + } + } + assertThat(found).isTrue(); + } + + private static void assertHasParent(io.jaegertracing.thriftjava.Span jaegerSpan) { + boolean found = false; + for (SpanRef spanRef : jaegerSpan.getReferences()) { + if (SpanRefType.CHILD_OF.equals(spanRef.getRefType())) { + assertThat(traceIdFromLongs(spanRef.getTraceIdHigh(), spanRef.getTraceIdLow())) + .isEqualTo(TRACE_ID); + assertThat(spanIdFromLong(spanRef.getSpanId())).isEqualTo(PARENT_SPAN_ID); + found = true; + } + } + assertThat(found).isTrue(); + assertThat(spanIdFromLong(jaegerSpan.getParentSpanId())).isEqualTo(PARENT_SPAN_ID); + } + + private static String traceIdFromLongs(long high, long low) { + return hex.encode( + ByteBuffer.allocate(16).order(ByteOrder.BIG_ENDIAN).putLong(high).putLong(low).array()); + } + + private static String spanIdFromLong(long id) { + return hex.encode(ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putLong(id).array()); + } +} diff --git a/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftIntegrationTest.java b/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftIntegrationTest.java new file mode 100644 index 000000000..2d6a64253 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftIntegrationTest.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger.thrift; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.time.Duration; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class JaegerThriftIntegrationTest { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final OkHttpClient client = new OkHttpClient(); + + private static final int QUERY_PORT = 16686; + private static final int THRIFT_HTTP_PORT = 14268; + private static final int HEALTH_PORT = 14269; + private static final String SERVICE_NAME = "E2E-test"; + private static final String JAEGER_URL = "http://localhost"; + + @Container + public static GenericContainer jaegerContainer = + new GenericContainer<>("ghcr.io/open-telemetry/java-test-containers:jaeger") + .withExposedPorts(THRIFT_HTTP_PORT, QUERY_PORT, HEALTH_PORT) + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("jaeger"))) + .waitingFor(Wait.forHttp("/").forPort(HEALTH_PORT)); + + @Test + void testJaegerIntegration() { + OpenTelemetry openTelemetry = initOpenTelemetry(); + imitateWork(openTelemetry); + Awaitility.await() + .atMost(Duration.ofSeconds(30)) + .until(JaegerThriftIntegrationTest::assertJaegerHasATrace); + } + + private static OpenTelemetry initOpenTelemetry() { + Integer mappedPort = jaegerContainer.getMappedPort(THRIFT_HTTP_PORT); + + SpanExporter jaegerExporter = + JaegerThriftSpanExporter.builder() + .setEndpoint(JAEGER_URL + ":" + mappedPort + "/api/traces") + .build(); + return OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(jaegerExporter)) + .setResource( + Resource.getDefault().toBuilder() + .put(ResourceAttributes.SERVICE_NAME, SERVICE_NAME) + .build()) + .build()) + .build(); + } + + private void imitateWork(OpenTelemetry openTelemetry) { + Span span = + openTelemetry.getTracer(getClass().getCanonicalName()).spanBuilder("Test span").startSpan(); + span.addEvent("some event"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + span.end(); + } + + private static boolean assertJaegerHasATrace() { + try { + Integer mappedPort = jaegerContainer.getMappedPort(QUERY_PORT); + String url = + String.format( + "%s/api/traces?service=%s", + String.format(JAEGER_URL + ":%d", mappedPort), SERVICE_NAME); + + Request request = + new Request.Builder() + .url(url) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .build(); + + final JsonNode json; + try (Response response = client.newCall(request).execute()) { + json = objectMapper.readTree(response.body().byteStream()); + } + + return json.get("data").get(0).get("traceID") != null; + } catch (Exception e) { + return false; + } + } +} diff --git a/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporterTest.java b/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporterTest.java new file mode 100644 index 000000000..3e51fc245 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger-thrift/src/test/java/io/opentelemetry/exporter/jaeger/thrift/JaegerThriftSpanExporterTest.java @@ -0,0 +1,254 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger.thrift; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +import io.jaegertracing.internal.exceptions.SenderException; +import io.jaegertracing.thrift.internal.senders.ThriftSender; +import io.jaegertracing.thriftjava.Process; +import io.jaegertracing.thriftjava.Span; +import io.jaegertracing.thriftjava.SpanRef; +import io.jaegertracing.thriftjava.SpanRefType; +import io.jaegertracing.thriftjava.Tag; +import io.jaegertracing.thriftjava.TagType; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class JaegerThriftSpanExporterTest { + + private static final String TRACE_ID = "a0000000000000000000000000abc123"; + private static final long TRACE_ID_HIGH = 0xa000000000000000L; + private static final long TRACE_ID_LOW = 0x0000000000abc123L; + private static final String SPAN_ID = "00000f0000def456"; + private static final long SPAN_ID_LONG = 0x00000f0000def456L; + private static final String SPAN_ID_2 = "00a0000000aef789"; + private static final long SPAN_ID_2_LONG = 0x00a0000000aef789L; + private static final SpanContext SPAN_CONTEXT = + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()); + private static final SpanContext SPAN_CONTEXT_2 = + SpanContext.create(TRACE_ID, SPAN_ID_2, TraceFlags.getDefault(), TraceState.getDefault()); + + private JaegerThriftSpanExporter exporter; + @Mock private ThriftSender thriftSender; + + @BeforeEach + void beforeEach() { + exporter = JaegerThriftSpanExporter.builder().setThriftSender(thriftSender).build(); + } + + @Test + void testExport() throws SenderException, UnknownHostException { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + SpanData span = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(SPAN_CONTEXT) + .setParentSpanContext(SPAN_CONTEXT_2) + .setName("GET /api/endpoint") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setStatus(StatusData.ok()) + .setKind(SpanKind.CONSUMER) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .setInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("io.opentelemetry.auto", "1.0.0")) + .setResource( + Resource.create( + Attributes.of( + ResourceAttributes.SERVICE_NAME, + "myServiceName", + AttributeKey.stringKey("resource-attr-key"), + "resource-attr-value"))) + .build(); + + // test + CompletableResultCode result = exporter.export(Collections.singletonList(span)); + result.join(1, TimeUnit.SECONDS); + assertThat(result.isSuccess()).isEqualTo(true); + + // verify + Process expectedProcess = new Process("myServiceName"); + expectedProcess.addToTags( + new Tag("jaeger.version", TagType.STRING).setVStr("opentelemetry-java")); + expectedProcess.addToTags( + new Tag("ip", TagType.STRING).setVStr(InetAddress.getLocalHost().getHostAddress())); + expectedProcess.addToTags( + new Tag("hostname", TagType.STRING).setVStr(InetAddress.getLocalHost().getHostName())); + expectedProcess.addToTags( + new Tag("resource-attr-key", TagType.STRING).setVStr("resource-attr-value")); + expectedProcess.addToTags(new Tag("service.name", TagType.STRING).setVStr("myServiceName")); + + Span expectedSpan = + new Span() + .setTraceIdHigh(TRACE_ID_HIGH) + .setTraceIdLow(TRACE_ID_LOW) + .setSpanId(SPAN_ID_LONG) + .setOperationName("GET /api/endpoint") + .setReferences( + Collections.singletonList( + new SpanRef() + .setSpanId(SPAN_ID_2_LONG) + .setTraceIdHigh(TRACE_ID_HIGH) + .setTraceIdLow(TRACE_ID_LOW) + .setRefType(SpanRefType.CHILD_OF))) + .setParentSpanId(SPAN_ID_2_LONG) + .setStartTime(TimeUnit.MILLISECONDS.toMicros(startMs)) + .setDuration(TimeUnit.MILLISECONDS.toMicros(duration)) + .setLogs(Collections.emptyList()); + expectedSpan.addToTags(new Tag("span.kind", TagType.STRING).setVStr("consumer")); + expectedSpan.addToTags(new Tag("otel.status_code", TagType.STRING).setVStr("OK")); + expectedSpan.addToTags( + new Tag("otel.library.name", TagType.STRING).setVStr("io.opentelemetry.auto")); + expectedSpan.addToTags(new Tag("otel.library.version", TagType.STRING).setVStr("1.0.0")); + + List expectedSpans = Collections.singletonList(expectedSpan); + verify(thriftSender).send(expectedProcess, expectedSpans); + } + + @Test + void testExportMultipleResources() throws SenderException, UnknownHostException { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + SpanData span = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(SPAN_CONTEXT) + .setName("GET /api/endpoint/1") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setStatus(StatusData.ok()) + .setKind(SpanKind.CONSUMER) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .setInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("io.opentelemetry.auto", "1.0.0")) + .setResource( + Resource.create( + Attributes.of( + ResourceAttributes.SERVICE_NAME, + "myServiceName1", + AttributeKey.stringKey("resource-attr-key-1"), + "resource-attr-value-1"))) + .build(); + + SpanData span2 = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(SPAN_CONTEXT_2) + .setName("GET /api/endpoint/2") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setStatus(StatusData.ok()) + .setKind(SpanKind.CONSUMER) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .setInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("io.opentelemetry.auto", "1.0.0")) + .setResource( + Resource.create( + Attributes.of( + ResourceAttributes.SERVICE_NAME, + "myServiceName2", + AttributeKey.stringKey("resource-attr-key-2"), + "resource-attr-value-2"))) + .build(); + + // test + CompletableResultCode result = exporter.export(Arrays.asList(span, span2)); + result.join(1, TimeUnit.SECONDS); + assertThat(result.isSuccess()).isEqualTo(true); + + // verify + Process expectedProcess1 = new Process("myServiceName1"); + expectedProcess1.addToTags( + new Tag("jaeger.version", TagType.STRING).setVStr("opentelemetry-java")); + expectedProcess1.addToTags( + new Tag("ip", TagType.STRING).setVStr(InetAddress.getLocalHost().getHostAddress())); + expectedProcess1.addToTags( + new Tag("hostname", TagType.STRING).setVStr(InetAddress.getLocalHost().getHostName())); + expectedProcess1.addToTags( + new Tag("resource-attr-key-1", TagType.STRING).setVStr("resource-attr-value-1")); + expectedProcess1.addToTags(new Tag("service.name", TagType.STRING).setVStr("myServiceName1")); + + Process expectedProcess2 = new Process("myServiceName2"); + expectedProcess2.addToTags( + new Tag("jaeger.version", TagType.STRING).setVStr("opentelemetry-java")); + expectedProcess2.addToTags( + new Tag("ip", TagType.STRING).setVStr(InetAddress.getLocalHost().getHostAddress())); + expectedProcess2.addToTags( + new Tag("hostname", TagType.STRING).setVStr(InetAddress.getLocalHost().getHostName())); + expectedProcess2.addToTags( + new Tag("resource-attr-key-2", TagType.STRING).setVStr("resource-attr-value-2")); + expectedProcess2.addToTags(new Tag("service.name", TagType.STRING).setVStr("myServiceName2")); + + Span expectedSpan1 = + new Span() + .setTraceIdHigh(TRACE_ID_HIGH) + .setTraceIdLow(TRACE_ID_LOW) + .setSpanId(SPAN_ID_LONG) + .setOperationName("GET /api/endpoint/1") + .setReferences(Collections.emptyList()) + .setStartTime(TimeUnit.MILLISECONDS.toMicros(startMs)) + .setDuration(TimeUnit.MILLISECONDS.toMicros(duration)) + .setLogs(Collections.emptyList()); + expectedSpan1.addToTags(new Tag("span.kind", TagType.STRING).setVStr("consumer")); + expectedSpan1.addToTags(new Tag("otel.status_code", TagType.STRING).setVStr("OK")); + expectedSpan1.addToTags( + new Tag("otel.library.name", TagType.STRING).setVStr("io.opentelemetry.auto")); + expectedSpan1.addToTags(new Tag("otel.library.version", TagType.STRING).setVStr("1.0.0")); + + Span expectedSpan2 = + new Span() + .setTraceIdHigh(TRACE_ID_HIGH) + .setTraceIdLow(TRACE_ID_LOW) + .setSpanId(SPAN_ID_2_LONG) + .setOperationName("GET /api/endpoint/2") + .setReferences(Collections.emptyList()) + .setStartTime(TimeUnit.MILLISECONDS.toMicros(startMs)) + .setDuration(TimeUnit.MILLISECONDS.toMicros(duration)) + .setLogs(Collections.emptyList()); + expectedSpan2.addToTags(new Tag("span.kind", TagType.STRING).setVStr("consumer")); + expectedSpan2.addToTags(new Tag("otel.status_code", TagType.STRING).setVStr("OK")); + expectedSpan2.addToTags( + new Tag("otel.library.name", TagType.STRING).setVStr("io.opentelemetry.auto")); + expectedSpan2.addToTags(new Tag("otel.library.version", TagType.STRING).setVStr("1.0.0")); + + verify(thriftSender).send(expectedProcess2, Collections.singletonList(expectedSpan2)); + verify(thriftSender).send(expectedProcess1, Collections.singletonList(expectedSpan1)); + } +} diff --git a/opentelemetry-java/exporters/jaeger/README.md b/opentelemetry-java/exporters/jaeger/README.md new file mode 100644 index 000000000..d3b135b90 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/README.md @@ -0,0 +1,36 @@ +# OpenTelemetry - Jaeger Exporter - gRPC + +[![Javadocs][javadoc-image]][javadoc-url] + +This is the OpenTelemetry exporter, sending span data to Jaeger via gRPC. + +## Configuration + +The Jaeger gRPC span exporter can be configured programmatically. + +An example of simple Jaeger gRPC exporter initialization. In this case +spans will be sent to a Jaeger gRPC endpoint running on `localhost`: + +```java +JaegerGrpcSpanExporter exporter = + JaegerGrpcSpanExporter.builder() + .setEndpoint("http://localhost:14250") + .build(); +``` + +If you need configuration via environment variables and/or system properties, you will want to use +the [autoconfigure](../../sdk-extensions/autoconfigure) module. + +## Compatibility + +As with the OpenTelemetry SDK itself, this exporter is compatible with Java 8+ and Android API level 24+. + +## Proto files + +The proto files in this repository were copied over from the [Jaeger main repository][proto-origin]. +At this moment, they have to be manually synchronized, but a [discussion exists][proto-discussion] on how to properly consume them in a more appropriate manner. + +[proto-origin]: https://github.com/jaegertracing/jaeger/tree/5b8c1f40f932897b9322bf3f110d830536ae4c71/model/proto +[proto-discussion]: https://github.com/open-telemetry/opentelemetry-java/issues/235 +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-exporters-jaeger.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-exporters-jaeger diff --git a/opentelemetry-java/exporters/jaeger/build.gradle.kts b/opentelemetry-java/exporters/jaeger/build.gradle.kts new file mode 100644 index 000000000..94a41fa15 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + `java-library` + `maven-publish` + + id("com.google.protobuf") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry - Jaeger Exporter" +extra["moduleName"] = "io.opentelemetry.exporter.jaeger" + +dependencies { + api(project(":sdk:all")) + api("io.grpc:grpc-api") + + implementation(project(":sdk:all")) + implementation(project(":semconv")) + + implementation("io.grpc:grpc-protobuf") + implementation("io.grpc:grpc-stub") + implementation("com.google.protobuf:protobuf-java") + implementation("com.google.protobuf:protobuf-java-util") + + testImplementation("io.grpc:grpc-testing") + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("com.squareup.okhttp3:okhttp") + + testImplementation(project(":sdk:testing")) + + testRuntimeOnly("io.grpc:grpc-netty-shaded") +} + +// IntelliJ complains that the generated classes are not found, ask IntelliJ to include the +// generated Java directories as source folders. +idea { + module { + sourceDirs.add(file("build/generated/source/proto/main/java")) + // If you have additional sourceSets and/or codegen plugins, add all of them + } +} diff --git a/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/Adapter.java b/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/Adapter.java new file mode 100644 index 000000000..8833117ec --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/Adapter.java @@ -0,0 +1,278 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gson.Gson; +import com.google.protobuf.ByteString; +import com.google.protobuf.Timestamp; +import com.google.protobuf.util.Timestamps; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.exporter.jaeger.proto.api_v2.Model; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import javax.annotation.concurrent.ThreadSafe; + +/** Adapts OpenTelemetry objects to Jaeger objects. */ +@ThreadSafe +final class Adapter { + static final AttributeKey KEY_ERROR = booleanKey("error"); + static final String KEY_LOG_EVENT = "event"; + static final String KEY_EVENT_DROPPED_ATTRIBUTES_COUNT = "otel.event.dropped_attributes_count"; + static final String KEY_DROPPED_ATTRIBUTES_COUNT = "otel.dropped_attributes_count"; + static final String KEY_DROPPED_EVENTS_COUNT = "otel.dropped_events_count"; + static final String KEY_SPAN_KIND = "span.kind"; + static final String KEY_SPAN_STATUS_MESSAGE = "otel.status_description"; + static final String KEY_SPAN_STATUS_CODE = "otel.status_code"; + static final String KEY_INSTRUMENTATION_LIBRARY_NAME = "otel.library.name"; + static final String KEY_INSTRUMENTATION_LIBRARY_VERSION = "otel.library.version"; + static final String KEY_HERACONTEXT = "span.hera_context"; + + private Adapter() {} + + /** + * Converts a list of {@link SpanData} into a collection of Jaeger's {@link Model.Span}. + * + * @param spans the list of spans to be converted + * @return the collection of Jaeger spans + * @see #toJaeger(SpanData) + */ + static Collection toJaeger(Collection spans) { + List convertedList = new ArrayList<>(spans.size()); + for (SpanData span : spans) { + convertedList.add(toJaeger(span)); + } + return convertedList; + } + + /** + * Converts a single {@link SpanData} into a Jaeger's {@link Model.Span}. + * + * @param span the span to be converted + * @return the Jaeger span + */ + static Model.Span toJaeger(SpanData span) { + Model.Span.Builder target = Model.Span.newBuilder(); + + SpanContext spanContext = span.getSpanContext(); + target.setTraceId(ByteString.copyFrom(spanContext.getTraceIdBytes())); + target.setSpanId(ByteString.copyFrom(spanContext.getSpanIdBytes())); + target.setOperationName(span.getName()); + Timestamp startTimestamp = Timestamps.fromNanos(span.getStartEpochNanos()); + target.setStartTime(startTimestamp); + target.setDuration( + Timestamps.between(startTimestamp, Timestamps.fromNanos(span.getEndEpochNanos()))); + + target.addAllTags(toKeyValues(span.getAttributes())); + int droppedAttributes = span.getTotalAttributeCount() - span.getAttributes().size(); + if (droppedAttributes > 0) { + target.addTags(toKeyValue(KEY_DROPPED_ATTRIBUTES_COUNT, droppedAttributes)); + } + + target.addAllLogs(toJaegerLogs(span.getEvents())); + int droppedEvents = span.getTotalRecordedEvents() - span.getEvents().size(); + if (droppedEvents > 0) { + target.addTags(toKeyValue(KEY_DROPPED_EVENTS_COUNT, droppedEvents)); + } + target.addAllReferences(toSpanRefs(span.getLinks())); + + // add the parent span + SpanContext parentSpanContext = span.getParentSpanContext(); + if (parentSpanContext.isValid()) { + target.addReferences( + Model.SpanRef.newBuilder() + .setTraceId(ByteString.copyFrom(parentSpanContext.getTraceIdBytes())) + .setSpanId(ByteString.copyFrom(parentSpanContext.getSpanIdBytes())) + .setRefType(Model.SpanRefType.CHILD_OF)); + } + + if (span.getKind() != SpanKind.INTERNAL) { + target.addTags(toKeyValue(KEY_SPAN_KIND, span.getKind().name().toLowerCase(Locale.ROOT))); + } + + if (!span.getStatus().getDescription().isEmpty()) { + target.addTags(toKeyValue(KEY_SPAN_STATUS_MESSAGE, span.getStatus().getDescription())); + } + + if (span.getStatus().getStatusCode() != StatusCode.UNSET) { + target.addTags(toKeyValue(KEY_SPAN_STATUS_CODE, span.getStatus().getStatusCode().name())); + } + + target.addTags( + toKeyValue( + KEY_INSTRUMENTATION_LIBRARY_NAME, span.getInstrumentationLibraryInfo().getName())); + + if (span.getInstrumentationLibraryInfo().getVersion() != null) { + target.addTags( + toKeyValue( + KEY_INSTRUMENTATION_LIBRARY_VERSION, + span.getInstrumentationLibraryInfo().getVersion())); + } + + if (span.getStatus().getStatusCode() == StatusCode.ERROR) { + target.addTags(toKeyValue(KEY_ERROR, true)); + } + // heraContext + String heraContext = spanContext.getHeraContext() == null ? "" : spanContext.getHeraContext().get("heracontext"); + if(heraContext == null){ + heraContext = ""; + } + target.addTags(toKeyValue(KEY_HERACONTEXT,heraContext)); + + return target.build(); + } + + /** + * Converts {@link EventData}s into a collection of Jaeger's {@link Model.Log}. + * + * @param timeEvents the timed events to be converted + * @return a collection of Jaeger logs + * @see #toJaegerLog(EventData) + */ + @VisibleForTesting + static Collection toJaegerLogs(List timeEvents) { + List logs = new ArrayList<>(timeEvents.size()); + for (EventData e : timeEvents) { + logs.add(toJaegerLog(e)); + } + return logs; + } + + /** + * Converts a {@link EventData} into Jaeger's {@link Model.Log}. + * + * @param event the timed event to be converted + * @return a Jaeger log + */ + @VisibleForTesting + static Model.Log toJaegerLog(EventData event) { + Model.Log.Builder builder = Model.Log.newBuilder(); + builder.setTimestamp(Timestamps.fromNanos(event.getEpochNanos())); + + // name is a top-level property in OpenTelemetry + builder.addFields(toKeyValue(KEY_LOG_EVENT, event.getName())); + + int droppedAttributesCount = event.getDroppedAttributesCount(); + if (droppedAttributesCount > 0) { + builder.addFields( + Model.KeyValue.newBuilder() + .setKey(KEY_EVENT_DROPPED_ATTRIBUTES_COUNT) + .setVInt64(droppedAttributesCount) + .build()); + } + builder.addAllFields(toKeyValues(event.getAttributes())); + + return builder.build(); + } + + /** + * Converts a map of attributes into a collection of Jaeger's {@link Model.KeyValue}. + * + * @param attributes the span attributes + * @return a collection of Jaeger key values + * @see #toKeyValue + */ + @VisibleForTesting + static Collection toKeyValues(Attributes attributes) { + final List tags = new ArrayList<>(attributes.size()); + attributes.forEach((key, value) -> tags.add(toKeyValue(key, value))); + return tags; + } + + /** + * Converts the given {@link AttributeKey} and value into Jaeger's {@link Model.KeyValue}. + * + * @param key the entry key as string + * @param value the entry value + * @return a Jaeger key value + */ + @VisibleForTesting + static Model.KeyValue toKeyValue(AttributeKey key, Object value) { + Model.KeyValue.Builder builder = Model.KeyValue.newBuilder(); + builder.setKey(key.getKey()); + + switch (key.getType()) { + case STRING: + builder.setVStr((String) value); + builder.setVType(Model.ValueType.STRING); + break; + case LONG: + builder.setVInt64((long) value); + builder.setVType(Model.ValueType.INT64); + break; + case BOOLEAN: + builder.setVBool((boolean) value); + builder.setVType(Model.ValueType.BOOL); + break; + case DOUBLE: + builder.setVFloat64((double) value); + builder.setVType(Model.ValueType.FLOAT64); + break; + case STRING_ARRAY: + case LONG_ARRAY: + case BOOLEAN_ARRAY: + case DOUBLE_ARRAY: + builder.setVStr(new Gson().toJson(value)); + builder.setVType(Model.ValueType.STRING); + break; + } + return builder.build(); + } + + private static Model.KeyValue toKeyValue(String key, String value) { + return Model.KeyValue.newBuilder().setKey(key).setVStr(value).build(); + } + + private static Model.KeyValue toKeyValue(String key, long value) { + return Model.KeyValue.newBuilder().setKey(key).setVInt64(value).build(); + } + + /** + * Converts {@link LinkData}s into a collection of Jaeger's {@link Model.SpanRef}. + * + * @param links the span's links property to be converted + * @return a collection of Jaeger span references + */ + @VisibleForTesting + static Collection toSpanRefs(List links) { + List spanRefs = new ArrayList<>(links.size()); + for (LinkData link : links) { + spanRefs.add(toSpanRef(link)); + } + return spanRefs; + } + + /** + * Converts a single {@link LinkData} into a Jaeger's {@link Model.SpanRef}. + * + * @param link the OpenTelemetry link to be converted + * @return the Jaeger span reference + */ + @VisibleForTesting + static Model.SpanRef toSpanRef(LinkData link) { + Model.SpanRef.Builder builder = Model.SpanRef.newBuilder(); + builder.setTraceId(ByteString.copyFrom(link.getSpanContext().getTraceIdBytes())); + builder.setSpanId(ByteString.copyFrom(link.getSpanContext().getSpanIdBytes())); + + // we can assume that all links are *follows from* + // https://github.com/open-telemetry/opentelemetry-java/issues/475 + // https://github.com/open-telemetry/opentelemetry-java/pull/481/files#r312577862 + builder.setRefType(Model.SpanRefType.FOLLOWS_FROM); + + return builder.build(); + } +} diff --git a/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporter.java b/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporter.java new file mode 100644 index 000000000..c70e4a5a3 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporter.java @@ -0,0 +1,218 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.ConnectivityState; +import io.grpc.ManagedChannel; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.exporter.jaeger.proto.api_v2.Collector; +import io.opentelemetry.exporter.jaeger.proto.api_v2.CollectorServiceGrpc; +import io.opentelemetry.exporter.jaeger.proto.api_v2.Model; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import io.opentelemetry.sdk.common.SystemCommon; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.concurrent.ThreadSafe; + +/** Exports spans to Jaeger via gRPC, using Jaeger's protobuf model. */ +@ThreadSafe +public final class JaegerGrpcSpanExporter implements SpanExporter { + + private static final String IP_KEY = "ip"; + private static final String ENV_KEY = "service.env"; + private static final String ENV_ID_KEY = "service.env.id"; + private static final String IP_DEFAULT = "0.0.0.0"; + private final ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(JaegerGrpcSpanExporter.class.getName())); + + private final CollectorServiceGrpc.CollectorServiceFutureStub stub; + private final Model.Process.Builder processBuilder; + private final ManagedChannel managedChannel; + private final long timeoutNanos; + + /** + * Creates a new Jaeger gRPC Span Reporter with the given name, using the given channel. + * + * @param channel the channel to use when communicating with the Jaeger Collector. + * @param timeoutNanos max waiting time for the collector to process each span batch. When set to + * 0 or to a negative value, the exporter will wait indefinitely. + */ + JaegerGrpcSpanExporter(ManagedChannel channel, long timeoutNanos) { + String ipv4; + + try { + String ipv4Env = SystemCommon.getEnvOrProperties(EnvOrJvmProperties.ENV_HOST_IP.getKey()); + if (!StringUtils.isNullOrEmpty(ipv4Env)) { + ipv4 = ipv4Env; + } else { + ipv4 = InetAddress.getLocalHost().getHostAddress(); + } + } catch (UnknownHostException e) { + ipv4 = IP_DEFAULT; + } + // mifaas env name + String env = SystemCommon.getEnvOrProperties(EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_NAME.getKey()); + // env id + String envId = SystemCommon.getEnvOrProperties(EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_ID.getKey()); + if (envId == null) { + envId = ""; + } + Model.KeyValue ipv4Tag = Model.KeyValue.newBuilder().setKey(IP_KEY).setVStr(ipv4).build(); + Model.KeyValue envTag = Model.KeyValue.newBuilder().setKey(ENV_KEY).setVStr(env).build(); + Model.KeyValue envIdTag = Model.KeyValue.newBuilder().setKey(ENV_ID_KEY).setVStr(envId).build(); + this.processBuilder = + Model.Process.newBuilder().addTags(ipv4Tag).addTags(envTag).addTags(envIdTag); + this.managedChannel = channel; + this.stub = CollectorServiceGrpc.newFutureStub(channel); + this.timeoutNanos = timeoutNanos; + } + + /** + * Submits all the given spans in a single batch to the Jaeger collector. + * + * @param spans the list of sampled Spans to be exported. + * @return the result of the operation + */ + @Override + public CompletableResultCode export(Collection spans) { + CollectorServiceGrpc.CollectorServiceFutureStub stub = this.stub; + if (timeoutNanos > 0) { + stub = stub.withDeadlineAfter(timeoutNanos, TimeUnit.NANOSECONDS); + } + + List requests = new ArrayList<>(); + spans.stream() + .collect(Collectors.groupingBy(SpanData::getResource)) + .forEach((resource, spanData) -> requests.add(buildRequest(resource, spanData))); + + List> listenableFutures = + new ArrayList<>(requests.size()); + for (Collector.PostSpansRequest request : requests) { + listenableFutures.add(stub.postSpans(request)); + } + + final CompletableResultCode result = new CompletableResultCode(); + AtomicInteger pending = new AtomicInteger(listenableFutures.size()); + AtomicReference error = new AtomicReference<>(); + for (ListenableFuture future : listenableFutures) { + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Collector.PostSpansResponse result) { + fulfill(); + } + + @Override + public void onFailure(Throwable t) { + error.set(t); + fulfill(); + } + + private void fulfill() { + if (pending.decrementAndGet() == 0) { + Throwable t = error.get(); + if (t != null) { + logger.log(Level.WARNING, "Failed to export spans", t); + result.fail(); + } else { + result.succeed(); + } + } + } + }, + MoreExecutors.directExecutor()); + } + return result; + } + + private Collector.PostSpansRequest buildRequest(Resource resource, List spans) { + Model.Process.Builder builder = this.processBuilder.clone(); + + String serviceName = resource.getAttributes().get(ResourceAttributes.SERVICE_NAME); + if (serviceName == null || serviceName.isEmpty()) { + serviceName = Resource.getDefault().getAttributes().get(ResourceAttributes.SERVICE_NAME); + } + builder.setServiceName(serviceName); + + builder.addAllTags(Adapter.toKeyValues(resource.getAttributes())); + + return Collector.PostSpansRequest.newBuilder() + .setBatch( + Model.Batch.newBuilder() + .addAllSpans(Adapter.toJaeger(spans)) + .setProcess(builder.build()) + .build()) + .build(); + } + + /** + * The Jaeger exporter does not batch spans, so this method will immediately return with success. + * + * @return always Success + */ + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + /** + * Returns a new builder instance for this exporter. + * + * @return a new builder instance for this exporter. + */ + public static JaegerGrpcSpanExporterBuilder builder() { + return new JaegerGrpcSpanExporterBuilder(); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately + * cancelled. + */ + @Override + public CompletableResultCode shutdown() { + final CompletableResultCode result = new CompletableResultCode(); + managedChannel.notifyWhenStateChanged( + ConnectivityState.SHUTDOWN, + new Runnable() { + @Override + public void run() { + result.succeed(); + } + }); + managedChannel.shutdown(); + return result; + } + + // Visible for testing + Model.Process.Builder getProcessBuilder() { + return processBuilder; + } + + // Visible for testing + ManagedChannel getManagedChannel() { + return managedChannel; + } +} diff --git a/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporterBuilder.java b/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporterBuilder.java new file mode 100644 index 000000000..26cec910b --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporterBuilder.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger; + +import static io.opentelemetry.api.internal.Utils.checkArgument; +import static java.util.Objects.requireNonNull; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** Builder utility for this exporter. */ +public final class JaegerGrpcSpanExporterBuilder { + + private static final String DEFAULT_ENDPOINT_URL = "http://localhost:14250"; + private static final URI DEFAULT_ENDPOINT = URI.create(DEFAULT_ENDPOINT_URL); + private static final long DEFAULT_TIMEOUT_SECS = 10; + + private URI endpoint = DEFAULT_ENDPOINT; + private ManagedChannel channel; + private long timeoutNanos = TimeUnit.SECONDS.toNanos(DEFAULT_TIMEOUT_SECS); + + /** + * Sets the managed channel to use when communicating with the backend. Takes precedence over + * {@link #setEndpoint(String)} if both are called. + * + * @param channel the channel to use. + * @return this. + */ + public JaegerGrpcSpanExporterBuilder setChannel(ManagedChannel channel) { + this.channel = channel; + return this; + } + + /** + * Sets the Jaeger endpoint to connect to. If unset, defaults to {@value DEFAULT_ENDPOINT_URL}. + * The endpoint must start with either http:// or https://. + */ + public JaegerGrpcSpanExporterBuilder setEndpoint(String endpoint) { + requireNonNull(endpoint, "endpoint"); + + URI uri; + try { + uri = new URI(endpoint); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid endpoint, must be a URL: " + endpoint, e); + } + + if (uri.getScheme() == null + || (!uri.getScheme().equals("http") && !uri.getScheme().equals("https"))) { + throw new IllegalArgumentException( + "Invalid endpoint, must start with http:// or https://: " + uri); + } + + this.endpoint = uri; + return this; + } + + /** + * Sets the maximum time to wait for the collector to process an exported batch of spans. If + * unset, defaults to {@value DEFAULT_TIMEOUT_SECS}s. + */ + public JaegerGrpcSpanExporterBuilder setTimeout(long timeout, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(timeout >= 0, "timeout must be non-negative"); + timeoutNanos = unit.toNanos(timeout); + return this; + } + + /** + * Sets the maximum time to wait for the collector to process an exported batch of spans. If + * unset, defaults to {@value DEFAULT_TIMEOUT_SECS}s. + */ + public JaegerGrpcSpanExporterBuilder setTimeout(Duration timeout) { + requireNonNull(timeout, "timeout"); + return setTimeout(timeout.toNanos(), TimeUnit.NANOSECONDS); + } + + /** + * Constructs a new instance of the exporter based on the builder's values. + * + * @return a new exporter's instance. + */ + public JaegerGrpcSpanExporter build() { + if (channel == null) { + ManagedChannelBuilder managedChannelBuilder = + ManagedChannelBuilder.forTarget(endpoint.getAuthority()); + + if (endpoint.getScheme().equals("https")) { + managedChannelBuilder.useTransportSecurity(); + } else { + managedChannelBuilder.usePlaintext(); + } + + channel = managedChannelBuilder.build(); + } + return new JaegerGrpcSpanExporter(channel, timeoutNanos); + } + + JaegerGrpcSpanExporterBuilder() {} +} diff --git a/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/package-info.java b/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/package-info.java new file mode 100644 index 000000000..23e486e2f --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/main/java/io/opentelemetry/exporter/jaeger/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.jaeger; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/jaeger/src/main/proto/jaeger/api_v2/collector.proto b/opentelemetry-java/exporters/jaeger/src/main/proto/jaeger/api_v2/collector.proto new file mode 100644 index 000000000..03d6b6be8 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/main/proto/jaeger/api_v2/collector.proto @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +syntax="proto3"; + +package jaeger.api_v2; + +import "jaeger/api_v2/model.proto"; + +option java_package = "io.opentelemetry.exporter.jaeger.proto.api_v2"; + +message PostSpansRequest { + Batch batch = 1; +} + +message PostSpansResponse { +} + +service CollectorService { + rpc PostSpans(PostSpansRequest) returns (PostSpansResponse) {} +} diff --git a/opentelemetry-java/exporters/jaeger/src/main/proto/jaeger/api_v2/model.proto b/opentelemetry-java/exporters/jaeger/src/main/proto/jaeger/api_v2/model.proto new file mode 100644 index 000000000..0d2bd2127 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/main/proto/jaeger/api_v2/model.proto @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +syntax="proto3"; + +package jaeger.api_v2; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; + +option java_package = "io.opentelemetry.exporter.jaeger.proto.api_v2"; + +enum ValueType { + STRING = 0; + BOOL = 1; + INT64 = 2; + FLOAT64 = 3; + BINARY = 4; +}; + +message Log { + google.protobuf.Timestamp timestamp = 1; + repeated KeyValue fields = 2; +} + +message KeyValue { + string key = 1; + ValueType v_type = 2; + string v_str = 3; + bool v_bool = 4; + int64 v_int64 = 5; + double v_float64 = 6; + bytes v_binary = 7; +} + +enum SpanRefType { + CHILD_OF = 0; + FOLLOWS_FROM = 1; +}; + +message SpanRef { + bytes trace_id = 1; + bytes span_id = 2; + SpanRefType ref_type = 3; +} + +message Process { + string service_name = 1; + repeated KeyValue tags = 2; +} + +message Span { + bytes trace_id = 1; + bytes span_id = 2; + string operation_name = 3; + repeated SpanRef references = 4; + uint32 flags = 5; + google.protobuf.Timestamp start_time = 6; + google.protobuf.Duration duration = 7; + repeated KeyValue tags = 8; + repeated Log logs = 9; + Process process = 10; + string process_id = 11; + repeated string warnings = 12; +} + +message Trace { + message ProcessMapping { + string process_id = 1; + Process process = 2; + } + repeated Span spans = 1; + repeated ProcessMapping process_map = 2; + repeated string warnings = 3; +} + +message Batch { + repeated Span spans = 1; + Process process = 2; +} + +message DependencyLink { + string parent = 1; + string child = 2; + uint64 call_count = 3; + string source = 4; +} diff --git a/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/AdapterTest.java b/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/AdapterTest.java new file mode 100644 index 000000000..fa6bde9b9 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/AdapterTest.java @@ -0,0 +1,376 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.util.Durations; +import com.google.protobuf.util.Timestamps; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.exporter.jaeger.proto.api_v2.Model; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link Adapter}. */ +class AdapterTest { + + private static final String LINK_TRACE_ID = "00000000000000000000000000cba123"; + private static final String LINK_SPAN_ID = "0000000000fed456"; + private static final String TRACE_ID = "00000000000000000000000000abc123"; + private static final String SPAN_ID = "0000000000def456"; + private static final String PARENT_SPAN_ID = "0000000000aef789"; + + @Test + void testProtoSpans() { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + + SpanData span = getSpanData(startMs, endMs, SpanKind.SERVER); + List spans = Collections.singletonList(span); + + Collection jaegerSpans = Adapter.toJaeger(spans); + + // the span contents are checked somewhere else + assertThat(jaegerSpans).hasSize(1); + } + + @Test + void testProtoSpan() { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + + SpanData span = getSpanData(startMs, endMs, SpanKind.SERVER, 2); + + // test + Model.Span jaegerSpan = Adapter.toJaeger(span); + assertThat(TraceId.fromBytes(jaegerSpan.getTraceId().toByteArray())) + .isEqualTo(span.getTraceId()); + assertThat(SpanId.fromBytes(jaegerSpan.getSpanId().toByteArray())).isEqualTo(span.getSpanId()); + assertThat(jaegerSpan.getOperationName()).isEqualTo("GET /api/endpoint"); + assertThat(jaegerSpan.getStartTime()).isEqualTo(Timestamps.fromMillis(startMs)); + assertThat(Durations.toMillis(jaegerSpan.getDuration())).isEqualTo(duration); + + assertThat(jaegerSpan.getTagsCount()).isEqualTo(6); + Model.KeyValue keyValue = getValue(jaegerSpan.getTagsList(), Adapter.KEY_SPAN_KIND); + assertThat(keyValue).isNotNull(); + assertThat(keyValue.getVStr()).isEqualTo("server"); + + Model.KeyValue droppedAttributes = + getValue(jaegerSpan.getTagsList(), Adapter.KEY_DROPPED_ATTRIBUTES_COUNT); + assertThat(droppedAttributes) + .isEqualTo( + Model.KeyValue.newBuilder() + .setKey(Adapter.KEY_DROPPED_ATTRIBUTES_COUNT) + .setVInt64(2) + .build()); + + assertThat(jaegerSpan.getLogsCount()).isEqualTo(1); + Model.KeyValue droppedEvents = + getValue(jaegerSpan.getTagsList(), Adapter.KEY_DROPPED_EVENTS_COUNT); + assertThat(droppedEvents) + .isEqualTo( + Model.KeyValue.newBuilder() + .setKey(Adapter.KEY_DROPPED_EVENTS_COUNT) + .setVInt64(1) + .build()); + + Model.Log log = jaegerSpan.getLogs(0); + keyValue = getValue(log.getFieldsList(), Adapter.KEY_LOG_EVENT); + assertThat(keyValue).isNotNull(); + assertThat(keyValue.getVStr()).isEqualTo("the log message"); + keyValue = getValue(log.getFieldsList(), "foo"); + assertThat(keyValue).isNotNull(); + assertThat(keyValue.getVStr()).isEqualTo("bar"); + + assertThat(jaegerSpan.getReferencesCount()).isEqualTo(2); + + assertHasFollowsFrom(jaegerSpan); + assertHasParent(jaegerSpan); + } + + @Test + void testProtoSpan_internal() { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + + SpanData span = getSpanData(startMs, endMs, SpanKind.INTERNAL); + + // test + Model.Span jaegerSpan = Adapter.toJaeger(span); + Model.KeyValue keyValue = getValue(jaegerSpan.getTagsList(), Adapter.KEY_SPAN_KIND); + assertThat(keyValue).isNull(); + } + + @Test + void testJaegerLogs() { + // prepare + EventData eventsData = getTimedEvent(); + + // test + Collection logs = Adapter.toJaegerLogs(Collections.singletonList(eventsData)); + + // verify + assertThat(logs).hasSize(1); + } + + @Test + void testJaegerLog() { + // prepare + EventData event = getTimedEvent(); + + // test + Model.Log log = Adapter.toJaegerLog(event); + + // verify + assertThat(log.getFieldsCount()).isEqualTo(2); + + Model.KeyValue keyValue = getValue(log.getFieldsList(), Adapter.KEY_LOG_EVENT); + assertThat(keyValue).isNotNull(); + assertThat(keyValue.getVStr()).isEqualTo("the log message"); + keyValue = getValue(log.getFieldsList(), "foo"); + assertThat(keyValue).isNotNull(); + assertThat(keyValue.getVStr()).isEqualTo("bar"); + keyValue = getValue(log.getFieldsList(), Adapter.KEY_EVENT_DROPPED_ATTRIBUTES_COUNT); + assertThat(keyValue).isNull(); + + // verify dropped_attributes_count + event = getTimedEvent(3); + log = Adapter.toJaegerLog(event); + keyValue = getValue(log.getFieldsList(), Adapter.KEY_EVENT_DROPPED_ATTRIBUTES_COUNT); + assertThat(keyValue).isNotNull(); + assertThat(keyValue.getVInt64()).isEqualTo(2); + } + + @Test + void testKeyValue() { + // test + Model.KeyValue kvB = Adapter.toKeyValue(booleanKey("valueB"), true); + Model.KeyValue kvD = Adapter.toKeyValue(doubleKey("valueD"), 1.); + Model.KeyValue kvI = Adapter.toKeyValue(longKey("valueI"), 2L); + Model.KeyValue kvS = Adapter.toKeyValue(stringKey("valueS"), "foobar"); + Model.KeyValue kvArrayB = + Adapter.toKeyValue(booleanArrayKey("valueArrayB"), Arrays.asList(true, false)); + Model.KeyValue kvArrayD = + Adapter.toKeyValue(doubleArrayKey("valueArrayD"), Arrays.asList(1.2345, 6.789)); + Model.KeyValue kvArrayI = + Adapter.toKeyValue(longArrayKey("valueArrayI"), Arrays.asList(12345L, 67890L)); + Model.KeyValue kvArrayS = + Adapter.toKeyValue(stringArrayKey("valueArrayS"), Arrays.asList("foobar", "barfoo")); + + // verify + assertThat(kvB.getVBool()).isTrue(); + assertThat(kvB.getVType()).isEqualTo(Model.ValueType.BOOL); + assertThat(kvD.getVFloat64()).isEqualTo(1.); + assertThat(kvD.getVType()).isEqualTo(Model.ValueType.FLOAT64); + assertThat(kvI.getVInt64()).isEqualTo(2); + assertThat(kvI.getVType()).isEqualTo(Model.ValueType.INT64); + assertThat(kvS.getVStr()).isEqualTo("foobar"); + assertThat(kvS.getVStrBytes().toStringUtf8()).isEqualTo("foobar"); + assertThat(kvS.getVType()).isEqualTo(Model.ValueType.STRING); + assertThat(kvArrayB.getVStr()).isEqualTo("[true,false]"); + assertThat(kvArrayB.getVStrBytes().toStringUtf8()).isEqualTo("[true,false]"); + assertThat(kvArrayB.getVType()).isEqualTo(Model.ValueType.STRING); + assertThat(kvArrayD.getVStr()).isEqualTo("[1.2345,6.789]"); + assertThat(kvArrayD.getVStrBytes().toStringUtf8()).isEqualTo("[1.2345,6.789]"); + assertThat(kvArrayD.getVType()).isEqualTo(Model.ValueType.STRING); + assertThat(kvArrayI.getVStr()).isEqualTo("[12345,67890]"); + assertThat(kvArrayI.getVStrBytes().toStringUtf8()).isEqualTo("[12345,67890]"); + assertThat(kvArrayI.getVType()).isEqualTo(Model.ValueType.STRING); + assertThat(kvArrayS.getVStr()).isEqualTo("[\"foobar\",\"barfoo\"]"); + assertThat(kvArrayS.getVStrBytes().toStringUtf8()).isEqualTo("[\"foobar\",\"barfoo\"]"); + assertThat(kvArrayS.getVType()).isEqualTo(Model.ValueType.STRING); + } + + @Test + void testSpanRefs() { + // prepare + LinkData link = + LinkData.create(createSpanContext("00000000000000000000000000cba123", "0000000000fed456")); + + // test + Collection spanRefs = Adapter.toSpanRefs(Collections.singletonList(link)); + + // verify + assertThat(spanRefs).hasSize(1); // the actual span ref is tested in another test + } + + @Test + void testSpanRef() { + // prepare + LinkData link = LinkData.create(createSpanContext(TRACE_ID, SPAN_ID)); + + // test + Model.SpanRef spanRef = Adapter.toSpanRef(link); + + // verify + assertThat(SpanId.fromBytes(spanRef.getSpanId().toByteArray())).isEqualTo(SPAN_ID); + assertThat(TraceId.fromBytes(spanRef.getTraceId().toByteArray())).isEqualTo(TRACE_ID); + assertThat(spanRef.getRefType()).isEqualTo(Model.SpanRefType.FOLLOWS_FROM); + } + + @Test + void testStatusNotUnset() { + long startMs = System.currentTimeMillis(); + long endMs = startMs + 900; + SpanData span = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(createSpanContext(TRACE_ID, SPAN_ID)) + .setName("GET /api/endpoint") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setKind(SpanKind.SERVER) + .setStatus(StatusData.error()) + .setTotalRecordedEvents(0) + .setTotalRecordedLinks(0) + .build(); + + assertThat(Adapter.toJaeger(span)).isNotNull(); + } + + @Test + void testSpanError() { + Attributes attributes = + Attributes.of( + stringKey("error.type"), + this.getClass().getName(), + stringKey("error.message"), + "server error"); + long startMs = System.currentTimeMillis(); + long endMs = startMs + 900; + SpanData span = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(createSpanContext(TRACE_ID, SPAN_ID)) + .setName("GET /api/endpoint") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setKind(SpanKind.SERVER) + .setStatus(StatusData.error()) + .setAttributes(attributes) + .setTotalRecordedEvents(0) + .setTotalRecordedLinks(0) + .build(); + + Model.Span jaegerSpan = Adapter.toJaeger(span); + Model.KeyValue errorType = getValue(jaegerSpan.getTagsList(), "error.type"); + assertThat(errorType).isNotNull(); + assertThat(errorType.getVStr()).isEqualTo(this.getClass().getName()); + Model.KeyValue error = getValue(jaegerSpan.getTagsList(), "error"); + assertThat(error).isNotNull(); + assertThat(error.getVBool()).isTrue(); + } + + private static EventData getTimedEvent() { + return getTimedEvent(-1); + } + + private static EventData getTimedEvent(int totalAttributeCount) { + long epochNanos = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + Attributes attributes = Attributes.of(stringKey("foo"), "bar"); + if (totalAttributeCount <= 0) { + totalAttributeCount = attributes.size(); + } + return EventData.create(epochNanos, "the log message", attributes, totalAttributeCount); + } + + private static SpanData getSpanData(long startMs, long endMs, SpanKind kind) { + return getSpanData(startMs, endMs, kind, 1); + } + + private static SpanData getSpanData( + long startMs, long endMs, SpanKind kind, int totalRecordedEvents) { + Attributes attributes = Attributes.of(booleanKey("valueB"), true); + + LinkData link = LinkData.create(createSpanContext(LINK_TRACE_ID, LINK_SPAN_ID), attributes); + + return TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(createSpanContext(TRACE_ID, SPAN_ID)) + .setParentSpanContext( + SpanContext.create( + TRACE_ID, PARENT_SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())) + .setName("GET /api/endpoint") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setAttributes(Attributes.of(booleanKey("valueB"), true)) + .setTotalAttributeCount(3) + .setEvents(Collections.singletonList(getTimedEvent())) + .setTotalRecordedEvents(totalRecordedEvents) + .setLinks(Collections.singletonList(link)) + .setTotalRecordedLinks(1) + .setKind(kind) + .setResource(Resource.create(Attributes.empty())) + .setStatus(StatusData.ok()) + .build(); + } + + private static SpanContext createSpanContext(String traceId, String spanId) { + return SpanContext.create(traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()); + } + + @Nullable + private static Model.KeyValue getValue(List tagsList, String s) { + for (Model.KeyValue kv : tagsList) { + if (kv.getKey().equals(s)) { + return kv; + } + } + return null; + } + + private static void assertHasFollowsFrom(Model.Span jaegerSpan) { + boolean found = false; + for (Model.SpanRef spanRef : jaegerSpan.getReferencesList()) { + if (Model.SpanRefType.FOLLOWS_FROM.equals(spanRef.getRefType())) { + assertThat(TraceId.fromBytes(spanRef.getTraceId().toByteArray())).isEqualTo(LINK_TRACE_ID); + assertThat(SpanId.fromBytes(spanRef.getSpanId().toByteArray())).isEqualTo(LINK_SPAN_ID); + found = true; + } + } + assertThat(found).withFailMessage("Should have found the follows-from reference").isTrue(); + } + + private static void assertHasParent(Model.Span jaegerSpan) { + boolean found = false; + for (Model.SpanRef spanRef : jaegerSpan.getReferencesList()) { + if (Model.SpanRefType.CHILD_OF.equals(spanRef.getRefType())) { + assertThat(TraceId.fromBytes(spanRef.getTraceId().toByteArray())).isEqualTo(TRACE_ID); + assertThat(SpanId.fromBytes(spanRef.getSpanId().toByteArray())).isEqualTo(PARENT_SPAN_ID); + found = true; + } + } + assertThat(found).withFailMessage("Should have found the parent reference").isTrue(); + } +} diff --git a/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporterTest.java b/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporterTest.java new file mode 100644 index 000000000..aefe6ff6f --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/JaegerGrpcSpanExporterTest.java @@ -0,0 +1,315 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.google.common.collect.Lists; +import com.google.common.io.Closer; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.exporter.jaeger.proto.api_v2.Collector; +import io.opentelemetry.exporter.jaeger.proto.api_v2.CollectorServiceGrpc; +import io.opentelemetry.exporter.jaeger.proto.api_v2.Model; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +class JaegerGrpcSpanExporterTest { + private static final String TRACE_ID = "00000000000000000000000000abc123"; + private static final String SPAN_ID = "0000000000def456"; + private static final String SPAN_ID_2 = "0000000000aef789"; + + private final Closer closer = Closer.create(); + private ArgumentCaptor requestCaptor; + private JaegerGrpcSpanExporter exporter; + + @BeforeEach + public void beforeEach() throws Exception { + String serverName = InProcessServerBuilder.generateName(); + requestCaptor = ArgumentCaptor.forClass(Collector.PostSpansRequest.class); + + Server server = + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(service) + .build() + .start(); + closer.register(server::shutdownNow); + + ManagedChannel channel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); + exporter = JaegerGrpcSpanExporter.builder().setChannel(channel).build(); + } + + @AfterEach + void tearDown() throws Exception { + closer.close(); + } + + private final CollectorServiceGrpc.CollectorServiceImplBase service = + mock( + CollectorServiceGrpc.CollectorServiceImplBase.class, + delegatesTo(new MockCollectorService())); + + @Test + void testExport() throws Exception { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + SpanData span = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())) + .setName("GET /api/endpoint") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setStatus(StatusData.ok()) + .setKind(SpanKind.CONSUMER) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .setInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("io.opentelemetry.auto", "1.0.0")) + .setResource( + Resource.create( + Attributes.of( + ResourceAttributes.SERVICE_NAME, + "myServiceName", + AttributeKey.stringKey("resource-attr-key"), + "resource-attr-value"))) + .build(); + + // test + CompletableResultCode result = exporter.export(Collections.singletonList(span)); + result.join(1, TimeUnit.SECONDS); + assertThat(result.isSuccess()).isEqualTo(true); + + // verify + verify(service).postSpans(requestCaptor.capture(), ArgumentMatchers.any()); + + Model.Batch batch = requestCaptor.getValue().getBatch(); + assertThat(batch.getSpans(0).getOperationName()).isEqualTo("GET /api/endpoint"); + assertThat(SpanId.fromBytes(batch.getSpans(0).getSpanId().toByteArray())).isEqualTo(SPAN_ID); + + assertThat( + getTagValue(batch.getProcess().getTagsList(), "resource-attr-key") + .orElseThrow(() -> new AssertionError("resource-attr-key not found")) + .getVStr()) + .isEqualTo("resource-attr-value"); + + verifyBatch(batch); + assertThat(batch.getProcess().getServiceName()).isEqualTo("myServiceName"); + } + + @Test + void testExportMultipleResources() throws Exception { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + SpanData span = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())) + .setName("GET /api/endpoint/1") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setStatus(StatusData.ok()) + .setKind(SpanKind.CONSUMER) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .setInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("io.opentelemetry.auto", "1.0.0")) + .setResource( + Resource.create( + Attributes.of( + ResourceAttributes.SERVICE_NAME, + "myServiceName1", + AttributeKey.stringKey("resource-attr-key-1"), + "resource-attr-value-1"))) + .build(); + + SpanData span2 = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID_2, TraceFlags.getSampled(), TraceState.getDefault())) + .setName("GET /api/endpoint/2") + .setStartEpochNanos(TimeUnit.MILLISECONDS.toNanos(startMs)) + .setEndEpochNanos(TimeUnit.MILLISECONDS.toNanos(endMs)) + .setStatus(StatusData.ok()) + .setKind(SpanKind.CONSUMER) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .setInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("io.opentelemetry.auto", "1.0.0")) + .setResource( + Resource.create( + Attributes.of( + ResourceAttributes.SERVICE_NAME, + "myServiceName2", + AttributeKey.stringKey("resource-attr-key-2"), + "resource-attr-value-2"))) + .build(); + + // test + CompletableResultCode result = exporter.export(Lists.newArrayList(span, span2)); + result.join(1, TimeUnit.SECONDS); + assertThat(result.isSuccess()).isEqualTo(true); + + // verify + verify(service, times(2)).postSpans(requestCaptor.capture(), ArgumentMatchers.any()); + + List requests = requestCaptor.getAllValues(); + assertThat(requests).hasSize(2); + for (Collector.PostSpansRequest request : requests) { + Model.Batch batch = request.getBatch(); + + verifyBatch(batch); + + Optional processTag = + getTagValue(batch.getProcess().getTagsList(), "resource-attr-key-1"); + Optional processTag2 = + getTagValue(batch.getProcess().getTagsList(), "resource-attr-key-2"); + if (processTag.isPresent()) { + assertThat(processTag2.isPresent()).isFalse(); + assertThat(batch.getSpans(0).getOperationName()).isEqualTo("GET /api/endpoint/1"); + assertThat(SpanId.fromBytes(batch.getSpans(0).getSpanId().toByteArray())) + .isEqualTo(SPAN_ID); + assertThat(processTag.get().getVStr()).isEqualTo("resource-attr-value-1"); + assertThat(batch.getProcess().getServiceName()).isEqualTo("myServiceName1"); + } else if (processTag2.isPresent()) { + assertThat(batch.getSpans(0).getOperationName()).isEqualTo("GET /api/endpoint/2"); + assertThat(SpanId.fromBytes(batch.getSpans(0).getSpanId().toByteArray())) + .isEqualTo(SPAN_ID_2); + assertThat(processTag2.get().getVStr()).isEqualTo("resource-attr-value-2"); + assertThat(batch.getProcess().getServiceName()).isEqualTo("myServiceName2"); + } else { + fail("No process tag resource-attr-key-1 or resource-attr-key-2"); + } + } + } + + private static void verifyBatch(Model.Batch batch) throws Exception { + assertThat(batch.getSpansCount()).isEqualTo(1); + assertThat(TraceId.fromBytes(batch.getSpans(0).getTraceId().toByteArray())).isEqualTo(TRACE_ID); + assertThat(batch.getProcess().getTagsCount()).isEqualTo(5); + + assertThat( + getSpanTagValue(batch.getSpans(0), "otel.library.name") + .orElseThrow(() -> new AssertionError("otel.library.name not found")) + .getVStr()) + .isEqualTo("io.opentelemetry.auto"); + + assertThat( + getSpanTagValue(batch.getSpans(0), "otel.library.version") + .orElseThrow(() -> new AssertionError("otel.library.version not found")) + .getVStr()) + .isEqualTo("1.0.0"); + + assertThat( + getTagValue(batch.getProcess().getTagsList(), "ip") + .orElseThrow(() -> new AssertionError("ip not found")) + .getVStr()) + .isEqualTo(InetAddress.getLocalHost().getHostAddress()); + + assertThat( + getTagValue(batch.getProcess().getTagsList(), "hostname") + .orElseThrow(() -> new AssertionError("hostname not found")) + .getVStr()) + .isEqualTo(InetAddress.getLocalHost().getHostName()); + + assertThat( + getTagValue(batch.getProcess().getTagsList(), "jaeger.version") + .orElseThrow(() -> new AssertionError("jaeger.version not found")) + .getVStr()) + .isEqualTo("opentelemetry-java"); + } + + private static Optional getSpanTagValue(Model.Span span, String tagKey) { + return getTagValue(span.getTagsList(), tagKey); + } + + private static Optional getTagValue(List tags, String tagKey) { + return tags.stream().filter(kv -> kv.getKey().equals(tagKey)).findFirst(); + } + + @Test + @SuppressWarnings("PreferJavaTimeOverload") + void invalidConfig() { + assertThatThrownBy(() -> JaegerGrpcSpanExporter.builder().setTimeout(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("timeout must be non-negative"); + assertThatThrownBy(() -> JaegerGrpcSpanExporter.builder().setTimeout(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy(() -> JaegerGrpcSpanExporter.builder().setTimeout(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("timeout"); + + assertThatThrownBy(() -> JaegerGrpcSpanExporter.builder().setEndpoint(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("endpoint"); + assertThatThrownBy(() -> JaegerGrpcSpanExporter.builder().setEndpoint("😺://localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must be a URL: 😺://localhost") + .hasCauseInstanceOf(URISyntaxException.class); + assertThatThrownBy(() -> JaegerGrpcSpanExporter.builder().setEndpoint("localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must start with http:// or https://: localhost"); + assertThatThrownBy(() -> JaegerGrpcSpanExporter.builder().setEndpoint("gopher://localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must start with http:// or https://: gopher://localhost"); + } + + static class MockCollectorService extends CollectorServiceGrpc.CollectorServiceImplBase { + @Override + public void postSpans( + Collector.PostSpansRequest request, + StreamObserver responseObserver) { + responseObserver.onNext(Collector.PostSpansResponse.newBuilder().build()); + responseObserver.onCompleted(); + } + } +} diff --git a/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/JaegerIntegrationTest.java b/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/JaegerIntegrationTest.java new file mode 100644 index 000000000..906297798 --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/test/java/io/opentelemetry/exporter/jaeger/JaegerIntegrationTest.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.jaeger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.time.Duration; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class JaegerIntegrationTest { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final OkHttpClient client = new OkHttpClient(); + + private static final int QUERY_PORT = 16686; + private static final int COLLECTOR_PORT = 14250; + private static final int HEALTH_PORT = 14269; + private static final String SERVICE_NAME = "E2E-test"; + private static final String JAEGER_URL = "http://localhost"; + + @Container + public static GenericContainer jaegerContainer = + new GenericContainer<>("ghcr.io/open-telemetry/java-test-containers:jaeger") + .withExposedPorts(COLLECTOR_PORT, QUERY_PORT, HEALTH_PORT) + .waitingFor(Wait.forHttp("/").forPort(HEALTH_PORT)); + + @Test + void testJaegerIntegration() { + OpenTelemetry openTelemetry = initOpenTelemetry(); + imitateWork(openTelemetry); + Awaitility.await() + .atMost(Duration.ofSeconds(30)) + .until(JaegerIntegrationTest::assertJaegerHaveTrace); + } + + private static OpenTelemetry initOpenTelemetry() { + ManagedChannel jaegerChannel = + ManagedChannelBuilder.forAddress("127.0.0.1", jaegerContainer.getMappedPort(COLLECTOR_PORT)) + .usePlaintext() + .build(); + SpanExporter jaegerExporter = + JaegerGrpcSpanExporter.builder() + .setChannel(jaegerChannel) + .setTimeout(Duration.ofSeconds(30)) + .build(); + return OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(jaegerExporter)) + .setResource( + Resource.getDefault().toBuilder() + .put(ResourceAttributes.SERVICE_NAME, SERVICE_NAME) + .build()) + .build()) + .build(); + } + + private void imitateWork(OpenTelemetry openTelemetry) { + Span span = + openTelemetry.getTracer(getClass().getCanonicalName()).spanBuilder("Test span").startSpan(); + span.addEvent("some event"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + span.end(); + } + + private static boolean assertJaegerHaveTrace() { + try { + String url = + String.format( + "%s/api/traces?service=%s", + String.format(JAEGER_URL + ":%d", jaegerContainer.getMappedPort(QUERY_PORT)), + SERVICE_NAME); + + Request request = + new Request.Builder() + .url(url) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .build(); + + final JsonNode json; + try (Response response = client.newCall(request).execute()) { + json = objectMapper.readTree(response.body().byteStream()); + } + + return json.get("data").get(0).get("traceID") != null; + } catch (Exception e) { + return false; + } + } +} diff --git a/opentelemetry-java/exporters/jaeger/src/test/resources/logback-test.xml b/opentelemetry-java/exporters/jaeger/src/test/resources/logback-test.xml new file mode 100644 index 000000000..0f157506f --- /dev/null +++ b/opentelemetry-java/exporters/jaeger/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + \ No newline at end of file diff --git a/opentelemetry-java/exporters/logging-otlp/README.md b/opentelemetry-java/exporters/logging-otlp/README.md new file mode 100644 index 000000000..3615e4d5c --- /dev/null +++ b/opentelemetry-java/exporters/logging-otlp/README.md @@ -0,0 +1,9 @@ +# OpenTelemetry - OTLP JSON Logging Exporter + +[![Javadocs][javadoc-image]][javadoc-url] + +Exporters for writing data to logs using OTLP JSON format. They are appropriate for writing spans or +metrics to logs in a way that is both human-readable and structured for machine parsing. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-exporter-logging-otlp.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-exporter-logging-otlp \ No newline at end of file diff --git a/opentelemetry-java/exporters/logging-otlp/build.gradle.kts b/opentelemetry-java/exporters/logging-otlp/build.gradle.kts new file mode 100644 index 000000000..66bb0590c --- /dev/null +++ b/opentelemetry-java/exporters/logging-otlp/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Protocol JSON Logging Exporters" +extra["moduleName"] = "io.opentelemetry.exporter.logging.otlp" + +dependencies { + compileOnly(project(":sdk:trace")) + compileOnly(project(":sdk:metrics")) + + implementation(project(":exporters:otlp:common")) + + implementation("org.curioswitch.curiostack:protobuf-jackson") + + testImplementation(project(":sdk:testing")) + + testImplementation("org.skyscreamer:jsonassert") +} diff --git a/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/HexEncodingStringJsonGenerator.java b/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/HexEncodingStringJsonGenerator.java new file mode 100644 index 000000000..cbef55b45 --- /dev/null +++ b/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/HexEncodingStringJsonGenerator.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp; + +import com.fasterxml.jackson.core.Base64Variant; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.io.SegmentedStringWriter; +import com.fasterxml.jackson.core.util.JsonGeneratorDelegate; +import io.opentelemetry.api.internal.TemporaryBuffers; +import java.io.IOException; + +final class HexEncodingStringJsonGenerator extends JsonGeneratorDelegate { + + static final JsonFactory JSON_FACTORY = new JsonFactory(); + + static JsonGenerator create(SegmentedStringWriter stringWriter) { + final JsonGenerator delegate; + try { + delegate = JSON_FACTORY.createGenerator(stringWriter); + } catch (IOException e) { + throw new IllegalStateException("Unable to create in-memory JsonGenerator, can't happen.", e); + } + return new HexEncodingStringJsonGenerator(delegate); + } + + private HexEncodingStringJsonGenerator(JsonGenerator delegate) { + super(delegate); + } + + @Override + public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) + throws IOException { + writeString(bytesToHex(data, offset, len)); + } + + private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); + + private static String bytesToHex(byte[] bytes, int offset, int len) { + int hexLength = len * 2; + char[] hexChars = TemporaryBuffers.chars(hexLength); + for (int i = 0; i < len; i++) { + int v = bytes[offset + i] & 0xFF; + hexChars[i * 2] = HEX_ARRAY[v >>> 4]; + hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars, 0, hexLength); + } +} diff --git a/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java b/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java new file mode 100644 index 000000000..7c4baf5a1 --- /dev/null +++ b/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp; + +import static io.opentelemetry.exporter.logging.otlp.HexEncodingStringJsonGenerator.JSON_FACTORY; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.io.SegmentedStringWriter; +import io.opentelemetry.exporter.otlp.internal.MetricAdapter; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.curioswitch.common.protobuf.json.MessageMarshaller; + +/** + * A {@link MetricExporter} which writes {@linkplain MetricData spans} to a {@link Logger} in OTLP + * JSON format. Each log line will include a single {@link ResourceMetrics}. + */ +public final class OtlpJsonLoggingMetricExporter implements MetricExporter { + + private static final MessageMarshaller marshaller = + MessageMarshaller.builder() + .register(ResourceMetrics.class) + .omittingInsignificantWhitespace(true) + .build(); + + private static final Logger logger = + Logger.getLogger(OtlpJsonLoggingMetricExporter.class.getName()); + + /** Returns a new {@link OtlpJsonLoggingMetricExporter}. */ + public static MetricExporter create() { + return new OtlpJsonLoggingMetricExporter(); + } + + private OtlpJsonLoggingMetricExporter() {} + + @Override + public CompletableResultCode export(Collection metrics) { + List allResourceMetrics = MetricAdapter.toProtoResourceMetrics(metrics); + for (ResourceMetrics resourceMetrics : allResourceMetrics) { + SegmentedStringWriter sw = new SegmentedStringWriter(JSON_FACTORY._getBufferRecycler()); + try (JsonGenerator gen = HexEncodingStringJsonGenerator.create(sw)) { + marshaller.writeValue(resourceMetrics, gen); + } catch (IOException e) { + // Shouldn't happen in practice, just skip it. + continue; + } + logger.log(Level.INFO, sw.getAndClear()); + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporter.java b/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporter.java new file mode 100644 index 000000000..d628f008b --- /dev/null +++ b/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporter.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.io.SegmentedStringWriter; +import io.opentelemetry.exporter.otlp.internal.SpanAdapter; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.curioswitch.common.protobuf.json.MessageMarshaller; + +/** + * A {@link SpanExporter} which writes {@linkplain SpanData spans} to a {@link Logger} in OTLP JSON + * format. Each log line will include a single {@link ResourceSpans}. + */ +public final class OtlpJsonLoggingSpanExporter implements SpanExporter { + + private static final MessageMarshaller marshaller = + MessageMarshaller.builder() + .register(ResourceSpans.class) + .omittingInsignificantWhitespace(true) + .build(); + + private static final Logger logger = + Logger.getLogger(OtlpJsonLoggingSpanExporter.class.getName()); + + /** Returns a new {@link OtlpJsonLoggingSpanExporter}. */ + public static SpanExporter create() { + return new OtlpJsonLoggingSpanExporter(); + } + + private OtlpJsonLoggingSpanExporter() {} + + @Override + public CompletableResultCode export(Collection spans) { + List allResourceSpans = SpanAdapter.toProtoResourceSpans(spans); + for (ResourceSpans resourceSpans : allResourceSpans) { + SegmentedStringWriter sw = + new SegmentedStringWriter( + HexEncodingStringJsonGenerator.JSON_FACTORY._getBufferRecycler()); + try (JsonGenerator gen = HexEncodingStringJsonGenerator.create(sw)) { + marshaller.writeValue(resourceSpans, gen); + } catch (IOException e) { + // Shouldn't happen in practice, just skip it. + continue; + } + logger.log(Level.INFO, sw.getAndClear()); + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/package-info.java b/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/package-info.java new file mode 100644 index 000000000..f6c93513b --- /dev/null +++ b/opentelemetry-java/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** OpenTelemetry exporters which writes spans or metrics to log using OTLP JSON format. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.logging.otlp; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporterTest.java b/opentelemetry-java/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporterTest.java new file mode 100644 index 000000000..83795e3ce --- /dev/null +++ b/opentelemetry-java/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporterTest.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.skyscreamer.jsonassert.JSONAssert; +import org.slf4j.event.Level; + +class OtlpJsonLoggingMetricExporterTest { + + private static final Resource RESOURCE = + Resource.create(Attributes.builder().put("key", "value").build()); + + private static final MetricData METRIC1 = + MetricData.createDoubleSum( + RESOURCE, + InstrumentationLibraryInfo.create("instrumentation", "1"), + "metric1", + "metric1 description", + "m", + DoubleSumData.create( + true, + AggregationTemporality.CUMULATIVE, + Arrays.asList(DoublePointData.create(1, 2, Labels.of("cat", "meow"), 4)))); + + private static final MetricData METRIC2 = + MetricData.createDoubleSum( + RESOURCE, + InstrumentationLibraryInfo.create("instrumentation2", "2"), + "metric2", + "metric2 description", + "s", + DoubleSumData.create( + true, + AggregationTemporality.CUMULATIVE, + Arrays.asList(DoublePointData.create(1, 2, Labels.of("cat", "meow"), 4)))); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(OtlpJsonLoggingMetricExporter.class); + + private MetricExporter exporter; + + @BeforeEach + void setUp() { + exporter = OtlpJsonLoggingMetricExporter.create(); + } + + @Test + void log() throws Exception { + exporter.export(Arrays.asList(METRIC1, METRIC2)); + + assertThat(logs.getEvents()) + .hasSize(1) + .allSatisfy(log -> assertThat(log.getLevel()).isEqualTo(Level.INFO)); + JSONAssert.assertEquals( + "{" + + " \"resource\": {" + + " \"attributes\": [{" + + " \"key\": \"key\"," + + " \"value\": {" + + " \"stringValue\": \"value\"" + + " }" + + " }]" + + " }," + + " \"instrumentationLibraryMetrics\": [{" + + " \"instrumentationLibrary\": {" + + " \"name\": \"instrumentation2\"," + + " \"version\": \"2\"" + + " }," + + " \"metrics\": [{" + + " \"name\": \"metric2\"," + + " \"description\": \"metric2 description\"," + + " \"unit\": \"s\"," + + " \"sum\": {" + + " \"dataPoints\": [{" + + " \"attributes\": [{" + + " \"key\": \"cat\"," + + " \"value\": {\"stringValue\": \"meow\"}" + + " }]," + + " \"startTimeUnixNano\": \"1\"," + + " \"timeUnixNano\": \"2\"," + + " \"asDouble\": 4.0" + + " }]," + + " \"aggregationTemporality\": \"AGGREGATION_TEMPORALITY_CUMULATIVE\"," + + " \"isMonotonic\": true" + + " }" + + " }]" + + " }, {" + + " \"instrumentationLibrary\": {" + + " \"name\": \"instrumentation\"," + + " \"version\": \"1\"" + + " }," + + " \"metrics\": [{" + + " \"name\": \"metric1\"," + + " \"description\": \"metric1 description\"," + + " \"unit\": \"m\"," + + " \"sum\": {" + + " \"dataPoints\": [{" + + " \"attributes\": [{" + + " \"key\": \"cat\"," + + " \"value\": {\"stringValue\": \"meow\"}" + + " }]," + + " \"startTimeUnixNano\": \"1\"," + + " \"timeUnixNano\": \"2\"," + + " \"asDouble\": 4.0" + + " }]," + + " \"aggregationTemporality\": \"AGGREGATION_TEMPORALITY_CUMULATIVE\"," + + " \"isMonotonic\": true" + + " }" + + " }]" + + " }]" + + "}", + logs.getEvents().get(0).getMessage(), + /* strict= */ true); + assertThat(logs.getEvents().get(0).getMessage()).doesNotContain("\n"); + } + + @Test + void flush() { + assertThat(exporter.flush().isSuccess()).isTrue(); + } + + @Test + void shutdown() { + assertThat(exporter.shutdown().isSuccess()).isTrue(); + } +} diff --git a/opentelemetry-java/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporterTest.java b/opentelemetry-java/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporterTest.java new file mode 100644 index 000000000..d937ccc6d --- /dev/null +++ b/opentelemetry-java/exporters/logging-otlp/src/test/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingSpanExporterTest.java @@ -0,0 +1,182 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging.otlp; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.skyscreamer.jsonassert.JSONAssert; +import org.slf4j.event.Level; + +class OtlpJsonLoggingSpanExporterTest { + + private static final Resource RESOURCE = + Resource.create(Attributes.builder().put("key", "value").build()); + + private static final SpanData SPAN1 = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create( + "12345678876543211234567887654321", + "8765432112345678", + TraceFlags.getSampled(), + TraceState.getDefault())) + .setStartEpochNanos(100) + .setEndEpochNanos(100 + 1000) + .setStatus(StatusData.ok()) + .setName("testSpan1") + .setKind(SpanKind.INTERNAL) + .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) + .setEvents( + Collections.singletonList( + EventData.create( + 100 + 500, + "somethingHappenedHere", + Attributes.of(booleanKey("important"), true)))) + .setTotalAttributeCount(2) + .setTotalRecordedEvents(1) + .setTotalRecordedLinks(0) + .setInstrumentationLibraryInfo(InstrumentationLibraryInfo.create("instrumentation", "1")) + .setResource(RESOURCE) + .build(); + + private static final SpanData SPAN2 = + TestSpanData.builder() + .setHasEnded(false) + .setSpanContext( + SpanContext.create( + "12340000000043211234000000004321", + "8765000000005678", + TraceFlags.getSampled(), + TraceState.getDefault())) + .setStartEpochNanos(500) + .setEndEpochNanos(500 + 1001) + .setStatus(StatusData.error()) + .setName("testSpan2") + .setKind(SpanKind.CLIENT) + .setResource(RESOURCE) + .setInstrumentationLibraryInfo(InstrumentationLibraryInfo.create("instrumentation2", "2")) + .build(); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(OtlpJsonLoggingSpanExporter.class); + + SpanExporter exporter; + + @BeforeEach + void setUp() { + exporter = OtlpJsonLoggingSpanExporter.create(); + } + + @Test + void log() throws Exception { + exporter.export(Arrays.asList(SPAN1, SPAN2)); + + assertThat(logs.getEvents()) + .hasSize(1) + .allSatisfy(log -> assertThat(log.getLevel()).isEqualTo(Level.INFO)); + JSONAssert.assertEquals( + "{" + + " \"resource\": {" + + " \"attributes\": [{" + + " \"key\": \"key\"," + + " \"value\": {" + + " \"stringValue\": \"value\"" + + " }" + + " }]" + + " }," + + " \"instrumentationLibrarySpans\": [{" + + " \"instrumentationLibrary\": {" + + " \"name\": \"instrumentation2\"," + + " \"version\": \"2\"" + + " }," + + " \"spans\": [{" + + " \"traceId\": \"12340000000043211234000000004321\"," + + " \"spanId\": \"8765000000005678\"," + + " \"name\": \"testSpan2\"," + + " \"kind\": \"SPAN_KIND_CLIENT\"," + + " \"startTimeUnixNano\": \"500\"," + + " \"endTimeUnixNano\": \"1501\"," + + " \"status\": {" + + " \"deprecatedCode\": \"DEPRECATED_STATUS_CODE_UNKNOWN_ERROR\"," + + " \"code\": \"STATUS_CODE_ERROR\"" + + " }" + + " }]" + + " }, {" + + " \"instrumentationLibrary\": {" + + " \"name\": \"instrumentation\"," + + " \"version\": \"1\"" + + " }," + + " \"spans\": [{" + + " \"traceId\": \"12345678876543211234567887654321\"," + + " \"spanId\": \"8765432112345678\"," + + " \"name\": \"testSpan1\"," + + " \"kind\": \"SPAN_KIND_INTERNAL\"," + + " \"startTimeUnixNano\": \"100\"," + + " \"endTimeUnixNano\": \"1100\"," + + " \"attributes\": [{" + + " \"key\": \"animal\"," + + " \"value\": {" + + " \"stringValue\": \"cat\"" + + " }" + + " }, {" + + " \"key\": \"lives\"," + + " \"value\": {" + + " \"intValue\": \"9\"" + + " }" + + " }]," + + " \"events\": [{" + + " \"timeUnixNano\": \"600\"," + + " \"name\": \"somethingHappenedHere\"," + + " \"attributes\": [{" + + " \"key\": \"important\"," + + " \"value\": {" + + " \"boolValue\": true" + + " }" + + " }]" + + " }]," + + " \"status\": {" + + " \"code\": \"STATUS_CODE_OK\"" + + " }" + + " }]" + + " }]" + + "}", + logs.getEvents().get(0).getMessage(), + /* strict= */ true); + assertThat(logs.getEvents().get(0).getMessage()).doesNotContain("\n"); + } + + @Test + void flush() { + assertThat(exporter.flush().isSuccess()).isTrue(); + } + + @Test + void shutdown() { + assertThat(exporter.shutdown().isSuccess()).isTrue(); + } +} diff --git a/opentelemetry-java/exporters/logging/README.md b/opentelemetry-java/exporters/logging/README.md new file mode 100644 index 000000000..8c8ecec23 --- /dev/null +++ b/opentelemetry-java/exporters/logging/README.md @@ -0,0 +1,6 @@ +# OpenTelemetry - Logging Exporter + +[![Javadocs][javadoc-image]][javadoc-url] + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-exporters-logging.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-exporters-logging \ No newline at end of file diff --git a/opentelemetry-java/exporters/logging/build.gradle.kts b/opentelemetry-java/exporters/logging/build.gradle.kts new file mode 100644 index 000000000..4f18d78f8 --- /dev/null +++ b/opentelemetry-java/exporters/logging/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry - Logging Exporter" +extra["moduleName"] = "io.opentelemetry.exporter.logging" + +dependencies { + api(project(":sdk:all")) + api(project(":sdk:metrics")) + implementation(project(":semconv")) + implementation("org.apache.logging.log4j:log4j-core:2.17.0") + implementation("org.apache.logging.log4j:log4j-api:2.17.0") + implementation("com.lmax:disruptor:3.4.2") + + testImplementation(project(":sdk:testing")) +} diff --git a/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/Log4j2Factory.java b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/Log4j2Factory.java new file mode 100644 index 000000000..143f74f78 --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/Log4j2Factory.java @@ -0,0 +1,86 @@ +package io.opentelemetry.exporter.logging; + +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder; +import org.apache.logging.log4j.core.config.builder.api.ComponentBuilder; +import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder; +import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder; +import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration; +import org.apache.logging.log4j.core.config.builder.impl.DefaultConfigurationBuilder; + +@SuppressWarnings({"unused", "rawtypes", "SystemOut", "unchecked", + "PrivateConstructorForUtilityClass"}) +public class Log4j2Factory { + + public static final String IS_ASYNC_PROPERTY_NAME = EnvOrJvmProperties.JVM_OTEL_EXPORTER_LOG_ISASYNC.getKey(); + public static final String LOG_INTERVAL_PROPERTY_NAME = EnvOrJvmProperties.JVM_OTEL_EXPORTER_LOG_INTERVAL.getKey(); + public static final String LOG_DELETE_AGE_PROPERTY_NAME = EnvOrJvmProperties.JVM_OTEL_EXPORTER_LOG_DELETE_AGE.getKey(); + + public static Logger getLogger() { + Configuration config = createConfiguration("TraceConfiguration"); + LoggerContext ctx = new LoggerContext("TraceLogContext"); + ctx.setConfiguration(config); + return ctx.getLogger("TraceLogger"); + } + + + static Configuration createConfiguration(final String name) { + String interval = + StringUtils.isNullOrEmpty(System.getProperty(LOG_INTERVAL_PROPERTY_NAME)) ? "30" + : System.getProperty(LOG_INTERVAL_PROPERTY_NAME); + String deleteAge = + StringUtils.isNullOrEmpty(System.getProperty(LOG_DELETE_AGE_PROPERTY_NAME)) ? "PT2H" + : System.getProperty(LOG_DELETE_AGE_PROPERTY_NAME); + String logPath = LogFileNameUtil.getLogPathFile(); + ConfigurationBuilder builder = new DefaultConfigurationBuilder(); + builder.setConfigurationName(name); + builder.setStatusLevel(Level.ERROR); + builder.add(builder.newFilter("ThresholdFilter", Filter.Result.ACCEPT, Filter.Result.NEUTRAL). + addAttribute("level", Level.INFO)); + LayoutComponentBuilder layoutBuilder = builder.newLayout("PatternLayout") + .addAttribute("pattern", "%d ||| %msg%n"); + ComponentBuilder triggeringPolicy = builder.newComponent("Policies") + .addComponent( + builder.newComponent("TimeBasedTriggeringPolicy").addAttribute("interval", interval)); + ComponentBuilder defaultRolloverStrategy = builder.newComponent("DefaultRolloverStrategy") + .addAttribute("max", 5) + .addComponent(builder.newComponent("Delete") + .addAttribute("basePath", LogFileNameUtil.getLogPath()) + .addComponent(builder.newComponent("IfLastModified") + .addAttribute("age", deleteAge))); + AppenderComponentBuilder appenderBuilder = builder.newAppender("rolling", "RollingFile") + .addAttribute("fileName", logPath) + .addAttribute("filePattern", logPath + "-%d{yyyy-MM-dd-HH-mm}"); + String property = System.getProperty(IS_ASYNC_PROPERTY_NAME); + System.out.println("log4j2 isAsync : " + property); + if ("true".equals(property)) { + appenderBuilder.addAttribute("immediateFlush", false); + } + appenderBuilder.add(layoutBuilder) + .addComponent(triggeringPolicy) + .addComponent(defaultRolloverStrategy); + builder.add(appenderBuilder); + + if (!"true".equals(property)) { + builder.add(builder.newRootLogger(Level.INFO) + .add(builder.newAppenderRef("rolling"))); + builder.add(builder.newLogger("TraceLogger", Level.INFO) + .addComponent(builder.newAppenderRef("rolling")) + .addAttribute("additivity", false)); + } else { + // Asynchronous root logger + builder.add(builder.newAsyncRootLogger(Level.INFO) + .add(builder.newAppenderRef("rolling"))); + builder.add(builder.newAsyncLogger("TraceLogger", Level.INFO) + .addComponent(builder.newAppenderRef("rolling")) + .addAttribute("additivity", false)); + } + return builder.build(); + } +} diff --git a/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/Log4j2SpanExporter.java b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/Log4j2SpanExporter.java new file mode 100644 index 000000000..1cc9b12e0 --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/Log4j2SpanExporter.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.SystemCommon; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import org.apache.logging.log4j.Logger; +import java.util.Collection; +import java.util.Random; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings({"unused", "FieldCanBeFinal", "FutureReturnValueIgnored", "SystemOut", + "CatchAndPrintStackTrace"}) +public final class Log4j2SpanExporter implements SpanExporter { + + private Logger log; + + // Interval for generating log files, unit: m + private static final int GENERATE_LOG_GAP = 20; + private static final String[] traceIDChars = new String[] {"a", "b", "c", "d", "e", "f", "0", "1", + "2", "3", "4", "5", "6", "7", "8", "9"}; + private static Random r = new Random(); + + private static String ipv4Env = SystemCommon.getEnvOrProperties("host.ip"); + + public Log4j2SpanExporter() { + log = Log4j2Factory.getLogger(); + // Automatically generate trace logs to prevent the mismatch between log collection line numbers and the absence of data for a long time, thus unable to collect. + scheduledSpanDateForLog(); + } + + public Log4j2SpanExporter(String logPath, String isAsync, String logInterval, + String logDeleteAge) { + System.setProperty(LogFileNameUtil.LOGPATH_PROPERTY_NAME, logPath); + System.setProperty(Log4j2Factory.IS_ASYNC_PROPERTY_NAME, isAsync); + System.setProperty(Log4j2Factory.LOG_INTERVAL_PROPERTY_NAME, logInterval); + System.setProperty(Log4j2Factory.LOG_DELETE_AGE_PROPERTY_NAME, logDeleteAge); + log = Log4j2Factory.getLogger(); + } + + @Override + public CompletableResultCode export(Collection spans) { + for (SpanData span : spans) { + String spanMessage = SpanToLogUtil.convert(span); + log.info(spanMessage); + } + return CompletableResultCode.ofSuccess(); + } + + /** + * Flushes the data. + * + * @return the result of the operation + */ + @Override + public CompletableResultCode flush() { + CompletableResultCode resultCode = new CompletableResultCode(); + return resultCode.succeed(); + } + + @Override + public CompletableResultCode shutdown() { + return flush(); + } + + private void scheduledSpanDateForLog() { + new ScheduledThreadPoolExecutor(1).scheduleAtFixedRate( + () -> { + generateSpanDateForLog(); + }, + 0, + GENERATE_LOG_GAP, + TimeUnit.MINUTES); + } + + private void generateSpanDateForLog() { + try { + long currNano = System.currentTimeMillis() * 1000 * 1000; + String autoGenerator = currNano + " ### 123 ### " + ipv4Env + + " ### auto-generator ### dbDriver ### UNSET ### " + buildTraceId() + " ### " + + buildSpanId() + + " ### [] ### [] ### {\"tags\":[{\"key\":\"service.name\",\"type\":\"string\",\"value\":\"auto-generator\"}]} ### [] ### "; + log.info(autoGenerator); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + private static String buildTraceId() { + // Generate a random traceID. + StringBuilder stringBuilder = new StringBuilder(); + for (int j = 0; j < 32; j++) { + int i = r.nextInt(traceIDChars.length); + stringBuilder.append(traceIDChars[i]); + } + return stringBuilder.toString(); + } + + private static String buildSpanId() { + StringBuilder stringBuilder = new StringBuilder(); + for (int j = 0; j < 16; j++) { + int i = r.nextInt(traceIDChars.length); + stringBuilder.append(traceIDChars[i]); + } + return stringBuilder.toString(); + } +} diff --git a/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LogFileNameUtil.java b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LogFileNameUtil.java new file mode 100644 index 000000000..7fe8d2ed1 --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LogFileNameUtil.java @@ -0,0 +1,75 @@ +package io.opentelemetry.exporter.logging; + +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import io.opentelemetry.sdk.common.SystemCommon; + +@SuppressWarnings({"PrivateConstructorForUtilityClass", "CatchingUnchecked"}) +public class LogFileNameUtil { + + public static final String LOGPATH_PROPERTY_NAME = EnvOrJvmProperties.JVM_OTEL_EXPORTER_LOG_PATH_PREFIX.getKey(); + private static final String LOG_PATH_SUFFIX = "/trace/"; + private static final String LOG_FILE_NAME = "trace.log"; + + public static String getLogPathFile() { + return getLogPath() + LOG_FILE_NAME; + } + + public static String getLogPath() { + String logPathPrefixStr = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_MIONE_LOG_PATH.getKey()); + if (StringUtils.isNullOrEmpty(logPathPrefixStr)) { + String logPathPrefix = System.getProperty(LOGPATH_PROPERTY_NAME); + if (StringUtils.isNullOrEmpty(logPathPrefix)) { + logPathPrefix = "/home/work/log/"; + } + String applicationName = getServiceName(); + logPathPrefixStr = logPathPrefix + applicationName; + } + return logPathPrefixStr + LOG_PATH_SUFFIX; + } + + /** + * get service name without project id + */ + public static String getServiceName() { + String applicationName = + SystemCommon.getEnvOrProperties(EnvOrJvmProperties.JVM_OTEL_RESOURCE_ATTRIBUTES.getKey()) + == null ? SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.MIONE_PROJECT_NAME.getKey()) : + SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.JVM_OTEL_RESOURCE_ATTRIBUTES.getKey()).split("=")[1]; + if (applicationName == null) { + applicationName = EnvOrJvmProperties.MIONE_PROJECT_NAME.getDefaultValue(); + } + // Delete the project name ID generated in mione. + int i = applicationName.indexOf("-"); + if (i >= 0) { + String id = applicationName.substring(0, i); + if (isNumeric(id)) { + return applicationName.substring(i + 1); + } + } + int j = applicationName.indexOf("_"); + if (j >= 0) { + String id = applicationName.substring(0, j); + if (isNumeric(id)) { + return applicationName.substring(j + 1); + } + } + return applicationName; + } + + public static boolean isNumeric(final String cs) { + if (cs == null || cs.length() == 0) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (!Character.isDigit(cs.charAt(i))) { + return false; + } + } + return true; + } +} diff --git a/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LoggingMetricExporter.java b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LoggingMetricExporter.java new file mode 100644 index 000000000..ca9dc3d44 --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LoggingMetricExporter.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.util.Collection; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class LoggingMetricExporter implements MetricExporter { + private static final Logger logger = Logger.getLogger(LoggingMetricExporter.class.getName()); + + @Override + public CompletableResultCode export(Collection metrics) { + logger.info("Received a collection of " + metrics.size() + " metrics for export."); + for (MetricData metricData : metrics) { + logger.log(Level.INFO, "metric: {0}", metricData); + } + return CompletableResultCode.ofSuccess(); + } + + /** + * Flushes the data. + * + * @return the result of the operation + */ + @Override + public CompletableResultCode flush() { + CompletableResultCode resultCode = new CompletableResultCode(); + for (Handler handler : logger.getHandlers()) { + try { + handler.flush(); + } catch (Throwable t) { + return resultCode.fail(); + } + } + return resultCode.succeed(); + } + + @Override + public CompletableResultCode shutdown() { + // no-op + this.flush(); + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LoggingSpanExporter.java b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LoggingSpanExporter.java new file mode 100644 index 000000000..48a0628df --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/LoggingSpanExporter.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** A Span Exporter that logs every span at INFO level using java.util.logging. */ +public final class LoggingSpanExporter implements SpanExporter { + private static final Logger logger = Logger.getLogger(LoggingSpanExporter.class.getName()); + + @Override + public CompletableResultCode export(Collection spans) { + // We always have 32 + 16 + name + several whitespace, 60 seems like an OK initial guess. + StringBuilder sb = new StringBuilder(60); + for (SpanData span : spans) { + sb.setLength(0); + InstrumentationLibraryInfo instrumentationLibraryInfo = span.getInstrumentationLibraryInfo(); + sb.append("'") + .append(span.getName()) + .append("' : ") + .append(span.getTraceId()) + .append(" ") + .append(span.getSpanId()) + .append(" ") + .append(span.getKind()) + .append(" [tracer: ") + .append(instrumentationLibraryInfo.getName()) + .append(":") + .append( + instrumentationLibraryInfo.getVersion() == null + ? "" + : instrumentationLibraryInfo.getVersion()) + .append("] ") + .append(span.getAttributes()); + logger.log(Level.INFO, sb.toString()); + } + return CompletableResultCode.ofSuccess(); + } + + /** + * Flushes the data. + * + * @return the result of the operation + */ + @Override + public CompletableResultCode flush() { + CompletableResultCode resultCode = new CompletableResultCode(); + for (Handler handler : logger.getHandlers()) { + try { + handler.flush(); + } catch (Throwable t) { + resultCode.fail(); + } + } + return resultCode.succeed(); + } + + @Override + public CompletableResultCode shutdown() { + return flush(); + } +} diff --git a/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/SpanToLogUtil.java b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/SpanToLogUtil.java new file mode 100644 index 000000000..d2886939f --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/SpanToLogUtil.java @@ -0,0 +1,358 @@ +package io.opentelemetry.exporter.logging; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import io.opentelemetry.sdk.common.SystemCommon; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.util.List; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings({"unused", "PrivateConstructorForUtilityClass"}) +public class SpanToLogUtil { + + private static final ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(SpanToLogUtil.class.getName())); + + private static final String split = " ### "; + private static String hostName = ""; + private static String ipv4Env = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_HOST_IP.getKey()); + private static final String env = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_NAME.getKey()); + private static String envId = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_ID.getKey()); + private static final String FUNCTION_MODULE_KEY = "service.function.module"; + private static final String FUNCTION_NAME_KEY = "service.function.name"; + private static final String FUNCTION_ID_KEY = "service.function.id"; + private static final int STRING_MAX_LENGTH = 500; + private static final String KEY_INSTRUMENTATION_LIBRARY_NAME = "otel.library.name"; + private static final String DEFAULT_ENV = "default_env"; + static final String KEY_HERACONTEXT = "span.hera_context"; + + static { + if (envId == null) { + envId = ""; + } + try { + if (StringUtils.isNullOrEmpty(ipv4Env)) { + ipv4Env = InetAddress.getLocalHost().getHostAddress(); + } + hostName = InetAddress.getLocalHost().getHostAddress(); + } catch (Exception e) { + logger.log(Level.WARNING, "fail to init SpanToLogbackUtil", e); + } + } + + /** + * Log format: + * 0:startTime| + * 1:duration| + * 2:ip| + * 3:appName| + * 4:spanName(operationName)| + * 5:statusCode| + * 6:traceId| + * 7:spanId| + * 8:tags[]: + * 9:evnets(logs)[]| + * 10:resource(process)[]| + * 11:references[] + */ + public static String convert(SpanData spanData) { + StringBuilder sb = new StringBuilder(); + Resource resource = spanData.getResource(); + String applicationName = ""; + if (resource != null) { + applicationName = resource.getAttributes().get(ResourceAttributes.SERVICE_NAME); + if (applicationName == null || applicationName.isEmpty()) { + applicationName = Resource.getDefault().getAttributes() + .get(ResourceAttributes.SERVICE_NAME); + } + } + sb.append(spanData.getStartEpochNanos()).append(split) + .append(spanData.getEndEpochNanos() - spanData.getStartEpochNanos()).append(split) + .append(notNull(ipv4Env)).append(split) + .append(notNull(applicationName)).append(split) + .append(notNull(spanData.getName())).append(split) + .append(notNull(spanData.getStatus().getStatusCode().name())).append(split) + .append(notNull(spanData.getTraceId())).append(split) + .append(notNull(spanData.getSpanId())).append(split); + // tags + Attributes tagsAttributes = spanData.getAttributes(); + sb.append("["); + if (tagsAttributes != null || tagsAttributes.size() > 0) { + tagsAttributes.forEach((key, value) -> { + if (filterTagsByKey(key.toString())) { + sb.append("{"); + sb.append("\"").append("key").append("\"").append(":").append("\"").append(key) + .append("\"") + .append(",") + .append("\"").append("type").append("\"").append(":").append("\"") + .append(getType(key)) + .append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"") + .append(subString(encodeLineBreak(value))) + .append("\"") + .append("}").append(","); + } + }); + } + if (spanData.getKind() != SpanKind.INTERNAL) { + sb.append("{"); + sb.append("\"").append("key").append("\"").append(":").append("\"").append("span.kind") + .append("\"") + .append(",") + .append("\"").append("type").append("\"").append(":").append("\"").append("string") + .append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"") + .append(spanData.getKind().name().toLowerCase(Locale.ROOT)) + .append("\"") + .append("}").append(","); + } + if (spanData.getStatus().getStatusCode() == StatusCode.ERROR) { + sb.append("{"); + sb.append("\"").append("key").append("\"").append(":").append("\"").append("error") + .append("\"") + .append(",") + .append("\"").append("type").append("\"").append(":").append("\"").append("bool") + .append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"").append(true) + .append("\"") + .append("}").append(","); + } + // heraContext + SpanContext spanContext = spanData.getSpanContext(); + String heraContext = + spanContext.getHeraContext() == null ? "" : spanContext.getHeraContext().get("heracontext"); + if (heraContext == null) { + heraContext = ""; + } + sb.append("{"); + sb.append("\"").append("key").append("\"").append(":").append("\"").append(KEY_HERACONTEXT) + .append("\"") + .append(",") + .append("\"").append("type").append("\"").append(":").append("\"").append("string") + .append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"").append(heraContext) + .append("\"") + .append("}").append(","); + sb.append("{"); + sb.append("\"").append("key").append("\"").append(":").append("\"") + .append(KEY_INSTRUMENTATION_LIBRARY_NAME).append("\"") + .append(",") + .append("\"").append("type").append("\"").append(":").append("\"").append("string") + .append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"") + .append(spanData.getInstrumentationLibraryInfo().getName()).append("\"") + .append("}").append(","); + sb.deleteCharAt(sb.length() - 1); + sb.append("]"); + sb.append(split); + // event(logs) + sb.append("["); + List events = spanData.getEvents(); + if (events != null && events.size() > 0) { + for (EventData ed : events) { + if (ed != null) { + sb.append("{") + .append("\"").append("timestamp").append("\"") + .append(":") + .append(ed.getEpochNanos() / 1000).append(",") + .append("\"").append("fields").append("\"").append(":") + .append("[") + .append("{"); + sb.append("\"").append("key").append("\"").append(":").append("\"").append("event") + .append("\"").append(",") + .append("\"").append("type").append("\"").append(":").append("\"").append("string") + .append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"") + .append(ed.getName()).append("\"").append("}").append(","); + Attributes eventAttributes = ed.getAttributes(); + if (eventAttributes != null || eventAttributes.size() > 0) { + eventAttributes.forEach((key, value) -> { + sb.append("{"); + sb.append("\"").append("key").append("\"").append(":").append("\"").append(key) + .append("\"").append(",") + .append("\"").append("type").append("\"").append(":").append("\"") + .append(getType(key)).append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"") + .append(encodeLineBreak(value)) + .append("\""); + sb.append("}").append(","); + }); + } + sb.deleteCharAt(sb.length() - 1); + sb.append("]").append("}").append(","); + } + } + if (sb.toString().endsWith(",")) { + sb.deleteCharAt(sb.length() - 1); + } + } + sb.append("]"); + sb.append(split); + // resource(process) + sb.append("{").append("\"").append("serviceName").append("\"").append(":") + .append("\"").append(applicationName).append("\"").append(",") + .append("\"").append("tags").append("\"").append(":").append("["); + sb.append("{") + .append("\"").append("key").append("\"").append(":").append("\"").append("ip") + .append("\"").append(",") + .append("\"").append("type").append("\"").append(":").append("\"") + .append("string").append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"").append(ipv4Env) + .append("\""); + sb.append("}").append(","); + sb.append("{") + .append("\"").append("key").append("\"").append(":").append("\"").append("service.env") + .append("\"").append(",") + .append("\"").append("type").append("\"").append(":").append("\"") + .append("string").append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"").append(env) + .append("\""); + sb.append("}").append(","); + sb.append("{") + .append("\"").append("key").append("\"").append(":").append("\"").append("service.env.id") + .append("\"").append(",") + .append("\"").append("type").append("\"").append(":").append("\"") + .append("string").append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"").append(envId) + .append("\""); + sb.append("}").append(","); + if (resource != null) { + Attributes resouceAttributes = resource.getAttributes(); + if (resouceAttributes != null && resouceAttributes.size() > 0) { + resouceAttributes.forEach((key, value) -> { + if (filterProcessByKey(key.toString())) { + sb.append("{") + .append("\"").append("key").append("\"").append(":").append("\"").append(key) + .append("\"").append(",") + .append("\"").append("type").append("\"").append(":").append("\"") + .append(getType(key)).append("\"").append(",") + .append("\"").append("value").append("\"").append(":").append("\"").append(value) + .append("\""); + sb.append("}").append(","); + } + }); + } + } + sb.deleteCharAt(sb.length() - 1); + sb.append("]").append("}"); + sb.append(split); + // reference + sb.append("["); + List links = spanData.getLinks(); + if (links != null && links.size() > 0) { + for (LinkData linkData : links) { + sb.append("{") + .append("\"").append("traceID").append("\"").append(":").append("\"") + .append(linkData.getSpanContext().getTraceId()).append("\"").append(",") + .append("\"").append("spanID").append("\"").append(":").append("\"") + .append(linkData.getSpanContext().getSpanId()).append("\"").append(",") + .append("\"refType\":\"CHILD_OF\"").append("},"); + } + } + SpanContext parentSpanContext = spanData.getParentSpanContext(); + if (parentSpanContext.isValid()) { + sb.append("{") + .append("\"").append("traceID").append("\"").append(":").append("\"") + .append(parentSpanContext.getTraceId()).append("\"").append(",") + .append("\"").append("spanID").append("\"").append(":").append("\"") + .append(parentSpanContext.getSpanId()).append("\"").append(",") + .append("\"refType\":\"CHILD_OF\"").append("}"); + } + sb.append("]"); + sb.append(split); + return sb.toString(); + } + + public static String notNull(String string) { + return string == null ? "" : string; + } + + @SuppressWarnings("UnnecessaryDefaultInEnumSwitch") + public static String getType(AttributeKey key) { + switch (key.getType()) { + case STRING: + return "string"; + case LONG: + return "int64"; + case BOOLEAN: + return "bool"; + case DOUBLE: + return "float64"; + case STRING_ARRAY: + case LONG_ARRAY: + case BOOLEAN_ARRAY: + case DOUBLE_ARRAY: + return "string"; + default: + return "string"; + } + } + + private static String encodeLineBreak(Object value) { + String s = String.valueOf(value); + if (!StringUtils.isNullOrEmpty(s)) { + return s.replaceAll("\\n", "##n").replaceAll("\\r", "##r").replaceAll("\\t", "##t") + .replaceAll("\\tat", "##tat").replaceAll("\\\\\"", "##r'").replaceAll("\"", "##'") + .replaceAll("\\\\", "\\\\\\\\"); + } + return s; + } + + private static String subString(String value) { + if (value != null && value.length() > STRING_MAX_LENGTH) { + return value.substring(0, STRING_MAX_LENGTH) + "......"; + } + return value; + } + + private static boolean filterProcessByKey(String key) { + if (StringUtils.isNullOrEmpty(key)) { + return false; + } + if ("telemetry.sdk.name".equals(key)) { + return false; + } + if ("telemetry.sdk.language".equals(key)) { + return false; + } + if ("telemetry.sdk.version".equals(key)) { + return false; + } + if ("telemetry.auto.version".equals(key)) { + return false; + } + if ("ip".equals(key)) { + return false; + } + return true; + } + + private static boolean filterTagsByKey(String key) { + if (StringUtils.isNullOrEmpty(key)) { + return false; + } + if ("thread.id".equals(key)) { + return false; + } + if ("thread.name".equals(key)) { + return false; + } + return true; + } +} diff --git a/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/package-info.java b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/package-info.java new file mode 100644 index 000000000..6b9026d33 --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.logging; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LogbackSpanExporterTest.java b/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LogbackSpanExporterTest.java new file mode 100644 index 000000000..1f161eb36 --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LogbackSpanExporterTest.java @@ -0,0 +1,51 @@ +package io.opentelemetry.exporter.logging; + + +import org.junit.jupiter.api.Test; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("SystemOut") +public class LogbackSpanExporterTest { + + @Test + public void test1(){ + String a = "a ### b ### ### ### c ### "; + String[] split = a.split(" ### "); + System.out.println(split.length); + } + + @Test + public void test2(){ + Map map = new HashMap<>(); + map.forEach((key,value) -> System.out.println(key+":"+value)); + } + + @Test + public void test3(){ + StringBuilder sb = new StringBuilder(); + sb.append("a,").append("b,").append("c,").append("d,"); + String substring = sb.substring(0, sb.length() - 1); + sb.deleteCharAt(sb.length()-1); + System.out.println(substring); + } + + @Test + public void test4(){ + String a = "123_sdada_sds"; + int i = a.indexOf("_"); + String pre = a.substring(0,i); + + System.out.println(pre); + String substring = a.substring(i+1); + + System.out.println(substring); + } + + @Test + public void test5(){ + String a = "|a||"; + String[] split = a.split("\\|"); + System.out.println(split.length); + } +} diff --git a/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingMetricExporterTest.java b/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingMetricExporterTest.java new file mode 100644 index 000000000..e0e13fead --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingMetricExporterTest.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.resources.Resource; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import java.util.logging.StreamHandler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests for the {@link LoggingMetricExporter}. */ +class LoggingMetricExporterTest { + + LoggingMetricExporter exporter; + + @BeforeEach + void setUp() { + exporter = new LoggingMetricExporter(); + } + + @AfterEach + void tearDown() { + exporter.shutdown(); + } + + @Test + void testExport() { + long nowEpochNanos = System.currentTimeMillis() * 1000 * 1000; + Resource resource = Resource.create(Attributes.of(stringKey("host"), "localhost")); + InstrumentationLibraryInfo instrumentationLibraryInfo = + InstrumentationLibraryInfo.create("manualInstrumentation", "1.0"); + exporter.export( + Arrays.asList( + MetricData.createDoubleSummary( + resource, + instrumentationLibraryInfo, + "measureOne", + "A summarized test measure", + "ms", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + nowEpochNanos, + nowEpochNanos + 245, + Labels.of("a", "b", "c", "d"), + 1010, + 50000, + Arrays.asList( + ValueAtPercentile.create(0.0, 25), + ValueAtPercentile.create(100.0, 433)))))), + MetricData.createLongSum( + resource, + instrumentationLibraryInfo, + "counterOne", + "A simple counter", + "one", + LongSumData.create( + true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + nowEpochNanos, + nowEpochNanos + 245, + Labels.of("z", "y", "x", "w"), + 1010)))), + MetricData.createDoubleSum( + resource, + instrumentationLibraryInfo, + "observedValue", + "an observer gauge", + "kb", + DoubleSumData.create( + true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + nowEpochNanos, + nowEpochNanos + 245, + Labels.of("1", "2", "3", "4"), + 33.7767)))))); + } + + @Test + void testFlush() { + final AtomicBoolean flushed = new AtomicBoolean(false); + Logger.getLogger(LoggingMetricExporter.class.getName()) + .addHandler( + new StreamHandler(new PrintStream(new ByteArrayOutputStream()), new SimpleFormatter()) { + @Override + public synchronized void flush() { + flushed.set(true); + } + }); + exporter.flush(); + assertThat(flushed.get()).isTrue(); + } +} diff --git a/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingSpanExporterTest.java b/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingSpanExporterTest.java new file mode 100644 index 000000000..ad153abe4 --- /dev/null +++ b/opentelemetry-java/exporters/logging/src/test/java/io/opentelemetry/exporter/logging/LoggingSpanExporterTest.java @@ -0,0 +1,165 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.logging; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import java.util.logging.StreamHandler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.event.Level; + +/** Tests for the {@link LoggingSpanExporter}. */ +class LoggingSpanExporterTest { + + private static final SpanData SPAN1 = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create( + "12345678876543211234567887654321", + "8765432112345678", + TraceFlags.getSampled(), + TraceState.getDefault(),new HashMap<>())) + .setStartEpochNanos(100) + .setEndEpochNanos(100 + 1000) + .setStatus(StatusData.ok()) + .setName("testSpan1") + .setKind(SpanKind.INTERNAL) + .setAttributes(Attributes.of(stringKey("animal"), "cat", longKey("lives"), 9L)) + .setEvents( + Collections.singletonList( + EventData.create( + 100 + 500, + "somethingHappenedHere", + Attributes.of(booleanKey("important"), true)))) + .setTotalRecordedEvents(1) + .setTotalRecordedLinks(0) + .setInstrumentationLibraryInfo(InstrumentationLibraryInfo.create("tracer1", null)) + .build(); + + private static final SpanData SPAN2 = + TestSpanData.builder() + .setHasEnded(false) + .setSpanContext( + SpanContext.create( + "12340000000043211234000000004321", + "8765000000005678", + TraceFlags.getSampled(), + TraceState.getDefault(),new HashMap<>())) + .setStartEpochNanos(500) + .setEndEpochNanos(500 + 1001) + .setStatus(StatusData.error()) + .setName("testSpan2") + .setKind(SpanKind.CLIENT) + .setInstrumentationLibraryInfo(InstrumentationLibraryInfo.create("tracer2", "1.0")) + .build(); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(LoggingSpanExporter.class); + + LoggingSpanExporter exporter; + + @BeforeEach + void setUp() { + exporter = new LoggingSpanExporter(); + } + + @AfterEach + void tearDown() { + exporter.close(); + } + + @Test + void log() { + exporter.export(Arrays.asList(SPAN1, SPAN2)); + + assertThat(logs.getEvents()) + .hasSize(2) + .allSatisfy(log -> assertThat(log.getLevel()).isEqualTo(Level.INFO)); + assertThat(logs.getEvents().get(0).getMessage()) + .isEqualTo( + "'testSpan1' : 12345678876543211234567887654321 8765432112345678 " + + "INTERNAL [tracer: tracer1:] " + + "{animal=\"cat\", lives=9}"); + assertThat(logs.getEvents().get(1).getMessage()) + .isEqualTo( + "'testSpan2' : 12340000000043211234000000004321 8765000000005678 " + + "CLIENT [tracer: tracer2:1.0] {}"); + } + + @Test + void returnCode() { + long epochNanos = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + SpanData spanData = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create( + "12345678876543211234567887654321", + "8765432112345678", + TraceFlags.getSampled(), + TraceState.getDefault(),new HashMap<>())) + .setStartEpochNanos(epochNanos) + .setEndEpochNanos(epochNanos + 1000) + .setStatus(StatusData.ok()) + .setName("testSpan") + .setKind(SpanKind.INTERNAL) + .setEvents( + Collections.singletonList( + EventData.create( + epochNanos + 500, + "somethingHappenedHere", + Attributes.of(booleanKey("important"), true)))) + .setTotalRecordedEvents(1) + .setTotalRecordedLinks(0) + .build(); + CompletableResultCode resultCode = exporter.export(singletonList(spanData)); + assertThat(resultCode.isSuccess()).isTrue(); + } + + @Test + void testFlush() { + final AtomicBoolean flushed = new AtomicBoolean(false); + Logger.getLogger(LoggingSpanExporter.class.getName()) + .addHandler( + new StreamHandler(new PrintStream(new ByteArrayOutputStream()), new SimpleFormatter()) { + @Override + public synchronized void flush() { + flushed.set(true); + } + }); + exporter.flush(); + assertThat(flushed.get()).isTrue(); + } +} diff --git a/opentelemetry-java/exporters/otlp/all/build.gradle.kts b/opentelemetry-java/exporters/otlp/all/build.gradle.kts new file mode 100644 index 000000000..a8531742a --- /dev/null +++ b/opentelemetry-java/exporters/otlp/all/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` + `maven-publish` + + id("me.champeau.jmh") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Protocol Exporters" +extra["moduleName"] = "io.opentelemetry.exporter.otlp" +base.archivesBaseName = "opentelemetry-exporter-otlp" + +dependencies { + api(project(":exporters:otlp:trace")) +} diff --git a/opentelemetry-java/exporters/otlp/build.gradle.kts b/opentelemetry-java/exporters/otlp/build.gradle.kts new file mode 100644 index 000000000..1c405515a --- /dev/null +++ b/opentelemetry-java/exporters/otlp/build.gradle.kts @@ -0,0 +1,8 @@ +subprojects { + val proj = this + plugins.withId("java") { + configure { + archivesBaseName = "opentelemetry-exporter-otlp-${proj.name}" + } + } +} diff --git a/opentelemetry-java/exporters/otlp/common/README.md b/opentelemetry-java/exporters/otlp/common/README.md new file mode 100644 index 000000000..b2a45ac6f --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/README.md @@ -0,0 +1,9 @@ +# OpenTelemetry Proto Utils + +[![Javadocs][javadoc-image]][javadoc-url] + +This module contains code to helps with conversions betewen OpenTelemetry proto objects and API or +SDK objects (e.g. SpanId, TraceId, TraceConfig, SpanData etc.). + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-sdk-contrib-otproto.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-contrib-otproto \ No newline at end of file diff --git a/opentelemetry-java/exporters/otlp/common/build.gradle.kts b/opentelemetry-java/exporters/otlp/common/build.gradle.kts new file mode 100644 index 000000000..d95c30523 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + `java-library` + `maven-publish` + + id("me.champeau.jmh") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Protocol Exporter" +extra["moduleName"] = "io.opentelemetry.exporter.otlp.internal" + +dependencies { + api(project(":api:all")) + api(project(":proto")) + api(project(":sdk:all")) + api(project(":sdk:metrics")) + + implementation("com.google.protobuf:protobuf-java") + + testImplementation(project(":sdk:testing")) + + testImplementation("io.grpc:grpc-testing") + testRuntimeOnly("io.grpc:grpc-netty-shaded") + + jmhImplementation(project(":sdk:testing")) + jmhImplementation(project(":sdk-extensions:resources")) +} diff --git a/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/CommonAdapterBenchmark.java b/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/CommonAdapterBenchmark.java new file mode 100644 index 000000000..4dcd161df --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/CommonAdapterBenchmark.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import io.opentelemetry.proto.common.v1.InstrumentationLibrary; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({Mode.AverageTime}) +@Fork(3) +@Measurement(iterations = 15, time = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1) +public class CommonAdapterBenchmark { + + private static final InstrumentationLibraryInfo INFO = + InstrumentationLibraryInfo.create("io.opentelemetry.instrumentation.benchmark-1.0", "1.2.0"); + + @Benchmark + public InstrumentationLibrary toProto() { + return CommonAdapter.toProtoInstrumentationLibrary(INFO); + } +} diff --git a/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapterBenchmark.java b/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapterBenchmark.java new file mode 100644 index 000000000..06333cf1d --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapterBenchmark.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import io.opentelemetry.sdk.extension.resources.HostResource; +import io.opentelemetry.sdk.extension.resources.OsResource; +import io.opentelemetry.sdk.extension.resources.ProcessResource; +import io.opentelemetry.sdk.extension.resources.ProcessRuntimeResource; +import io.opentelemetry.sdk.resources.Resource; +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({Mode.AverageTime}) +@Fork(3) +@Measurement(iterations = 15, time = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1) +public class ResourceAdapterBenchmark { + + // A default resource, which is pretty big. Resource in practice will generally be even bigger by + // containing cloud attributes. + private static final Resource RESOURCE = + ProcessResource.get() + .merge(ProcessRuntimeResource.get()) + .merge(OsResource.get()) + .merge(HostResource.get()) + .merge(Resource.getDefault()); + + @Benchmark + public io.opentelemetry.proto.resource.v1.Resource toProto() { + return ResourceAdapter.toProtoResource(RESOURCE); + } +} diff --git a/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/SpanAdapterBenchmark.java b/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/SpanAdapterBenchmark.java new file mode 100644 index 000000000..7f79dcead --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/jmh/java/io/opentelemetry/exporter/otlp/internal/SpanAdapterBenchmark.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.extension.resources.HostResource; +import io.opentelemetry.sdk.extension.resources.OsResource; +import io.opentelemetry.sdk.extension.resources.ProcessResource; +import io.opentelemetry.sdk.extension.resources.ProcessRuntimeResource; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({Mode.AverageTime}) +@Fork(3) +@Measurement(iterations = 15, time = 1) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 1) +public class SpanAdapterBenchmark { + // A default resource, which is pretty big. Resource in practice will generally be even bigger by + // containing cloud attributes. + private static final Resource RESOURCE = + ProcessResource.get() + .merge(ProcessRuntimeResource.get()) + .merge(OsResource.get()) + .merge(HostResource.get()) + .merge(Resource.getDefault()); + + private static final InstrumentationLibraryInfo LIBRARY1 = + InstrumentationLibraryInfo.create("io.opentelemetry.instrumentation.benchmark-1.0", "1.2.0"); + private static final InstrumentationLibraryInfo LIBRARY2 = + InstrumentationLibraryInfo.create("io.opentelemetry.instrumentation.benchmark-2.0", "1.3.0"); + private static final InstrumentationLibraryInfo LIBRARY3 = + InstrumentationLibraryInfo.create("io.opentelemetry.instrumentation.benchmark-3.0", "1.4.0"); + + private static final String TRACE_ID = "0102030405060708090a0b0c0d0e0f00"; + private static final String SPAN_ID = "090a0b0c0d0e0f00"; + private static final SpanContext SPAN_CONTEXT = + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()); + + // One resource, 3 libraries, 2 spans each. + private static final List SPANS = + Arrays.asList( + spanData(LIBRARY1), + spanData(LIBRARY1), + spanData(LIBRARY2), + spanData(LIBRARY2), + spanData(LIBRARY3), + spanData(LIBRARY3)); + + @Benchmark + public List toProto() { + return SpanAdapter.toProtoResourceSpans(SPANS); + } + + private static SpanData spanData(InstrumentationLibraryInfo library) { + return TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(SPAN_CONTEXT) + .setParentSpanContext(SpanContext.getInvalid()) + .setName("GET /api/endpoint") + .setKind(SpanKind.SERVER) + .setStartEpochNanos(12345) + .setEndEpochNanos(12349) + .setAttributes(Attributes.of(booleanKey("key"), true)) + .setTotalAttributeCount(2) + .setEvents( + Collections.singletonList(EventData.create(12347, "my_event", Attributes.empty()))) + .setTotalRecordedEvents(3) + .setLinks(Collections.singletonList(LinkData.create(SPAN_CONTEXT))) + .setTotalRecordedLinks(2) + .setStatus(StatusData.ok()) + .setResource(RESOURCE) + .setInstrumentationLibraryInfo(library) + .build(); + } +} diff --git a/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/CommonAdapter.java b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/CommonAdapter.java new file mode 100644 index 000000000..3c108490f --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/CommonAdapter.java @@ -0,0 +1,173 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.ArrayValue; +import io.opentelemetry.proto.common.v1.InstrumentationLibrary; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import java.util.List; + +final class CommonAdapter { + + private static final WeakConcurrentMap + INSTRUMENTATION_LIBRARY_PROTO_CACHE = new WeakConcurrentMap.WithInlinedExpunction<>(); + + @SuppressWarnings("unchecked") + public static KeyValue toProtoAttribute(AttributeKey key, Object value) { + switch (key.getType()) { + case STRING: + return makeStringKeyValue(key, (String) value); + case BOOLEAN: + return makeBooleanKeyValue(key, (boolean) value); + case LONG: + return makeLongKeyValue(key, (Long) value); + case DOUBLE: + return makeDoubleKeyValue(key, (Double) value); + case BOOLEAN_ARRAY: + return makeBooleanArrayKeyValue(key, (List) value); + case LONG_ARRAY: + return makeLongArrayKeyValue(key, (List) value); + case DOUBLE_ARRAY: + return makeDoubleArrayKeyValue(key, (List) value); + case STRING_ARRAY: + return makeStringArrayKeyValue(key, (List) value); + } + return KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.getDefaultInstance()) + .build(); + } + + private static KeyValue makeLongArrayKeyValue(AttributeKey key, List value) { + KeyValue.Builder keyValueBuilder = + KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.newBuilder().setArrayValue(makeLongArrayAnyValue(value)).build()); + + return keyValueBuilder.build(); + } + + private static KeyValue makeDoubleArrayKeyValue(AttributeKey key, List value) { + KeyValue.Builder keyValueBuilder = + KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.newBuilder().setArrayValue(makeDoubleArrayAnyValue(value)).build()); + + return keyValueBuilder.build(); + } + + private static KeyValue makeBooleanArrayKeyValue(AttributeKey key, List value) { + KeyValue.Builder keyValueBuilder = + KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.newBuilder().setArrayValue(makeBooleanArrayAnyValue(value)).build()); + + return keyValueBuilder.build(); + } + + private static KeyValue makeStringArrayKeyValue(AttributeKey key, List value) { + KeyValue.Builder keyValueBuilder = + KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.newBuilder().setArrayValue(makeStringArrayAnyValue(value)).build()); + + return keyValueBuilder.build(); + } + + private static KeyValue makeLongKeyValue(AttributeKey key, long value) { + KeyValue.Builder keyValueBuilder = + KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.newBuilder().setIntValue(value).build()); + + return keyValueBuilder.build(); + } + + private static KeyValue makeDoubleKeyValue(AttributeKey key, double value) { + KeyValue.Builder keyValueBuilder = + KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.newBuilder().setDoubleValue(value).build()); + + return keyValueBuilder.build(); + } + + private static KeyValue makeBooleanKeyValue(AttributeKey key, boolean value) { + KeyValue.Builder keyValueBuilder = + KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.newBuilder().setBoolValue(value).build()); + + return keyValueBuilder.build(); + } + + static KeyValue makeStringKeyValue(AttributeKey key, String value) { + KeyValue.Builder keyValueBuilder = + KeyValue.newBuilder() + .setKey(key.getKey()) + .setValue(AnyValue.newBuilder().setStringValue(value).build()); + + return keyValueBuilder.build(); + } + + private static ArrayValue makeDoubleArrayAnyValue(List doubleArrayValue) { + ArrayValue.Builder builder = ArrayValue.newBuilder(); + for (Double doubleValue : doubleArrayValue) { + builder.addValues(AnyValue.newBuilder().setDoubleValue(doubleValue).build()); + } + return builder.build(); + } + + private static ArrayValue makeLongArrayAnyValue(List longArrayValue) { + ArrayValue.Builder builder = ArrayValue.newBuilder(); + for (Long intValue : longArrayValue) { + builder.addValues(AnyValue.newBuilder().setIntValue(intValue).build()); + } + return builder.build(); + } + + private static ArrayValue makeStringArrayAnyValue(List stringArrayValue) { + ArrayValue.Builder builder = ArrayValue.newBuilder(); + for (String string : stringArrayValue) { + builder.addValues(AnyValue.newBuilder().setStringValue(string).build()); + } + return builder.build(); + } + + private static ArrayValue makeBooleanArrayAnyValue(List booleanArrayValue) { + ArrayValue.Builder builder = ArrayValue.newBuilder(); + for (Boolean bool : booleanArrayValue) { + builder.addValues(AnyValue.newBuilder().setBoolValue(bool).build()); + } + return builder.build(); + } + + static InstrumentationLibrary toProtoInstrumentationLibrary( + InstrumentationLibraryInfo instrumentationLibraryInfo) { + InstrumentationLibrary cached = + INSTRUMENTATION_LIBRARY_PROTO_CACHE.get(instrumentationLibraryInfo); + if (cached == null) { + // Since WeakConcurrentMap doesn't support computeIfAbsent, we may end up doing the conversion + // a few times until the cache gets filled which is fine. + cached = + InstrumentationLibrary.newBuilder() + .setName(instrumentationLibraryInfo.getName()) + .setVersion( + instrumentationLibraryInfo.getVersion() == null + ? "" + : instrumentationLibraryInfo.getVersion()) + .build(); + INSTRUMENTATION_LIBRARY_PROTO_CACHE.put(instrumentationLibraryInfo, cached); + } + return cached; + } + + private CommonAdapter() {} +} diff --git a/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/MetricAdapter.java b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/MetricAdapter.java new file mode 100644 index 000000000..282db75d0 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/MetricAdapter.java @@ -0,0 +1,280 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import static io.opentelemetry.proto.metrics.v1.AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE; +import static io.opentelemetry.proto.metrics.v1.AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA; +import static io.opentelemetry.proto.metrics.v1.AggregationTemporality.AGGREGATION_TEMPORALITY_UNSPECIFIED; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.metrics.v1.AggregationTemporality; +import io.opentelemetry.proto.metrics.v1.Gauge; +import io.opentelemetry.proto.metrics.v1.Histogram; +import io.opentelemetry.proto.metrics.v1.HistogramDataPoint; +import io.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import io.opentelemetry.proto.metrics.v1.Sum; +import io.opentelemetry.proto.metrics.v1.Summary; +import io.opentelemetry.proto.metrics.v1.SummaryDataPoint; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramData; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramPointData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Converter from SDK {@link MetricData} to OTLP {@link ResourceMetrics}. */ +public final class MetricAdapter { + + /** Converts the provided {@link MetricData} to {@link ResourceMetrics}. */ + public static List toProtoResourceMetrics(Collection metricData) { + Map>> resourceAndLibraryMap = + groupByResourceAndLibrary(metricData); + List resourceMetrics = new ArrayList<>(resourceAndLibraryMap.size()); + for (Map.Entry>> entryResource : + resourceAndLibraryMap.entrySet()) { + List instrumentationLibraryMetrics = + new ArrayList<>(entryResource.getValue().size()); + for (Map.Entry> entryLibrary : + entryResource.getValue().entrySet()) { + instrumentationLibraryMetrics.add( + InstrumentationLibraryMetrics.newBuilder() + .setInstrumentationLibrary( + CommonAdapter.toProtoInstrumentationLibrary(entryLibrary.getKey())) + .addAllMetrics(entryLibrary.getValue()) + .build()); + } + resourceMetrics.add( + ResourceMetrics.newBuilder() + .setResource(ResourceAdapter.toProtoResource(entryResource.getKey())) + .addAllInstrumentationLibraryMetrics(instrumentationLibraryMetrics) + .build()); + } + return resourceMetrics; + } + + private static Map>> + groupByResourceAndLibrary(Collection metricDataList) { + Map>> result = new HashMap<>(); + for (MetricData metricData : metricDataList) { + if (metricData.isEmpty()) { + // If no points available then ignore. + continue; + } + + Resource resource = metricData.getResource(); + Map> libraryInfoListMap = + result.get(metricData.getResource()); + if (libraryInfoListMap == null) { + libraryInfoListMap = new HashMap<>(); + result.put(resource, libraryInfoListMap); + } + List metricList = + libraryInfoListMap.computeIfAbsent( + metricData.getInstrumentationLibraryInfo(), k -> new ArrayList<>()); + metricList.add(toProtoMetric(metricData)); + } + return result; + } + + // fall through comment isn't working for some reason. + @SuppressWarnings("fallthrough") + static Metric toProtoMetric(MetricData metricData) { + Metric.Builder builder = + Metric.newBuilder() + .setName(metricData.getName()) + .setDescription(metricData.getDescription()) + .setUnit(metricData.getUnit()); + + switch (metricData.getType()) { + case LONG_SUM: + LongSumData longSumData = metricData.getLongSumData(); + builder.setSum( + Sum.newBuilder() + .setIsMonotonic(longSumData.isMonotonic()) + .setAggregationTemporality( + mapToTemporality(longSumData.getAggregationTemporality())) + .addAllDataPoints(toIntDataPoints(longSumData.getPoints())) + .build()); + break; + case DOUBLE_SUM: + DoubleSumData doubleSumData = metricData.getDoubleSumData(); + builder.setSum( + Sum.newBuilder() + .setIsMonotonic(doubleSumData.isMonotonic()) + .setAggregationTemporality( + mapToTemporality(doubleSumData.getAggregationTemporality())) + .addAllDataPoints(toDoubleDataPoints(doubleSumData.getPoints())) + .build()); + break; + case SUMMARY: + DoubleSummaryData doubleSummaryData = metricData.getDoubleSummaryData(); + builder.setSummary( + Summary.newBuilder() + .addAllDataPoints(toSummaryDataPoints(doubleSummaryData.getPoints())) + .build()); + break; + case LONG_GAUGE: + LongGaugeData longGaugeData = metricData.getLongGaugeData(); + builder.setGauge( + Gauge.newBuilder() + .addAllDataPoints(toIntDataPoints(longGaugeData.getPoints())) + .build()); + break; + case DOUBLE_GAUGE: + DoubleGaugeData doubleGaugeData = metricData.getDoubleGaugeData(); + builder.setGauge( + Gauge.newBuilder() + .addAllDataPoints(toDoubleDataPoints(doubleGaugeData.getPoints())) + .build()); + break; + case HISTOGRAM: + DoubleHistogramData doubleHistogramData = metricData.getDoubleHistogramData(); + builder.setHistogram( + Histogram.newBuilder() + .setAggregationTemporality( + mapToTemporality(doubleHistogramData.getAggregationTemporality())) + .addAllDataPoints(toHistogramDataPoints(doubleHistogramData.getPoints())) + .build()); + break; + } + return builder.build(); + } + + private static AggregationTemporality mapToTemporality( + io.opentelemetry.sdk.metrics.data.AggregationTemporality temporality) { + switch (temporality) { + case CUMULATIVE: + return AGGREGATION_TEMPORALITY_CUMULATIVE; + case DELTA: + return AGGREGATION_TEMPORALITY_DELTA; + } + return AGGREGATION_TEMPORALITY_UNSPECIFIED; + } + + static List toIntDataPoints(Collection points) { + List result = new ArrayList<>(points.size()); + for (LongPointData longPoint : points) { + NumberDataPoint.Builder builder = + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(longPoint.getStartEpochNanos()) + .setTimeUnixNano(longPoint.getEpochNanos()) + .setAsInt(longPoint.getValue()); + Collection labels = toProtoLabels(longPoint.getLabels()); + if (!labels.isEmpty()) { + builder.addAllAttributes(labels); + } + result.add(builder.build()); + } + return result; + } + + static Collection toDoubleDataPoints(Collection points) { + List result = new ArrayList<>(points.size()); + for (DoublePointData doublePoint : points) { + NumberDataPoint.Builder builder = + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(doublePoint.getStartEpochNanos()) + .setTimeUnixNano(doublePoint.getEpochNanos()) + .setAsDouble(doublePoint.getValue()); + Collection labels = toProtoLabels(doublePoint.getLabels()); + if (!labels.isEmpty()) { + builder.addAllAttributes(labels); + } + result.add(builder.build()); + } + return result; + } + + static List toSummaryDataPoints(Collection points) { + List result = new ArrayList<>(points.size()); + for (DoubleSummaryPointData doubleSummaryPoint : points) { + SummaryDataPoint.Builder builder = + SummaryDataPoint.newBuilder() + .setStartTimeUnixNano(doubleSummaryPoint.getStartEpochNanos()) + .setTimeUnixNano(doubleSummaryPoint.getEpochNanos()) + .setCount(doubleSummaryPoint.getCount()) + .setSum(doubleSummaryPoint.getSum()); + List labels = toProtoLabels(doubleSummaryPoint.getLabels()); + if (!labels.isEmpty()) { + builder.addAllAttributes(labels); + } + // Not calling directly addAllQuantileValues because that generates couple of unnecessary + // allocations if empty list. + if (!doubleSummaryPoint.getPercentileValues().isEmpty()) { + for (ValueAtPercentile valueAtPercentile : doubleSummaryPoint.getPercentileValues()) { + builder.addQuantileValues( + SummaryDataPoint.ValueAtQuantile.newBuilder() + .setQuantile(valueAtPercentile.getPercentile() / 100.0) + .setValue(valueAtPercentile.getValue()) + .build()); + } + } + result.add(builder.build()); + } + return result; + } + + static Collection toHistogramDataPoints( + Collection points) { + List result = new ArrayList<>(points.size()); + for (DoubleHistogramPointData doubleHistogramPoint : points) { + HistogramDataPoint.Builder builder = + HistogramDataPoint.newBuilder() + .setStartTimeUnixNano(doubleHistogramPoint.getStartEpochNanos()) + .setTimeUnixNano(doubleHistogramPoint.getEpochNanos()) + .setCount(doubleHistogramPoint.getCount()) + .setSum(doubleHistogramPoint.getSum()) + .addAllBucketCounts(doubleHistogramPoint.getCounts()); + List boundaries = doubleHistogramPoint.getBoundaries(); + if (!boundaries.isEmpty()) { + builder.addAllExplicitBounds(boundaries); + } + Collection labels = toProtoLabels(doubleHistogramPoint.getLabels()); + if (!labels.isEmpty()) { + builder.addAllAttributes(labels); + } + result.add(builder.build()); + } + return result; + } + + @SuppressWarnings("MixedMutabilityReturnType") + static List toProtoLabels(Labels labels) { + if (labels.isEmpty()) { + return Collections.emptyList(); + } + final List result = new ArrayList<>(labels.size()); + labels.forEach( + (key, value) -> + result.add( + KeyValue.newBuilder() + .setKey(key) + .setValue(AnyValue.newBuilder().setStringValue(value).build()) + .build())); + return result; + } + + private MetricAdapter() {} +} diff --git a/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapter.java b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapter.java new file mode 100644 index 000000000..bc9ea7f20 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapter.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; +import io.opentelemetry.proto.resource.v1.Resource; + +final class ResourceAdapter { + + private static final WeakConcurrentMap + RESOURCE_PROTO_CACHE = new WeakConcurrentMap.WithInlinedExpunction<>(); + + static Resource toProtoResource(io.opentelemetry.sdk.resources.Resource resource) { + Resource cached = RESOURCE_PROTO_CACHE.get(resource); + if (cached == null) { + // Since WeakConcurrentMap doesn't support computeIfAbsent, we may end up doing the conversion + // a few times until the cache gets filled which is fine. + Resource.Builder builder = Resource.newBuilder(); + resource + .getAttributes() + .forEach( + (key, value) -> builder.addAttributes(CommonAdapter.toProtoAttribute(key, value))); + cached = builder.build(); + RESOURCE_PROTO_CACHE.put(resource, cached); + } + return cached; + } + + private ResourceAdapter() {} +} diff --git a/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/SpanAdapter.java b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/SpanAdapter.java new file mode 100644 index 000000000..007f132fc --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/SpanAdapter.java @@ -0,0 +1,249 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CONSUMER; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_INTERNAL; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_PRODUCER; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_SERVER; +import static io.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode.DEPRECATED_STATUS_CODE_OK; +import static io.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR; + +import com.google.protobuf.ByteString; +import com.google.protobuf.UnsafeByteOperations; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.Span; +import io.opentelemetry.proto.trace.v1.Status; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Converter from SDK {@link SpanData} to OTLP {@link ResourceSpans}. */ +public final class SpanAdapter { + + // In practice, there is often only one thread that calls this code in the BatchSpanProcessor so + // reusing buffers for the thread is almost free. Even with multiple threads, it should still be + // worth it and is common practice in serialization libraries such as Jackson. + private static final ThreadLocal THREAD_LOCAL_CACHE = new ThreadLocal<>(); + + // Still set DeprecatedCode + @SuppressWarnings("deprecation") + private static final Status STATUS_OK = + Status.newBuilder() + .setCode(Status.StatusCode.STATUS_CODE_OK) + .setDeprecatedCode(DEPRECATED_STATUS_CODE_OK) + .build(); + + // Still set DeprecatedCode + @SuppressWarnings("deprecation") + private static final Status STATUS_ERROR = + Status.newBuilder() + .setCode(Status.StatusCode.STATUS_CODE_ERROR) + .setDeprecatedCode(DEPRECATED_STATUS_CODE_UNKNOWN_ERROR) + .build(); + + // Still set DeprecatedCode + @SuppressWarnings("deprecation") + private static final Status STATUS_UNSET = + Status.newBuilder() + .setCode(Status.StatusCode.STATUS_CODE_UNSET) + .setDeprecatedCode(DEPRECATED_STATUS_CODE_OK) + .build(); + + /** Converts the provided {@link SpanData} to {@link ResourceSpans}. */ + public static List toProtoResourceSpans(Collection spanDataList) { + Map>> resourceAndLibraryMap = + groupByResourceAndLibrary(spanDataList); + List resourceSpans = new ArrayList<>(resourceAndLibraryMap.size()); + resourceAndLibraryMap.forEach( + (resource, librarySpans) -> { + ResourceSpans.Builder resourceSpansBuilder = + ResourceSpans.newBuilder().setResource(ResourceAdapter.toProtoResource(resource)); + librarySpans.forEach( + (library, spans) -> + resourceSpansBuilder.addInstrumentationLibrarySpans( + InstrumentationLibrarySpans.newBuilder() + .setInstrumentationLibrary( + CommonAdapter.toProtoInstrumentationLibrary(library)) + .addAllSpans(spans) + .build())); + resourceSpans.add(resourceSpansBuilder.build()); + }); + return resourceSpans; + } + + private static Map>> + groupByResourceAndLibrary(Collection spanDataList) { + Map>> result = new HashMap<>(); + ThreadLocalCache threadLocalCache = getThreadLocalCache(); + for (SpanData spanData : spanDataList) { + Map> libraryInfoListMap = + result.computeIfAbsent(spanData.getResource(), unused -> new HashMap<>()); + List spanList = + libraryInfoListMap.computeIfAbsent( + spanData.getInstrumentationLibraryInfo(), unused -> new ArrayList<>()); + spanList.add(toProtoSpan(spanData, threadLocalCache)); + } + threadLocalCache.idBytesCache.clear(); + return result; + } + + // Visible for testing + static Span toProtoSpan(SpanData spanData, ThreadLocalCache threadLocalCache) { + Map idBytesCache = threadLocalCache.idBytesCache; + Span.Builder builder = threadLocalCache.spanBuilder; + builder.setTraceId( + idBytesCache.computeIfAbsent( + spanData.getSpanContext().getTraceId(), + unused -> + UnsafeByteOperations.unsafeWrap(spanData.getSpanContext().getTraceIdBytes()))); + builder.setSpanId( + idBytesCache.computeIfAbsent( + spanData.getSpanContext().getSpanId(), + unused -> UnsafeByteOperations.unsafeWrap(spanData.getSpanContext().getSpanIdBytes()))); + // TODO: Set TraceState; + if (spanData.getParentSpanContext().isValid()) { + builder.setParentSpanId( + idBytesCache.computeIfAbsent( + spanData.getParentSpanContext().getSpanId(), + unused -> + UnsafeByteOperations.unsafeWrap( + spanData.getParentSpanContext().getSpanIdBytes()))); + } + builder.setName(spanData.getName()); + builder.setKind(toProtoSpanKind(spanData.getKind())); + builder.setStartTimeUnixNano(spanData.getStartEpochNanos()); + builder.setEndTimeUnixNano(spanData.getEndEpochNanos()); + spanData + .getAttributes() + .forEach((key, value) -> builder.addAttributes(CommonAdapter.toProtoAttribute(key, value))); + builder.setDroppedAttributesCount( + spanData.getTotalAttributeCount() - spanData.getAttributes().size()); + for (EventData event : spanData.getEvents()) { + builder.addEvents(toProtoSpanEvent(event, threadLocalCache)); + } + builder.setDroppedEventsCount(spanData.getTotalRecordedEvents() - spanData.getEvents().size()); + for (LinkData link : spanData.getLinks()) { + builder.addLinks(toProtoSpanLink(link, threadLocalCache)); + } + builder.setDroppedLinksCount(spanData.getTotalRecordedLinks() - spanData.getLinks().size()); + builder.setStatus(toStatusProto(spanData.getStatus())); + Span span = builder.build(); + // We reuse the builder instance to create multiple spans to reduce allocation of intermediary + // storage. It means we MUST clear here or we'd keep on building on the same object. + builder.clear(); + return span; + } + + static Span.SpanKind toProtoSpanKind(SpanKind kind) { + switch (kind) { + case INTERNAL: + return SPAN_KIND_INTERNAL; + case SERVER: + return SPAN_KIND_SERVER; + case CLIENT: + return SPAN_KIND_CLIENT; + case PRODUCER: + return SPAN_KIND_PRODUCER; + case CONSUMER: + return SPAN_KIND_CONSUMER; + } + return Span.SpanKind.UNRECOGNIZED; + } + + // Visible for testing + static Span.Event toProtoSpanEvent(EventData event, ThreadLocalCache threadLocalCache) { + Span.Event.Builder builder = threadLocalCache.spanEventBuilder; + builder.setName(event.getName()); + builder.setTimeUnixNano(event.getEpochNanos()); + event + .getAttributes() + .forEach((key, value) -> builder.addAttributes(CommonAdapter.toProtoAttribute(key, value))); + builder.setDroppedAttributesCount( + event.getTotalAttributeCount() - event.getAttributes().size()); + Span.Event built = builder.build(); + // We reuse the builder instance to create multiple spans to reduce allocation of intermediary + // storage. It means we MUST clear here or we'd keep on building on the same object. + builder.clear(); + return built; + } + + // Visible for testing + static Span.Link toProtoSpanLink(LinkData link, ThreadLocalCache threadLocalCache) { + Map idBytesCache = threadLocalCache.idBytesCache; + Span.Link.Builder builder = threadLocalCache.spanLinkBuilder; + builder.setTraceId( + idBytesCache.computeIfAbsent( + link.getSpanContext().getTraceId(), + unused -> UnsafeByteOperations.unsafeWrap(link.getSpanContext().getTraceIdBytes()))); + builder.setSpanId( + idBytesCache.computeIfAbsent( + link.getSpanContext().getSpanId(), + unused -> UnsafeByteOperations.unsafeWrap(link.getSpanContext().getSpanIdBytes()))); + // TODO: Set TraceState; + Attributes attributes = link.getAttributes(); + attributes.forEach( + (key, value) -> builder.addAttributes(CommonAdapter.toProtoAttribute(key, value))); + + builder.setDroppedAttributesCount(link.getTotalAttributeCount() - attributes.size()); + Span.Link built = builder.build(); + // We reuse the builder instance to create multiple spans to reduce allocation of intermediary + // storage. It means we MUST clear here or we'd keep on building on the same object. + builder.clear(); + return built; + } + + // Visible for testing + static Status toStatusProto(StatusData status) { + final Status withoutDescription; + switch (status.getStatusCode()) { + case OK: + withoutDescription = STATUS_OK; + break; + case ERROR: + withoutDescription = STATUS_ERROR; + break; + case UNSET: + default: + withoutDescription = STATUS_UNSET; + break; + } + if (status.getDescription().isEmpty()) { + return withoutDescription; + } + return withoutDescription.toBuilder().setMessage(status.getDescription()).build(); + } + + private static ThreadLocalCache getThreadLocalCache() { + ThreadLocalCache result = THREAD_LOCAL_CACHE.get(); + if (result == null) { + result = new ThreadLocalCache(); + THREAD_LOCAL_CACHE.set(result); + } + return result; + } + + static final class ThreadLocalCache { + final Map idBytesCache = new HashMap<>(); + final Span.Builder spanBuilder = Span.newBuilder(); + final Span.Event.Builder spanEventBuilder = Span.Event.newBuilder(); + final Span.Link.Builder spanLinkBuilder = Span.Link.newBuilder(); + } + + private SpanAdapter() {} +} diff --git a/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/package-info.java b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/package-info.java new file mode 100644 index 000000000..215d0c916 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/otlp/internal/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Utilities for working with the OTLP format. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.otlp.internal; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/CommonAdapterTest.java b/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/CommonAdapterTest.java new file mode 100644 index 000000000..a5391ac6e --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/CommonAdapterTest.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.ArrayValue; +import io.opentelemetry.proto.common.v1.InstrumentationLibrary; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class CommonAdapterTest { + @Test + void toProtoAttribute_Bool() { + assertThat(CommonAdapter.toProtoAttribute(booleanKey("key"), true)) + .isEqualTo( + KeyValue.newBuilder() + .setKey("key") + .setValue(AnyValue.newBuilder().setBoolValue(true).build()) + .build()); + } + + @Test + void toProtoAttribute_BoolArray() { + assertThat(CommonAdapter.toProtoAttribute(booleanArrayKey("key"), Arrays.asList(true, false))) + .isEqualTo( + KeyValue.newBuilder() + .setKey("key") + .setValue( + AnyValue.newBuilder() + .setArrayValue( + ArrayValue.newBuilder() + .addValues(AnyValue.newBuilder().setBoolValue(true).build()) + .addValues(AnyValue.newBuilder().setBoolValue(false).build()) + .build()) + .build()) + .build()); + } + + @Test + void toProtoAttribute_String() { + assertThat(CommonAdapter.toProtoAttribute(stringKey("key"), "string")) + .isEqualTo( + KeyValue.newBuilder() + .setKey("key") + .setValue(AnyValue.newBuilder().setStringValue("string").build()) + .build()); + } + + @Test + void toProtoAttribute_StringArray() { + assertThat( + CommonAdapter.toProtoAttribute( + stringArrayKey("key"), Arrays.asList("string1", "string2"))) + .isEqualTo( + KeyValue.newBuilder() + .setKey("key") + .setValue( + AnyValue.newBuilder() + .setArrayValue( + ArrayValue.newBuilder() + .addValues(AnyValue.newBuilder().setStringValue("string1").build()) + .addValues(AnyValue.newBuilder().setStringValue("string2").build()) + .build()) + .build()) + .build()); + } + + @Test + void toProtoAttribute_Int() { + assertThat(CommonAdapter.toProtoAttribute(longKey("key"), 100L)) + .isEqualTo( + KeyValue.newBuilder() + .setKey("key") + .setValue(AnyValue.newBuilder().setIntValue(100).build()) + .build()); + } + + @Test + void toProtoAttribute_IntArray() { + assertThat(CommonAdapter.toProtoAttribute(longArrayKey("key"), Arrays.asList(100L, 200L))) + .isEqualTo( + KeyValue.newBuilder() + .setKey("key") + .setValue( + AnyValue.newBuilder() + .setArrayValue( + ArrayValue.newBuilder() + .addValues(AnyValue.newBuilder().setIntValue(100).build()) + .addValues(AnyValue.newBuilder().setIntValue(200).build()) + .build()) + .build()) + .build()); + } + + @Test + void toProtoAttribute_Double() { + assertThat(CommonAdapter.toProtoAttribute(doubleKey("key"), 100.3d)) + .isEqualTo( + KeyValue.newBuilder() + .setKey("key") + .setValue(AnyValue.newBuilder().setDoubleValue(100.3).build()) + .build()); + } + + @Test + void toProtoAttribute_DoubleArray() { + assertThat(CommonAdapter.toProtoAttribute(doubleArrayKey("key"), Arrays.asList(100.3, 200.5))) + .isEqualTo( + KeyValue.newBuilder() + .setKey("key") + .setValue( + AnyValue.newBuilder() + .setArrayValue( + ArrayValue.newBuilder() + .addValues(AnyValue.newBuilder().setDoubleValue(100.3).build()) + .addValues(AnyValue.newBuilder().setDoubleValue(200.5).build()) + .build()) + .build()) + .build()); + } + + @Test + void toProtoInstrumentationLibrary() { + InstrumentationLibraryInfo info = InstrumentationLibraryInfo.create("name", "version"); + InstrumentationLibrary instrumentationLibrary = + CommonAdapter.toProtoInstrumentationLibrary(info); + assertThat(instrumentationLibrary.getName()).isEqualTo("name"); + assertThat(instrumentationLibrary.getVersion()).isEqualTo("version"); + // Memoized + assertThat(CommonAdapter.toProtoInstrumentationLibrary(info)).isSameAs(instrumentationLibrary); + } + + @Test + void toProtoInstrumentationLibrary_NoVersion() { + InstrumentationLibrary instrumentationLibrary = + CommonAdapter.toProtoInstrumentationLibrary( + InstrumentationLibraryInfo.create("name", null)); + assertThat(instrumentationLibrary.getName()).isEqualTo("name"); + assertThat(instrumentationLibrary.getVersion()).isEmpty(); + } +} diff --git a/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/MetricAdapterTest.java b/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/MetricAdapterTest.java new file mode 100644 index 000000000..429c2326b --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/MetricAdapterTest.java @@ -0,0 +1,671 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.proto.metrics.v1.AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE; +import static io.opentelemetry.proto.metrics.v1.AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.InstrumentationLibrary; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.metrics.v1.Gauge; +import io.opentelemetry.proto.metrics.v1.Histogram; +import io.opentelemetry.proto.metrics.v1.HistogramDataPoint; +import io.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import io.opentelemetry.proto.metrics.v1.Sum; +import io.opentelemetry.proto.metrics.v1.Summary; +import io.opentelemetry.proto.metrics.v1.SummaryDataPoint; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramData; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramPointData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class MetricAdapterTest { + @Test + void toProtoLabels() { + assertThat(MetricAdapter.toProtoLabels(Labels.empty())).isEmpty(); + assertThat(MetricAdapter.toProtoLabels(Labels.of("k", "v"))) + .containsExactly(KeyValue.newBuilder().setKey("k").setValue(stringValue("v")).build()); + assertThat(MetricAdapter.toProtoLabels(Labels.of("k1", "v1", "k2", "v2"))) + .containsExactly( + KeyValue.newBuilder().setKey("k1").setValue(stringValue("v1")).build(), + KeyValue.newBuilder().setKey("k2").setValue(stringValue("v2")).build()); + } + + private static AnyValue stringValue(String v) { + return AnyValue.newBuilder().setStringValue(v).build(); + } + + @Test + void toInt64DataPoints() { + assertThat(MetricAdapter.toIntDataPoints(Collections.emptyList())).isEmpty(); + assertThat( + MetricAdapter.toIntDataPoints( + singletonList(LongPointData.create(123, 456, Labels.of("k", "v"), 5)))) + .containsExactly( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder().setKey("k").setValue(stringValue("v")).build())) + .setAsInt(5) + .build()); + assertThat( + MetricAdapter.toIntDataPoints( + ImmutableList.of( + LongPointData.create(123, 456, Labels.empty(), 5), + LongPointData.create(321, 654, Labels.of("k", "v"), 7)))) + .containsExactly( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .setAsInt(5) + .build(), + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(321) + .setTimeUnixNano(654) + .addAllAttributes( + singletonList( + KeyValue.newBuilder().setKey("k").setValue(stringValue("v")).build())) + .setAsInt(7) + .build()); + } + + @Test + void toDoubleDataPoints() { + assertThat(MetricAdapter.toDoubleDataPoints(Collections.emptyList())).isEmpty(); + assertThat( + MetricAdapter.toDoubleDataPoints( + singletonList(DoublePointData.create(123, 456, Labels.of("k", "v"), 5.1)))) + .containsExactly( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder().setKey("k").setValue(stringValue("v")).build())) + .setAsDouble(5.1) + .build()); + assertThat( + MetricAdapter.toDoubleDataPoints( + ImmutableList.of( + DoublePointData.create(123, 456, Labels.empty(), 5.1), + DoublePointData.create(321, 654, Labels.of("k", "v"), 7.1)))) + .containsExactly( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .setAsDouble(5.1) + .build(), + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(321) + .setTimeUnixNano(654) + .addAllAttributes( + singletonList( + KeyValue.newBuilder().setKey("k").setValue(stringValue("v")).build())) + .setAsDouble(7.1) + .build()); + } + + @Test + void toSummaryDataPoints() { + assertThat( + MetricAdapter.toSummaryDataPoints( + singletonList( + DoubleSummaryPointData.create( + 123, + 456, + Labels.of("k", "v"), + 5, + 14.2, + singletonList(ValueAtPercentile.create(0.0, 1.1)))))) + .containsExactly( + SummaryDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder().setKey("k").setValue(stringValue("v")).build())) + .setCount(5) + .setSum(14.2) + .addQuantileValues( + SummaryDataPoint.ValueAtQuantile.newBuilder() + .setQuantile(0.0) + .setValue(1.1) + .build()) + .build()); + assertThat( + MetricAdapter.toSummaryDataPoints( + ImmutableList.of( + DoubleSummaryPointData.create( + 123, 456, Labels.empty(), 7, 15.3, Collections.emptyList()), + DoubleSummaryPointData.create( + 321, + 654, + Labels.of("k", "v"), + 9, + 18.3, + ImmutableList.of( + ValueAtPercentile.create(0.0, 1.1), + ValueAtPercentile.create(100.0, 20.3)))))) + .containsExactly( + SummaryDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .setCount(7) + .setSum(15.3) + .build(), + SummaryDataPoint.newBuilder() + .setStartTimeUnixNano(321) + .setTimeUnixNano(654) + .addAllAttributes( + singletonList( + KeyValue.newBuilder().setKey("k").setValue(stringValue("v")).build())) + .setCount(9) + .setSum(18.3) + .addQuantileValues( + SummaryDataPoint.ValueAtQuantile.newBuilder() + .setQuantile(0.0) + .setValue(1.1) + .build()) + .addQuantileValues( + SummaryDataPoint.ValueAtQuantile.newBuilder() + .setQuantile(1.0) + .setValue(20.3) + .build()) + .build()); + } + + @Test + void toHistogramDataPoints() { + assertThat( + MetricAdapter.toHistogramDataPoints( + ImmutableList.of( + DoubleHistogramPointData.create( + 123, + 456, + Labels.of("k", "v"), + 14.2, + ImmutableList.of(1.0), + ImmutableList.of(1L, 5L)), + DoubleHistogramPointData.create( + 123, 456, Labels.empty(), 15.3, ImmutableList.of(), ImmutableList.of(7L))))) + .containsExactly( + HistogramDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder().setKey("k").setValue(stringValue("v")).build())) + .setCount(6) + .setSum(14.2) + .addBucketCounts(1) + .addBucketCounts(5) + .addExplicitBounds(1.0) + .build(), + HistogramDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .setCount(7) + .setSum(15.3) + .addBucketCounts(7) + .build()); + } + + @Test + void toProtoMetric_monotonic() { + assertThat( + MetricAdapter.toProtoMetric( + MetricData.createLongSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + singletonList(LongPointData.create(123, 456, Labels.of("k", "v"), 5)))))) + .isEqualTo( + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setSum( + Sum.newBuilder() + .setIsMonotonic(true) + .setAggregationTemporality(AGGREGATION_TEMPORALITY_CUMULATIVE) + .addDataPoints( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setAsInt(5) + .build()) + .build()) + .build()); + assertThat( + MetricAdapter.toProtoMetric( + MetricData.createDoubleSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + singletonList( + DoublePointData.create(123, 456, Labels.of("k", "v"), 5.1)))))) + .isEqualTo( + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setSum( + Sum.newBuilder() + .setIsMonotonic(true) + .setAggregationTemporality(AGGREGATION_TEMPORALITY_CUMULATIVE) + .addDataPoints( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setAsDouble(5.1) + .build()) + .build()) + .build()); + } + + @Test + void toProtoMetric_nonMonotonic() { + assertThat( + MetricAdapter.toProtoMetric( + MetricData.createLongSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + singletonList(LongPointData.create(123, 456, Labels.of("k", "v"), 5)))))) + .isEqualTo( + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setSum( + Sum.newBuilder() + .setIsMonotonic(false) + .setAggregationTemporality(AGGREGATION_TEMPORALITY_CUMULATIVE) + .addDataPoints( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setAsInt(5) + .build()) + .build()) + .build()); + assertThat( + MetricAdapter.toProtoMetric( + MetricData.createDoubleSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + singletonList( + DoublePointData.create(123, 456, Labels.of("k", "v"), 5.1)))))) + .isEqualTo( + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setSum( + Sum.newBuilder() + .setIsMonotonic(false) + .setAggregationTemporality(AGGREGATION_TEMPORALITY_CUMULATIVE) + .addDataPoints( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setAsDouble(5.1) + .build()) + .build()) + .build()); + } + + @Test + void toProtoMetric_gauges() { + assertThat( + MetricAdapter.toProtoMetric( + MetricData.createLongGauge( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + LongGaugeData.create( + singletonList(LongPointData.create(123, 456, Labels.of("k", "v"), 5)))))) + .isEqualTo( + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setGauge( + Gauge.newBuilder() + .addDataPoints( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setAsInt(5) + .build()) + .build()) + .build()); + assertThat( + MetricAdapter.toProtoMetric( + MetricData.createDoubleGauge( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + DoubleGaugeData.create( + singletonList( + DoublePointData.create(123, 456, Labels.of("k", "v"), 5.1)))))) + .isEqualTo( + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setGauge( + Gauge.newBuilder() + .addDataPoints( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setAsDouble(5.1) + .build()) + .build()) + .build()); + } + + @Test + void toProtoMetric_summary() { + assertThat( + MetricAdapter.toProtoMetric( + MetricData.createDoubleSummary( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + DoubleSummaryData.create( + singletonList( + DoubleSummaryPointData.create( + 123, + 456, + Labels.of("k", "v"), + 5, + 33d, + ImmutableList.of( + ValueAtPercentile.create(0, 1.1), + ValueAtPercentile.create(100.0, 20.3)))))))) + .isEqualTo( + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setSummary( + Summary.newBuilder() + .addDataPoints( + SummaryDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setCount(5) + .setSum(33d) + .addQuantileValues( + SummaryDataPoint.ValueAtQuantile.newBuilder() + .setQuantile(0) + .setValue(1.1) + .build()) + .addQuantileValues( + SummaryDataPoint.ValueAtQuantile.newBuilder() + .setQuantile(1.0) + .setValue(20.3) + .build()) + .build()) + .build()) + .build()); + } + + @Test + void toProtoMetric_histogram() { + assertThat( + MetricAdapter.toProtoMetric( + MetricData.createDoubleHistogram( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + DoubleHistogramData.create( + AggregationTemporality.DELTA, + singletonList( + DoubleHistogramPointData.create( + 123, + 456, + Labels.of("k", "v"), + 4.0, + ImmutableList.of(), + ImmutableList.of(33L))))))) + .isEqualTo( + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setHistogram( + Histogram.newBuilder() + .setAggregationTemporality(AGGREGATION_TEMPORALITY_DELTA) + .addDataPoints( + HistogramDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setCount(33) + .setSum(4.0) + .addBucketCounts(33) + .build()) + .build()) + .build()); + } + + @Test + void toProtoResourceMetrics() { + Resource resource = Resource.create(Attributes.of(stringKey("ka"), "va")); + io.opentelemetry.proto.resource.v1.Resource resourceProto = + io.opentelemetry.proto.resource.v1.Resource.newBuilder() + .addAllAttributes( + singletonList( + KeyValue.newBuilder().setKey("ka").setValue(stringValue("va")).build())) + .build(); + io.opentelemetry.proto.resource.v1.Resource emptyResourceProto = + io.opentelemetry.proto.resource.v1.Resource.newBuilder().build(); + InstrumentationLibraryInfo instrumentationLibraryInfo = + InstrumentationLibraryInfo.create("name", "version"); + InstrumentationLibrary instrumentationLibraryProto = + InstrumentationLibrary.newBuilder().setName("name").setVersion("version").build(); + InstrumentationLibrary emptyInstrumentationLibraryProto = + InstrumentationLibrary.newBuilder().setName("").setVersion("").build(); + Metric metricDoubleSum = + Metric.newBuilder() + .setName("name") + .setDescription("description") + .setUnit("1") + .setSum( + Sum.newBuilder() + .setIsMonotonic(true) + .setAggregationTemporality(AGGREGATION_TEMPORALITY_CUMULATIVE) + .addDataPoints( + NumberDataPoint.newBuilder() + .setStartTimeUnixNano(123) + .setTimeUnixNano(456) + .addAllAttributes( + singletonList( + KeyValue.newBuilder() + .setKey("k") + .setValue(stringValue("v")) + .build())) + .setAsDouble(5.0) + .build()) + .build()) + .build(); + + assertThat( + MetricAdapter.toProtoResourceMetrics( + ImmutableList.of( + MetricData.createDoubleSum( + resource, + instrumentationLibraryInfo, + "name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("k", "v"), 5.0)))), + MetricData.createDoubleSum( + resource, + instrumentationLibraryInfo, + "name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("k", "v"), 5.0)))), + MetricData.createDoubleSum( + Resource.empty(), + instrumentationLibraryInfo, + "name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("k", "v"), 5.0)))), + MetricData.createDoubleSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("k", "v"), 5.0))))))) + .containsExactlyInAnyOrder( + ResourceMetrics.newBuilder() + .setResource(resourceProto) + .addAllInstrumentationLibraryMetrics( + singletonList( + InstrumentationLibraryMetrics.newBuilder() + .setInstrumentationLibrary(instrumentationLibraryProto) + .addAllMetrics(ImmutableList.of(metricDoubleSum, metricDoubleSum)) + .build())) + .build(), + ResourceMetrics.newBuilder() + .setResource(emptyResourceProto) + .addAllInstrumentationLibraryMetrics( + ImmutableList.of( + InstrumentationLibraryMetrics.newBuilder() + .setInstrumentationLibrary(emptyInstrumentationLibraryProto) + .addAllMetrics(singletonList(metricDoubleSum)) + .build(), + InstrumentationLibraryMetrics.newBuilder() + .setInstrumentationLibrary(instrumentationLibraryProto) + .addAllMetrics(singletonList(metricDoubleSum)) + .build())) + .build()); + } +} diff --git a/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapterTest.java b/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapterTest.java new file mode 100644 index 000000000..a25faa6ff --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/ResourceAdapterTest.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.Test; + +class ResourceAdapterTest { + + @Test + void toProtoResource() { + Resource resource = + Resource.create( + Attributes.of( + booleanKey("key_bool"), + true, + stringKey("key_string"), + "string", + longKey("key_int"), + 100L, + doubleKey("key_double"), + 100.3)); + io.opentelemetry.proto.resource.v1.Resource protoResource = + ResourceAdapter.toProtoResource(resource); + + assertThat(protoResource.getAttributesList()) + .containsExactlyInAnyOrder( + KeyValue.newBuilder() + .setKey("key_bool") + .setValue(AnyValue.newBuilder().setBoolValue(true).build()) + .build(), + KeyValue.newBuilder() + .setKey("key_string") + .setValue(AnyValue.newBuilder().setStringValue("string").build()) + .build(), + KeyValue.newBuilder() + .setKey("key_int") + .setValue(AnyValue.newBuilder().setIntValue(100).build()) + .build(), + KeyValue.newBuilder() + .setKey("key_double") + .setValue(AnyValue.newBuilder().setDoubleValue(100.3).build()) + .build()); + // Memoized + assertThat(ResourceAdapter.toProtoResource(resource)).isSameAs(protoResource); + } + + @Test + void toProtoResource_Empty() { + assertThat(ResourceAdapter.toProtoResource(Resource.empty())) + .isEqualTo(io.opentelemetry.proto.resource.v1.Resource.newBuilder().build()); + } +} diff --git a/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/SpanAdapterTest.java b/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/SpanAdapterTest.java new file mode 100644 index 000000000..eccf44bb4 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/otlp/internal/SpanAdapterTest.java @@ -0,0 +1,213 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.internal; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CONSUMER; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_INTERNAL; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_PRODUCER; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_SERVER; +import static io.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode.DEPRECATED_STATUS_CODE_OK; +import static io.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR; +import static io.opentelemetry.proto.trace.v1.Status.StatusCode.STATUS_CODE_ERROR; +import static io.opentelemetry.proto.trace.v1.Status.StatusCode.STATUS_CODE_OK; +import static io.opentelemetry.proto.trace.v1.Status.StatusCode.STATUS_CODE_UNSET; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.ByteString; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.trace.v1.Span; +import io.opentelemetry.proto.trace.v1.Status; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.Collections; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +class SpanAdapterTest { + private static final byte[] TRACE_ID_BYTES = + new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}; + private static final String TRACE_ID = TraceId.fromBytes(TRACE_ID_BYTES); + private static final byte[] SPAN_ID_BYTES = new byte[] {0, 0, 0, 0, 4, 3, 2, 1}; + private static final String SPAN_ID = SpanId.fromBytes(SPAN_ID_BYTES); + private static final SpanContext SPAN_CONTEXT = + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()); + + private static final SpanAdapter.ThreadLocalCache threadLocalCache = + new SpanAdapter.ThreadLocalCache(); + + // Repeat to reuse the cache. If we forgot to clear any reused builder it will fail. + @RepeatedTest(3) + void toProtoSpan() { + Span span = + SpanAdapter.toProtoSpan( + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(SPAN_CONTEXT) + .setParentSpanContext(SpanContext.getInvalid()) + .setName("GET /api/endpoint") + .setKind(SpanKind.SERVER) + .setStartEpochNanos(12345) + .setEndEpochNanos(12349) + .setAttributes(Attributes.of(booleanKey("key"), true)) + .setTotalAttributeCount(2) + .setEvents( + Collections.singletonList( + EventData.create(12347, "my_event", Attributes.empty()))) + .setTotalRecordedEvents(3) + .setLinks(Collections.singletonList(LinkData.create(SPAN_CONTEXT))) + .setTotalRecordedLinks(2) + .setStatus(StatusData.ok()) + .build(), + threadLocalCache); + + assertThat(span.getTraceId().toByteArray()).isEqualTo(TRACE_ID_BYTES); + assertThat(span.getSpanId().toByteArray()).isEqualTo(SPAN_ID_BYTES); + assertThat(span.getParentSpanId().toByteArray()).isEqualTo(new byte[] {}); + assertThat(span.getName()).isEqualTo("GET /api/endpoint"); + assertThat(span.getKind()).isEqualTo(SPAN_KIND_SERVER); + assertThat(span.getStartTimeUnixNano()).isEqualTo(12345); + assertThat(span.getEndTimeUnixNano()).isEqualTo(12349); + assertThat(span.getAttributesList()) + .containsExactly( + KeyValue.newBuilder() + .setKey("key") + .setValue(AnyValue.newBuilder().setBoolValue(true).build()) + .build()); + assertThat(span.getDroppedAttributesCount()).isEqualTo(1); + assertThat(span.getEventsList()) + .containsExactly( + Span.Event.newBuilder().setTimeUnixNano(12347).setName("my_event").build()); + assertThat(span.getDroppedEventsCount()).isEqualTo(2); // 3 - 1 + assertThat(span.getLinksList()) + .containsExactly( + Span.Link.newBuilder() + .setTraceId(ByteString.copyFrom(TRACE_ID_BYTES)) + .setSpanId(ByteString.copyFrom(SPAN_ID_BYTES)) + .build()); + assertThat(span.getDroppedLinksCount()).isEqualTo(1); // 2 - 1 + assertThat(span.getStatus()).isEqualTo(Status.newBuilder().setCode(STATUS_CODE_OK).build()); + } + + @Test + void toProtoSpanKind() { + assertThat(SpanAdapter.toProtoSpanKind(SpanKind.INTERNAL)).isEqualTo(SPAN_KIND_INTERNAL); + assertThat(SpanAdapter.toProtoSpanKind(SpanKind.CLIENT)).isEqualTo(SPAN_KIND_CLIENT); + assertThat(SpanAdapter.toProtoSpanKind(SpanKind.SERVER)).isEqualTo(SPAN_KIND_SERVER); + assertThat(SpanAdapter.toProtoSpanKind(SpanKind.PRODUCER)).isEqualTo(SPAN_KIND_PRODUCER); + assertThat(SpanAdapter.toProtoSpanKind(SpanKind.CONSUMER)).isEqualTo(SPAN_KIND_CONSUMER); + } + + @Test + @SuppressWarnings("deprecation") // setDeprecatedCode is deprecated. + void toProtoStatus() { + assertThat(SpanAdapter.toStatusProto(StatusData.unset())) + .isEqualTo( + Status.newBuilder() + .setCode(STATUS_CODE_UNSET) + .setDeprecatedCode(DEPRECATED_STATUS_CODE_OK) + .build()); + assertThat(SpanAdapter.toStatusProto(StatusData.create(StatusCode.ERROR, "ERROR"))) + .isEqualTo( + Status.newBuilder() + .setCode(STATUS_CODE_ERROR) + .setDeprecatedCode(DEPRECATED_STATUS_CODE_UNKNOWN_ERROR) + .setMessage("ERROR") + .build()); + assertThat(SpanAdapter.toStatusProto(StatusData.create(StatusCode.ERROR, "UNKNOWN"))) + .isEqualTo( + Status.newBuilder() + .setCode(STATUS_CODE_ERROR) + .setDeprecatedCode(DEPRECATED_STATUS_CODE_UNKNOWN_ERROR) + .setMessage("UNKNOWN") + .build()); + assertThat(SpanAdapter.toStatusProto(StatusData.create(StatusCode.OK, "OK_OVERRIDE"))) + .isEqualTo( + Status.newBuilder() + .setCode(STATUS_CODE_OK) + .setDeprecatedCode(DEPRECATED_STATUS_CODE_OK) + .setMessage("OK_OVERRIDE") + .build()); + } + + @Test + void toProtoSpanEvent_WithoutAttributes() { + assertThat( + SpanAdapter.toProtoSpanEvent( + EventData.create(12345, "test_without_attributes", Attributes.empty()), + threadLocalCache)) + .isEqualTo( + Span.Event.newBuilder() + .setTimeUnixNano(12345) + .setName("test_without_attributes") + .build()); + } + + @Test + void toProtoSpanEvent_WithAttributes() { + assertThat( + SpanAdapter.toProtoSpanEvent( + EventData.create( + 12345, + "test_with_attributes", + Attributes.of(stringKey("key_string"), "string"), + 5), + threadLocalCache)) + .isEqualTo( + Span.Event.newBuilder() + .setTimeUnixNano(12345) + .setName("test_with_attributes") + .addAttributes( + KeyValue.newBuilder() + .setKey("key_string") + .setValue(AnyValue.newBuilder().setStringValue("string").build()) + .build()) + .setDroppedAttributesCount(4) + .build()); + } + + @Test + void toProtoSpanLink_WithoutAttributes() { + assertThat(SpanAdapter.toProtoSpanLink(LinkData.create(SPAN_CONTEXT), threadLocalCache)) + .isEqualTo( + Span.Link.newBuilder() + .setTraceId(ByteString.copyFrom(TRACE_ID_BYTES)) + .setSpanId(ByteString.copyFrom(SPAN_ID_BYTES)) + .build()); + } + + @Test + void toProtoSpanLink_WithAttributes() { + assertThat( + SpanAdapter.toProtoSpanLink( + LinkData.create(SPAN_CONTEXT, Attributes.of(stringKey("key_string"), "string"), 5), + threadLocalCache)) + .isEqualTo( + Span.Link.newBuilder() + .setTraceId(ByteString.copyFrom(TRACE_ID_BYTES)) + .setSpanId(ByteString.copyFrom(SPAN_ID_BYTES)) + .addAttributes( + KeyValue.newBuilder() + .setKey("key_string") + .setValue(AnyValue.newBuilder().setStringValue("string").build()) + .build()) + .setDroppedAttributesCount(4) + .build()); + } +} diff --git a/opentelemetry-java/exporters/otlp/metrics/README.md b/opentelemetry-java/exporters/otlp/metrics/README.md new file mode 100644 index 000000000..d18224b95 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/metrics/README.md @@ -0,0 +1,8 @@ +# OpenTelemetry - OTLP Metrics Exporter - gRPC + +[![Javadocs][javadoc-image]][javadoc-url] + +This is the OpenTelemetry exporter, sending metrics data to OpenTelemetry collector via gRPC. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-exporters-otlp.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-exporters-otlp \ No newline at end of file diff --git a/opentelemetry-java/exporters/otlp/metrics/build.gradle.kts b/opentelemetry-java/exporters/otlp/metrics/build.gradle.kts new file mode 100644 index 000000000..bbcd760fb --- /dev/null +++ b/opentelemetry-java/exporters/otlp/metrics/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Protocol Metrics Exporter" +extra["moduleName"] = "io.opentelemetry.exporter.otlp.metrics" + +dependencies { + api(project(":sdk:metrics")) + + implementation(project(":exporters:otlp:common")) + + implementation("io.grpc:grpc-api") + implementation("io.grpc:grpc-protobuf") + implementation("io.grpc:grpc-stub") + implementation("com.google.protobuf:protobuf-java") + + testImplementation(project(":sdk:testing")) + + testImplementation("io.grpc:grpc-testing") + testRuntimeOnly("io.grpc:grpc-netty-shaded") +} diff --git a/opentelemetry-java/exporters/otlp/metrics/gradle.properties b/opentelemetry-java/exporters/otlp/metrics/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/exporters/otlp/metrics/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporter.java b/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporter.java new file mode 100644 index 000000000..db984d891 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporter.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.metrics; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.ManagedChannel; +import io.grpc.Status; +import io.opentelemetry.exporter.otlp.internal.MetricAdapter; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc.MetricsServiceFutureStub; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** Exports metrics using OTLP via gRPC, using OpenTelemetry's protobuf model. */ +@ThreadSafe +public final class OtlpGrpcMetricExporter implements MetricExporter { + + private final ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(OtlpGrpcMetricExporter.class.getName())); + + private final MetricsServiceFutureStub metricsService; + private final ManagedChannel managedChannel; + private final long timeoutNanos; + + /** + * Creates a new OTLP gRPC Metric Reporter with the given name, using the given channel. + * + * @param channel the channel to use when communicating with the OpenTelemetry Collector. + * @param timeoutNanos max waiting time for the collector to process each metric batch. When set + * to 0 or to a negative value, the exporter will wait indefinitely. + */ + OtlpGrpcMetricExporter(ManagedChannel channel, long timeoutNanos) { + this.managedChannel = channel; + this.timeoutNanos = timeoutNanos; + metricsService = MetricsServiceGrpc.newFutureStub(channel); + } + + /** + * Submits all the given metrics in a single batch to the OpenTelemetry collector. + * + * @param metrics the list of Metrics to be exported. + * @return the result of the operation + */ + @Override + public CompletableResultCode export(Collection metrics) { + ExportMetricsServiceRequest exportMetricsServiceRequest = + ExportMetricsServiceRequest.newBuilder() + .addAllResourceMetrics(MetricAdapter.toProtoResourceMetrics(metrics)) + .build(); + + final CompletableResultCode result = new CompletableResultCode(); + MetricsServiceFutureStub exporter; + if (timeoutNanos > 0) { + exporter = metricsService.withDeadlineAfter(timeoutNanos, TimeUnit.NANOSECONDS); + } else { + exporter = metricsService; + } + + Futures.addCallback( + exporter.export(exportMetricsServiceRequest), + new FutureCallback() { + @Override + public void onSuccess(@Nullable ExportMetricsServiceResponse response) { + result.succeed(); + } + + @Override + public void onFailure(Throwable t) { + Status status = Status.fromThrowable(t); + switch (status.getCode()) { + case UNIMPLEMENTED: + logger.log( + Level.SEVERE, + "Failed to export metrics. Server responded with UNIMPLEMENTED. " + + "This usually means that your collector is not configured with an otlp " + + "receiver in the \"pipelines\" section of the configuration. " + + "Full error message: " + + t.getMessage()); + break; + case UNAVAILABLE: + logger.log( + Level.SEVERE, + "Failed to export metrics. Server is UNAVAILABLE. " + + "Make sure your collector is running and reachable from this network." + + t.getMessage()); + break; + default: + logger.log( + Level.WARNING, "Failed to export metrics. Error message: " + t.getMessage()); + break; + } + logger.log(Level.FINEST, "Failed to export metrics. Details follow: " + t); + result.fail(); + } + }, + MoreExecutors.directExecutor()); + return result; + } + + /** + * The OTLP exporter does not batch metrics, so this method will immediately return with success. + * + * @return always Success + */ + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + /** + * Returns a new builder instance for this exporter. + * + * @return a new builder instance for this exporter. + */ + public static OtlpGrpcMetricExporterBuilder builder() { + return new OtlpGrpcMetricExporterBuilder(); + } + + /** + * Returns a new {@link OtlpGrpcMetricExporter} reading the configuration values from the + * environment and from system properties. System properties override values defined in the + * environment. If a configuration value is missing, it uses the default value. + * + * @return a new {@link OtlpGrpcMetricExporter} instance. + */ + public static OtlpGrpcMetricExporter getDefault() { + return builder().build(); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately + * cancelled. The channel is forcefully closed after a timeout. + */ + @Override + public CompletableResultCode shutdown() { + try { + managedChannel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Failed to shutdown the gRPC channel", e); + return CompletableResultCode.ofFailure(); + } + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporterBuilder.java b/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporterBuilder.java new file mode 100644 index 000000000..595f5d61a --- /dev/null +++ b/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporterBuilder.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.metrics; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.opentelemetry.api.internal.Utils.checkArgument; +import static java.util.Objects.requireNonNull; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.stub.MetadataUtils; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +/** Builder utility for this exporter. */ +public final class OtlpGrpcMetricExporterBuilder { + + private static final String DEFAULT_ENDPOINT_URL = "http://localhost:4317"; + private static final URI DEFAULT_ENDPOINT = URI.create(DEFAULT_ENDPOINT_URL); + private static final long DEFAULT_TIMEOUT_SECS = 10; + + private ManagedChannel channel; + private long timeoutNanos = TimeUnit.SECONDS.toNanos(DEFAULT_TIMEOUT_SECS); + private URI endpoint = DEFAULT_ENDPOINT; + + @Nullable private Metadata metadata; + + /** + * Sets the managed chanel to use when communicating with the backend. Takes precedence over + * {@link #setEndpoint(String)} if both are called. + * + * @param channel the channel to use + * @return this builder's instance + */ + public OtlpGrpcMetricExporterBuilder setChannel(ManagedChannel channel) { + this.channel = channel; + return this; + } + + /** + * Sets the maximum time to wait for the collector to process an exported batch of metrics. If + * unset, defaults to {@value DEFAULT_TIMEOUT_SECS}s. + */ + public OtlpGrpcMetricExporterBuilder setTimeout(long timeout, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(timeout >= 0, "timeout must be non-negative"); + timeoutNanos = unit.toNanos(timeout); + return this; + } + + /** + * Sets the maximum time to wait for the collector to process an exported batch of metrics. If + * unset, defaults to {@value DEFAULT_TIMEOUT_SECS}s. + */ + public OtlpGrpcMetricExporterBuilder setTimeout(Duration timeout) { + requireNonNull(timeout, "timeout"); + return setTimeout(timeout.toNanos(), TimeUnit.NANOSECONDS); + } + + /** + * Sets the OTLP endpoint to connect to. If unset, defaults to {@value DEFAULT_ENDPOINT_URL}. The + * endpoint must start with either http:// or https://. + */ + public OtlpGrpcMetricExporterBuilder setEndpoint(String endpoint) { + requireNonNull(endpoint, "endpoint"); + + URI uri; + try { + uri = new URI(endpoint); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid endpoint, must be a URL: " + endpoint, e); + } + + if (uri.getScheme() == null + || (!uri.getScheme().equals("http") && !uri.getScheme().equals("https"))) { + throw new IllegalArgumentException( + "Invalid endpoint, must start with http:// or https://: " + uri); + } + + this.endpoint = uri; + return this; + } + + /** + * Add header to request. Optional. Applicable only if {@link + * OtlpGrpcMetricExporterBuilder#endpoint} is set to build channel. + * + * @param key header key + * @param value header value + * @return this builder's instance + */ + public OtlpGrpcMetricExporterBuilder addHeader(String key, String value) { + if (metadata == null) { + metadata = new Metadata(); + } + metadata.put(Metadata.Key.of(key, ASCII_STRING_MARSHALLER), value); + return this; + } + + /** + * Constructs a new instance of the exporter based on the builder's values. + * + * @return a new exporter's instance + */ + public OtlpGrpcMetricExporter build() { + if (channel == null) { + final ManagedChannelBuilder managedChannelBuilder = + ManagedChannelBuilder.forTarget(endpoint.getAuthority()); + + if (endpoint.getScheme().equals("https")) { + managedChannelBuilder.useTransportSecurity(); + } else { + managedChannelBuilder.usePlaintext(); + } + + if (metadata != null) { + managedChannelBuilder.intercept(MetadataUtils.newAttachHeadersInterceptor(metadata)); + } + + channel = managedChannelBuilder.build(); + } + return new OtlpGrpcMetricExporter(channel, timeoutNanos); + } + + OtlpGrpcMetricExporterBuilder() {} +} diff --git a/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/package-info.java b/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/package-info.java new file mode 100644 index 000000000..9182d3976 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/metrics/src/main/java/io/opentelemetry/exporter/otlp/metrics/package-info.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * OpenTelemetry exporter which sends metric data to OpenTelemetry collector via gRPC. + * + *

    Configuration options for {@link + * io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter} can be read from system + * properties, environment variables, or {@link java.util.Properties} objects. + * + *

    For system properties and {@link java.util.Properties} objects, {@link + * io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter} will look for the following names: + * + *

      + *
    • {@code otel.exporter.otlp.metric.timeout}: to set the max waiting time allowed to send each + * metric batch. + *
    • {@code otel.exporter.otlp.metric.endpoint}: to set the endpoint to connect to. + *
    • {@code otel.exporter.otlp.metric.insecure}: whether to enable client transport security for + * the connection. + *
    • {@code otel.exporter.otlp.metric.headers}: the headers associated with the requests. + *
    + * + *

    For environment variables, {@link + * io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter} will look for the following names: + * + *

      + *
    • {@code OTEL_EXPORTER_OTLP_METRIC_TIMEOUT}: to set the max waiting time allowed to send each + * * span batch. * + *
    • {@code OTEL_EXPORTER_OTLP_METRIC_ENDPOINT}: to set the endpoint to connect to. * + *
    • {@code OTEL_EXPORTER_OTLP_METRIC_INSECURE}: whether to enable client transport security for + * * the connection. * + *
    • {@code OTEL_EXPORTER_OTLP_METRIC_HEADERS}: the headers associated with the requests. * + *
    + * + * In both cases, if a property is missing, the name without "metric" is used to resolve the value. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.otlp.metrics; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/otlp/metrics/src/test/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporterTest.java b/opentelemetry-java/exporters/otlp/metrics/src/test/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporterTest.java new file mode 100644 index 000000000..f1ce49735 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/metrics/src/test/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporterTest.java @@ -0,0 +1,336 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.io.Closer; +import io.github.netmikey.logunit.api.LogCapturer; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.exporter.otlp.internal.MetricAdapter; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.io.IOException; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; + +class OtlpGrpcMetricExporterTest { + + private final FakeCollector fakeCollector = new FakeCollector(); + private final String serverName = InProcessServerBuilder.generateName(); + private final ManagedChannel inProcessChannel = + InProcessChannelBuilder.forName(serverName).directExecutor().build(); + + private final Closer closer = Closer.create(); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(OtlpGrpcMetricExporter.class); + + @BeforeEach + public void setup() throws IOException { + Server server = + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(fakeCollector) + .build() + .start(); + closer.register(server::shutdownNow); + closer.register(inProcessChannel::shutdownNow); + } + + @AfterEach + void tearDown() throws Exception { + closer.close(); + } + + @Test + @SuppressWarnings("PreferJavaTimeOverload") + void invalidConfig() { + assertThatThrownBy(() -> OtlpGrpcMetricExporter.builder().setTimeout(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("timeout must be non-negative"); + assertThatThrownBy(() -> OtlpGrpcMetricExporter.builder().setTimeout(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy(() -> OtlpGrpcMetricExporter.builder().setTimeout(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("timeout"); + + assertThatThrownBy(() -> OtlpGrpcMetricExporter.builder().setEndpoint(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("endpoint"); + assertThatThrownBy(() -> OtlpGrpcMetricExporter.builder().setEndpoint("😺://localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must be a URL: 😺://localhost") + .hasCauseInstanceOf(URISyntaxException.class); + assertThatThrownBy(() -> OtlpGrpcMetricExporter.builder().setEndpoint("localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must start with http:// or https://: localhost"); + assertThatThrownBy(() -> OtlpGrpcMetricExporter.builder().setEndpoint("gopher://localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must start with http:// or https://: gopher://localhost"); + } + + @Test + void testExport() { + MetricData span = generateFakeMetric(); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(span)).isSuccess()).isTrue(); + assertThat(fakeCollector.getReceivedMetrics()) + .isEqualTo(MetricAdapter.toProtoResourceMetrics(Collections.singletonList(span))); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_MultipleMetrics() { + List spans = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + spans.add(generateFakeMetric()); + } + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(spans).isSuccess()).isTrue(); + assertThat(fakeCollector.getReceivedMetrics()) + .isEqualTo(MetricAdapter.toProtoResourceMetrics(spans)); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_DeadlineSetPerExport() throws Exception { + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder() + .setChannel(inProcessChannel) + .setTimeout(Duration.ofMillis(1500)) + .build(); + + try { + TimeUnit.MILLISECONDS.sleep(2000); + CompletableResultCode result = + exporter.export(Collections.singletonList(generateFakeMetric())); + Awaitility.await().untilAsserted(() -> assertThat(result.isSuccess()).isTrue()); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_AfterShutdown() { + MetricData span = generateFakeMetric(); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + exporter.shutdown(); + assertThat(exporter.export(Collections.singletonList(span)).isSuccess()).isFalse(); + } + + @Test + void testExport_Cancelled() { + fakeCollector.setReturnedStatus(Status.CANCELLED); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeMetric())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_DeadlineExceeded() { + fakeCollector.setReturnedStatus(Status.DEADLINE_EXCEEDED); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeMetric())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_ResourceExhausted() { + fakeCollector.setReturnedStatus(Status.RESOURCE_EXHAUSTED); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeMetric())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_OutOfRange() { + fakeCollector.setReturnedStatus(Status.OUT_OF_RANGE); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeMetric())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_Unavailable() { + fakeCollector.setReturnedStatus(Status.UNAVAILABLE); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeMetric())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + LoggingEvent log = + logs.assertContains( + "Failed to export metrics. Server is UNAVAILABLE. " + + "Make sure your collector is running and reachable from this network."); + assertThat(log.getLevel()).isEqualTo(Level.ERROR); + } + + @Test + void testExport_Unimplemented() { + fakeCollector.setReturnedStatus(Status.UNIMPLEMENTED); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeMetric())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + LoggingEvent log = + logs.assertContains( + "Failed to export metrics. Server responded with UNIMPLEMENTED. " + + "This usually means that your collector is not configured with an otlp " + + "receiver in the \"pipelines\" section of the configuration. " + + "Full error message: UNIMPLEMENTED"); + assertThat(log.getLevel()).isEqualTo(Level.ERROR); + } + + @Test + void testExport_DataLoss() { + fakeCollector.setReturnedStatus(Status.DATA_LOSS); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeMetric())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_PermissionDenied() { + fakeCollector.setReturnedStatus(Status.PERMISSION_DENIED); + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeMetric())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_flush() { + OtlpGrpcMetricExporter exporter = + OtlpGrpcMetricExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.flush().isSuccess()).isTrue(); + } finally { + exporter.shutdown(); + } + } + + private static MetricData generateFakeMetric() { + long startNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + long endNs = startNs + TimeUnit.MILLISECONDS.toNanos(900); + return MetricData.createLongSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create(startNs, endNs, Labels.of("k", "v"), 5)))); + } + + private static final class FakeCollector extends MetricsServiceGrpc.MetricsServiceImplBase { + private final List receivedMetrics = new ArrayList<>(); + private Status returnedStatus = Status.OK; + + @Override + public void export( + ExportMetricsServiceRequest request, + StreamObserver responseObserver) { + + receivedMetrics.addAll(request.getResourceMetricsList()); + responseObserver.onNext(ExportMetricsServiceResponse.newBuilder().build()); + if (!returnedStatus.isOk()) { + if (returnedStatus.getCode() == Code.DEADLINE_EXCEEDED) { + // Do not call onCompleted to simulate a deadline exceeded. + return; + } + responseObserver.onError(returnedStatus.asRuntimeException()); + return; + } + responseObserver.onCompleted(); + } + + List getReceivedMetrics() { + return receivedMetrics; + } + + void setReturnedStatus(Status returnedStatus) { + this.returnedStatus = returnedStatus; + } + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/README.md b/opentelemetry-java/exporters/otlp/trace/README.md new file mode 100644 index 000000000..a6995ecf9 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/README.md @@ -0,0 +1,8 @@ +# OpenTelemetry - OTLP Trace Exporter - gRPC + +[![Javadocs][javadoc-image]][javadoc-url] + +This is the OpenTelemetry exporter, sending span data to OpenTelemetry collector via gRPC. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-exporters-otlp.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-exporters-otlp \ No newline at end of file diff --git a/opentelemetry-java/exporters/otlp/trace/build.gradle.kts b/opentelemetry-java/exporters/otlp/trace/build.gradle.kts new file mode 100644 index 000000000..e2768ee10 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/build.gradle.kts @@ -0,0 +1,58 @@ +plugins { + `java-library` + `maven-publish` + + id("me.champeau.jmh") + id("org.unbroken-dome.test-sets") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Protocol Trace Exporter" +extra["moduleName"] = "io.opentelemetry.exporter.otlp.trace" + +testSets { + create("testGrpcNetty") + create("testGrpcNettyShaded") + create("testGrpcOkhttp") +} + +dependencies { + api(project(":sdk:trace")) + + compileOnly("io.grpc:grpc-netty") + compileOnly("io.grpc:grpc-netty-shaded") + + implementation(project(":exporters:otlp:common")) + implementation("io.grpc:grpc-api") + implementation("io.grpc:grpc-protobuf") + implementation("io.grpc:grpc-stub") + implementation("com.google.protobuf:protobuf-java") + + testImplementation(project(":sdk:testing")) + + testImplementation("io.grpc:grpc-testing") + testImplementation("org.slf4j:slf4j-simple") + + add("testGrpcNettyImplementation", "com.linecorp.armeria:armeria-grpc") + add("testGrpcNettyImplementation", "com.linecorp.armeria:armeria-junit5") + + add("testGrpcNettyShadedImplementation", "com.linecorp.armeria:armeria-grpc") + add("testGrpcNettyShadedImplementation", "com.linecorp.armeria:armeria-junit5") + + add("testGrpcOkhttpImplementation", "com.linecorp.armeria:armeria-grpc") + add("testGrpcOkhttpImplementation", "com.linecorp.armeria:armeria-junit5") + + add("testGrpcNettyRuntimeOnly", "io.grpc:grpc-netty") + + add("testGrpcNettyShadedRuntimeOnly", "io.grpc:grpc-netty-shaded") + + add("testGrpcOkhttpRuntimeOnly", "io.grpc:grpc-okhttp") + + jmh(project(":sdk:testing")) +} + +tasks { + named("check") { + dependsOn("testGrpcNetty", "testGrpcNettyShaded", "testGrpcOkhttp") + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/jmh/java/io/opentelemetry/exporter/otlp/trace/RequestMarshalBenchmarks.java b/opentelemetry-java/exporters/otlp/trace/src/jmh/java/io/opentelemetry/exporter/otlp/trace/RequestMarshalBenchmarks.java new file mode 100644 index 000000000..66b1f557e --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/jmh/java/io/opentelemetry/exporter/otlp/trace/RequestMarshalBenchmarks.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import com.google.protobuf.CodedOutputStream; +import io.opentelemetry.exporter.otlp.internal.SpanAdapter; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import java.io.IOException; +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({Mode.AverageTime}) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +@Fork(1) +public class RequestMarshalBenchmarks { + + @Benchmark + @Threads(1) + public byte[] createProtoMarshal(RequestMarshalState state) { + ExportTraceServiceRequest protoRequest = + ExportTraceServiceRequest.newBuilder() + .addAllResourceSpans(SpanAdapter.toProtoResourceSpans(state.spanDataList)) + .build(); + return new byte[protoRequest.getSerializedSize()]; + } + + @Benchmark + @Threads(1) + public byte[] marshalProto(RequestMarshalState state) throws IOException { + ExportTraceServiceRequest protoRequest = + ExportTraceServiceRequest.newBuilder() + .addAllResourceSpans(SpanAdapter.toProtoResourceSpans(state.spanDataList)) + .build(); + byte[] protoOutput = new byte[protoRequest.getSerializedSize()]; + protoRequest.writeTo(CodedOutputStream.newInstance(protoOutput)); + return protoOutput; + } + + @Benchmark + @Threads(1) + public byte[] createCustomMarshal(RequestMarshalState state) { + TraceMarshaler.RequestMarshaler requestMarshaler = + TraceMarshaler.RequestMarshaler.create(state.spanDataList); + return new byte[requestMarshaler.getSerializedSize()]; + } + + @Benchmark + @Threads(1) + public byte[] marshalCustom(RequestMarshalState state) throws IOException { + TraceMarshaler.RequestMarshaler requestMarshaler = + TraceMarshaler.RequestMarshaler.create(state.spanDataList); + byte[] customOutput = new byte[requestMarshaler.getSerializedSize()]; + requestMarshaler.writeTo(CodedOutputStream.newInstance(customOutput)); + return customOutput; + } + + @Benchmark + @Threads(1) + public byte[] marshalProtoCustom(RequestMarshalState state) throws IOException { + ExportTraceServiceRequest protoRequest = + TraceMarshaler.RequestMarshaler.create(state.spanDataList).toRequest(); + byte[] protoOutput = new byte[protoRequest.getSerializedSize()]; + protoRequest.writeTo(CodedOutputStream.newInstance(protoOutput)); + return protoOutput; + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/jmh/java/io/opentelemetry/exporter/otlp/trace/RequestMarshalState.java b/opentelemetry-java/exporters/otlp/trace/src/jmh/java/io/opentelemetry/exporter/otlp/trace/RequestMarshalState.java new file mode 100644 index 000000000..7f0d329b4 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/jmh/java/io/opentelemetry/exporter/otlp/trace/RequestMarshalState.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +@State(Scope.Benchmark) +public class RequestMarshalState { + private static final Resource RESOURCE = + Resource.create( + Attributes.builder() + .put(AttributeKey.booleanKey("key_bool"), true) + .put(AttributeKey.stringKey("key_string"), "string") + .put(AttributeKey.longKey("key_int"), 100L) + .put(AttributeKey.doubleKey("key_double"), 100.3) + .put( + AttributeKey.stringArrayKey("key_string_array"), + Arrays.asList("string", "string")) + .put(AttributeKey.longArrayKey("key_long_array"), Arrays.asList(12L, 23L)) + .put(AttributeKey.doubleArrayKey("key_double_array"), Arrays.asList(12.3, 23.1)) + .put(AttributeKey.booleanArrayKey("key_boolean_array"), Arrays.asList(true, false)) + .build()); + + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create("name", null); + private static final String TRACE_ID = "7b2e170db4df2d593ddb4ddf2ddf2d59"; + private static final String SPAN_ID = "170d3ddb4d23e81f"; + private static final SpanContext SPAN_CONTEXT = + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()); + + @Param({"16"}) + int numSpans; + + List spanDataList; + + @Setup + public void setup() { + spanDataList = new ArrayList<>(numSpans); + for (int i = 0; i < numSpans; i++) { + spanDataList.add(createSpanData()); + } + } + + private static SpanData createSpanData() { + return TestSpanData.builder() + .setResource(RESOURCE) + .setInstrumentationLibraryInfo(INSTRUMENTATION_LIBRARY_INFO) + .setHasEnded(true) + .setSpanContext(SPAN_CONTEXT) + .setParentSpanContext(SpanContext.getInvalid()) + .setName("GET /api/endpoint") + .setKind(SpanKind.SERVER) + .setStartEpochNanos(12345) + .setEndEpochNanos(12349) + .setAttributes( + Attributes.builder() + .put(AttributeKey.booleanKey("key_bool"), true) + .put(AttributeKey.stringKey("key_string"), "string") + .put(AttributeKey.longKey("key_int"), 100L) + .put(AttributeKey.doubleKey("key_double"), 100.3) + .build()) + .setTotalAttributeCount(2) + .setEvents( + Arrays.asList( + EventData.create(12347, "my_event_1", Attributes.empty()), + EventData.create( + 12348, + "my_event_2", + Attributes.of(AttributeKey.longKey("event_attr_key"), 1234L)), + EventData.create(12349, "my_event_3", Attributes.empty()))) + .setTotalRecordedEvents(4) + .setLinks( + Arrays.asList( + LinkData.create(SPAN_CONTEXT), + LinkData.create( + SPAN_CONTEXT, Attributes.of(AttributeKey.stringKey("link_attr_key"), "value")))) + .setTotalRecordedLinks(3) + .setStatus(StatusData.ok()) + .build(); + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/AttributeMarshaler.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/AttributeMarshaler.java new file mode 100644 index 000000000..b80a9996d --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/AttributeMarshaler.java @@ -0,0 +1,330 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.WireFormat; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.ArrayValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import java.io.IOException; +import java.util.List; +import java.util.function.BiConsumer; + +abstract class AttributeMarshaler extends MarshalerWithSize { + private static final AttributeMarshaler[] EMPTY_REPEATED = new AttributeMarshaler[0]; + private final byte[] key; + private final int valueSize; + + static AttributeMarshaler[] createRepeated(Attributes attributes) { + if (attributes.isEmpty()) { + return EMPTY_REPEATED; + } + + AttributeMarshaler[] attributeMarshalers = new AttributeMarshaler[attributes.size()]; + // TODO: Revisit how to avoid the atomic integer creation. + attributes.forEach( + new BiConsumer, Object>() { + int index = 0; + + @Override + public void accept(AttributeKey attributeKey, Object o) { + attributeMarshalers[index++] = AttributeMarshaler.create(attributeKey, o); + } + }); + return attributeMarshalers; + } + + @SuppressWarnings("unchecked") + static AttributeMarshaler create(AttributeKey attributeKey, Object value) { + byte[] key = MarshalerUtil.toBytes(attributeKey.getKey()); + if (value == null) { + return new KeyValueNullMarshaler(key); + } + switch (attributeKey.getType()) { + case STRING: + return new KeyValueStringMarshaler(key, MarshalerUtil.toBytes((String) value)); + case LONG: + return new KeyValueLongMarshaler(key, (Long) value); + case BOOLEAN: + return new KeyValueBooleanMarshaler(key, (Boolean) value); + case DOUBLE: + return new KeyValueDoubleMarshaler(key, (Double) value); + case STRING_ARRAY: + return new KeyValueArrayStringMarshaler(key, (List) value); + case LONG_ARRAY: + return new KeyValueArrayLongMarshaler(key, (List) value); + case BOOLEAN_ARRAY: + return new KeyValueArrayBooleanMarshaler(key, (List) value); + case DOUBLE_ARRAY: + return new KeyValueArrayDoubleMarshaler(key, (List) value); + } + throw new IllegalArgumentException("Unsupported attribute type."); + } + + private AttributeMarshaler(byte[] key, int valueSize) { + super(calculateSize(key, valueSize)); + this.key = key; + this.valueSize = valueSize; + } + + @Override + public final void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalBytes(KeyValue.KEY_FIELD_NUMBER, key, output); + if (valueSize > 0) { + output.writeTag(KeyValue.VALUE_FIELD_NUMBER, WireFormat.WIRETYPE_LENGTH_DELIMITED); + output.writeUInt32NoTag(valueSize); + writeValueTo(output); + } + } + + abstract void writeValueTo(CodedOutputStream output) throws IOException; + + private static int calculateSize(byte[] key, int valueSize) { + return MarshalerUtil.sizeBytes(KeyValue.KEY_FIELD_NUMBER, key) + + CodedOutputStream.computeTagSize(KeyValue.VALUE_FIELD_NUMBER) + + CodedOutputStream.computeUInt32SizeNoTag(valueSize) + + valueSize; + } + + private static final class KeyValueNullMarshaler extends AttributeMarshaler { + private KeyValueNullMarshaler(byte[] key) { + super(key, 0); + } + + @Override + void writeValueTo(CodedOutputStream output) {} + } + + private static final class KeyValueStringMarshaler extends AttributeMarshaler { + private final byte[] value; + + private KeyValueStringMarshaler(byte[] key, byte[] value) { + super(key, CodedOutputStream.computeByteArraySize(AnyValue.STRING_VALUE_FIELD_NUMBER, value)); + this.value = value; + } + + @Override + public void writeValueTo(CodedOutputStream output) throws IOException { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + output.writeByteArray(AnyValue.STRING_VALUE_FIELD_NUMBER, value); + } + } + + private static final class KeyValueLongMarshaler extends AttributeMarshaler { + private final long value; + + private KeyValueLongMarshaler(byte[] key, long value) { + super(key, CodedOutputStream.computeInt64Size(AnyValue.INT_VALUE_FIELD_NUMBER, value)); + this.value = value; + } + + @Override + public void writeValueTo(CodedOutputStream output) throws IOException { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + output.writeInt64(AnyValue.INT_VALUE_FIELD_NUMBER, value); + } + } + + private static final class KeyValueBooleanMarshaler extends AttributeMarshaler { + private final boolean value; + + private KeyValueBooleanMarshaler(byte[] key, boolean value) { + super(key, CodedOutputStream.computeBoolSize(AnyValue.BOOL_VALUE_FIELD_NUMBER, value)); + this.value = value; + } + + @Override + public void writeValueTo(CodedOutputStream output) throws IOException { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + output.writeBool(AnyValue.BOOL_VALUE_FIELD_NUMBER, value); + } + } + + private static final class KeyValueDoubleMarshaler extends AttributeMarshaler { + private final double value; + + private KeyValueDoubleMarshaler(byte[] key, double value) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + super(key, CodedOutputStream.computeDoubleSize(AnyValue.DOUBLE_VALUE_FIELD_NUMBER, value)); + this.value = value; + } + + @Override + public void writeValueTo(CodedOutputStream output) throws IOException { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + output.writeDouble(AnyValue.DOUBLE_VALUE_FIELD_NUMBER, value); + } + } + + private abstract static class KeyValueArrayMarshaler extends AttributeMarshaler { + private final List values; + private final int valuesSize; + + private KeyValueArrayMarshaler(byte[] key, List values, int valuesSize) { + super(key, calculateWrapperSize(valuesSize) + valuesSize); + this.values = values; + this.valuesSize = valuesSize; + } + + @Override + public final void writeValueTo(CodedOutputStream output) throws IOException { + output.writeTag(AnyValue.ARRAY_VALUE_FIELD_NUMBER, WireFormat.WIRETYPE_LENGTH_DELIMITED); + output.writeUInt32NoTag(valuesSize); + for (T value : values) { + output.writeTag(ArrayValue.VALUES_FIELD_NUMBER, WireFormat.WIRETYPE_LENGTH_DELIMITED); + output.writeUInt32NoTag(getArrayElementSerializedSize(value)); + writeArrayElementTo(value, output); + } + } + + abstract void writeArrayElementTo(T value, CodedOutputStream output) throws IOException; + + abstract int getArrayElementSerializedSize(T value); + + private static int calculateWrapperSize(int valuesSize) { + return CodedOutputStream.computeTagSize(AnyValue.ARRAY_VALUE_FIELD_NUMBER) + + CodedOutputStream.computeUInt32SizeNoTag(valuesSize); + } + } + + private static final class KeyValueArrayStringMarshaler extends KeyValueArrayMarshaler { + private KeyValueArrayStringMarshaler(byte[] key, List values) { + super(key, values, calculateValuesSize(values)); + } + + @Override + void writeArrayElementTo(String value, CodedOutputStream output) throws IOException { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + output.writeString(AnyValue.STRING_VALUE_FIELD_NUMBER, value); + } + + @Override + int getArrayElementSerializedSize(String value) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + return CodedOutputStream.computeStringSize(AnyValue.STRING_VALUE_FIELD_NUMBER, value); + } + + static int calculateValuesSize(List values) { + int size = 0; + int fieldTagSize = CodedOutputStream.computeTagSize(ArrayValue.VALUES_FIELD_NUMBER); + for (String value : values) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + int fieldSize = + CodedOutputStream.computeStringSize(AnyValue.STRING_VALUE_FIELD_NUMBER, value); + size += fieldTagSize + CodedOutputStream.computeUInt32SizeNoTag(fieldSize) + fieldSize; + } + return size; + } + } + + private static final class KeyValueArrayLongMarshaler extends KeyValueArrayMarshaler { + private KeyValueArrayLongMarshaler(byte[] key, List values) { + super(key, values, calculateValuesSize(values)); + } + + @Override + void writeArrayElementTo(Long value, CodedOutputStream output) throws IOException { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + output.writeInt64(AnyValue.INT_VALUE_FIELD_NUMBER, value); + } + + @Override + int getArrayElementSerializedSize(Long value) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + return CodedOutputStream.computeInt64Size(AnyValue.INT_VALUE_FIELD_NUMBER, value); + } + + static int calculateValuesSize(List values) { + int size = 0; + int fieldTagSize = CodedOutputStream.computeTagSize(ArrayValue.VALUES_FIELD_NUMBER); + for (Long value : values) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + int fieldSize = CodedOutputStream.computeInt64Size(AnyValue.INT_VALUE_FIELD_NUMBER, value); + size += fieldTagSize + CodedOutputStream.computeUInt32SizeNoTag(fieldSize) + fieldSize; + } + return size; + } + } + + private static final class KeyValueArrayBooleanMarshaler extends KeyValueArrayMarshaler { + private KeyValueArrayBooleanMarshaler(byte[] key, List values) { + super(key, values, calculateValuesSize(values)); + } + + @Override + void writeArrayElementTo(Boolean value, CodedOutputStream output) throws IOException { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + output.writeBool(AnyValue.BOOL_VALUE_FIELD_NUMBER, value); + } + + @Override + int getArrayElementSerializedSize(Boolean value) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + return CodedOutputStream.computeBoolSize(AnyValue.BOOL_VALUE_FIELD_NUMBER, value); + } + + static int calculateValuesSize(List values) { + int size = 0; + int fieldTagSize = CodedOutputStream.computeTagSize(ArrayValue.VALUES_FIELD_NUMBER); + for (Boolean value : values) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + int fieldSize = CodedOutputStream.computeBoolSize(AnyValue.BOOL_VALUE_FIELD_NUMBER, value); + size += fieldTagSize + CodedOutputStream.computeUInt32SizeNoTag(fieldSize) + fieldSize; + } + return size; + } + } + + private static final class KeyValueArrayDoubleMarshaler extends KeyValueArrayMarshaler { + private KeyValueArrayDoubleMarshaler(byte[] key, List values) { + super(key, values, calculateValuesSize(values)); + } + + @Override + void writeArrayElementTo(Double value, CodedOutputStream output) throws IOException { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + output.writeDouble(AnyValue.DOUBLE_VALUE_FIELD_NUMBER, value); + } + + @Override + int getArrayElementSerializedSize(Double value) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + return CodedOutputStream.computeDoubleSize(AnyValue.DOUBLE_VALUE_FIELD_NUMBER, value); + } + + static int calculateValuesSize(List values) { + int size = 0; + int fieldTagSize = CodedOutputStream.computeTagSize(ArrayValue.VALUES_FIELD_NUMBER); + for (Double value : values) { + // Do not call MarshalUtil because we always have to write the message tag even if the value + // is empty. + int fieldSize = + CodedOutputStream.computeDoubleSize(AnyValue.DOUBLE_VALUE_FIELD_NUMBER, value); + size += fieldTagSize + CodedOutputStream.computeUInt32SizeNoTag(fieldSize) + fieldSize; + } + return size; + } + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/InstrumentationLibraryMarshaler.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/InstrumentationLibraryMarshaler.java new file mode 100644 index 000000000..18dcc2642 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/InstrumentationLibraryMarshaler.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import com.google.protobuf.CodedOutputStream; +import io.opentelemetry.proto.common.v1.InstrumentationLibrary; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import java.io.IOException; + +final class InstrumentationLibraryMarshaler extends MarshalerWithSize { + private final byte[] name; + private final byte[] version; + + static InstrumentationLibraryMarshaler create(InstrumentationLibraryInfo libraryInfo) { + byte[] name = MarshalerUtil.toBytes(libraryInfo.getName()); + byte[] version = MarshalerUtil.toBytes(libraryInfo.getVersion()); + return new InstrumentationLibraryMarshaler(name, version); + } + + private InstrumentationLibraryMarshaler(byte[] name, byte[] version) { + super(computeSize(name, version)); + this.name = name; + this.version = version; + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalBytes(InstrumentationLibrary.NAME_FIELD_NUMBER, name, output); + MarshalerUtil.marshalBytes(InstrumentationLibrary.VERSION_FIELD_NUMBER, version, output); + } + + private static int computeSize(byte[] name, byte[] version) { + return MarshalerUtil.sizeBytes(InstrumentationLibrary.NAME_FIELD_NUMBER, name) + + MarshalerUtil.sizeBytes(InstrumentationLibrary.VERSION_FIELD_NUMBER, version); + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/Marshaler.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/Marshaler.java new file mode 100644 index 000000000..8976ae690 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/Marshaler.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import com.google.protobuf.CodedOutputStream; +import java.io.IOException; + +interface Marshaler { + void writeTo(CodedOutputStream output) throws IOException; + + int getSerializedSize(); +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/MarshalerUtil.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/MarshalerUtil.java new file mode 100644 index 000000000..6c3318454 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/MarshalerUtil.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.WireFormat; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import javax.annotation.Nullable; + +final class MarshalerUtil { + static final byte[] EMPTY_BYTES = new byte[0]; + + static void marshalRepeatedMessage( + int fieldNumber, T[] repeatedMessage, CodedOutputStream output) throws IOException { + for (Marshaler message : repeatedMessage) { + marshalMessage(fieldNumber, message, output); + } + } + + static void marshalRepeatedMessage( + int fieldNumber, List repeatedMessage, CodedOutputStream output) + throws IOException { + for (Marshaler message : repeatedMessage) { + marshalMessage(fieldNumber, message, output); + } + } + + static void marshalMessage(int fieldNumber, Marshaler message, CodedOutputStream output) + throws IOException { + output.writeTag(fieldNumber, WireFormat.WIRETYPE_LENGTH_DELIMITED); + output.writeUInt32NoTag(message.getSerializedSize()); + message.writeTo(output); + } + + static void marshalUInt32(int fieldNumber, int message, CodedOutputStream output) + throws IOException { + if (message == 0) { + return; + } + output.writeUInt32(fieldNumber, message); + } + + static void marshalFixed64(int fieldNumber, long message, CodedOutputStream output) + throws IOException { + if (message == 0L) { + return; + } + output.writeFixed64(fieldNumber, message); + } + + static void marshalBytes(int fieldNumber, byte[] message, CodedOutputStream output) + throws IOException { + if (message.length == 0) { + return; + } + output.writeByteArray(fieldNumber, message); + } + + static int sizeRepeatedMessage(int fieldNumber, T[] repeatedMessage) { + int size = 0; + int fieldTagSize = CodedOutputStream.computeTagSize(fieldNumber); + for (Marshaler message : repeatedMessage) { + int fieldSize = message.getSerializedSize(); + size += fieldTagSize + CodedOutputStream.computeUInt32SizeNoTag(fieldSize) + fieldSize; + } + return size; + } + + static int sizeRepeatedMessage(int fieldNumber, List repeatedMessage) { + int size = 0; + int fieldTagSize = CodedOutputStream.computeTagSize(fieldNumber); + for (Marshaler message : repeatedMessage) { + int fieldSize = message.getSerializedSize(); + size += fieldTagSize + CodedOutputStream.computeUInt32SizeNoTag(fieldSize) + fieldSize; + } + return size; + } + + static int sizeMessage(int fieldNumber, Marshaler message) { + int fieldSize = message.getSerializedSize(); + return CodedOutputStream.computeTagSize(fieldNumber) + + CodedOutputStream.computeUInt32SizeNoTag(fieldSize) + + fieldSize; + } + + static int sizeUInt32(int fieldNumber, int message) { + if (message == 0) { + return 0; + } + return CodedOutputStream.computeUInt32Size(fieldNumber, message); + } + + static int sizeFixed64(int fieldNumber, long message) { + if (message == 0L) { + return 0; + } + return CodedOutputStream.computeFixed64Size(fieldNumber, message); + } + + static int sizeBytes(int fieldNumber, byte[] message) { + if (message.length == 0) { + return 0; + } + return CodedOutputStream.computeByteArraySize(fieldNumber, message); + } + + static byte[] toBytes(@Nullable String value) { + if (value == null || value.isEmpty()) { + return EMPTY_BYTES; + } + return value.getBytes(StandardCharsets.UTF_8); + } + + private MarshalerUtil() {} +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/MarshalerWithSize.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/MarshalerWithSize.java new file mode 100644 index 000000000..12b71f260 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/MarshalerWithSize.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +abstract class MarshalerWithSize implements Marshaler { + private final int size; + + protected MarshalerWithSize(int size) { + this.size = size; + } + + @Override + public final int getSerializedSize() { + return size; + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporter.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporter.java new file mode 100644 index 000000000..d3f065f00 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporter.java @@ -0,0 +1,195 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.ConnectivityState; +import io.grpc.ManagedChannel; +import io.grpc.Status; +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.exporter.otlp.internal.SpanAdapter; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc.TraceServiceFutureStub; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** Exports spans using OTLP via gRPC, using OpenTelemetry's protobuf model. */ +@ThreadSafe +public final class OtlpGrpcSpanExporter implements SpanExporter { + + private static final String EXPORTER_NAME = OtlpGrpcSpanExporter.class.getSimpleName(); + private static final Labels EXPORTER_NAME_LABELS = Labels.of("exporter", EXPORTER_NAME); + private static final Labels EXPORT_SUCCESS_LABELS = + Labels.of("exporter", EXPORTER_NAME, "success", "true"); + private static final Labels EXPORT_FAILURE_LABELS = + Labels.of("exporter", EXPORTER_NAME, "success", "false"); + + private final ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(OtlpGrpcSpanExporter.class.getName())); + + private final TraceServiceFutureStub traceService; + + private final ManagedChannel managedChannel; + private final long timeoutNanos; + private final BoundLongCounter spansSeen; + private final BoundLongCounter spansExportedSuccess; + private final BoundLongCounter spansExportedFailure; + + /** + * Creates a new OTLP gRPC Span Reporter with the given name, using the given channel. + * + * @param channel the channel to use when communicating with the OpenTelemetry Collector. + * @param timeoutNanos max waiting time for the collector to process each span batch. When set to + * 0 or to a negative value, the exporter will wait indefinitely. + */ + OtlpGrpcSpanExporter(ManagedChannel channel, long timeoutNanos) { + Meter meter = GlobalMeterProvider.getMeter("io.opentelemetry.exporters.otlp"); + this.spansSeen = + meter.longCounterBuilder("spansSeenByExporter").build().bind(EXPORTER_NAME_LABELS); + LongCounter spansExportedCounter = meter.longCounterBuilder("spansExportedByExporter").build(); + this.spansExportedSuccess = spansExportedCounter.bind(EXPORT_SUCCESS_LABELS); + this.spansExportedFailure = spansExportedCounter.bind(EXPORT_FAILURE_LABELS); + this.managedChannel = channel; + this.timeoutNanos = timeoutNanos; + + this.traceService = TraceServiceGrpc.newFutureStub(channel); + } + + /** + * Submits all the given spans in a single batch to the OpenTelemetry collector. + * + * @param spans the list of sampled Spans to be exported. + * @return the result of the operation + */ + @Override + public CompletableResultCode export(Collection spans) { + spansSeen.add(spans.size()); + ExportTraceServiceRequest exportTraceServiceRequest = + ExportTraceServiceRequest.newBuilder() + .addAllResourceSpans(SpanAdapter.toProtoResourceSpans(spans)) + .build(); + + final CompletableResultCode result = new CompletableResultCode(); + + TraceServiceFutureStub exporter; + if (timeoutNanos > 0) { + exporter = traceService.withDeadlineAfter(timeoutNanos, TimeUnit.NANOSECONDS); + } else { + exporter = traceService; + } + + Futures.addCallback( + exporter.export(exportTraceServiceRequest), + new FutureCallback() { + @Override + public void onSuccess(@Nullable ExportTraceServiceResponse response) { + spansExportedSuccess.add(spans.size()); + result.succeed(); + } + + @Override + public void onFailure(Throwable t) { + spansExportedFailure.add(spans.size()); + Status status = Status.fromThrowable(t); + switch (status.getCode()) { + case UNIMPLEMENTED: + logger.log( + Level.SEVERE, + "Failed to export spans. Server responded with UNIMPLEMENTED. " + + "This usually means that your collector is not configured with an otlp " + + "receiver in the \"pipelines\" section of the configuration. " + + "Full error message: " + + t.getMessage()); + break; + case UNAVAILABLE: + logger.log( + Level.SEVERE, + "Failed to export spans. Server is UNAVAILABLE. " + + "Make sure your collector is running and reachable from this network. " + + "Full error message:" + + t.getMessage()); + break; + default: + logger.log( + Level.WARNING, "Failed to export spans. Error message: " + t.getMessage()); + break; + } + if (logger.isLoggable(Level.FINEST)) { + logger.log(Level.FINEST, "Failed to export spans. Details follow: " + t); + } + result.fail(); + } + }, + MoreExecutors.directExecutor()); + return result; + } + + /** + * The OTLP exporter does not batch spans, so this method will immediately return with success. + * + * @return always Success + */ + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + /** + * Returns a new builder instance for this exporter. + * + * @return a new builder instance for this exporter. + */ + public static OtlpGrpcSpanExporterBuilder builder() { + return new OtlpGrpcSpanExporterBuilder(); + } + + /** + * Returns a new {@link OtlpGrpcSpanExporter} reading the configuration values from the + * environment and from system properties. System properties override values defined in the + * environment. If a configuration value is missing, it uses the default value. + * + * @return a new {@link OtlpGrpcSpanExporter} instance. + */ + public static OtlpGrpcSpanExporter getDefault() { + return builder().build(); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately + * cancelled. + */ + @Override + public CompletableResultCode shutdown() { + final CompletableResultCode result = new CompletableResultCode(); + managedChannel.notifyWhenStateChanged(ConnectivityState.SHUTDOWN, result::succeed); + managedChannel.shutdown(); + this.spansSeen.unbind(); + this.spansExportedSuccess.unbind(); + this.spansExportedFailure.unbind(); + return result; + } + + // Visible for testing + long getTimeoutNanos() { + return timeoutNanos; + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporterBuilder.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporterBuilder.java new file mode 100644 index 000000000..aab2fc944 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporterBuilder.java @@ -0,0 +1,192 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.opentelemetry.api.internal.Utils.checkArgument; +import static java.util.Objects.requireNonNull; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.stub.MetadataUtils; +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import javax.net.ssl.SSLException; + +/** Builder utility for this exporter. */ +public final class OtlpGrpcSpanExporterBuilder { + + private static final String DEFAULT_ENDPOINT_URL = "http://localhost:4317"; + private static final URI DEFAULT_ENDPOINT = URI.create(DEFAULT_ENDPOINT_URL); + private static final long DEFAULT_TIMEOUT_SECS = 10; + + private ManagedChannel channel; + private long timeoutNanos = TimeUnit.SECONDS.toNanos(DEFAULT_TIMEOUT_SECS); + private URI endpoint = DEFAULT_ENDPOINT; + @Nullable private Metadata metadata; + @Nullable private byte[] trustedCertificatesPem; + + /** + * Sets the managed chanel to use when communicating with the backend. Takes precedence over + * {@link #setEndpoint(String)} if both are called. + * + * @param channel the channel to use + * @return this builder's instance + */ + public OtlpGrpcSpanExporterBuilder setChannel(ManagedChannel channel) { + this.channel = channel; + return this; + } + + /** + * Sets the maximum time to wait for the collector to process an exported batch of spans. If + * unset, defaults to {@value DEFAULT_TIMEOUT_SECS}s. + */ + public OtlpGrpcSpanExporterBuilder setTimeout(long timeout, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(timeout >= 0, "timeout must be non-negative"); + timeoutNanos = unit.toNanos(timeout); + return this; + } + + /** + * Sets the maximum time to wait for the collector to process an exported batch of spans. If + * unset, defaults to {@value DEFAULT_TIMEOUT_SECS}s. + */ + public OtlpGrpcSpanExporterBuilder setTimeout(Duration timeout) { + requireNonNull(timeout, "timeout"); + return setTimeout(timeout.toNanos(), TimeUnit.NANOSECONDS); + } + + /** + * Sets the OTLP endpoint to connect to. If unset, defaults to {@value DEFAULT_ENDPOINT_URL}. The + * endpoint must start with either http:// or https://. + */ + public OtlpGrpcSpanExporterBuilder setEndpoint(String endpoint) { + requireNonNull(endpoint, "endpoint"); + + URI uri; + try { + uri = new URI(endpoint); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid endpoint, must be a URL: " + endpoint, e); + } + + if (uri.getScheme() == null + || (!uri.getScheme().equals("http") && !uri.getScheme().equals("https"))) { + throw new IllegalArgumentException( + "Invalid endpoint, must start with http:// or https://: " + uri); + } + + this.endpoint = uri; + return this; + } + + /** + * Sets the certificate chain to use for verifying servers when TLS is enabled. The {@code byte[]} + * should contain an X.509 certificate collection in PEM format. If not set, TLS connections will + * use the system default trusted certificates. + */ + public OtlpGrpcSpanExporterBuilder setTrustedCertificates(byte[] trustedCertificatesPem) { + this.trustedCertificatesPem = trustedCertificatesPem; + return this; + } + + /** + * Add header to request. Optional. Applicable only if {@link + * OtlpGrpcSpanExporterBuilder#endpoint} is set to build channel. + * + * @param key header key + * @param value header value + * @return this builder's instance + */ + public OtlpGrpcSpanExporterBuilder addHeader(String key, String value) { + if (metadata == null) { + metadata = new Metadata(); + } + metadata.put(Metadata.Key.of(key, ASCII_STRING_MARSHALLER), value); + return this; + } + + /** + * Constructs a new instance of the exporter based on the builder's values. + * + * @return a new exporter's instance + */ + public OtlpGrpcSpanExporter build() { + if (channel == null) { + final ManagedChannelBuilder managedChannelBuilder = + ManagedChannelBuilder.forTarget(endpoint.getAuthority()); + + if (endpoint.getScheme().equals("https")) { + managedChannelBuilder.useTransportSecurity(); + } else { + managedChannelBuilder.usePlaintext(); + } + + if (metadata != null) { + managedChannelBuilder.intercept(MetadataUtils.newAttachHeadersInterceptor(metadata)); + } + + if (trustedCertificatesPem != null) { + // gRPC does not abstract TLS configuration so we need to check the implementation and act + // accordingly. + if (managedChannelBuilder + .getClass() + .getName() + .equals("io.grpc.netty.NettyChannelBuilder")) { + NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) managedChannelBuilder; + try { + nettyBuilder.sslContext( + GrpcSslContexts.forClient() + .trustManager(new ByteArrayInputStream(trustedCertificatesPem)) + .build()); + } catch (IllegalArgumentException | SSLException e) { + throw new IllegalStateException( + "Could not set trusted certificates for gRPC TLS connection, are they valid " + + "X.509 in PEM format?", + e); + } + } else if (managedChannelBuilder + .getClass() + .getName() + .equals("io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder")) { + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder nettyBuilder = + (io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder) managedChannelBuilder; + try { + nettyBuilder.sslContext( + io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts.forClient() + .trustManager(new ByteArrayInputStream(trustedCertificatesPem)) + .build()); + } catch (IllegalArgumentException | SSLException e) { + throw new IllegalStateException( + "Could not set trusted certificates for gRPC TLS connection, are they valid " + + "X.509 in PEM format?", + e); + } + } else { + throw new IllegalStateException( + "TLS cerificate configuration only supported with Netty. " + + "If you need to configure a certificate, switch to grpc-netty or " + + "grpc-netty-shaded."); + } + // TODO(anuraaga): Support okhttp. + } + + channel = managedChannelBuilder.build(); + } + return new OtlpGrpcSpanExporter(channel, timeoutNanos); + } + + OtlpGrpcSpanExporterBuilder() {} +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/ResourceMarshaler.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/ResourceMarshaler.java new file mode 100644 index 000000000..8328aa4ea --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/ResourceMarshaler.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import com.google.protobuf.CodedOutputStream; +import io.opentelemetry.proto.resource.v1.Resource; +import java.io.IOException; + +final class ResourceMarshaler extends MarshalerWithSize { + private final AttributeMarshaler[] attributeMarshalers; + + static ResourceMarshaler create(io.opentelemetry.sdk.resources.Resource resource) { + return new ResourceMarshaler(AttributeMarshaler.createRepeated(resource.getAttributes())); + } + + private ResourceMarshaler(AttributeMarshaler[] attributeMarshalers) { + super(calculateSize(attributeMarshalers)); + this.attributeMarshalers = attributeMarshalers; + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalRepeatedMessage( + Resource.ATTRIBUTES_FIELD_NUMBER, attributeMarshalers, output); + } + + private static int calculateSize(AttributeMarshaler[] attributeMarshalers) { + return MarshalerUtil.sizeRepeatedMessage(Resource.ATTRIBUTES_FIELD_NUMBER, attributeMarshalers); + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/TraceMarshaler.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/TraceMarshaler.java new file mode 100644 index 000000000..03aa904c0 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/TraceMarshaler.java @@ -0,0 +1,564 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CONSUMER; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_INTERNAL; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_PRODUCER; +import static io.opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_SERVER; +import static io.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode.DEPRECATED_STATUS_CODE_OK; +import static io.opentelemetry.proto.trace.v1.Status.DeprecatedStatusCode.DEPRECATED_STATUS_CODE_UNKNOWN_ERROR; + +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.UnknownFieldSet; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.Span; +import io.opentelemetry.proto.trace.v1.Status; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class TraceMarshaler { + + static final class RequestMarshaler extends MarshalerWithSize { + private final ResourceSpansMarshaler[] resourceSpansMarshalers; + + static RequestMarshaler create(Collection spanDataList) { + Map>> resourceAndLibraryMap = + TraceMarshaler.groupByResourceAndLibrary(spanDataList); + + final ResourceSpansMarshaler[] resourceSpansMarshalers = + new ResourceSpansMarshaler[resourceAndLibraryMap.size()]; + int posResource = 0; + for (Map.Entry>> entry : + resourceAndLibraryMap.entrySet()) { + final InstrumentationLibrarySpansMarshaler[] instrumentationLibrarySpansMarshalers = + new InstrumentationLibrarySpansMarshaler[entry.getValue().size()]; + int posInstrumentation = 0; + for (Map.Entry> entryIs : + entry.getValue().entrySet()) { + instrumentationLibrarySpansMarshalers[posInstrumentation++] = + new InstrumentationLibrarySpansMarshaler( + InstrumentationLibraryMarshaler.create(entryIs.getKey()), entryIs.getValue()); + } + resourceSpansMarshalers[posResource++] = + new ResourceSpansMarshaler( + ResourceMarshaler.create(entry.getKey()), instrumentationLibrarySpansMarshalers); + } + + return new RequestMarshaler(resourceSpansMarshalers); + } + + private RequestMarshaler(ResourceSpansMarshaler[] resourceSpansMarshalers) { + super( + MarshalerUtil.sizeRepeatedMessage( + ExportTraceServiceRequest.RESOURCE_SPANS_FIELD_NUMBER, resourceSpansMarshalers)); + this.resourceSpansMarshalers = resourceSpansMarshalers; + } + + ExportTraceServiceRequest toRequest() throws IOException { + byte[] buf = new byte[getSerializedSize()]; + writeTo(CodedOutputStream.newInstance(buf)); + return ExportTraceServiceRequest.newBuilder() + .setUnknownFields(UnknownFieldSet.newBuilder().mergeFrom(buf).build()) + .build(); + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalRepeatedMessage( + ExportTraceServiceRequest.RESOURCE_SPANS_FIELD_NUMBER, resourceSpansMarshalers, output); + } + } + + private static final class ResourceSpansMarshaler extends MarshalerWithSize { + private final ResourceMarshaler resourceMarshaler; + private final InstrumentationLibrarySpansMarshaler[] instrumentationLibrarySpansMarshalers; + + private ResourceSpansMarshaler( + ResourceMarshaler resourceMarshaler, + InstrumentationLibrarySpansMarshaler[] instrumentationLibrarySpansMarshalers) { + super(calculateSize(resourceMarshaler, instrumentationLibrarySpansMarshalers)); + this.resourceMarshaler = resourceMarshaler; + this.instrumentationLibrarySpansMarshalers = instrumentationLibrarySpansMarshalers; + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalMessage(ResourceSpans.RESOURCE_FIELD_NUMBER, resourceMarshaler, output); + MarshalerUtil.marshalRepeatedMessage( + ResourceSpans.INSTRUMENTATION_LIBRARY_SPANS_FIELD_NUMBER, + instrumentationLibrarySpansMarshalers, + output); + } + + private static int calculateSize( + ResourceMarshaler resourceMarshaler, + InstrumentationLibrarySpansMarshaler[] instrumentationLibrarySpansMarshalers) { + int size = 0; + size += MarshalerUtil.sizeMessage(ResourceSpans.RESOURCE_FIELD_NUMBER, resourceMarshaler); + size += + MarshalerUtil.sizeRepeatedMessage( + ResourceSpans.INSTRUMENTATION_LIBRARY_SPANS_FIELD_NUMBER, + instrumentationLibrarySpansMarshalers); + return size; + } + } + + private static final class InstrumentationLibrarySpansMarshaler extends MarshalerWithSize { + private final InstrumentationLibraryMarshaler instrumentationLibrary; + private final List spanMarshalers; + + private InstrumentationLibrarySpansMarshaler( + InstrumentationLibraryMarshaler instrumentationLibrary, + List spanMarshalers) { + super(calculateSize(instrumentationLibrary, spanMarshalers)); + this.instrumentationLibrary = instrumentationLibrary; + this.spanMarshalers = spanMarshalers; + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalMessage( + InstrumentationLibrarySpans.INSTRUMENTATION_LIBRARY_FIELD_NUMBER, + instrumentationLibrary, + output); + MarshalerUtil.marshalRepeatedMessage( + InstrumentationLibrarySpans.SPANS_FIELD_NUMBER, spanMarshalers, output); + } + + private static int calculateSize( + InstrumentationLibraryMarshaler instrumentationLibrary, + List spanMarshalers) { + int size = 0; + size += + MarshalerUtil.sizeMessage( + InstrumentationLibrarySpans.INSTRUMENTATION_LIBRARY_FIELD_NUMBER, + instrumentationLibrary); + size += + MarshalerUtil.sizeRepeatedMessage( + InstrumentationLibrarySpans.SPANS_FIELD_NUMBER, spanMarshalers); + return size; + } + } + + private static final class SpanMarshaler extends MarshalerWithSize { + private final byte[] traceId; + private final byte[] spanId; + private final byte[] parentSpanId; + private final byte[] name; + private final int spanKind; + private final long startEpochNanos; + private final long endEpochNanos; + private final AttributeMarshaler[] attributeMarshalers; + private final int droppedAttributesCount; + private final SpanEventMarshaler[] spanEventMarshalers; + private final int droppedEventsCount; + private final SpanLinkMarshaler[] spanLinkMarshalers; + private final int droppedLinksCount; + private final SpanStatusMarshaler spanStatusMarshaler; + + // Because SpanMarshaler is always part of a repeated field, it cannot return "null". + private static SpanMarshaler create(SpanData spanData) { + AttributeMarshaler[] attributeMarshalers = + AttributeMarshaler.createRepeated(spanData.getAttributes()); + SpanEventMarshaler[] spanEventMarshalers = SpanEventMarshaler.create(spanData.getEvents()); + SpanLinkMarshaler[] spanLinkMarshalers = SpanLinkMarshaler.create(spanData.getLinks()); + + byte[] parentSpanId = MarshalerUtil.EMPTY_BYTES; + SpanContext parentSpanContext = spanData.getParentSpanContext(); + if (parentSpanContext.isValid()) { + parentSpanId = parentSpanContext.getSpanIdBytes(); + } + + return new SpanMarshaler( + spanData.getSpanContext().getTraceIdBytes(), + spanData.getSpanContext().getSpanIdBytes(), + parentSpanId, + MarshalerUtil.toBytes(spanData.getName()), + toProtoSpanKind(spanData.getKind()).getNumber(), + spanData.getStartEpochNanos(), + spanData.getEndEpochNanos(), + attributeMarshalers, + spanData.getTotalAttributeCount() - spanData.getAttributes().size(), + spanEventMarshalers, + spanData.getTotalRecordedEvents() - spanData.getEvents().size(), + spanLinkMarshalers, + spanData.getTotalRecordedLinks() - spanData.getLinks().size(), + SpanStatusMarshaler.create(spanData.getStatus())); + } + + private SpanMarshaler( + byte[] traceId, + byte[] spanId, + byte[] parentSpanId, + byte[] name, + int spanKind, + long startEpochNanos, + long endEpochNanos, + AttributeMarshaler[] attributeMarshalers, + int droppedAttributesCount, + SpanEventMarshaler[] spanEventMarshalers, + int droppedEventsCount, + SpanLinkMarshaler[] spanLinkMarshalers, + int droppedLinksCount, + SpanStatusMarshaler spanStatusMarshaler) { + super( + calculateSize( + traceId, + spanId, + parentSpanId, + name, + spanKind, + startEpochNanos, + endEpochNanos, + attributeMarshalers, + droppedAttributesCount, + spanEventMarshalers, + droppedEventsCount, + spanLinkMarshalers, + droppedLinksCount, + spanStatusMarshaler)); + this.traceId = traceId; + this.spanId = spanId; + this.parentSpanId = parentSpanId; + this.name = name; + this.spanKind = spanKind; + this.startEpochNanos = startEpochNanos; + this.endEpochNanos = endEpochNanos; + this.attributeMarshalers = attributeMarshalers; + this.droppedAttributesCount = droppedAttributesCount; + this.spanEventMarshalers = spanEventMarshalers; + this.droppedEventsCount = droppedEventsCount; + this.spanLinkMarshalers = spanLinkMarshalers; + this.droppedLinksCount = droppedLinksCount; + this.spanStatusMarshaler = spanStatusMarshaler; + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalBytes(Span.TRACE_ID_FIELD_NUMBER, traceId, output); + MarshalerUtil.marshalBytes(Span.SPAN_ID_FIELD_NUMBER, spanId, output); + // TODO: Set TraceState; + MarshalerUtil.marshalBytes(Span.PARENT_SPAN_ID_FIELD_NUMBER, parentSpanId, output); + MarshalerUtil.marshalBytes(Span.NAME_FIELD_NUMBER, name, output); + + // TODO: Make this a MarshalerUtil helper. + output.writeEnum(Span.KIND_FIELD_NUMBER, spanKind); + + MarshalerUtil.marshalFixed64(Span.START_TIME_UNIX_NANO_FIELD_NUMBER, startEpochNanos, output); + MarshalerUtil.marshalFixed64(Span.END_TIME_UNIX_NANO_FIELD_NUMBER, endEpochNanos, output); + + MarshalerUtil.marshalRepeatedMessage( + Span.ATTRIBUTES_FIELD_NUMBER, attributeMarshalers, output); + MarshalerUtil.marshalUInt32( + Span.DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER, droppedAttributesCount, output); + + MarshalerUtil.marshalRepeatedMessage(Span.EVENTS_FIELD_NUMBER, spanEventMarshalers, output); + MarshalerUtil.marshalUInt32( + Span.DROPPED_EVENTS_COUNT_FIELD_NUMBER, droppedEventsCount, output); + + MarshalerUtil.marshalRepeatedMessage(Span.LINKS_FIELD_NUMBER, spanLinkMarshalers, output); + MarshalerUtil.marshalUInt32(Span.DROPPED_LINKS_COUNT_FIELD_NUMBER, droppedLinksCount, output); + + MarshalerUtil.marshalMessage(Span.STATUS_FIELD_NUMBER, spanStatusMarshaler, output); + } + + private static int calculateSize( + byte[] traceId, + byte[] spanId, + byte[] parentSpanId, + byte[] name, + int spanKind, + long startEpochNanos, + long endEpochNanos, + AttributeMarshaler[] attributeMarshalers, + int droppedAttributesCount, + SpanEventMarshaler[] spanEventMarshalers, + int droppedEventsCount, + SpanLinkMarshaler[] spanLinkMarshalers, + int droppedLinksCount, + SpanStatusMarshaler spanStatusMarshaler) { + int size = 0; + size += MarshalerUtil.sizeBytes(Span.TRACE_ID_FIELD_NUMBER, traceId); + size += MarshalerUtil.sizeBytes(Span.SPAN_ID_FIELD_NUMBER, spanId); + // TODO: Set TraceState; + size += MarshalerUtil.sizeBytes(Span.PARENT_SPAN_ID_FIELD_NUMBER, parentSpanId); + size += MarshalerUtil.sizeBytes(Span.NAME_FIELD_NUMBER, name); + + // TODO: Make this a MarshalerUtil helper. + size += CodedOutputStream.computeEnumSize(Span.KIND_FIELD_NUMBER, spanKind); + + size += MarshalerUtil.sizeFixed64(Span.START_TIME_UNIX_NANO_FIELD_NUMBER, startEpochNanos); + size += MarshalerUtil.sizeFixed64(Span.END_TIME_UNIX_NANO_FIELD_NUMBER, endEpochNanos); + + size += MarshalerUtil.sizeRepeatedMessage(Span.ATTRIBUTES_FIELD_NUMBER, attributeMarshalers); + size += + MarshalerUtil.sizeUInt32( + Span.DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER, droppedAttributesCount); + + size += MarshalerUtil.sizeRepeatedMessage(Span.EVENTS_FIELD_NUMBER, spanEventMarshalers); + size += MarshalerUtil.sizeUInt32(Span.DROPPED_EVENTS_COUNT_FIELD_NUMBER, droppedEventsCount); + + size += MarshalerUtil.sizeRepeatedMessage(Span.LINKS_FIELD_NUMBER, spanLinkMarshalers); + size += MarshalerUtil.sizeUInt32(Span.DROPPED_LINKS_COUNT_FIELD_NUMBER, droppedLinksCount); + + size += MarshalerUtil.sizeMessage(Span.STATUS_FIELD_NUMBER, spanStatusMarshaler); + return size; + } + } + + private static final class SpanEventMarshaler extends MarshalerWithSize { + private static final SpanEventMarshaler[] EMPTY = new SpanEventMarshaler[0]; + private final long epochNanos; + private final byte[] name; + private final AttributeMarshaler[] attributeMarshalers; + private final int droppedAttributesCount; + + private static SpanEventMarshaler[] create(List events) { + if (events.isEmpty()) { + return EMPTY; + } + + SpanEventMarshaler[] result = new SpanEventMarshaler[events.size()]; + int pos = 0; + for (EventData event : events) { + result[pos++] = + new SpanEventMarshaler( + event.getEpochNanos(), + MarshalerUtil.toBytes(event.getName()), + AttributeMarshaler.createRepeated(event.getAttributes()), + event.getTotalAttributeCount() - event.getAttributes().size()); + } + + return result; + } + + private SpanEventMarshaler( + long epochNanos, + byte[] name, + AttributeMarshaler[] attributeMarshalers, + int droppedAttributesCount) { + super(calculateSize(epochNanos, name, attributeMarshalers, droppedAttributesCount)); + this.epochNanos = epochNanos; + this.name = name; + this.attributeMarshalers = attributeMarshalers; + this.droppedAttributesCount = droppedAttributesCount; + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalFixed64(Span.Event.TIME_UNIX_NANO_FIELD_NUMBER, epochNanos, output); + MarshalerUtil.marshalBytes(Span.Event.NAME_FIELD_NUMBER, name, output); + MarshalerUtil.marshalRepeatedMessage( + Span.Event.ATTRIBUTES_FIELD_NUMBER, attributeMarshalers, output); + MarshalerUtil.marshalUInt32( + Span.Event.DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER, droppedAttributesCount, output); + } + + private static int calculateSize( + long epochNanos, + byte[] name, + AttributeMarshaler[] attributeMarshalers, + int droppedAttributesCount) { + int size = 0; + size += MarshalerUtil.sizeFixed64(Span.Event.TIME_UNIX_NANO_FIELD_NUMBER, epochNanos); + size += MarshalerUtil.sizeBytes(Span.Event.NAME_FIELD_NUMBER, name); + size += + MarshalerUtil.sizeRepeatedMessage( + Span.Event.ATTRIBUTES_FIELD_NUMBER, attributeMarshalers); + size += + MarshalerUtil.sizeUInt32( + Span.Event.DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER, droppedAttributesCount); + return size; + } + } + + private static final class SpanLinkMarshaler extends MarshalerWithSize { + private static final SpanLinkMarshaler[] EMPTY = new SpanLinkMarshaler[0]; + private final byte[] traceId; + private final byte[] spanId; + private final AttributeMarshaler[] attributeMarshalers; + private final int droppedAttributesCount; + + private static SpanLinkMarshaler[] create(List links) { + if (links.isEmpty()) { + return EMPTY; + } + + SpanLinkMarshaler[] result = new SpanLinkMarshaler[links.size()]; + int pos = 0; + for (LinkData link : links) { + result[pos++] = + new SpanLinkMarshaler( + link.getSpanContext().getTraceIdBytes(), + link.getSpanContext().getSpanIdBytes(), + AttributeMarshaler.createRepeated(link.getAttributes()), + link.getTotalAttributeCount() - link.getAttributes().size()); + } + + return result; + } + + private SpanLinkMarshaler( + byte[] traceId, + byte[] spanId, + AttributeMarshaler[] attributeMarshalers, + int droppedAttributesCount) { + super(calculateSize(traceId, spanId, attributeMarshalers, droppedAttributesCount)); + this.traceId = traceId; + this.spanId = spanId; + this.attributeMarshalers = attributeMarshalers; + this.droppedAttributesCount = droppedAttributesCount; + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + MarshalerUtil.marshalBytes(Span.Link.TRACE_ID_FIELD_NUMBER, traceId, output); + MarshalerUtil.marshalBytes(Span.Link.SPAN_ID_FIELD_NUMBER, spanId, output); + // TODO: Set TraceState; + MarshalerUtil.marshalRepeatedMessage( + Span.Link.ATTRIBUTES_FIELD_NUMBER, attributeMarshalers, output); + MarshalerUtil.marshalUInt32( + Span.Link.DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER, droppedAttributesCount, output); + } + + private static int calculateSize( + byte[] traceId, + byte[] spanId, + AttributeMarshaler[] attributeMarshalers, + int droppedAttributesCount) { + int size = 0; + size += MarshalerUtil.sizeBytes(Span.Link.TRACE_ID_FIELD_NUMBER, traceId); + size += MarshalerUtil.sizeBytes(Span.Link.SPAN_ID_FIELD_NUMBER, spanId); + // TODO: Set TraceState; + size += + MarshalerUtil.sizeRepeatedMessage(Span.Link.ATTRIBUTES_FIELD_NUMBER, attributeMarshalers); + size += + MarshalerUtil.sizeUInt32( + Span.Link.DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER, droppedAttributesCount); + return size; + } + } + + private static final class SpanStatusMarshaler extends MarshalerWithSize { + private final Status.StatusCode protoStatusCode; + private final Status.DeprecatedStatusCode deprecatedStatusCode; + private final byte[] description; + + static SpanStatusMarshaler create(StatusData status) { + Status.StatusCode protoStatusCode = Status.StatusCode.STATUS_CODE_UNSET; + Status.DeprecatedStatusCode deprecatedStatusCode = DEPRECATED_STATUS_CODE_OK; + if (status.getStatusCode() == StatusCode.OK) { + protoStatusCode = Status.StatusCode.STATUS_CODE_OK; + } else if (status.getStatusCode() == StatusCode.ERROR) { + protoStatusCode = Status.StatusCode.STATUS_CODE_ERROR; + deprecatedStatusCode = DEPRECATED_STATUS_CODE_UNKNOWN_ERROR; + } + byte[] description = MarshalerUtil.toBytes(status.getDescription()); + return new SpanStatusMarshaler(protoStatusCode, deprecatedStatusCode, description); + } + + private SpanStatusMarshaler( + Status.StatusCode protoStatusCode, + Status.DeprecatedStatusCode deprecatedStatusCode, + byte[] description) { + super(computeSize(protoStatusCode, deprecatedStatusCode, description)); + this.protoStatusCode = protoStatusCode; + this.deprecatedStatusCode = deprecatedStatusCode; + this.description = description; + } + + @Override + public void writeTo(CodedOutputStream output) throws IOException { + // TODO: Make this a MarshalerUtil helper. + if (deprecatedStatusCode != DEPRECATED_STATUS_CODE_OK) { + output.writeEnum(Status.DEPRECATED_CODE_FIELD_NUMBER, deprecatedStatusCode.getNumber()); + } + MarshalerUtil.marshalBytes(Status.MESSAGE_FIELD_NUMBER, description, output); + // TODO: Make this a MarshalerUtil helper. + if (protoStatusCode != Status.StatusCode.STATUS_CODE_UNSET) { + output.writeEnum(Status.CODE_FIELD_NUMBER, protoStatusCode.getNumber()); + } + } + + private static int computeSize( + Status.StatusCode protoStatusCode, + Status.DeprecatedStatusCode deprecatedStatusCode, + byte[] description) { + int size = 0; + // TODO: Make this a MarshalerUtil helper. + if (deprecatedStatusCode != DEPRECATED_STATUS_CODE_OK) { + size += + CodedOutputStream.computeEnumSize( + Status.DEPRECATED_CODE_FIELD_NUMBER, deprecatedStatusCode.getNumber()); + } + size += MarshalerUtil.sizeBytes(Status.MESSAGE_FIELD_NUMBER, description); + // TODO: Make this a MarshalerUtil helper. + if (protoStatusCode != Status.StatusCode.STATUS_CODE_UNSET) { + size += + CodedOutputStream.computeEnumSize( + Status.CODE_FIELD_NUMBER, protoStatusCode.getNumber()); + } + return size; + } + } + + private static Map>> + groupByResourceAndLibrary(Collection spanDataList) { + Map>> result = new HashMap<>(); + for (SpanData spanData : spanDataList) { + Resource resource = spanData.getResource(); + Map> libraryInfoListMap = + result.get(spanData.getResource()); + if (libraryInfoListMap == null) { + libraryInfoListMap = new HashMap<>(); + result.put(resource, libraryInfoListMap); + } + List spanList = + libraryInfoListMap.get(spanData.getInstrumentationLibraryInfo()); + if (spanList == null) { + spanList = new ArrayList<>(); + libraryInfoListMap.put(spanData.getInstrumentationLibraryInfo(), spanList); + } + spanList.add(SpanMarshaler.create(spanData)); + } + return result; + } + + private static Span.SpanKind toProtoSpanKind(SpanKind kind) { + switch (kind) { + case INTERNAL: + return SPAN_KIND_INTERNAL; + case SERVER: + return SPAN_KIND_SERVER; + case CLIENT: + return SPAN_KIND_CLIENT; + case PRODUCER: + return SPAN_KIND_PRODUCER; + case CONSUMER: + return SPAN_KIND_CONSUMER; + } + return Span.SpanKind.UNRECOGNIZED; + } + + private TraceMarshaler() {} +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/package-info.java b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/package-info.java new file mode 100644 index 000000000..37bb15613 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/main/java/io/opentelemetry/exporter/otlp/trace/package-info.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * OpenTelemetry exporter which sends span data to OpenTelemetry collector via gRPC. + * + *

    Configuration options for {@link io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter} + * can be read from system properties, environment variables, or {@link java.util.Properties} + * objects. + * + *

    For system properties and {@link java.util.Properties} objects, {@link + * io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter} will look for the following names: + * + *

      + *
    • {@code otel.exporter.otlp.span.timeout}: to set the max waiting time allowed to send each + * span batch. + *
    • {@code otel.exporter.otlp.span.endpoint}: to set the endpoint to connect to. + *
    • {@code otel.exporter.otlp.span.insecure}: whether to enable client transport security for + * the connection. + *
    • {@code otel.exporter.otlp.span.headers}: the headers associated with the requests. + *
    + * + *

    For environment variables, {@link io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter} + * will look for the following names: + * + *

      + *
    • {@code OTEL_EXPORTER_OTLP_SPAN_TIMEOUT}: to set the max waiting time allowed to send each + * span batch. + *
    • {@code OTEL_EXPORTER_OTLP_SPAN_ENDPOINT}: to set the endpoint to connect to. + *
    • {@code OTEL_EXPORTER_OTLP_SPAN_INSECURE}: whether to enable client transport security for + * the connection. + *
    • {@code OTEL_EXPORTER_OTLP_SPAN_HEADERS}: the headers associated with the requests. + *
    + * + * In both cases, if a property is missing, the name without "span" is used to resolve the value. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.otlp.trace; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/otlp/trace/src/test/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporterTest.java b/opentelemetry-java/exporters/otlp/trace/src/test/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporterTest.java new file mode 100644 index 000000000..48d188613 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/test/java/io/opentelemetry/exporter/otlp/trace/OtlpGrpcSpanExporterTest.java @@ -0,0 +1,328 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import com.google.common.io.Closer; +import io.github.netmikey.logunit.api.LogCapturer; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.exporter.otlp.internal.SpanAdapter; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; + +class OtlpGrpcSpanExporterTest { + + private static final String TRACE_ID = "00000000000000000000000000abc123"; + private static final String SPAN_ID = "0000000000def456"; + + private final FakeCollector fakeCollector = new FakeCollector(); + private final String serverName = InProcessServerBuilder.generateName(); + private final ManagedChannel inProcessChannel = + InProcessChannelBuilder.forName(serverName).directExecutor().build(); + + private final Closer closer = Closer.create(); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(OtlpGrpcSpanExporter.class); + + @BeforeEach + public void setup() throws IOException { + Server server = + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(fakeCollector) + .build() + .start(); + closer.register(server::shutdownNow); + closer.register(inProcessChannel::shutdownNow); + } + + @AfterEach + void tearDown() throws Exception { + closer.close(); + } + + @Test + @SuppressWarnings("PreferJavaTimeOverload") + void invalidConfig() { + assertThatThrownBy(() -> OtlpGrpcSpanExporter.builder().setTimeout(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("timeout must be non-negative"); + assertThatThrownBy(() -> OtlpGrpcSpanExporter.builder().setTimeout(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy(() -> OtlpGrpcSpanExporter.builder().setTimeout(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("timeout"); + + assertThatThrownBy(() -> OtlpGrpcSpanExporter.builder().setEndpoint(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("endpoint"); + assertThatThrownBy(() -> OtlpGrpcSpanExporter.builder().setEndpoint("😺://localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must be a URL: 😺://localhost"); + assertThatThrownBy(() -> OtlpGrpcSpanExporter.builder().setEndpoint("localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must start with http:// or https://: localhost"); + assertThatThrownBy(() -> OtlpGrpcSpanExporter.builder().setEndpoint("gopher://localhost")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must start with http:// or https://: gopher://localhost"); + } + + @Test + void testExport() { + SpanData span = generateFakeSpan(); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(span)).isSuccess()).isTrue(); + assertThat(fakeCollector.getReceivedSpans()) + .isEqualTo(SpanAdapter.toProtoResourceSpans(Collections.singletonList(span))); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_MultipleSpans() { + List spans = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + spans.add(generateFakeSpan()); + } + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(spans).isSuccess()).isTrue(); + assertThat(fakeCollector.getReceivedSpans()) + .isEqualTo(SpanAdapter.toProtoResourceSpans(spans)); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_DeadlineSetPerExport() throws InterruptedException { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder() + .setChannel(inProcessChannel) + .setTimeout(Duration.ofMillis(1500)) + .build(); + + try { + TimeUnit.MILLISECONDS.sleep(2000); + CompletableResultCode result = exporter.export(Collections.singletonList(generateFakeSpan())); + await().untilAsserted(() -> assertThat(result.isSuccess()).isTrue()); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_AfterShutdown() { + SpanData span = generateFakeSpan(); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + exporter.shutdown(); + // TODO: This probably should not be retryable because we never restart the channel. + assertThat(exporter.export(Collections.singletonList(span)).isSuccess()).isFalse(); + } + + @Test + void testExport_Cancelled() { + fakeCollector.setReturnedStatus(Status.CANCELLED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_DeadlineExceeded() { + fakeCollector.setReturnedStatus(Status.DEADLINE_EXCEEDED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_ResourceExhausted() { + fakeCollector.setReturnedStatus(Status.RESOURCE_EXHAUSTED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_OutOfRange() { + fakeCollector.setReturnedStatus(Status.OUT_OF_RANGE); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_Unavailable() { + fakeCollector.setReturnedStatus(Status.UNAVAILABLE); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + LoggingEvent log = + logs.assertContains( + "Failed to export spans. Server is UNAVAILABLE. " + + "Make sure your collector is running and reachable from this network."); + assertThat(log.getLevel()).isEqualTo(Level.ERROR); + } + + @Test + void testExport_Unimplemented() { + fakeCollector.setReturnedStatus(Status.UNIMPLEMENTED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + LoggingEvent log = + logs.assertContains( + "Failed to export spans. Server responded with UNIMPLEMENTED. " + + "This usually means that your collector is not configured with an otlp " + + "receiver in the \"pipelines\" section of the configuration. " + + "Full error message: UNIMPLEMENTED"); + assertThat(log.getLevel()).isEqualTo(Level.ERROR); + } + + @Test + void testExport_DataLoss() { + fakeCollector.setReturnedStatus(Status.DATA_LOSS); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + @Test + void testExport_PermissionDenied() { + fakeCollector.setReturnedStatus(Status.PERMISSION_DENIED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan())).isSuccess()) + .isFalse(); + } finally { + exporter.shutdown(); + } + } + + private static SpanData generateFakeSpan() { + long duration = TimeUnit.MILLISECONDS.toNanos(900); + long startNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + long endNs = startNs + duration; + return TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())) + .setName("GET /api/endpoint") + .setStartEpochNanos(startNs) + .setEndEpochNanos(endNs) + .setStatus(StatusData.ok()) + .setKind(SpanKind.SERVER) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .build(); + } + + private static final class FakeCollector extends TraceServiceGrpc.TraceServiceImplBase { + private final List receivedSpans = new ArrayList<>(); + private Status returnedStatus = Status.OK; + + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + receivedSpans.addAll(request.getResourceSpansList()); + responseObserver.onNext(ExportTraceServiceResponse.newBuilder().build()); + if (!returnedStatus.isOk()) { + if (returnedStatus.getCode() == Code.DEADLINE_EXCEEDED) { + // Do not call onCompleted to simulate a deadline exceeded. + return; + } + responseObserver.onError(returnedStatus.asRuntimeException()); + return; + } + responseObserver.onCompleted(); + } + + List getReceivedSpans() { + return receivedSpans; + } + + void setReturnedStatus(Status returnedStatus) { + this.returnedStatus = returnedStatus; + } + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/test/java/io/opentelemetry/exporter/otlp/trace/TraceMarshalerTest.java b/opentelemetry-java/exporters/otlp/trace/src/test/java/io/opentelemetry/exporter/otlp/trace/TraceMarshalerTest.java new file mode 100644 index 000000000..2cd3ac55e --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/test/java/io/opentelemetry/exporter/otlp/trace/TraceMarshalerTest.java @@ -0,0 +1,205 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.CodedOutputStream; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.exporter.otlp.internal.SpanAdapter; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class TraceMarshalerTest { + private static final Resource RESOURCE = + Resource.create( + Attributes.builder() + .put(AttributeKey.booleanKey("key_bool"), true) + .put(AttributeKey.stringKey("key_string"), "string") + .put(AttributeKey.longKey("key_int"), 100L) + .put(AttributeKey.doubleKey("key_double"), 100.3) + .put( + AttributeKey.stringArrayKey("key_string_array"), + Arrays.asList("string", "string")) + .put(AttributeKey.longArrayKey("key_long_array"), Arrays.asList(12L, 23L)) + .put(AttributeKey.doubleArrayKey("key_double_array"), Arrays.asList(12.3, 23.1)) + .put(AttributeKey.booleanArrayKey("key_boolean_array"), Arrays.asList(true, false)) + .put(AttributeKey.booleanKey(""), true) + .put(AttributeKey.stringKey("null_value"), null) + .put(AttributeKey.stringKey("empty_value"), "") + .build()); + + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create("name", null); + private static final String TRACE_ID = "00000000000000000000000001020304"; + private static final String SPAN_ID = "0000000004030201"; + + private static final SpanContext SPAN_CONTEXT = + SpanContext.create( + "0123456789abcdef0123456789abcdef", + "0123456789abcdef", + TraceFlags.getSampled(), + TraceState.getDefault()); + private static final SpanContext PARENT_SPAN_CONTEXT = + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()); + + @Test + void marshalAndSizeRequest() throws IOException { + assertMarshalAndSize(Arrays.asList(testSpanData(), testSpanData(), testSpanData())); + } + + @Test + void marshalAndSizeRequest_Empty() throws IOException { + assertMarshalAndSize( + Collections.singletonList( + TestSpanData.builder() + .setSpanContext(SPAN_CONTEXT) + .setKind(SpanKind.INTERNAL) + .setName("") + .setStartEpochNanos(0) + .setEndEpochNanos(0) + .setHasEnded(true) + .setStatus(StatusData.unset()) + .build())); + } + + @Test + void marshalAndSizeRequest_ErrorStatus() throws IOException { + assertMarshalAndSize( + Collections.singletonList( + TestSpanData.builder() + .setSpanContext(SPAN_CONTEXT) + .setKind(SpanKind.INTERNAL) + .setName("") + .setStartEpochNanos(0) + .setEndEpochNanos(0) + .setHasEnded(true) + .setStatus(StatusData.error()) + .build())); + } + + @Test + void marshalAndSizeRequest_ValidParent() throws IOException { + assertMarshalAndSize( + Collections.singletonList( + TestSpanData.builder() + .setSpanContext(SPAN_CONTEXT) + .setParentSpanContext(PARENT_SPAN_CONTEXT) + .setKind(SpanKind.INTERNAL) + .setName("") + .setStartEpochNanos(0) + .setEndEpochNanos(0) + .setHasEnded(true) + .setStatus(StatusData.unset()) + .build())); + } + + @Test + void marshalAndSizeRequest_InstrumentationLibrary() throws IOException { + assertMarshalAndSize( + Arrays.asList( + testSpanDataWithInstrumentationLibrary(InstrumentationLibraryInfo.create("name", null)), + testSpanDataWithInstrumentationLibrary(InstrumentationLibraryInfo.create("name", "")), + testSpanDataWithInstrumentationLibrary( + InstrumentationLibraryInfo.create("name", "version")), + testSpanDataWithInstrumentationLibrary(InstrumentationLibraryInfo.empty()), + testSpanDataWithInstrumentationLibrary(InstrumentationLibraryInfo.create("", "")))); + } + + private static SpanData testSpanDataWithInstrumentationLibrary( + InstrumentationLibraryInfo instrumentationLibraryInfo) { + return TestSpanData.builder() + .setInstrumentationLibraryInfo(instrumentationLibraryInfo) + .setSpanContext(SPAN_CONTEXT) + .setKind(SpanKind.INTERNAL) + .setName("") + .setStartEpochNanos(0) + .setEndEpochNanos(0) + .setHasEnded(true) + .setStatus(StatusData.unset()) + .build(); + } + + private static void assertMarshalAndSize(List spanDataList) throws IOException { + ExportTraceServiceRequest protoRequest = + ExportTraceServiceRequest.newBuilder() + .addAllResourceSpans(SpanAdapter.toProtoResourceSpans(spanDataList)) + .build(); + TraceMarshaler.RequestMarshaler requestMarshaler = + TraceMarshaler.RequestMarshaler.create(spanDataList); + int protoSize = protoRequest.getSerializedSize(); + assertThat(requestMarshaler.getSerializedSize()).isEqualTo(protoSize); + + ExportTraceServiceRequest protoCustomRequest = + TraceMarshaler.RequestMarshaler.create(spanDataList).toRequest(); + assertThat(protoCustomRequest.getSerializedSize()).isEqualTo(protoRequest.getSerializedSize()); + + byte[] protoOutput = new byte[protoRequest.getSerializedSize()]; + protoRequest.writeTo(CodedOutputStream.newInstance(protoOutput)); + + byte[] customOutput = new byte[requestMarshaler.getSerializedSize()]; + requestMarshaler.writeTo(CodedOutputStream.newInstance(customOutput)); + assertThat(customOutput).isEqualTo(protoOutput); + + byte[] protoCustomOutput = new byte[protoRequest.getSerializedSize()]; + protoCustomRequest.writeTo(CodedOutputStream.newInstance(protoCustomOutput)); + assertThat(protoCustomOutput).isEqualTo(protoOutput); + } + + private static SpanData testSpanData() { + return TestSpanData.builder() + .setResource(RESOURCE) + .setInstrumentationLibraryInfo(INSTRUMENTATION_LIBRARY_INFO) + .setHasEnded(true) + .setSpanContext(SPAN_CONTEXT) + .setParentSpanContext(SpanContext.getInvalid()) + .setName("GET /api/endpoint") + .setKind(SpanKind.SERVER) + .setStartEpochNanos(12345) + .setEndEpochNanos(12349) + .setAttributes( + Attributes.builder() + .put(AttributeKey.booleanKey("key_bool"), true) + .put(AttributeKey.stringKey("key_string"), "string") + .put(AttributeKey.longKey("key_int"), 100L) + .put(AttributeKey.doubleKey("key_double"), 100.3) + .build()) + .setTotalAttributeCount(2) + .setEvents( + Arrays.asList( + EventData.create(12347, "my_event_1", Attributes.empty()), + EventData.create( + 12348, + "my_event_2", + Attributes.of(AttributeKey.longKey("event_attr_key"), 1234L)))) + .setTotalRecordedEvents(3) + .setLinks( + Arrays.asList( + LinkData.create(PARENT_SPAN_CONTEXT), + LinkData.create( + PARENT_SPAN_CONTEXT, + Attributes.of(AttributeKey.stringKey("link_attr_key"), "value")))) + .setTotalRecordedLinks(3) + .setStatus(StatusData.ok()) + .build(); + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/testGrpcNetty/java/io/opentelemetry/exporter/otlp/trace/ExportTest.java b/opentelemetry-java/exporters/otlp/trace/src/testGrpcNetty/java/io/opentelemetry/exporter/otlp/trace/ExportTest.java new file mode 100644 index 000000000..01bd60888 --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/testGrpcNetty/java/io/opentelemetry/exporter/otlp/trace/ExportTest.java @@ -0,0 +1,119 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ExportTest { + + private static final List SPANS = + Collections.singletonList( + TestSpanData.builder() + .setName("name") + .setKind(SpanKind.CLIENT) + .setStartEpochNanos(1) + .setEndEpochNanos(2) + .setStatus(StatusData.ok()) + .setHasEnded(true) + .build()); + + @RegisterExtension + @Order(1) + public static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension(); + + @RegisterExtension + @Order(2) + public static ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + .addService( + new TraceServiceGrpc.TraceServiceImplBase() { + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .build()); + sb.http(0); + sb.https(0); + sb.tls(certificate.certificateFile(), certificate.privateKeyFile()); + } + }; + + @Test + void plainTextExport() { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setEndpoint("http://localhost:" + server.httpPort()).build(); + assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + } + + @Test + void authorityWithAuth() { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder() + .setEndpoint("http://foo:bar@localhost:" + server.httpPort()) + .build(); + assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + } + + @Test + void testTlsExport() throws Exception { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder() + .setEndpoint("https://localhost:" + server.httpsPort()) + .setTrustedCertificates(Files.readAllBytes(certificate.certificateFile().toPath())) + .build(); + assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + } + + @Test + void testTlsExport_untrusted() { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder() + .setEndpoint("https://localhost:" + server.httpsPort()) + .build(); + assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isFalse(); + } + + @Test + void tlsBadCert() { + assertThatThrownBy( + () -> + OtlpGrpcSpanExporter.builder() + .setTrustedCertificates("foobar".getBytes(StandardCharsets.UTF_8)) + .build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Could not set trusted certificates"); + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/testGrpcNettyShaded/java/io/opentelemetry/exporter/otlp/trace/TlsExportTest.java b/opentelemetry-java/exporters/otlp/trace/src/testGrpcNettyShaded/java/io/opentelemetry/exporter/otlp/trace/TlsExportTest.java new file mode 100644 index 000000000..a2d34fa3b --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/testGrpcNettyShaded/java/io/opentelemetry/exporter/otlp/trace/TlsExportTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TlsExportTest { + + @RegisterExtension + @Order(1) + public static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension(); + + @RegisterExtension + @Order(2) + public static ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + .addService( + new TraceServiceGrpc.TraceServiceImplBase() { + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .build()); + sb.tls(certificate.certificateFile(), certificate.privateKeyFile()); + } + }; + + @Test + void testTlsExport() throws Exception { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder() + .setEndpoint("https://localhost:" + server.httpsPort()) + .setTrustedCertificates(Files.readAllBytes(certificate.certificateFile().toPath())) + .build(); + assertThat( + exporter + .export( + Arrays.asList( + TestSpanData.builder() + .setName("name") + .setKind(SpanKind.CLIENT) + .setStartEpochNanos(1) + .setEndEpochNanos(2) + .setStatus(StatusData.ok()) + .setHasEnded(true) + .build())) + .join(10, TimeUnit.SECONDS) + .isSuccess()) + .isTrue(); + } + + @Test + void testTlsExport_untrusted() throws Exception { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder() + .setEndpoint("https://localhost:" + server.httpsPort()) + .build(); + assertThat( + exporter + .export( + Arrays.asList( + TestSpanData.builder() + .setName("name") + .setKind(SpanKind.CLIENT) + .setStartEpochNanos(1) + .setEndEpochNanos(2) + .setStatus(StatusData.ok()) + .setHasEnded(true) + .build())) + .join(10, TimeUnit.SECONDS) + .isSuccess()) + .isFalse(); + } + + @Test + void tlsBadCert() { + assertThatThrownBy( + () -> + OtlpGrpcSpanExporter.builder() + .setTrustedCertificates("foobar".getBytes(StandardCharsets.UTF_8)) + .build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Could not set trusted certificates"); + } +} diff --git a/opentelemetry-java/exporters/otlp/trace/src/testGrpcOkhttp/java/io/opentelemetry/exporter/otlp/trace/ExportTest.java b/opentelemetry-java/exporters/otlp/trace/src/testGrpcOkhttp/java/io/opentelemetry/exporter/otlp/trace/ExportTest.java new file mode 100644 index 000000000..99f6f399a --- /dev/null +++ b/opentelemetry-java/exporters/otlp/trace/src/testGrpcOkhttp/java/io/opentelemetry/exporter/otlp/trace/ExportTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ExportTest { + + private static final List SPANS = + Collections.singletonList( + TestSpanData.builder() + .setName("name") + .setKind(SpanKind.CLIENT) + .setStartEpochNanos(1) + .setEndEpochNanos(2) + .setStatus(StatusData.ok()) + .setHasEnded(true) + .build()); + + @RegisterExtension + @Order(1) + public static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension(); + + @RegisterExtension + @Order(2) + public static ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + .addService( + new TraceServiceGrpc.TraceServiceImplBase() { + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .build()); + sb.http(0); + sb.https(0); + sb.tls(certificate.certificateFile(), certificate.privateKeyFile()); + } + }; + + @Test + void plainTextExport() { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder().setEndpoint("http://localhost:" + server.httpPort()).build(); + assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + } + + @Test + void authorityWithAuth() { + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.builder() + .setEndpoint("http://foo:bar@localhost:" + server.httpPort()) + .build(); + assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + } + + @Test + void testTlsExport() { + // Currently not supported. + assertThatThrownBy( + () -> + OtlpGrpcSpanExporter.builder() + .setTrustedCertificates( + Files.readAllBytes(certificate.certificateFile().toPath())) + .build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("TLS cerificate configuration only supported with Netty."); + } +} diff --git a/opentelemetry-java/exporters/prometheus/README.md b/opentelemetry-java/exporters/prometheus/README.md new file mode 100644 index 000000000..5de842b73 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/README.md @@ -0,0 +1,8 @@ +# OpenTelemetry - Prometheus Exporter + +[![Javadocs][javadoc-image]][javadoc-url] + +This is the OpenTelemetry's Prometheus exporter, allowing Prometheus to query metrics data. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-exporters-prometheus.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-exporters-prometheus \ No newline at end of file diff --git a/opentelemetry-java/exporters/prometheus/build.gradle.kts b/opentelemetry-java/exporters/prometheus/build.gradle.kts new file mode 100644 index 000000000..05b37af49 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Prometheus Exporter" +extra["moduleName"] = "io.opentelemetry.exporter.prometheus" + +dependencies { + api(project(":sdk:metrics")) + + api("io.prometheus:simpleclient") + + testImplementation("io.prometheus:simpleclient_common") + testImplementation("com.google.guava:guava") +} diff --git a/opentelemetry-java/exporters/prometheus/gradle.properties b/opentelemetry-java/exporters/prometheus/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/LabelNameSanitizer.java b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/LabelNameSanitizer.java new file mode 100644 index 000000000..c0bf82162 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/LabelNameSanitizer.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import io.prometheus.client.Collector; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** Used to convert a label keys to a label names. Sanitizes the label keys. Not thread safe. */ +class LabelNameSanitizer implements Function { + + private final Function delegate; + private final Map cache = new ConcurrentHashMap<>(); + + public LabelNameSanitizer() { + this(Collector::sanitizeMetricName); + } + + // visible for testing + LabelNameSanitizer(Function delegate) { + this.delegate = delegate; + } + + @Override + public String apply(String labelName) { + return cache.computeIfAbsent(labelName, delegate); + } +} diff --git a/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/MetricAdapter.java b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/MetricAdapter.java new file mode 100644 index 000000000..35d31fea5 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/MetricAdapter.java @@ -0,0 +1,258 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import static io.prometheus.client.Collector.doubleToGoString; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.SystemCommon; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramPointData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.prometheus.client.Collector; +import io.prometheus.client.Collector.MetricFamilySamples; +import io.prometheus.client.Collector.MetricFamilySamples.Sample; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Util methods to convert OpenTelemetry Metrics data models to Prometheus data models. + * + *

    Each OpenTelemetry {@link MetricData} will be converted to a Prometheus {@link + * MetricFamilySamples}, and each {@code Point} of the {@link MetricData} will be converted to + * Prometheus {@link Sample}s. + * + *

    {@code DoublePoint}, {@code LongPoint} will be converted to a single {@link Sample}. {@code + * Summary} will be converted to two {@link Sample}s (sum and count) plus the number of Percentile + * values {@code Sample}s + * + *

    Please note that Prometheus Metric and Label name can only have alphanumeric characters and + * underscore. All other characters will be sanitized by underscores. + */ +final class MetricAdapter { + + static final String SAMPLE_SUFFIX_COUNT = "_count"; + static final String SAMPLE_SUFFIX_SUM = "_sum"; + static final String SAMPLE_SUFFIX_BUCKET = "_bucket"; + static final String LABEL_NAME_QUANTILE = "quantile"; + static final String LABEL_NAME_LE = "le"; + static final String ip = SystemCommon.getEnvOrProperties("host.ip") == null ? "" + : SystemCommon.getEnvOrProperties("host.ip"); + static String applicationName = + SystemCommon.getEnvOrProperties("otel.resource.attributes") == null ? "none" : + SystemCommon.getEnvOrProperties("otel.resource.attributes"); + + static { + // Replace "-" with "_" in the project name. + applicationName = applicationName.replaceAll("-", "_"); + } + + // Converts a MetricData to a Prometheus MetricFamilySamples. + static MetricFamilySamples toMetricFamilySamples(MetricData metricData) { + String cleanMetricName = cleanMetricName(metricData.getName()); + Collector.Type type = toMetricFamilyType(metricData); + + return new MetricFamilySamples( + cleanMetricName, + type, + metricData.getDescription(), + toSamples(cleanMetricName, metricData.getType(), getPoints(metricData))); + } + + private static String cleanMetricName(String descriptorMetricName) { + return Collector.sanitizeMetricName(descriptorMetricName); + } + + static Collector.Type toMetricFamilyType(MetricData metricData) { + switch (metricData.getType()) { + case LONG_GAUGE: + case DOUBLE_GAUGE: + return Collector.Type.GAUGE; + case LONG_SUM: + LongSumData longSumData = metricData.getLongSumData(); + if (longSumData.isMonotonic() + && longSumData.getAggregationTemporality() == AggregationTemporality.CUMULATIVE) { + return Collector.Type.COUNTER; + } + return Collector.Type.GAUGE; + case DOUBLE_SUM: + DoubleSumData doubleSumData = metricData.getDoubleSumData(); + if (doubleSumData.isMonotonic() + && doubleSumData.getAggregationTemporality() == AggregationTemporality.CUMULATIVE) { + return Collector.Type.COUNTER; + } + return Collector.Type.GAUGE; + case SUMMARY: + return Collector.Type.SUMMARY; + case HISTOGRAM: + return Collector.Type.HISTOGRAM; + } + return Collector.Type.UNKNOWN; + } + + private static final Function sanitizer = new LabelNameSanitizer(); + + // Converts a list of points from MetricData to a list of Prometheus Samples. + static List toSamples( + String name, MetricDataType type, Collection points) { + final List samples = new ArrayList<>(estimateNumSamples(points.size(), type)); + + for (PointData pointData : points) { + List labelNames = Collections.emptyList(); + List labelValues = Collections.emptyList(); + Labels labels = pointData.getLabels(); + if (labels.size() != 0) { + labelNames = new ArrayList<>(labels.size()); + labelValues = new ArrayList<>(labels.size()); + + labels.forEach(new Consumer(labelNames, labelValues)); + } + labelNames.add("serverIp"); + labelNames.add("application"); + labelValues.add(ip); + labelValues.add(applicationName); + switch (type) { + case DOUBLE_SUM: + case DOUBLE_GAUGE: + DoublePointData doublePoint = (DoublePointData) pointData; + samples.add(new Sample(name, labelNames, labelValues, doublePoint.getValue())); + break; + case LONG_SUM: + case LONG_GAUGE: + LongPointData longPoint = (LongPointData) pointData; + samples.add(new Sample(name, labelNames, labelValues, longPoint.getValue())); + break; + case SUMMARY: + addSummarySamples( + (DoubleSummaryPointData) pointData, name, labelNames, labelValues, samples); + break; + case HISTOGRAM: + addHistogramSamples( + (DoubleHistogramPointData) pointData, name, labelNames, labelValues, samples); + break; + } + } + return samples; + } + + private static final class Consumer implements BiConsumer { + final List labelNames; + final List labelValues; + + private Consumer(List labelNames, List labelValues) { + this.labelNames = labelNames; + this.labelValues = labelValues; + } + + @Override + public void accept(String labelName, String value) { + String sanitizedLabelName = sanitizer.apply(labelName); + labelNames.add(sanitizedLabelName); + labelValues.add(value == null ? "" : value); + } + } + + private static void addSummarySamples( + DoubleSummaryPointData doubleSummaryPoint, + String name, + List labelNames, + List labelValues, + List samples) { + samples.add( + new Sample( + name + SAMPLE_SUFFIX_COUNT, labelNames, labelValues, doubleSummaryPoint.getCount())); + samples.add( + new Sample(name + SAMPLE_SUFFIX_SUM, labelNames, labelValues, doubleSummaryPoint.getSum())); + List valueAtPercentiles = doubleSummaryPoint.getPercentileValues(); + List labelNamesWithQuantile = new ArrayList<>(labelNames.size()); + labelNamesWithQuantile.addAll(labelNames); + labelNamesWithQuantile.add(LABEL_NAME_QUANTILE); + for (ValueAtPercentile valueAtPercentile : valueAtPercentiles) { + List labelValuesWithQuantile = new ArrayList<>(labelValues.size()); + labelValuesWithQuantile.addAll(labelValues); + labelValuesWithQuantile.add(doubleToGoString(valueAtPercentile.getPercentile())); + samples.add( + new Sample( + name, labelNamesWithQuantile, labelValuesWithQuantile, valueAtPercentile.getValue())); + } + } + + private static void addHistogramSamples( + DoubleHistogramPointData doubleHistogramPointData, + String name, + List labelNames, + List labelValues, + List samples) { + samples.add( + new Sample( + name + SAMPLE_SUFFIX_COUNT, + labelNames, + labelValues, + doubleHistogramPointData.getCount())); + samples.add( + new Sample( + name + SAMPLE_SUFFIX_SUM, labelNames, labelValues, doubleHistogramPointData.getSum())); + + List labelNamesWithLe = new ArrayList<>(labelNames.size() + 1); + labelNamesWithLe.addAll(labelNames); + labelNamesWithLe.add(LABEL_NAME_LE); + + long cumulativeCount = 0; + List boundaries = doubleHistogramPointData.getBoundaries(); + List counts = doubleHistogramPointData.getCounts(); + for (int i = 0; i < counts.size(); i++) { + List labelValuesWithLe = new ArrayList<>(labelValues.size() + 1); + labelValuesWithLe.addAll(labelValues); + labelValuesWithLe.add( + doubleToGoString(i < boundaries.size() ? boundaries.get(i) : Double.POSITIVE_INFINITY)); + + cumulativeCount += counts.get(i); + samples.add( + new Sample( + name + SAMPLE_SUFFIX_BUCKET, labelNamesWithLe, labelValuesWithLe, cumulativeCount)); + } + } + + private static int estimateNumSamples(int numPoints, MetricDataType type) { + if (type == MetricDataType.SUMMARY) { + // count + sum + estimated 2 percentiles (default MinMaxSumCount aggregator). + return numPoints * 4; + } + return numPoints; + } + + private static Collection getPoints(MetricData metricData) { + switch (metricData.getType()) { + case DOUBLE_GAUGE: + return metricData.getDoubleGaugeData().getPoints(); + case DOUBLE_SUM: + return metricData.getDoubleSumData().getPoints(); + case LONG_GAUGE: + return metricData.getLongGaugeData().getPoints(); + case LONG_SUM: + return metricData.getLongSumData().getPoints(); + case SUMMARY: + return metricData.getDoubleSummaryData().getPoints(); + case HISTOGRAM: + return metricData.getDoubleHistogramData().getPoints(); + } + return Collections.emptyList(); + } + + private MetricAdapter() {} +} diff --git a/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollector.java b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollector.java new file mode 100644 index 000000000..a34cd1279 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollector.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.prometheus.client.Collector; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class PrometheusCollector extends Collector { + private final MetricProducer metricProducer; + + PrometheusCollector(MetricProducer metricProducer) { + this.metricProducer = metricProducer; + } + + @Override + public List collect() { + Collection allMetrics = metricProducer.collectAllMetrics(); + List allSamples = new ArrayList<>(allMetrics.size()); + for (MetricData metricData : allMetrics) { + allSamples.add(MetricAdapter.toMetricFamilySamples(metricData)); + } + return allSamples; + } + + /** + * Returns a new builder instance for this exporter. + * + * @return a new builder instance for this exporter. + */ + public static PrometheusCollectorBuilder builder() { + return new PrometheusCollectorBuilder(); + } +} diff --git a/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorBuilder.java b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorBuilder.java new file mode 100644 index 000000000..824388b99 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorBuilder.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.prometheus.client.Collector; +import java.util.Objects; + +/** Builder for {@link PrometheusCollector}. */ +public class PrometheusCollectorBuilder { + private MetricProducer metricProducer; + + PrometheusCollectorBuilder() {} + + /** + * Sets the metric producer for the collector. Required. + * + * @param metricProducer the {@link MetricProducer} to use. + * @return this builder's instance. + */ + public PrometheusCollectorBuilder setMetricProducer(MetricProducer metricProducer) { + this.metricProducer = metricProducer; + return this; + } + + /** + * Constructs a new instance of the {@link Collector} based on the builder's values. + * + * @return a new {@code Collector} based on the builder's values. + */ + public PrometheusCollector build() { + return new PrometheusCollector(Objects.requireNonNull(metricProducer, "metricProducer")); + } + + /** + * Constructs a new instance of the {@link Collector} based on the builder's values and registers + * it to Prometheus {@link io.prometheus.client.CollectorRegistry#defaultRegistry}. + * + * @return a new {@code Collector} based on the builder's values. + */ + public PrometheusCollector buildAndRegister() { + return build().register(); + } +} diff --git a/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/package-info.java b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/package-info.java new file mode 100644 index 000000000..d95c2e858 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.prometheus; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/LabelNameSanitizerTest.java b/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/LabelNameSanitizerTest.java new file mode 100644 index 000000000..f84d5f868 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/LabelNameSanitizerTest.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +class LabelNameSanitizerTest { + + @Test + void testSanitizerCaching() { + AtomicInteger count = new AtomicInteger(); + Function delegate = labelName -> labelName + count.incrementAndGet(); + LabelNameSanitizer sanitizer = new LabelNameSanitizer(delegate); + String labelName = "http.name"; + + assertEquals("http.name1", sanitizer.apply(labelName)); + assertEquals("http.name1", sanitizer.apply(labelName)); + assertEquals("http.name1", sanitizer.apply(labelName)); + assertEquals("http.name1", sanitizer.apply(labelName)); + assertEquals("http.name1", sanitizer.apply(labelName)); + assertEquals(1, count.get()); + } +} diff --git a/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/MetricAdapterTest.java b/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/MetricAdapterTest.java new file mode 100644 index 000000000..cb9764850 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/MetricAdapterTest.java @@ -0,0 +1,392 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramData; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramPointData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.resources.Resource; +import io.prometheus.client.Collector; +import io.prometheus.client.Collector.MetricFamilySamples; +import io.prometheus.client.Collector.MetricFamilySamples.Sample; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link MetricAdapter}. */ +class MetricAdapterTest { + + private static final MetricData MONOTONIC_CUMULATIVE_DOUBLE_SUM = + MetricData.createDoubleSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData NON_MONOTONIC_CUMULATIVE_DOUBLE_SUM = + MetricData.createDoubleSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData MONOTONIC_DELTA_DOUBLE_SUM = + MetricData.createDoubleSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData NON_MONOTONIC_DELTA_DOUBLE_SUM = + MetricData.createDoubleSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.DELTA, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData MONOTONIC_CUMULATIVE_LONG_SUM = + MetricData.createLongSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList(LongPointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData NON_MONOTONIC_CUMULATIVE_LONG_SUM = + MetricData.createLongSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList(LongPointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData MONOTONIC_DELTA_LONG_SUM = + MetricData.createLongSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList(LongPointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData NON_MONOTONIC_DELTA_LONG_SUM = + MetricData.createLongSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.DELTA, + Collections.singletonList(LongPointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + + private static final MetricData DOUBLE_GAUGE = + MetricData.createDoubleGauge( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + DoubleGaugeData.create( + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData LONG_GAUGE = + MetricData.createLongGauge( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + LongGaugeData.create( + Collections.singletonList(LongPointData.create(123, 456, Labels.of("kp", "vp"), 5)))); + private static final MetricData SUMMARY = + MetricData.createDoubleSummary( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + 123, 456, Labels.of("kp", "vp"), 5, 7, Collections.emptyList())))); + private static final MetricData HISTOGRAM = + MetricData.createDoubleHistogram( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("full", "version"), + "instrument.name", + "description", + "1", + DoubleHistogramData.create( + AggregationTemporality.DELTA, + Collections.singletonList( + DoubleHistogramPointData.create( + 123, + 456, + Labels.of("kp", "vp"), + 1.0, + Collections.emptyList(), + Collections.singletonList(2L))))); + + @Test + void toProtoMetricDescriptorType() { + MetricFamilySamples metricFamilySamples = + MetricAdapter.toMetricFamilySamples(MONOTONIC_CUMULATIVE_DOUBLE_SUM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.COUNTER); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(NON_MONOTONIC_CUMULATIVE_DOUBLE_SUM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.GAUGE); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(MONOTONIC_DELTA_DOUBLE_SUM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.GAUGE); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(NON_MONOTONIC_DELTA_DOUBLE_SUM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.GAUGE); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(MONOTONIC_CUMULATIVE_LONG_SUM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.COUNTER); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(NON_MONOTONIC_CUMULATIVE_LONG_SUM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.GAUGE); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(MONOTONIC_DELTA_LONG_SUM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.GAUGE); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(NON_MONOTONIC_DELTA_LONG_SUM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.GAUGE); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(SUMMARY); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.SUMMARY); + assertThat(metricFamilySamples.samples).hasSize(2); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(DOUBLE_GAUGE); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.GAUGE); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(LONG_GAUGE); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.GAUGE); + assertThat(metricFamilySamples.samples).hasSize(1); + + metricFamilySamples = MetricAdapter.toMetricFamilySamples(HISTOGRAM); + assertThat(metricFamilySamples.type).isEqualTo(Collector.Type.HISTOGRAM); + assertThat(metricFamilySamples.samples).hasSize(3); + } + + @Test + void toSamples_LongPoints() { + assertThat( + MetricAdapter.toSamples("full_name", MetricDataType.LONG_SUM, Collections.emptyList())) + .isEmpty(); + + assertThat( + MetricAdapter.toSamples( + "full_name", + MetricDataType.LONG_SUM, + ImmutableList.of( + LongPointData.create(123, 456, Labels.empty(), 5), + LongPointData.create(321, 654, Labels.of("kp", "vp"), 7)))) + .containsExactly( + new Sample("full_name", Collections.emptyList(), Collections.emptyList(), 5), + new Sample("full_name", ImmutableList.of("kp"), ImmutableList.of("vp"), 7)); + + assertThat( + MetricAdapter.toSamples( + "full_name", + MetricDataType.LONG_GAUGE, + ImmutableList.of( + LongPointData.create(123, 456, Labels.empty(), 5), + LongPointData.create(321, 654, Labels.of("kp", "vp"), 7)))) + .containsExactly( + new Sample("full_name", Collections.emptyList(), Collections.emptyList(), 5), + new Sample("full_name", ImmutableList.of("kp"), ImmutableList.of("vp"), 7)); + } + + @Test + void toSamples_DoublePoints() { + assertThat( + MetricAdapter.toSamples( + "full_name", MetricDataType.DOUBLE_SUM, Collections.emptyList())) + .isEmpty(); + + assertThat( + MetricAdapter.toSamples( + "full_name", + MetricDataType.DOUBLE_SUM, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("kp", "vp"), 5)))) + .containsExactly( + new Sample("full_name", ImmutableList.of("kp"), ImmutableList.of("vp"), 5)); + + assertThat( + MetricAdapter.toSamples( + "full_name", + MetricDataType.DOUBLE_GAUGE, + ImmutableList.of( + DoublePointData.create(123, 456, Labels.empty(), 5), + DoublePointData.create(321, 654, Labels.of("kp", "vp"), 7)))) + .containsExactly( + new Sample("full_name", Collections.emptyList(), Collections.emptyList(), 5), + new Sample("full_name", ImmutableList.of("kp"), ImmutableList.of("vp"), 7)); + } + + @Test + void toSamples_SummaryPoints() { + assertThat( + MetricAdapter.toSamples("full_name", MetricDataType.SUMMARY, Collections.emptyList())) + .isEmpty(); + + assertThat( + MetricAdapter.toSamples( + "full_name", + MetricDataType.SUMMARY, + ImmutableList.of( + DoubleSummaryPointData.create( + 321, + 654, + Labels.of("kp", "vp"), + 9, + 18.3, + ImmutableList.of(ValueAtPercentile.create(0.9, 1.1)))))) + .containsExactly( + new Sample("full_name_count", ImmutableList.of("kp"), ImmutableList.of("vp"), 9), + new Sample("full_name_sum", ImmutableList.of("kp"), ImmutableList.of("vp"), 18.3), + new Sample( + "full_name", + ImmutableList.of("kp", "quantile"), + ImmutableList.of("vp", "0.9"), + 1.1)); + + assertThat( + MetricAdapter.toSamples( + "full_name", + MetricDataType.SUMMARY, + ImmutableList.of( + DoubleSummaryPointData.create( + 123, 456, Labels.empty(), 7, 15.3, Collections.emptyList()), + DoubleSummaryPointData.create( + 321, + 654, + Labels.of("kp", "vp"), + 9, + 18.3, + ImmutableList.of( + ValueAtPercentile.create(0.9, 1.1), + ValueAtPercentile.create(0.99, 12.3)))))) + .containsExactly( + new Sample("full_name_count", Collections.emptyList(), Collections.emptyList(), 7), + new Sample("full_name_sum", Collections.emptyList(), Collections.emptyList(), 15.3), + new Sample("full_name_count", ImmutableList.of("kp"), ImmutableList.of("vp"), 9), + new Sample("full_name_sum", ImmutableList.of("kp"), ImmutableList.of("vp"), 18.3), + new Sample( + "full_name", + ImmutableList.of("kp", "quantile"), + ImmutableList.of("vp", "0.9"), + 1.1), + new Sample( + "full_name", + ImmutableList.of("kp", "quantile"), + ImmutableList.of("vp", "0.99"), + 12.3)); + } + + @Test + void toSamples_HistogramPoints() { + assertThat( + MetricAdapter.toSamples("full_name", MetricDataType.HISTOGRAM, Collections.emptyList())) + .isEmpty(); + + assertThat( + MetricAdapter.toSamples( + "full_name", + MetricDataType.HISTOGRAM, + ImmutableList.of( + DoubleHistogramPointData.create( + 321, + 654, + Labels.of("kp", "vp"), + 18.3, + ImmutableList.of(1.0), + ImmutableList.of(4L, 9L))))) + .containsExactly( + new Sample("full_name_count", ImmutableList.of("kp"), ImmutableList.of("vp"), 13), + new Sample("full_name_sum", ImmutableList.of("kp"), ImmutableList.of("vp"), 18.3), + new Sample( + "full_name_bucket", ImmutableList.of("kp", "le"), ImmutableList.of("vp", "1.0"), 4), + new Sample( + "full_name_bucket", + ImmutableList.of("kp", "le"), + ImmutableList.of("vp", "+Inf"), + 13)); + } + + @Test + void toMetricFamilySamples() { + MetricData metricData = MONOTONIC_CUMULATIVE_DOUBLE_SUM; + assertThat(MetricAdapter.toMetricFamilySamples(metricData)) + .isEqualTo( + new MetricFamilySamples( + "instrument_name", + Collector.Type.COUNTER, + metricData.getDescription(), + ImmutableList.of( + new Sample( + "instrument_name", ImmutableList.of("kp"), ImmutableList.of("vp"), 5)))); + } +} diff --git a/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorTest.java b/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorTest.java new file mode 100644 index 000000000..01dd3e296 --- /dev/null +++ b/opentelemetry-java/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusCollectorTest.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.resources.Resource; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PrometheusCollectorTest { + @Mock MetricProducer metricProducer; + PrometheusCollector prometheusCollector; + + @BeforeEach + void setUp() { + prometheusCollector = + PrometheusCollector.builder().setMetricProducer(metricProducer).buildAndRegister(); + } + + @Test + void registerToDefault() throws IOException { + when(metricProducer.collectAllMetrics()).thenReturn(generateTestData()); + StringWriter stringWriter = new StringWriter(); + TextFormat.write004(stringWriter, CollectorRegistry.defaultRegistry.metricFamilySamples()); + assertThat(stringWriter.toString()) + .isEqualTo( + "# HELP grpc_name_total long_description\n" + + "# TYPE grpc_name_total counter\n" + + "grpc_name_total{kp=\"vp\",} 5.0\n" + + "# HELP http_name_total double_description\n" + + "# TYPE http_name_total counter\n" + + "http_name_total{kp=\"vp\",} 3.5\n"); + } + + private static ImmutableList generateTestData() { + return ImmutableList.of( + MetricData.createLongSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("grpc", "version"), + "grpc.name", + "long_description", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create(123, 456, Labels.of("kp", "vp"), 5)))), + MetricData.createDoubleSum( + Resource.create(Attributes.of(stringKey("kr"), "vr")), + InstrumentationLibraryInfo.create("http", "version"), + "http.name", + "double_description", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create(123, 456, Labels.of("kp", "vp"), 3.5))))); + } +} diff --git a/opentelemetry-java/exporters/zipkin/README.md b/opentelemetry-java/exporters/zipkin/README.md new file mode 100644 index 000000000..51838875b --- /dev/null +++ b/opentelemetry-java/exporters/zipkin/README.md @@ -0,0 +1,61 @@ +# OpenTelemetry - Zipkin Span Exporter + +[![Javadocs][javadoc-image]][javadoc-url] + +This is an OpenTelemetry exporter that sends span data using the [io.zipkin.reporter2:zipkin-reporter](https://github.com/openzipkin/zipkin-reporter-java") library. + +By default, this POSTs json in [Zipkin format](https://zipkin.io/zipkin-api/#/default/post_spans) to +a specified HTTP URL. This could be to a [Zipkin](https://zipkin.io) service, or anything that +consumes the same format. + +You can alternatively use other formats, such as protobuf, or override the `Sender` to use a non-HTTP transport, such as Kafka. + +## Configuration + +The Zipkin span exporter can be configured programmatically. + +An example of simple Zipkin exporter initialization. In this case +spans will be sent to a Zipkin endpoint running on `localhost`: + +```java +ZipkinSpanExporter exporter = + ZipkinSpanExporter.builder() + .setEndpoint("http://localhost/api/v2/spans") + .setServiceName("my-service") + .build(); +``` + +Service name and Endpoint can be also configured via environment variables or system properties. + +```java +// Using environment variables +ZipkinSpanExporter exporter = + ZipkinSpanExporter.builder() + .readEnvironmentVariables() + .build() +``` + +```java +// Using system properties +ZipkinSpanExporter exporter = + ZipkinSpanExporter.builder() + .readSystemProperties() + .build() +``` + +The Zipkin span exporter will look for the following environment variables / system properties: +* `OTEL_ZIPKIN_SERVICE_NAME` / `otel.zipkin.service.name` +* `OTEL_ZIPKIN_ENDPOINT` / `otel.zipkin.endpoint` + + +## Compatibility + +As with the OpenTelemetry SDK itself, this exporter is compatible with Java 8+ and Android API level 24+. + +## Attribution + +The code in this module is based on the [OpenCensus Zipkin exporter][oc-origin] code. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-exporters-zipkin.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-exporters-zipkin +[oc-origin]: https://github.com/census-instrumentation/opencensus-java/ diff --git a/opentelemetry-java/exporters/zipkin/build.gradle.kts b/opentelemetry-java/exporters/zipkin/build.gradle.kts new file mode 100644 index 000000000..5e447d54f --- /dev/null +++ b/opentelemetry-java/exporters/zipkin/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry - Zipkin Exporter" +extra["moduleName"] = "io.opentelemetry.exporter.zipkin" + +dependencies { + compileOnly("com.google.auto.value:auto-value") + + api(project(":sdk:all")) + + api("io.zipkin.reporter2:zipkin-reporter") + + annotationProcessor("com.google.auto.value:auto-value") + + implementation(project(":semconv")) + + implementation("io.zipkin.reporter2:zipkin-sender-okhttp3") + + testImplementation(project(":sdk:testing")) + + testImplementation("io.zipkin.zipkin2:zipkin-junit") +} diff --git a/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporter.java b/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporter.java new file mode 100644 index 000000000..c362cc9fa --- /dev/null +++ b/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporter.java @@ -0,0 +1,270 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributeType; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +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.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import zipkin2.Callback; +import zipkin2.Endpoint; +import zipkin2.Span; +import zipkin2.codec.BytesEncoder; +import zipkin2.reporter.Sender; + +/** + * This class was based on the OpenCensus zipkin exporter code at + * https://github.com/census-instrumentation/opencensus-java/tree/c960b19889de5e4a7b25f90919d28b066590d4f0/exporters/trace/zipkin + */ +public final class ZipkinSpanExporter implements SpanExporter { + public static final String DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans"; + public static final Logger baseLogger = Logger.getLogger(ZipkinSpanExporter.class.getName()); + + private final ThrottlingLogger logger = new ThrottlingLogger(baseLogger); + + static final String OTEL_DROPPED_ATTRIBUTES_COUNT = "otel.dropped_attributes_count"; + static final String OTEL_DROPPED_EVENTS_COUNT = "otel.dropped_events_count"; + static final String OTEL_STATUS_CODE = "otel.status_code"; + static final AttributeKey STATUS_ERROR = stringKey("error"); + + static final String KEY_INSTRUMENTATION_LIBRARY_NAME = "otel.library.name"; + static final String KEY_INSTRUMENTATION_LIBRARY_VERSION = "otel.library.version"; + + private final BytesEncoder encoder; + private final Sender sender; + @Nullable private final InetAddress localAddress; + + ZipkinSpanExporter(BytesEncoder encoder, Sender sender) { + this.encoder = encoder; + this.sender = sender; + localAddress = produceLocalIp(); + } + + /** Logic borrowed from brave.internal.Platform.produceLocalEndpoint */ + static InetAddress produceLocalIp() { + try { + Enumeration nics = NetworkInterface.getNetworkInterfaces(); + while (nics.hasMoreElements()) { + NetworkInterface nic = nics.nextElement(); + Enumeration addresses = nic.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (address.isSiteLocalAddress()) { + return address; + } + } + } + } catch (Exception e) { + // don't crash the caller if there was a problem reading nics. + baseLogger.log(Level.FINE, "error reading nics", e); + } + return null; + } + + Span generateSpan(SpanData spanData) { + Endpoint endpoint = getEndpoint(spanData); + + long startTimestamp = toEpochMicros(spanData.getStartEpochNanos()); + long endTimestamp = toEpochMicros(spanData.getEndEpochNanos()); + + final Span.Builder spanBuilder = + Span.newBuilder() + .traceId(spanData.getTraceId()) + .id(spanData.getSpanId()) + .kind(toSpanKind(spanData)) + .name(spanData.getName()) + .timestamp(toEpochMicros(spanData.getStartEpochNanos())) + .duration(Math.max(1, endTimestamp - startTimestamp)) + .localEndpoint(endpoint); + + if (spanData.getParentSpanContext().isValid()) { + spanBuilder.parentId(spanData.getParentSpanId()); + } + + Attributes spanAttributes = spanData.getAttributes(); + spanAttributes.forEach( + (key, value) -> spanBuilder.putTag(key.getKey(), valueToString(key, value))); + int droppedAttributes = spanData.getTotalAttributeCount() - spanAttributes.size(); + if (droppedAttributes > 0) { + spanBuilder.putTag(OTEL_DROPPED_ATTRIBUTES_COUNT, String.valueOf(droppedAttributes)); + } + + StatusData status = spanData.getStatus(); + + // include status code & error. + if (status.getStatusCode() != StatusCode.UNSET) { + spanBuilder.putTag(OTEL_STATUS_CODE, status.getStatusCode().toString()); + + // add the error tag, if it isn't already in the source span. + if (status.getStatusCode() == StatusCode.ERROR && spanAttributes.get(STATUS_ERROR) == null) { + spanBuilder.putTag(STATUS_ERROR.getKey(), nullToEmpty(status.getDescription())); + } + } + + InstrumentationLibraryInfo instrumentationLibraryInfo = + spanData.getInstrumentationLibraryInfo(); + + if (!instrumentationLibraryInfo.getName().isEmpty()) { + spanBuilder.putTag(KEY_INSTRUMENTATION_LIBRARY_NAME, instrumentationLibraryInfo.getName()); + } + if (instrumentationLibraryInfo.getVersion() != null) { + spanBuilder.putTag( + KEY_INSTRUMENTATION_LIBRARY_VERSION, instrumentationLibraryInfo.getVersion()); + } + + for (EventData annotation : spanData.getEvents()) { + spanBuilder.addAnnotation(toEpochMicros(annotation.getEpochNanos()), annotation.getName()); + } + int droppedEvents = spanData.getTotalRecordedEvents() - spanData.getEvents().size(); + if (droppedEvents > 0) { + spanBuilder.putTag(OTEL_DROPPED_EVENTS_COUNT, String.valueOf(droppedEvents)); + } + + return spanBuilder.build(); + } + + private static String nullToEmpty(String value) { + return value != null ? value : ""; + } + + private Endpoint getEndpoint(SpanData spanData) { + Attributes resourceAttributes = spanData.getResource().getAttributes(); + + // use the service.name from the Resource, if it's been set. + String serviceNameValue = resourceAttributes.get(ResourceAttributes.SERVICE_NAME); + if (serviceNameValue == null) { + serviceNameValue = Resource.getDefault().getAttributes().get(ResourceAttributes.SERVICE_NAME); + } + return Endpoint.newBuilder().serviceName(serviceNameValue).ip(localAddress).build(); + } + + @Nullable + private static Span.Kind toSpanKind(SpanData spanData) { + switch (spanData.getKind()) { + case SERVER: + return Span.Kind.SERVER; + case CLIENT: + return Span.Kind.CLIENT; + case PRODUCER: + return Span.Kind.PRODUCER; + case CONSUMER: + return Span.Kind.CONSUMER; + case INTERNAL: + return null; + } + return null; + } + + private static long toEpochMicros(long epochNanos) { + return NANOSECONDS.toMicros(epochNanos); + } + + private static String valueToString(AttributeKey key, Object attributeValue) { + AttributeType type = key.getType(); + switch (type) { + case STRING: + case BOOLEAN: + case LONG: + case DOUBLE: + return String.valueOf(attributeValue); + case STRING_ARRAY: + case BOOLEAN_ARRAY: + case LONG_ARRAY: + case DOUBLE_ARRAY: + return commaSeparated((List) attributeValue); + } + throw new IllegalStateException("Unknown attribute type: " + type); + } + + private static String commaSeparated(List values) { + StringBuilder builder = new StringBuilder(); + for (Object value : values) { + if (builder.length() != 0) { + builder.append(','); + } + builder.append(value); + } + return builder.toString(); + } + + @Override + public CompletableResultCode export(final Collection spanDataList) { + List encodedSpans = new ArrayList<>(spanDataList.size()); + for (SpanData spanData : spanDataList) { + encodedSpans.add(encoder.encode(generateSpan(spanData))); + } + + final CompletableResultCode result = new CompletableResultCode(); + sender + .sendSpans(encodedSpans) + .enqueue( + new Callback() { + @Override + public void onSuccess(Void value) { + result.succeed(); + } + + @Override + public void onError(Throwable t) { + logger.log(Level.WARNING, "Failed to export spans", t); + result.fail(); + } + }); + return result; + } + + @Override + public CompletableResultCode flush() { + // nothing required here + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + try { + sender.close(); + } catch (IOException e) { + logger.log(Level.WARNING, "Exception while closing the Zipkin Sender instance", e); + } + return CompletableResultCode.ofSuccess(); + } + + /** + * Returns a new Builder for {@link ZipkinSpanExporter}. + * + * @return a new {@link ZipkinSpanExporter}. + */ + public static ZipkinSpanExporterBuilder builder() { + return new ZipkinSpanExporterBuilder(); + } + + // VisibleForTesting + InetAddress getLocalAddressForTest() { + return localAddress; + } +} diff --git a/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java b/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java new file mode 100644 index 000000000..211c2e599 --- /dev/null +++ b/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin; + +import static io.opentelemetry.api.internal.Utils.checkArgument; +import static java.util.Objects.requireNonNull; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import zipkin2.Span; +import zipkin2.codec.BytesEncoder; +import zipkin2.codec.SpanBytesEncoder; +import zipkin2.reporter.Sender; +import zipkin2.reporter.okhttp3.OkHttpSender; + +/** Builder class for {@link ZipkinSpanExporter}. */ +public final class ZipkinSpanExporterBuilder { + private BytesEncoder encoder = SpanBytesEncoder.JSON_V2; + private Sender sender; + private String endpoint = ZipkinSpanExporter.DEFAULT_ENDPOINT; + private long readTimeoutMillis = TimeUnit.SECONDS.toMillis(10); + + /** + * Sets the Zipkin sender. Implements the client side of the span transport. A {@link + * OkHttpSender} is a good default. + * + *

    The {@link Sender#close()} method will be called when the exporter is shut down. + * + * @param sender the Zipkin sender implementation. + * @return this. + */ + public ZipkinSpanExporterBuilder setSender(Sender sender) { + requireNonNull(sender, "sender"); + this.sender = sender; + return this; + } + + /** + * Sets the {@link BytesEncoder}, which controls the format used by the {@link Sender}. Defaults + * to the {@link SpanBytesEncoder#JSON_V2}. + * + * @param encoder the {@code BytesEncoder} to use. + * @return this. + * @see SpanBytesEncoder + */ + public ZipkinSpanExporterBuilder setEncoder(BytesEncoder encoder) { + requireNonNull(encoder, "encoder"); + this.encoder = encoder; + return this; + } + + /** + * Sets the zipkin endpoint. This will use the endpoint to assign a {@link OkHttpSender} instance + * to this builder. + * + * @param endpoint The Zipkin endpoint URL, ex. "http://zipkinhost:9411/api/v2/spans". + * @return this. + * @see OkHttpSender + */ + public ZipkinSpanExporterBuilder setEndpoint(String endpoint) { + requireNonNull(endpoint, "endpoint"); + this.endpoint = endpoint; + return this; + } + + /** + * Sets the maximum time to wait for the export of a batch of spans. If unset, defaults to 10s. + * + * @return this. + * @since 1.2.0 + */ + public ZipkinSpanExporterBuilder setReadTimeout(long timeout, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(timeout >= 0, "timeout must be non-negative"); + this.readTimeoutMillis = unit.toMillis(timeout); + return this; + } + + /** + * Sets the maximum time to wait for the export of a batch of spans. If unset, defaults to 10s. + * + * @return this. + * @since 1.2.0 + */ + public ZipkinSpanExporterBuilder setReadTimeout(Duration timeout) { + requireNonNull(timeout, "timeout"); + setReadTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS); + return this; + } + + /** + * Builds a {@link ZipkinSpanExporter}. + * + * @return a {@code ZipkinSpanExporter}. + */ + public ZipkinSpanExporter build() { + if (sender == null) { + sender = + OkHttpSender.newBuilder().endpoint(endpoint).readTimeout((int) readTimeoutMillis).build(); + } + return new ZipkinSpanExporter(this.encoder, this.sender); + } +} diff --git a/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/package-info.java b/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/package-info.java new file mode 100644 index 000000000..8e03649dd --- /dev/null +++ b/opentelemetry-java/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.zipkin; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterEndToEndHttpTest.java b/opentelemetry-java/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterEndToEndHttpTest.java new file mode 100644 index 000000000..0c06cdcf6 --- /dev/null +++ b/opentelemetry-java/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterEndToEndHttpTest.java @@ -0,0 +1,173 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import zipkin2.Endpoint; +import zipkin2.Span; +import zipkin2.codec.Encoding; +import zipkin2.codec.SpanBytesEncoder; +import zipkin2.junit.ZipkinRule; +import zipkin2.reporter.okhttp3.OkHttpSender; + +/** + * Tests which use Zipkin's {@link ZipkinRule} to verify that the {@link ZipkinSpanExporter} can + * send spans via HTTP to Zipkin's API using supported encodings. + */ +public class ZipkinSpanExporterEndToEndHttpTest { + + 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 String SPAN_NAME = "Recv.helloworld.Greeter.SayHello"; + private static final long START_EPOCH_NANOS = 1505855794_194009601L; + private static final long END_EPOCH_NANOS = 1505855799_465726528L; + private static final long RECEIVED_TIMESTAMP_NANOS = 1505855799_433901068L; + private static final long SENT_TIMESTAMP_NANOS = 1505855799_459486280L; + private static final Attributes attributes = Attributes.empty(); + private static final List annotations = + Collections.unmodifiableList( + Arrays.asList( + EventData.create(RECEIVED_TIMESTAMP_NANOS, "RECEIVED", Attributes.empty()), + EventData.create(SENT_TIMESTAMP_NANOS, "SENT", Attributes.empty()))); + + private static final String ENDPOINT_V1_SPANS = "/api/v1/spans"; + private static final String ENDPOINT_V2_SPANS = "/api/v2/spans"; + private static final String SERVICE_NAME = "myService"; + + @Rule public ZipkinRule zipkin = new ZipkinRule(); + + @Test + public void testExportWithDefaultEncoding() { + ZipkinSpanExporter exporter = + ZipkinSpanExporter.builder().setEndpoint(zipkin.httpUrl() + ENDPOINT_V2_SPANS).build(); + + exportAndVerify(exporter); + } + + @Test + public void testExportAsProtobuf() { + ZipkinSpanExporter exporter = + buildZipkinExporter( + zipkin.httpUrl() + ENDPOINT_V2_SPANS, Encoding.PROTO3, SpanBytesEncoder.PROTO3); + exportAndVerify(exporter); + } + + @Test + public void testExportAsThrift() { + @SuppressWarnings("deprecation") // we have to use the deprecated thrift encoding to test it + ZipkinSpanExporter exporter = + buildZipkinExporter( + zipkin.httpUrl() + ENDPOINT_V1_SPANS, Encoding.THRIFT, SpanBytesEncoder.THRIFT); + exportAndVerify(exporter); + } + + @Test + public void testExportAsJsonV1() { + ZipkinSpanExporter exporter = + buildZipkinExporter( + zipkin.httpUrl() + ENDPOINT_V1_SPANS, Encoding.JSON, SpanBytesEncoder.JSON_V1); + exportAndVerify(exporter); + } + + @Test + public void testExportFailedAsWrongEncoderUsed() { + ZipkinSpanExporter zipkinSpanExporter = + buildZipkinExporter( + zipkin.httpUrl() + ENDPOINT_V2_SPANS, Encoding.JSON, SpanBytesEncoder.PROTO3); + + SpanData spanData = buildStandardSpan().build(); + CompletableResultCode resultCode = zipkinSpanExporter.export(Collections.singleton(spanData)); + + assertThat(resultCode.isSuccess()).isFalse(); + List zipkinSpans = zipkin.getTrace(TRACE_ID); + assertThat(zipkinSpans).isNull(); + } + + private static ZipkinSpanExporter buildZipkinExporter( + String endpoint, Encoding encoding, SpanBytesEncoder encoder) { + return ZipkinSpanExporter.builder() + .setSender(OkHttpSender.newBuilder().endpoint(endpoint).encoding(encoding).build()) + .setEncoder(encoder) + .build(); + } + + /** + * Exports a span, verify that it was received by Zipkin, and check that the span stored by Zipkin + * matches what was sent. + */ + private void exportAndVerify(ZipkinSpanExporter zipkinSpanExporter) { + + SpanData spanData = buildStandardSpan().build(); + CompletableResultCode resultCode = zipkinSpanExporter.export(Collections.singleton(spanData)); + resultCode.join(10, TimeUnit.SECONDS); + + assertThat(resultCode.isSuccess()).isTrue(); + List zipkinSpans = zipkin.getTrace(TRACE_ID); + + assertThat(zipkinSpans).isNotNull(); + assertThat(zipkinSpans.size()).isEqualTo(1); + assertThat(zipkinSpans.get(0)) + .isEqualTo(buildZipkinSpan(zipkinSpanExporter.getLocalAddressForTest())); + } + + private static TestSpanData.Builder buildStandardSpan() { + return TestSpanData.builder() + .setSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())) + .setParentSpanContext( + SpanContext.create( + TRACE_ID, PARENT_SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())) + .setStatus(StatusData.ok()) + .setKind(SpanKind.SERVER) + .setName(SPAN_NAME) + .setStartEpochNanos(START_EPOCH_NANOS) + .setAttributes(attributes) + .setTotalAttributeCount(attributes.size()) + .setTotalRecordedEvents(annotations.size()) + .setEvents(annotations) + .setLinks(Collections.emptyList()) + .setEndEpochNanos(END_EPOCH_NANOS) + .setHasEnded(true) + .setResource(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, SERVICE_NAME))); + } + + private static Span buildZipkinSpan(InetAddress localAddress) { + return Span.newBuilder() + .traceId(TRACE_ID) + .parentId(PARENT_SPAN_ID) + .id(SPAN_ID) + .kind(Span.Kind.SERVER) + .name(SPAN_NAME) + .timestamp(START_EPOCH_NANOS / 1000) + .duration((END_EPOCH_NANOS / 1000) - (START_EPOCH_NANOS / 1000)) + .localEndpoint(Endpoint.newBuilder().serviceName(SERVICE_NAME).ip(localAddress).build()) + .addAnnotation(RECEIVED_TIMESTAMP_NANOS / 1000, "RECEIVED") + .addAnnotation(SENT_TIMESTAMP_NANOS / 1000, "SENT") + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build(); + } +} diff --git a/opentelemetry-java/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java b/opentelemetry-java/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java new file mode 100644 index 000000000..971d247f8 --- /dev/null +++ b/opentelemetry-java/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java @@ -0,0 +1,469 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import zipkin2.Call; +import zipkin2.Callback; +import zipkin2.Endpoint; +import zipkin2.Span; +import zipkin2.codec.SpanBytesEncoder; +import zipkin2.reporter.Sender; + +@ExtendWith(MockitoExtension.class) +class ZipkinSpanExporterTest { + + @Mock private Sender mockSender; + @Mock private SpanBytesEncoder mockEncoder; + @Mock private Call mockZipkinCall; + + 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 Attributes attributes = Attributes.empty(); + private static final List annotations = + Collections.unmodifiableList( + Arrays.asList( + EventData.create(1505855799_433901068L, "RECEIVED", Attributes.empty()), + EventData.create(1505855799_459486280L, "SENT", Attributes.empty()))); + + private final ZipkinSpanExporter exporter = ZipkinSpanExporter.builder().build(); + + @Test + void generateSpan_remoteParent() { + SpanData data = buildStandardSpan().build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + standardZipkinSpanBuilder(Span.Kind.SERVER) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build()); + } + + @Test + void generateSpan_subMicroDurations() { + SpanData data = + buildStandardSpan() + .setStartEpochNanos(1505855794_194009601L) + .setEndEpochNanos(1505855794_194009999L) + .build(); + + Span expected = + standardZipkinSpanBuilder(Span.Kind.SERVER) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .duration(1) + .build(); + assertThat(exporter.generateSpan(data)).isEqualTo(expected); + } + + @Test + void generateSpan_ServerKind() { + SpanData data = buildStandardSpan().setKind(SpanKind.SERVER).build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + standardZipkinSpanBuilder(Span.Kind.SERVER) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build()); + } + + @Test + void generateSpan_ClientKind() { + SpanData data = buildStandardSpan().setKind(SpanKind.CLIENT).build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + standardZipkinSpanBuilder(Span.Kind.CLIENT) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build()); + } + + @Test + void generateSpan_InternalKind() { + SpanData data = buildStandardSpan().setKind(SpanKind.INTERNAL).build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + standardZipkinSpanBuilder(null) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build()); + } + + @Test + void generateSpan_ConsumeKind() { + SpanData data = buildStandardSpan().setKind(SpanKind.CONSUMER).build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + standardZipkinSpanBuilder(Span.Kind.CONSUMER) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build()); + } + + @Test + void generateSpan_ProducerKind() { + SpanData data = buildStandardSpan().setKind(SpanKind.PRODUCER).build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + standardZipkinSpanBuilder(Span.Kind.PRODUCER) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build()); + } + + @Test + void generateSpan_ResourceServiceNameMapping() { + final Resource resource = + Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "super-zipkin-service")); + SpanData data = buildStandardSpan().setResource(resource).build(); + + Endpoint expectedEndpoint = + Endpoint.newBuilder() + .serviceName("super-zipkin-service") + .ip(exporter.getLocalAddressForTest()) + .build(); + Span expectedZipkinSpan = + buildZipkinSpan(Span.Kind.SERVER).toBuilder() + .localEndpoint(expectedEndpoint) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build(); + assertThat(exporter.generateSpan(data)).isEqualTo(expectedZipkinSpan); + } + + @Test + void generateSpan_defaultResourceServiceName() { + SpanData data = buildStandardSpan().setResource(Resource.empty()).build(); + + Endpoint expectedEndpoint = + Endpoint.newBuilder() + .serviceName(Resource.getDefault().getAttributes().get(ResourceAttributes.SERVICE_NAME)) + .ip(exporter.getLocalAddressForTest()) + .build(); + Span expectedZipkinSpan = + buildZipkinSpan(Span.Kind.SERVER).toBuilder() + .localEndpoint(expectedEndpoint) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build(); + assertThat(exporter.generateSpan(data)).isEqualTo(expectedZipkinSpan); + } + + @Test + void generateSpan_WithAttributes() { + Attributes attributes = + Attributes.builder() + .put(stringKey("string"), "string value") + .put(booleanKey("boolean"), false) + .put(longKey("long"), 9999L) + .put(doubleKey("double"), 222.333d) + .put(booleanArrayKey("booleanArray"), Arrays.asList(true, false)) + .put(stringArrayKey("stringArray"), Collections.singletonList("Hello")) + .put(doubleArrayKey("doubleArray"), Arrays.asList(32.33d, -98.3d)) + .put(longArrayKey("longArray"), Arrays.asList(33L, 999L)) + .build(); + SpanData data = + buildStandardSpan() + .setAttributes(attributes) + .setTotalAttributeCount(28) + .setTotalRecordedEvents(3) + .setKind(SpanKind.CLIENT) + .build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + buildZipkinSpan(Span.Kind.CLIENT).toBuilder() + .putTag("string", "string value") + .putTag("boolean", "false") + .putTag("long", "9999") + .putTag("double", "222.333") + .putTag("booleanArray", "true,false") + .putTag("stringArray", "Hello") + .putTag("doubleArray", "32.33,-98.3") + .putTag("longArray", "33,999") + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .putTag(ZipkinSpanExporter.OTEL_DROPPED_ATTRIBUTES_COUNT, "20") + .putTag(ZipkinSpanExporter.OTEL_DROPPED_EVENTS_COUNT, "1") + .build()); + } + + @Test + void generateSpan_WithInstrumentationLibraryInfo() { + SpanData data = + buildStandardSpan() + .setInstrumentationLibraryInfo( + InstrumentationLibraryInfo.create("io.opentelemetry.auto", "1.0.0")) + .setKind(SpanKind.CLIENT) + .build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + buildZipkinSpan(Span.Kind.CLIENT).toBuilder() + .putTag("otel.library.name", "io.opentelemetry.auto") + .putTag("otel.library.version", "1.0.0") + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build()); + } + + @Test + void generateSpan_AlreadyHasHttpStatusInfo() { + Attributes attributeMap = + Attributes.of( + SemanticAttributes.HTTP_STATUS_CODE, 404L, stringKey("error"), "A user provided error"); + SpanData data = + buildStandardSpan() + .setAttributes(attributeMap) + .setKind(SpanKind.CLIENT) + .setStatus(StatusData.error()) + .setTotalAttributeCount(2) + .build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + buildZipkinSpan(Span.Kind.CLIENT).toBuilder() + .clearTags() + .putTag(SemanticAttributes.HTTP_STATUS_CODE.getKey(), "404") + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "ERROR") + .putTag("error", "A user provided error") + .build()); + } + + @Test + void generateSpan_WithRpcTimeoutErrorStatus_WithTimeoutErrorDescription() { + Attributes attributeMap = Attributes.of(SemanticAttributes.RPC_SERVICE, "my service name"); + + String errorMessage = "timeout"; + + SpanData data = + buildStandardSpan() + .setStatus(StatusData.create(StatusCode.ERROR, errorMessage)) + .setAttributes(attributeMap) + .setTotalAttributeCount(1) + .build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + buildZipkinSpan(Span.Kind.SERVER).toBuilder() + .putTag(SemanticAttributes.RPC_SERVICE.getKey(), "my service name") + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "ERROR") + .putTag(ZipkinSpanExporter.STATUS_ERROR.getKey(), errorMessage) + .build()); + } + + @Test + void generateSpan_WithRpcErrorStatus_WithEmptyErrorDescription() { + Attributes attributeMap = Attributes.of(SemanticAttributes.RPC_SERVICE, "my service name"); + + SpanData data = + buildStandardSpan() + .setStatus(StatusData.create(StatusCode.ERROR, "")) + .setAttributes(attributeMap) + .setTotalAttributeCount(1) + .build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + buildZipkinSpan(Span.Kind.SERVER).toBuilder() + .putTag(SemanticAttributes.RPC_SERVICE.getKey(), "my service name") + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "ERROR") + .putTag(ZipkinSpanExporter.STATUS_ERROR.getKey(), "") + .build()); + } + + @Test + void generateSpan_WithRpcUnsetStatus() { + Attributes attributeMap = Attributes.of(SemanticAttributes.RPC_SERVICE, "my service name"); + + SpanData data = + buildStandardSpan() + .setStatus(StatusData.create(StatusCode.UNSET, "")) + .setAttributes(attributeMap) + .setTotalAttributeCount(1) + .build(); + + assertThat(exporter.generateSpan(data)) + .isEqualTo( + buildZipkinSpan(Span.Kind.SERVER).toBuilder() + .putTag(SemanticAttributes.RPC_SERVICE.getKey(), "my service name") + .build()); + } + + @Test + void testExport() { + ZipkinSpanExporter zipkinSpanExporter = new ZipkinSpanExporter(mockEncoder, mockSender); + + byte[] someBytes = new byte[0]; + when(mockEncoder.encode( + standardZipkinSpanBuilder(Span.Kind.SERVER) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build())) + .thenReturn(someBytes); + when(mockSender.sendSpans(Collections.singletonList(someBytes))).thenReturn(mockZipkinCall); + doAnswer( + invocation -> { + Callback callback = invocation.getArgument(0); + callback.onSuccess(null); + return null; + }) + .when(mockZipkinCall) + .enqueue(any()); + + CompletableResultCode resultCode = + zipkinSpanExporter.export(Collections.singleton(buildStandardSpan().build())); + + assertThat(resultCode.isSuccess()).isTrue(); + } + + @Test + void testExport_failed() { + ZipkinSpanExporter zipkinSpanExporter = new ZipkinSpanExporter(mockEncoder, mockSender); + + byte[] someBytes = new byte[0]; + when(mockEncoder.encode( + standardZipkinSpanBuilder(Span.Kind.SERVER) + .putTag(ZipkinSpanExporter.OTEL_STATUS_CODE, "OK") + .build())) + .thenReturn(someBytes); + when(mockSender.sendSpans(Collections.singletonList(someBytes))).thenReturn(mockZipkinCall); + doAnswer( + invocation -> { + Callback callback = invocation.getArgument(0); + callback.onError(new IOException()); + return null; + }) + .when(mockZipkinCall) + .enqueue(any()); + + CompletableResultCode resultCode = + zipkinSpanExporter.export(Collections.singleton(buildStandardSpan().build())); + + assertThat(resultCode.isSuccess()).isFalse(); + } + + @Test + void testCreate() { + ZipkinSpanExporter exporter = ZipkinSpanExporter.builder().setSender(mockSender).build(); + + assertThat(exporter).isNotNull(); + } + + @Test + void testShutdown() throws IOException { + ZipkinSpanExporter exporter = ZipkinSpanExporter.builder().setSender(mockSender).build(); + + exporter.shutdown(); + verify(mockSender).close(); + } + + private static TestSpanData.Builder buildStandardSpan() { + return TestSpanData.builder() + .setSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())) + .setParentSpanContext( + SpanContext.create( + TRACE_ID, PARENT_SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())) + .setResource( + Resource.create( + Attributes.builder().put(ResourceAttributes.SERVICE_NAME, "tweetiebird").build())) + .setStatus(StatusData.ok()) + .setKind(SpanKind.SERVER) + .setName("Recv.helloworld.Greeter.SayHello") + .setStartEpochNanos(1505855794_194009601L) + .setEndEpochNanos(1505855799_465726528L) + .setAttributes(attributes) + .setTotalAttributeCount(attributes.size()) + .setTotalRecordedEvents(annotations.size()) + .setEvents(annotations) + .setLinks(Collections.emptyList()) + .setHasEnded(true); + } + + private Span buildZipkinSpan(Span.Kind kind) { + return standardZipkinSpanBuilder(kind).build(); + } + + private Span.Builder standardZipkinSpanBuilder(Span.Kind kind) { + return Span.newBuilder() + .traceId(TRACE_ID) + .parentId(PARENT_SPAN_ID) + .id(SPAN_ID) + .kind(kind) + .name("Recv.helloworld.Greeter.SayHello") + .timestamp(1505855794000000L + 194009601L / 1000) + .duration((1505855799000000L + 465726528L / 1000) - (1505855794000000L + 194009601L / 1000)) + .localEndpoint( + Endpoint.newBuilder() + .ip(exporter.getLocalAddressForTest()) + .serviceName("tweetiebird") + .build()) + .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED") + .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT"); + } + + @Test + @SuppressWarnings("PreferJavaTimeOverload") + void invalidConfig() { + assertThatThrownBy(() -> ZipkinSpanExporter.builder().setReadTimeout(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("timeout must be non-negative"); + + assertThatThrownBy(() -> ZipkinSpanExporter.builder().setReadTimeout(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + + assertThatThrownBy(() -> ZipkinSpanExporter.builder().setReadTimeout(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("timeout"); + + assertThatThrownBy(() -> ZipkinSpanExporter.builder().setEndpoint(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("endpoint"); + + assertThatThrownBy(() -> ZipkinSpanExporter.builder().setSender(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("sender"); + + assertThatThrownBy(() -> ZipkinSpanExporter.builder().setEncoder(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("encoder"); + } +} diff --git a/opentelemetry-java/extensions/annotations/README.md b/opentelemetry-java/extensions/annotations/README.md new file mode 100644 index 000000000..939a11630 --- /dev/null +++ b/opentelemetry-java/extensions/annotations/README.md @@ -0,0 +1,10 @@ +OpenTelemetry Contrib Annotations +====================================================== + +[![Javadocs][javadoc-image]][javadoc-url] + +This module contains various annotations that can be used by clients of OpenTelemetry API. +Please see [Javadocs][javadoc-url] for more information. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-contrib-auto-annotations.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-contrib-auto-annotations diff --git a/opentelemetry-java/extensions/annotations/build.gradle.kts b/opentelemetry-java/extensions/annotations/build.gradle.kts new file mode 100644 index 000000000..33e3551ba --- /dev/null +++ b/opentelemetry-java/extensions/annotations/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Extension Annotations" +extra["moduleName"] = "io.opentelemetry.extension.annotations" + +dependencies { + api(project(":api:all")) +} diff --git a/opentelemetry-java/extensions/annotations/src/main/java/io/opentelemetry/extension/annotations/WithSpan.java b/opentelemetry-java/extensions/annotations/src/main/java/io/opentelemetry/extension/annotations/WithSpan.java new file mode 100644 index 000000000..ede93cf9b --- /dev/null +++ b/opentelemetry-java/extensions/annotations/src/main/java/io/opentelemetry/extension/annotations/WithSpan.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.annotations; + +import io.opentelemetry.api.trace.SpanKind; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation marks that an execution of this method or constructor should result in a new + * {@link io.opentelemetry.api.trace.Span}. + * + *

    Application developers can use this annotation to signal OpenTelemetry auto-instrumentation + * that a new span should be created whenever marked method is executed. + * + *

    If you are a library developer, then probably you should NOT use this annotation, because it + * is non-functional without the OpenTelemetry auto-instrumentation agent, or some other annotation + * processor. + * + * @see OpenTelemetry + * Auto-Instrumentation + */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithSpan { + /** + * Optional name of the created span. + * + *

    If not specified, an appropriate default name should be created by auto-instrumentation. + * E.g. {@code "className"."method"} + */ + String value() default ""; + + /** Specify the {@link SpanKind} of span to be created. Defaults to {@link SpanKind#INTERNAL}. */ + SpanKind kind() default SpanKind.INTERNAL; +} diff --git a/opentelemetry-java/extensions/annotations/src/main/java/io/opentelemetry/extension/annotations/package-info.java b/opentelemetry-java/extensions/annotations/src/main/java/io/opentelemetry/extension/annotations/package-info.java new file mode 100644 index 000000000..e0bc97a19 --- /dev/null +++ b/opentelemetry-java/extensions/annotations/src/main/java/io/opentelemetry/extension/annotations/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This module contains various annotations that can be used by clients of OpenTelemetry API. They + * don't provide any functionality by themselves, but other modules, e.g. OpenTelemetry + * Auto-Instrumentation can use them to enhance their functionality. + * + *

    Note: If you are a library developer, then you should NOT use this module, because it is + * useless without some kind of annotation processing, such as bytecode manipulation during runtime. + * You cannot guarantee that users of your library will use that in their production system. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.extension.annotations; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/extensions/annotations/src/test/java/io/opentelemetry/extension/annotations/WithSpanUsageExamples.java b/opentelemetry-java/extensions/annotations/src/test/java/io/opentelemetry/extension/annotations/WithSpanUsageExamples.java new file mode 100644 index 000000000..b26a45027 --- /dev/null +++ b/opentelemetry-java/extensions/annotations/src/test/java/io/opentelemetry/extension/annotations/WithSpanUsageExamples.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.annotations; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; + +/** + * This class is not a classical test. It's just a demonstration of possible usages of {@link + * WithSpan} annotation together with some explanations. The goal of this class is to serve as an + * early detection system for inconvenient API and unintended API breakage. + */ +@SuppressWarnings("unused") +public class WithSpanUsageExamples { + + /** + * A new {@link Span} will be created for this method's execution. The span's name will be + * automatically generated by OpenTelemetry auto-instrumentation, probably as + * "WithSpanUsageExamples.method1". + */ + @WithSpan + public void method1() {} + + /** Name of the generated span will be "shinyName". */ + @WithSpan("shinyName") + public void method2() {} + + /** + * A {@link Span} with the default name, and a {@link SpanKind} of {@link SpanKind#CONSUMER} will + * be created for this method. + */ + @WithSpan(kind = SpanKind.CONSUMER) + public void consume() {} +} diff --git a/opentelemetry-java/extensions/aws/build.gradle.kts b/opentelemetry-java/extensions/aws/build.gradle.kts new file mode 100644 index 000000000..2258c0b9d --- /dev/null +++ b/opentelemetry-java/extensions/aws/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry API Extensions for AWS" +extra["moduleName"] = "io.opentelemetry.extension.aws" + +dependencies { + api(project(":api:all")) + compileOnly(project(":sdk-extensions:autoconfigure")) +} diff --git a/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/AwsConfigurablePropagator.java b/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/AwsConfigurablePropagator.java new file mode 100644 index 000000000..773f0684d --- /dev/null +++ b/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/AwsConfigurablePropagator.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.aws; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +/** + * A {@link ConfigurablePropagatorProvider} which allows enabling the {@link AwsXrayPropagator} with + * the propagator name {@code xray}. + */ +public final class AwsConfigurablePropagator implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return AwsXrayPropagator.getInstance(); + } + + @Override + public String getName() { + return "xray"; + } +} diff --git a/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/AwsXrayPropagator.java b/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/AwsXrayPropagator.java new file mode 100644 index 000000000..177db9be0 --- /dev/null +++ b/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/AwsXrayPropagator.java @@ -0,0 +1,326 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.aws; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.baggage.BaggageEntry; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collection; +import java.util.Collections; +import java.util.function.BiConsumer; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * Implementation of the AWS X-Ray Trace Header propagation protocol. See AWS + * Tracing header spec + * + *

    To register the X-Ray propagator together with default propagator when using the SDK: + * + *

    {@code
    + * OpenTelemetrySdk.builder()
    + *   .setPropagators(
    + *     ContextPropagators.create(
    + *         TextMapPropagator.composite(
    + *             W3CTraceContextPropagator.getInstance(),
    + *             AWSXrayPropagator.getInstance())))
    + *    .build();
    + * }
    + */ +public final class AwsXrayPropagator implements TextMapPropagator { + + // Visible for testing + static final String TRACE_HEADER_KEY = "X-Amzn-Trace-Id"; + + private static final Logger logger = Logger.getLogger(AwsXrayPropagator.class.getName()); + + private static final char TRACE_HEADER_DELIMITER = ';'; + private static final char KV_DELIMITER = '='; + + private static final String TRACE_ID_KEY = "Root"; + private static final int TRACE_ID_LENGTH = 35; + private static final String TRACE_ID_VERSION = "1"; + private static final char TRACE_ID_DELIMITER = '-'; + private static final int TRACE_ID_DELIMITER_INDEX_1 = 1; + private static final int TRACE_ID_DELIMITER_INDEX_2 = 10; + private static final int TRACE_ID_FIRST_PART_LENGTH = 8; + + private static final String PARENT_ID_KEY = "Parent"; + private static final int PARENT_ID_LENGTH = 16; + + private static final String SAMPLED_FLAG_KEY = "Sampled"; + private static final int SAMPLED_FLAG_LENGTH = 1; + private static final char IS_SAMPLED = '1'; + private static final char NOT_SAMPLED = '0'; + + private static final Collection FIELDS = Collections.singletonList(TRACE_HEADER_KEY); + + private static final AwsXrayPropagator INSTANCE = new AwsXrayPropagator(); + + private AwsXrayPropagator() { + // singleton + } + + public static AwsXrayPropagator getInstance() { + return INSTANCE; + } + + @Override + public Collection fields() { + return FIELDS; + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null) { + return; + } + if (setter == null) { + return; + } + + Span span = Span.fromContext(context); + if (!span.getSpanContext().isValid()) { + return; + } + + SpanContext spanContext = span.getSpanContext(); + + String otTraceId = spanContext.getTraceId(); + String xrayTraceId = + TRACE_ID_VERSION + + TRACE_ID_DELIMITER + + otTraceId.substring(0, TRACE_ID_FIRST_PART_LENGTH) + + TRACE_ID_DELIMITER + + otTraceId.substring(TRACE_ID_FIRST_PART_LENGTH); + String parentId = spanContext.getSpanId(); + char samplingFlag = spanContext.isSampled() ? IS_SAMPLED : NOT_SAMPLED; + // TODO: Add OT trace state to the X-Ray trace header + + StringBuilder traceHeader = new StringBuilder(); + traceHeader + .append(TRACE_ID_KEY) + .append(KV_DELIMITER) + .append(xrayTraceId) + .append(TRACE_HEADER_DELIMITER) + .append(PARENT_ID_KEY) + .append(KV_DELIMITER) + .append(parentId) + .append(TRACE_HEADER_DELIMITER) + .append(SAMPLED_FLAG_KEY) + .append(KV_DELIMITER) + .append(samplingFlag); + + Baggage baggage = Baggage.fromContext(context); + // Truncate baggage to 256 chars per X-Ray spec. + baggage.forEach( + new BiConsumer() { + + private int baggageWrittenBytes; + + @Override + public void accept(String key, BaggageEntry entry) { + if (key.equals(TRACE_ID_KEY) + || key.equals(PARENT_ID_KEY) + || key.equals(SAMPLED_FLAG_KEY)) { + return; + } + // Size is key/value pair, excludes delimiter. + int size = key.length() + entry.getValue().length() + 1; + if (baggageWrittenBytes + size > 256) { + return; + } + traceHeader + .append(TRACE_HEADER_DELIMITER) + .append(key) + .append(KV_DELIMITER) + .append(entry.getValue()); + baggageWrittenBytes += size; + } + }); + + setter.set(carrier, TRACE_HEADER_KEY, traceHeader.toString()); + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Context.root(); + } + if (getter == null) { + return context; + } + + return getContextFromHeader(context, carrier, getter); + } + + private static Context getContextFromHeader( + Context context, @Nullable C carrier, TextMapGetter getter) { + String traceHeader = getter.get(carrier, TRACE_HEADER_KEY); + if (traceHeader == null || traceHeader.isEmpty()) { + return context; + } + + String traceId = TraceId.getInvalid(); + String spanId = SpanId.getInvalid(); + Boolean isSampled = false; + + BaggageBuilder baggage = null; + int baggageReadBytes = 0; + + int pos = 0; + while (pos < traceHeader.length()) { + int delimiterIndex = traceHeader.indexOf(TRACE_HEADER_DELIMITER, pos); + final String part; + if (delimiterIndex >= 0) { + part = traceHeader.substring(pos, delimiterIndex); + pos = delimiterIndex + 1; + } else { + // Last part. + part = traceHeader.substring(pos); + pos = traceHeader.length(); + } + String trimmedPart = part.trim(); + int equalsIndex = trimmedPart.indexOf(KV_DELIMITER); + if (equalsIndex < 0) { + logger.fine("Error parsing X-Ray trace header. Invalid key value pair: " + part); + return context; + } + + String value = trimmedPart.substring(equalsIndex + 1); + + if (trimmedPart.startsWith(TRACE_ID_KEY)) { + traceId = parseTraceId(value); + } else if (trimmedPart.startsWith(PARENT_ID_KEY)) { + spanId = parseSpanId(value); + } else if (trimmedPart.startsWith(SAMPLED_FLAG_KEY)) { + isSampled = parseTraceFlag(value); + } else if (baggageReadBytes + trimmedPart.length() <= 256) { + if (baggage == null) { + baggage = Baggage.builder(); + } + baggage.put(trimmedPart.substring(0, equalsIndex), value); + baggageReadBytes += trimmedPart.length(); + } + } + if (isSampled == null) { + logger.fine( + "Invalid Sampling flag in X-Ray trace header: '" + + TRACE_HEADER_KEY + + "' with value " + + traceHeader + + "'."); + return context; + } + String heraContext = getter.get(carrier, HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + SpanContext spanContext = + SpanContext.createFromRemoteParent( + StringUtils.padLeft(traceId, TraceId.getLength()), + spanId, + isSampled ? TraceFlags.getSampled() : TraceFlags.getDefault(), + TraceState.getDefault(), HeraContext.wrap(heraContext)); + if (spanContext.isValid()) { + context = context.with(Span.wrap(spanContext)); + } + if (baggage != null) { + context = context.with(baggage.build()); + } + return context; + } + + private static String parseTraceId(String xrayTraceId) { + return (xrayTraceId.length() == TRACE_ID_LENGTH + ? parseSpecTraceId(xrayTraceId) + : parseShortTraceId(xrayTraceId)); + } + + private static String parseSpecTraceId(String xrayTraceId) { + + // Check version trace id version + if (!xrayTraceId.startsWith(TRACE_ID_VERSION)) { + return TraceId.getInvalid(); + } + + // Check delimiters + if (xrayTraceId.charAt(TRACE_ID_DELIMITER_INDEX_1) != TRACE_ID_DELIMITER + || xrayTraceId.charAt(TRACE_ID_DELIMITER_INDEX_2) != TRACE_ID_DELIMITER) { + return TraceId.getInvalid(); + } + + String epochPart = + xrayTraceId.substring(TRACE_ID_DELIMITER_INDEX_1 + 1, TRACE_ID_DELIMITER_INDEX_2); + String uniquePart = xrayTraceId.substring(TRACE_ID_DELIMITER_INDEX_2 + 1, TRACE_ID_LENGTH); + + // X-Ray trace id format is 1-{8 digit hex}-{24 digit hex} + return epochPart + uniquePart; + } + + private static String parseShortTraceId(String xrayTraceId) { + if (xrayTraceId.length() > TRACE_ID_LENGTH) { + return TraceId.getInvalid(); + } + + // Check version trace id version + if (!xrayTraceId.startsWith(TRACE_ID_VERSION)) { + return TraceId.getInvalid(); + } + + // Check delimiters + int firstDelimiter = xrayTraceId.indexOf(TRACE_ID_DELIMITER); + // we don't allow the epoch part to be missing completely + int secondDelimiter = xrayTraceId.indexOf(TRACE_ID_DELIMITER, firstDelimiter + 2); + if (firstDelimiter != TRACE_ID_DELIMITER_INDEX_1 + || secondDelimiter == -1 + || secondDelimiter > TRACE_ID_DELIMITER_INDEX_2) { + return TraceId.getInvalid(); + } + + String epochPart = xrayTraceId.substring(firstDelimiter + 1, secondDelimiter); + String uniquePart = xrayTraceId.substring(secondDelimiter + 1, secondDelimiter + 25); + + // X-Ray trace id format is 1-{at most 8 digit hex}-{24 digit hex} + // epoch part can have leading 0s truncated + return epochPart + uniquePart; + } + + private static String parseSpanId(String xrayParentId) { + if (xrayParentId.length() != PARENT_ID_LENGTH) { + return SpanId.getInvalid(); + } + + return xrayParentId; + } + + @Nullable + private static Boolean parseTraceFlag(String xraySampledFlag) { + if (xraySampledFlag.length() != SAMPLED_FLAG_LENGTH) { + // Returning null as there is no invalid trace flag defined. + return null; + } + + char flag = xraySampledFlag.charAt(0); + if (flag == IS_SAMPLED) { + return true; + } else if (flag == NOT_SAMPLED) { + return false; + } else { + return null; + } + } +} diff --git a/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/package-info.java b/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/package-info.java new file mode 100644 index 000000000..f70fc7eac --- /dev/null +++ b/opentelemetry-java/extensions/aws/src/main/java/io/opentelemetry/extension/aws/package-info.java @@ -0,0 +1,5 @@ +/** OpenTelemetry API extensions for use with AWS. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.extension.aws; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/extensions/aws/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/opentelemetry-java/extensions/aws/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider new file mode 100644 index 000000000..838deff66 --- /dev/null +++ b/opentelemetry-java/extensions/aws/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -0,0 +1 @@ +io.opentelemetry.extension.aws.AwsConfigurablePropagator diff --git a/opentelemetry-java/extensions/aws/src/test/java/io/opentelemetry/extension/aws/AwsXrayPropagatorTest.java b/opentelemetry-java/extensions/aws/src/test/java/io/opentelemetry/extension/aws/AwsXrayPropagatorTest.java new file mode 100644 index 000000000..1ad62173d --- /dev/null +++ b/opentelemetry-java/extensions/aws/src/test/java/io/opentelemetry/extension/aws/AwsXrayPropagatorTest.java @@ -0,0 +1,468 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.aws; + +import static io.opentelemetry.extension.aws.AwsXrayPropagator.TRACE_HEADER_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class AwsXrayPropagatorTest { + + private static final String TRACE_ID = "8a3c60f7d188f8fa79d48a391a778fa6"; + private static final String SPAN_ID = "53995c3f42cd8ad8"; + + private static final TextMapSetter> setter = Map::put; + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + private final AwsXrayPropagator xrayPropagator = AwsXrayPropagator.getInstance(); + + @Test + void inject_SampledContext() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1"); + } + + @Test + void inject_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + } + + @Test + void inject_WithBaggage() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject( + withSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()) + .with( + Baggage.builder() + .put("cat", "meow") + .put("dog", "bark") + .put("Root", "ignored") + .put("Parent", "ignored") + .put("Sampled", "ignored") + .build()), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;" + + "cat=meow;dog=bark"); + } + + @Test + void inject_WithBaggage_LimitTruncates() { + Map carrier = new LinkedHashMap<>(); + // Limit is 256 characters for all baggage. We add a 254-character key/value pair and a + // 3 character key value pair. + String key1 = Stream.generate(() -> "a").limit(252).collect(Collectors.joining()); + String value1 = "a"; // 252 + 1 (=) + 1 = 254 + + String key2 = "b"; + String value2 = "b"; // 1 + 1 (=) + 1 = 3 + + Baggage baggage = Baggage.builder().put(key1, value1).put(key2, value2).build(); + + xrayPropagator.inject( + withSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()) + .with(baggage), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;" + + key1 + + '=' + + value1); + } + + @Test + void inject_WithTraceState() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject( + withSpanContext( + SpanContext.create( + TRACE_ID, + SPAN_ID, + TraceFlags.getDefault(), + TraceState.builder().put("foo", "bar").build()), + Context.current()), + carrier, + setter); + + // TODO: assert trace state when the propagator supports it, for general key/value pairs we are + // mapping with baggage. + assertThat(carrier) + .containsEntry( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + xrayPropagator.inject(null, carrier, setter); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + xrayPropagator.inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } + + @Test + void extract_Nothing() { + // Context remains untouched. + assertThat( + xrayPropagator.extract( + Context.current(), Collections.emptyMap(), getter)) + .isSameAs(Context.current()); + } + + @Test + void extract_SampledContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_DifferentPartOrder() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Parent=53995c3f42cd8ad8;Sampled=1;Root=1-8a3c60f7-d188f8fa79d48a391a778fa6"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_AdditionalFields() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + Context context = xrayPropagator.extract(Context.current(), carrier, getter); + assertThat(getSpanContext(context)) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertThat(Baggage.fromContext(context).getEntryValue("Foo")).isEqualTo("Bar"); + } + + @Test + void extract_Baggage_LimitTruncates() { + // Limit is 256 characters for all baggage. We add a 254-character key/value pair and a + // 3 character key value pair. + String key1 = Stream.generate(() -> "a").limit(252).collect(Collectors.joining()); + String value1 = "a"; // 252 + 1 (=) + 1 = 254 + + String key2 = "b"; + String value2 = "b"; // 1 + 1 (=) + 1 = 3 + + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;" + + key1 + + '=' + + value1 + + ';' + + key2 + + '=' + + value2); + + Context context = xrayPropagator.extract(Context.current(), carrier, getter); + assertThat(getSpanContext(context)) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertThat(Baggage.fromContext(context).getEntryValue(key1)).isEqualTo(value1); + assertThat(Baggage.fromContext(context).getEntryValue(key2)).isNull(); + } + + @Test + void extract_EmptyHeaderValue() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(TRACE_HEADER_KEY, ""); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=abcdefghijklmnopabcdefghijklmnop;Parent=53995c3f42cd8ad8;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa600;Parent=53995c3f42cd8ad8;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=abcdefghijklmnop;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad800;Sampled=0"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled="); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=10220"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags_NonNumeric() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=a"); + + verifyInvalidBehavior(invalidHeaders); + } + + private void verifyInvalidBehavior(Map invalidHeaders) { + Context input = Context.current(); + Context result = xrayPropagator.extract(input, invalidHeaders, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_nullContext() { + assertThat(xrayPropagator.extract(null, Collections.emptyMap(), getter)) + .isSameAs(Context.root()); + } + + @Test + void extract_nullGetter() { + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + assertThat(xrayPropagator.extract(context, Collections.emptyMap(), null)).isSameAs(context); + } + + @Test + void extract_EpochPart_ZeroedSingleDigit() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-0-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + "00000000d188f8fa79d48a391a778fa6", + SPAN_ID, + TraceFlags.getSampled(), + TraceState.getDefault())); + } + + @Test + void extract_EpochPart_TwoChars() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-1a-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + "0000001ad188f8fa79d48a391a778fa6", + SPAN_ID, + TraceFlags.getSampled(), + TraceState.getDefault())); + } + + @Test + void extract_EpochPart_Zeroed() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=1-00000000-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + "00000000d188f8fa79d48a391a778fa6", + SPAN_ID, + TraceFlags.getSampled(), + TraceState.getDefault())); + } + + @Test + void extract_InvalidTraceId_EpochPart_TooLong() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, + "Root=1-8a3c60f711-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTraceId_EpochPart_Empty() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, "Root=1--d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTraceId_EpochPart_Missing() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + TRACE_HEADER_KEY, "Root=1-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTraceId_WrongVersion() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + TRACE_HEADER_KEY, + "Root=2-1a2a3a4a-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar"); + + assertThat(getSpanContext(xrayPropagator.extract(Context.current(), carrier, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + private static Context withSpanContext(SpanContext spanContext, Context context) { + return context.with(Span.wrap(spanContext)); + } + + private static SpanContext getSpanContext(Context context) { + return Span.fromContext(context).getSpanContext(); + } +} diff --git a/opentelemetry-java/extensions/build.gradle.kts b/opentelemetry-java/extensions/build.gradle.kts new file mode 100644 index 000000000..d282c4c54 --- /dev/null +++ b/opentelemetry-java/extensions/build.gradle.kts @@ -0,0 +1,8 @@ +subprojects { + val proj = this + plugins.withId("java") { + configure { + archivesBaseName = "opentelemetry-extension-${proj.name}" + } + } +} diff --git a/opentelemetry-java/extensions/incubator/build.gradle.kts b/opentelemetry-java/extensions/incubator/build.gradle.kts new file mode 100644 index 000000000..3ce6c6004 --- /dev/null +++ b/opentelemetry-java/extensions/incubator/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `java-library` + `maven-publish` + + id("me.champeau.jmh") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry API Incubator" +extra["moduleName"] = "io.opentelemetry.extension.incubator" + +dependencies { + api(project(":api:all")) + + testImplementation(project(":sdk:testing")) +} diff --git a/opentelemetry-java/extensions/incubator/gradle.properties b/opentelemetry-java/extensions/incubator/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/extensions/incubator/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/extensions/incubator/src/jmh/java/io/opentelemetry/extension/incubator/PassThroughPropagatorBenchmark.java b/opentelemetry-java/extensions/incubator/src/jmh/java/io/opentelemetry/extension/incubator/PassThroughPropagatorBenchmark.java new file mode 100644 index 000000000..06b68e6d4 --- /dev/null +++ b/opentelemetry-java/extensions/incubator/src/jmh/java/io/opentelemetry/extension/incubator/PassThroughPropagatorBenchmark.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.extension.incubator.propagation.PassThroughPropagator; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@Threads(value = 1) +@Fork(3) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 20, time = 1) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class PassThroughPropagatorBenchmark { + + private static final SpanContext SPAN_CONTEXT = + SpanContext.create( + "0102030405060708090a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getDefault(), + TraceState.getDefault()); + private static final Map INCOMING; + + static { + Map incoming = new HashMap<>(); + W3CTraceContextPropagator.getInstance() + .inject(Context.root().with(Span.wrap(SPAN_CONTEXT)), incoming, Map::put); + INCOMING = Collections.unmodifiableMap(incoming); + } + + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private static final TextMapPropagator passthrough = + PassThroughPropagator.create(W3CTraceContextPropagator.getInstance().fields()); + + @Benchmark + public void passthrough() { + Context extracted = passthrough.extract(Context.root(), INCOMING, getter); + passthrough.inject(extracted, new HashMap<>(), Map::put); + } + + @Benchmark + public void parse() { + Context extracted = + W3CTraceContextPropagator.getInstance().extract(Context.root(), INCOMING, getter); + W3CTraceContextPropagator.getInstance().inject(extracted, new HashMap<>(), Map::put); + } +} diff --git a/opentelemetry-java/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/PassThroughPropagator.java b/opentelemetry-java/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/PassThroughPropagator.java new file mode 100644 index 000000000..64e6e40b1 --- /dev/null +++ b/opentelemetry-java/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/propagation/PassThroughPropagator.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.propagation; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import javax.annotation.Nullable; + +/** + * A {@link TextMapPropagator} which can be configured with a set of fields, which will be extracted + * and stored in {@link Context}. If the {@link Context} is used again to inject, the values will be + * injected as-is. This {@link TextMapPropagator} is appropriate for a service that does not need to + * participate in telemetry in any way and provides the most efficient way of propagating incoming + * context to outgoing requests. In almost all cases, you will configure this single {@link + * TextMapPropagator} when using {@link + * io.opentelemetry.api.OpenTelemetry#propagating(ContextPropagators)} to create an {@link + * io.opentelemetry.api.OpenTelemetry} that only propagates. Similarly, you will never need this + * when using the OpenTelemetry SDK to enable telemetry. + */ +public final class PassThroughPropagator implements TextMapPropagator { + + private static final ContextKey> EXTRACTED_KEY_VALUES = + ContextKey.named("passthroughpropagator-keyvalues"); + + private final List fields; + + private PassThroughPropagator(List fields) { + this.fields = Collections.unmodifiableList(fields); + } + + /** + * Returns a {@link TextMapPropagator} which will propagate the given {@code fields} from + * extraction to injection. + */ + public static TextMapPropagator create(String... fields) { + requireNonNull(fields, "fields"); + return create(Arrays.asList(fields)); + } + + /** + * Returns a {@link TextMapPropagator} which will propagate the given {@code fields} from + * extraction to injection. + */ + public static TextMapPropagator create(Iterable fields) { + requireNonNull(fields, "fields"); + List fieldsList = + StreamSupport.stream(fields.spliterator(), false) + .map(field -> requireNonNull(field, "field")) + .collect(Collectors.toList()); + if (fieldsList.isEmpty()) { + return TextMapPropagator.noop(); + } + return new PassThroughPropagator(fieldsList); + } + + @Override + public Collection fields() { + return fields; + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + List extracted = context.get(EXTRACTED_KEY_VALUES); + if (extracted != null) { + for (int i = 0; i < extracted.size(); i += 2) { + setter.set(carrier, extracted.get(i), extracted.get(i + 1)); + } + } + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + List extracted = null; + for (String field : fields) { + String value = getter.get(carrier, field); + if (value != null) { + if (extracted == null) { + extracted = new ArrayList<>(); + } + extracted.add(field); + extracted.add(value); + } + } + return extracted != null ? context.with(EXTRACTED_KEY_VALUES, extracted) : context; + } +} diff --git a/opentelemetry-java/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedTracer.java b/opentelemetry-java/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedTracer.java new file mode 100644 index 000000000..1be6bc899 --- /dev/null +++ b/opentelemetry-java/extensions/incubator/src/main/java/io/opentelemetry/extension/incubator/trace/ExtendedTracer.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.trace; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import java.util.concurrent.Callable; + +/** Provides easy mechanisms for wrapping standard Java constructs with an OpenTelemetry Span. */ +public final class ExtendedTracer implements Tracer { + + private final Tracer delegate; + + /** Create a new {@link ExtendedTracer} that wraps the provided Tracer. */ + public static ExtendedTracer create(Tracer delegate) { + return new ExtendedTracer(delegate); + } + + private ExtendedTracer(Tracer delegate) { + this.delegate = delegate; + } + + /** Run the provided {@link Runnable} and wrap with a {@link Span} with the provided name. */ + public void run(String spanName, Runnable runnable) { + Span span = delegate.spanBuilder(spanName).startSpan(); + try (Scope scope = span.makeCurrent()) { + runnable.run(); + } catch (Throwable e) { + span.recordException(e); + throw e; + } finally { + span.end(); + } + } + + /** Call the provided {@link Callable} and wrap with a {@link Span} with the provided name. */ + public T call(String spanName, Callable callable) throws Exception { + Span span = delegate.spanBuilder(spanName).startSpan(); + try (Scope scope = span.makeCurrent()) { + return callable.call(); + } catch (Throwable e) { + span.recordException(e); + throw e; + } finally { + span.end(); + } + } + + @Override + public SpanBuilder spanBuilder(String spanName) { + return delegate.spanBuilder(spanName); + } +} diff --git a/opentelemetry-java/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/propagation/PassThroughPropagatorTest.java b/opentelemetry-java/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/propagation/PassThroughPropagatorTest.java new file mode 100644 index 000000000..d23c9a5ca --- /dev/null +++ b/opentelemetry-java/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/propagation/PassThroughPropagatorTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.propagation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class PassThroughPropagatorTest { + private static final TextMapPropagator propagator = + PassThroughPropagator.create("animal", "food"); + + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + @Test + void propagates() { + Map incoming = new HashMap<>(); + incoming.put("animal", "cat"); + incoming.put("food", "pizza"); + incoming.put("country", "japan"); + + Context context = propagator.extract(Context.root(), incoming, getter); + + Map outgoing = new HashMap<>(); + propagator.inject(context, outgoing, Map::put); + assertThat(outgoing).containsOnly(entry("animal", "cat"), entry("food", "pizza")); + } + + @Test + void noFields() { + TextMapPropagator propagator = PassThroughPropagator.create(); + Map incoming = new HashMap<>(); + incoming.put("animal", "cat"); + incoming.put("food", "pizza"); + incoming.put("country", "japan"); + + Context context = propagator.extract(Context.root(), incoming, getter); + + Map outgoing = new HashMap<>(); + propagator.inject(context, outgoing, Map::put); + assertThat(outgoing).isEmpty(); + } + + @Test + void emptyMap() { + Map incoming = new HashMap<>(); + + Context context = propagator.extract(Context.root(), incoming, getter); + + Map outgoing = new HashMap<>(); + propagator.inject(context, outgoing, Map::put); + assertThat(outgoing).isEmpty(); + } + + @Test + void nullFields() { + assertThatThrownBy(() -> PassThroughPropagator.create((String[]) null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("fields"); + assertThatThrownBy(() -> PassThroughPropagator.create((Iterable) null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("fields"); + assertThatThrownBy(() -> PassThroughPropagator.create("cat", null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("field"); + } +} diff --git a/opentelemetry-java/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/trace/ExtendedTracerTest.java b/opentelemetry-java/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/trace/ExtendedTracerTest.java new file mode 100644 index 000000000..537fb8aa4 --- /dev/null +++ b/opentelemetry-java/extensions/incubator/src/test/java/io/opentelemetry/extension/incubator/trace/ExtendedTracerTest.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.incubator.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ExtendedTracerTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = otelTesting.getOpenTelemetry().getTracer("test"); + + @Test + void runRunnable() { + ExtendedTracer.create(tracer).run("testSpan", () -> Span.current().setAttribute("one", 1)); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + spanDataAssert -> + spanDataAssert + .hasName("testSpan") + .hasAttributes(Attributes.of(AttributeKey.longKey("one"), 1L)))); + } + + @Test + void runRunnable_throws() { + assertThatThrownBy( + () -> + ExtendedTracer.create(tracer) + .run( + "throwingRunnable", + () -> { + Span.current().setAttribute("one", 1); + throw new RuntimeException("failed"); + })) + .isInstanceOf(RuntimeException.class); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + span -> + span.hasName("throwingRunnable") + .hasAttributes(Attributes.of(AttributeKey.longKey("one"), 1L)) + .hasEventsSatisfying( + (events) -> + assertThat(events) + .singleElement() + .satisfies( + eventData -> + assertThat(eventData.getName()) + .isEqualTo("exception"))))); + } + + @Test + void callCallable() throws Exception { + assertThat( + ExtendedTracer.create(tracer) + .call( + "spanCallable", + () -> { + Span.current().setAttribute("one", 1); + return "hello"; + })) + .isEqualTo("hello"); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + spanDataAssert -> + spanDataAssert + .hasName("spanCallable") + .hasAttributes(Attributes.of(AttributeKey.longKey("one"), 1L)))); + } + + @Test + void callCallable_throws() { + assertThatThrownBy( + () -> + ExtendedTracer.create(tracer) + .call( + "throwingCallable", + () -> { + Span.current().setAttribute("one", 1); + throw new RuntimeException("failed"); + })) + .isInstanceOf(RuntimeException.class); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + spanDataAssert -> + spanDataAssert + .hasName("throwingCallable") + .hasAttributes(Attributes.of(AttributeKey.longKey("one"), 1L)))); + } +} diff --git a/opentelemetry-java/extensions/kotlin/build.gradle.kts b/opentelemetry-java/extensions/kotlin/build.gradle.kts new file mode 100644 index 000000000..931286b53 --- /dev/null +++ b/opentelemetry-java/extensions/kotlin/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + `maven-publish` + + id("me.champeau.jmh") + id("org.jetbrains.kotlin.jvm") + id("org.unbroken-dome.test-sets") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Kotlin Extensions" +extra["moduleName"] = "io.opentelemetry.extension.kotlin" + +testSets { + create("testStrictContext") +} + +dependencies { + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + + api(project(":api:all")) + + compileOnly("org.jetbrains.kotlin:kotlin-stdlib-common") + compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + + testImplementation(project(":sdk:testing")) + testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") +} + +tasks { + withType(KotlinCompile::class) { + kotlinOptions { + jvmTarget = "1.8" + } + } + + // We don't have any public Java classes + named("javadoc") { + enabled = false + } + + named("testStrictContext") { + jvmArgs("-Dio.opentelemetry.context.enableStrictContext=true") + } +} diff --git a/opentelemetry-java/extensions/kotlin/src/main/java/io/opentelemetry/extension/kotlin/KotlinContextElement.java b/opentelemetry-java/extensions/kotlin/src/main/java/io/opentelemetry/extension/kotlin/KotlinContextElement.java new file mode 100644 index 000000000..60b7645af --- /dev/null +++ b/opentelemetry-java/extensions/kotlin/src/main/java/io/opentelemetry/extension/kotlin/KotlinContextElement.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.kotlin; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import kotlin.coroutines.CoroutineContext; +import kotlin.jvm.functions.Function2; +import kotlinx.coroutines.ThreadContextElement; +import org.jetbrains.annotations.Nullable; + +/** + * {@link ThreadContextElement} for synchronizing a {@link Context} across coroutine suspension and + * resumption. Implemented in Java instead of Kotlin to allow usage in auto-instrumentation where + * there is an outstanding Kotlin bug preventing it https://youtrack.jetbrains.com/issue/KT-20869. + */ +class KotlinContextElement implements ThreadContextElement { + + static final CoroutineContext.Key KEY = + new CoroutineContext.Key() {}; + + private final Context otelContext; + + KotlinContextElement(Context otelContext) { + this.otelContext = otelContext; + } + + Context getContext() { + return otelContext; + } + + @Override + public CoroutineContext.Key getKey() { + return KEY; + } + + @Override + @SuppressWarnings("MustBeClosedChecker") + public Scope updateThreadContext(CoroutineContext coroutineContext) { + return otelContext.makeCurrent(); + } + + @Override + public void restoreThreadContext(CoroutineContext coroutineContext, Scope scope) { + scope.close(); + } + + @Override + public CoroutineContext plus(CoroutineContext coroutineContext) { + return CoroutineContext.DefaultImpls.plus(this, coroutineContext); + } + + @Override + public R fold( + R initial, Function2 operation) { + return CoroutineContext.Element.DefaultImpls.fold(this, initial, operation); + } + + @Nullable + @Override + public E get(CoroutineContext.Key key) { + return CoroutineContext.Element.DefaultImpls.get(this, key); + } + + @Override + public CoroutineContext minusKey(CoroutineContext.Key key) { + return CoroutineContext.Element.DefaultImpls.minusKey(this, key); + } +} diff --git a/opentelemetry-java/extensions/kotlin/src/main/java/io/opentelemetry/extension/kotlin/package-info.java b/opentelemetry-java/extensions/kotlin/src/main/java/io/opentelemetry/extension/kotlin/package-info.java new file mode 100644 index 000000000..de1825a77 --- /dev/null +++ b/opentelemetry-java/extensions/kotlin/src/main/java/io/opentelemetry/extension/kotlin/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.extension.kotlin; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/extensions/kotlin/src/main/kotlin/io/opentelemetry/extension/kotlin/ContextExtensions.kt b/opentelemetry-java/extensions/kotlin/src/main/kotlin/io/opentelemetry/extension/kotlin/ContextExtensions.kt new file mode 100644 index 000000000..b5050ff54 --- /dev/null +++ b/opentelemetry-java/extensions/kotlin/src/main/kotlin/io/opentelemetry/extension/kotlin/ContextExtensions.kt @@ -0,0 +1,32 @@ +package io.opentelemetry.extension.kotlin + +import io.opentelemetry.context.Context +import io.opentelemetry.context.ImplicitContextKeyed +import kotlin.coroutines.CoroutineContext + +/** + * Returns a [CoroutineContext] which will make this [Context] current when resuming a coroutine + * and restores the previous [Context] on suspension. + */ +fun Context.asContextElement(): CoroutineContext { + return KotlinContextElement(this) +} + +/** + * Returns a [CoroutineContext] which will make this [ImplicitContextKeyed] current when resuming a + * coroutine and restores the previous [Context] on suspension. + */ +fun ImplicitContextKeyed.asContextElement(): CoroutineContext { + return KotlinContextElement(Context.current().with(this)) +} + +/** + * Returns the [Context] in this [CoroutineContext] if present, or the root otherwise. + */ +fun CoroutineContext.getOpenTelemetryContext(): Context { + val element = get(KotlinContextElement.KEY) + if (element is KotlinContextElement) { + return element.context + } + return Context.root() +} diff --git a/opentelemetry-java/extensions/kotlin/src/test/kotlin/io/opentelemetry/extension/kotlin/KotlinCoroutinesTest.kt b/opentelemetry-java/extensions/kotlin/src/test/kotlin/io/opentelemetry/extension/kotlin/KotlinCoroutinesTest.kt new file mode 100644 index 000000000..8c21fcc00 --- /dev/null +++ b/opentelemetry-java/extensions/kotlin/src/test/kotlin/io/opentelemetry/extension/kotlin/KotlinCoroutinesTest.kt @@ -0,0 +1,96 @@ +package io.opentelemetry.extension.kotlin + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.context.ContextKey +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension +import kotlinx.coroutines.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class KotlinCoroutinesTest { + + companion object { + private val ANIMAL: ContextKey = ContextKey.named("animal") + + @JvmField + @RegisterExtension + val otelTesting = OpenTelemetryExtension.create() + } + + @Test + fun runWithContext() { + val context1 = Context.root().with(ANIMAL, "cat") + assertThat(Context.current().get(ANIMAL)).isNull() + runBlocking(Dispatchers.Default + context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + assertThat(coroutineContext.getOpenTelemetryContext()).isSameAs(Context.current()) + + withContext(context1.with(ANIMAL, "dog").asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("dog") + } + + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + + async(Dispatchers.IO) { + // Child coroutine inherits context automatically. + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + }.await() + + coroutineScope { + // Child coroutine inherits context automatically. + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + } + + CoroutineScope(Dispatchers.IO).async { + // Non-child coroutine does not inherit context automatically. + assertThat(Context.current().get(ANIMAL)).isNull() + }.await() + } + } + + @Test + fun runWithSpan() { + val span = otelTesting.openTelemetry.getTracer("test").spanBuilder("test") + .startSpan() + assertThat(Span.current()).isEqualTo(Span.getInvalid()) + runBlocking(Dispatchers.Default + span.asContextElement()) { + assertThat(Span.current()).isEqualTo(span) + } + } + + @Test + fun getOpenTelemetryContextOutsideOfContext() { + runBlocking(Dispatchers.Default) { + assertThat(Context.root()).isSameAs(coroutineContext.getOpenTelemetryContext()) + } + } + + // Check whether concurrent coroutines leak context + @Test + fun stressTest() { + val context1 = Context.root().with(ANIMAL, "cat") + runBlocking(context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + for (i in 0 until 100) { + GlobalScope.launch { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + withContext(context1.with(ANIMAL, "dog").asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("dog") + delay(10) + assertThat(Context.current().get(ANIMAL)).isEqualTo("dog") + } + } + GlobalScope.launch { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + withContext(context1.with(ANIMAL, "koala").asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("koala") + delay(10) + assertThat(Context.current().get(ANIMAL)).isEqualTo("koala") + } + } + } + } + } +} diff --git a/opentelemetry-java/extensions/kotlin/src/testStrictContext/kotlin/io/opentelemetry/extension/kotlin/StrictContextWithCoroutinesTest.kt b/opentelemetry-java/extensions/kotlin/src/testStrictContext/kotlin/io/opentelemetry/extension/kotlin/StrictContextWithCoroutinesTest.kt new file mode 100644 index 000000000..df4ef630b --- /dev/null +++ b/opentelemetry-java/extensions/kotlin/src/testStrictContext/kotlin/io/opentelemetry/extension/kotlin/StrictContextWithCoroutinesTest.kt @@ -0,0 +1,148 @@ +package io.opentelemetry.extension.kotlin + +import io.opentelemetry.context.Context +import io.opentelemetry.context.ContextKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class StrictContextWithCoroutinesTest { + + companion object { + private val ANIMAL: ContextKey = ContextKey.named("animal") + } + + @Test + fun noMakeCurrentSucceeds() { + val context1 = Context.root().with(ANIMAL, "cat") + assertThat(Context.current().get(ANIMAL)).isNull() + runBlocking(Dispatchers.Default + context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + } + } + + @Test + fun noMakeCurrentNestedContextSucceeds() { + val context1 = Context.root().with(ANIMAL, "cat") + assertThat(Context.current().get(ANIMAL)).isNull() + runBlocking(Dispatchers.Default + context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + withContext(Context.current().with(ANIMAL, "dog").asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("dog") + } + } + } + + @Test + fun makeCurrentInNormalFunctionSucceeds() { + assertThat(Context.current().get(ANIMAL)).isNull() + nonSuspendingContextFunction("dog") + } + + @Test + fun makeCurrentInTopLevelCoroutineFails() { + val context1 = Context.root().with(ANIMAL, "cat") + assertThat(Context.current().get(ANIMAL)).isNull() + assertThatThrownBy { + runBlocking(Dispatchers.Default + context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + Context.current().with(ANIMAL, "dog").makeCurrent().use { + assertThat(Context.current().get(ANIMAL)).isEqualTo("dog") + } + } + }.isInstanceOf(AssertionError::class.java) + } + + @Test + fun makeCurrentInNestedCoroutineFails() { + val context1 = Context.root().with(ANIMAL, "cat") + assertThat(Context.current().get(ANIMAL)).isNull() + assertThatThrownBy { + runBlocking(Dispatchers.Default + context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + runBlocking(Dispatchers.Default) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + Context.current().with(ANIMAL, "dog").makeCurrent().use { + assertThat(Context.current().get(ANIMAL)).isEqualTo("dog") + } + } + } + }.isInstanceOf(AssertionError::class.java) + } + + @Test + fun makeCurrentInSuspendingFunctionFails() { + val context1 = Context.root().with(ANIMAL, "cat") + assertThat(Context.current().get(ANIMAL)).isNull() + assertThatThrownBy { + runBlocking(Dispatchers.Default + context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + suspendingFunctionMakeCurrentWithAutoClose("dog") + } + }.isInstanceOf(AssertionError::class.java) + } + + @Test + fun makeCurrentInSuspendingFunctionWithManualCloseFails() { + val context1 = Context.root().with(ANIMAL, "cat") + assertThat(Context.current().get(ANIMAL)).isNull() + assertThatThrownBy { + runBlocking(Dispatchers.Default + context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + suspendingFunctionMakeCurrentWithManualClose("dog") + } + }.isInstanceOf(AssertionError::class.java) + } + + @Test + fun noMakeCurrentInSuspendingFunctionSucceeds() { + val context1 = Context.root().with(ANIMAL, "cat") + assertThat(Context.current().get(ANIMAL)).isNull() + runBlocking(Dispatchers.Default + context1.asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") + suspendingFunctionContextElement("dog") + } + } + + // makeCurrent in non-suspending function is ok, the thread is guaranteed not to switch out from + // under you. + fun nonSuspendingContextFunction(animal: String) { + Context.current().with(ANIMAL, animal).makeCurrent().use { + assertThat(Context.current().get(ANIMAL)).isEqualTo(animal) + } + } + suspend fun suspendingFunctionMakeCurrentWithAutoClose(animal: String) { + Context.current().with(ANIMAL, animal).makeCurrent().use { + assertThat(Context.current().get(ANIMAL)).isEqualTo(animal) + delay(10) + // The value of ANIMAL here is undefined - it may still be the original thread with + // the ThreadLocal set correctly, or a completely different one with a different value. + // So there's nothing we can assert here, and is precisely why we forbid makeCurrent in + // suspending functions. + } + } + + suspend fun suspendingFunctionMakeCurrentWithManualClose(animal: String) { + val scope = Context.current().with(ANIMAL, animal).makeCurrent() + assertThat(Context.current().get(ANIMAL)).isEqualTo(animal) + delay(10) + // The value of ANIMAL here is undefined - it may still be the original thread with + // the ThreadLocal set correctly, or a completely different one with a different value. + // So there's nothing we can assert here, and is precisely why we forbid makeCurrent in + // suspending functions. + scope.close() + } + + suspend fun suspendingFunctionContextElement(animal: String) { + withContext(Context.current().with(ANIMAL, animal).asContextElement()) { + assertThat(Context.current().get(ANIMAL)).isEqualTo(animal) + delay(10) + assertThat(Context.current().get(ANIMAL)).isEqualTo(animal) + } + } + +} diff --git a/opentelemetry-java/extensions/noop-api/build.gradle.kts b/opentelemetry-java/extensions/noop-api/build.gradle.kts new file mode 100644 index 000000000..cafbb7482 --- /dev/null +++ b/opentelemetry-java/extensions/noop-api/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java-library") + id("maven-publish") + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Noop API" +extra["moduleName"] = "io.opentelemetry.extension.noopapi" + +dependencies { + api(project(":api:all")) +} diff --git a/opentelemetry-java/extensions/noop-api/gradle.properties b/opentelemetry-java/extensions/noop-api/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/extensions/noop-api/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopContextStorageProvider.java b/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopContextStorageProvider.java new file mode 100644 index 000000000..536208e1e --- /dev/null +++ b/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopContextStorageProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.noopapi; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.ContextStorageProvider; +import io.opentelemetry.context.Scope; +import javax.annotation.Nullable; + +/** + * A {@link ContextStorageProvider} that returns a {@link ContextStorage} which is completely no-op. + */ +public class NoopContextStorageProvider implements ContextStorageProvider { + + /** Returns a no-op context storage. */ + @Override + public ContextStorage get() { + return NoopContextStorage.INSTANCE; + } + + enum NoopContextStorage implements ContextStorage { + INSTANCE; + + @Override + public Scope attach(Context toAttach) { + return Scope.noop(); + } + + @Override + public Context current() { + return NoopContext.INSTANCE; + } + } + + enum NoopContext implements Context { + INSTANCE; + + @Nullable + @Override + public V get(ContextKey key) { + return null; + } + + @Override + public Context with(ContextKey k1, V v1) { + return this; + } + } +} diff --git a/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopOpenTelemetry.java b/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopOpenTelemetry.java new file mode 100644 index 000000000..04d56af72 --- /dev/null +++ b/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopOpenTelemetry.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.noopapi; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.propagation.ContextPropagators; + +/** + * An implementation of {@link OpenTelemetry} that is completely no-op. Unlike {@link + * OpenTelemetry#noop()}, this implementation does not support in-process context propagation at + * all. This means that no objects are allocated nor {@link ThreadLocal}s used in an application + * using this implementation. This can be a good option for use in frameworks shared across a large + * number of servers to introduce instrumentation without forcing overhead on any users of the + * framework. If such overhead is not a concern, always use either {@link OpenTelemetry#noop()}, + * {@link OpenTelemetry#propagating(ContextPropagators)}, or the OpenTelemetry SDK. + * + *

    The following code will fail because context is not mounted. + * + *

    {@code
    + * try (Scope ignored = Context.current().with(Span.wrap(VALID_SPAN_CONTEXT).makeCurrent()) {
    + *   assert Span.current().spanContext().equals(VALID_SPAN_CONTEXT);
    + * }
    + * }
    + * + *

    In most cases when instrumenting a library, the above pattern does not happen because {@link + * io.opentelemetry.api.trace.Span#wrap(SpanContext)} is primarily for use in remote propagators. + * The common pattern looks like + * + *

    {@code
    + * Span span = tracer.spanBuilder().setAttribute(...).startSpan();
    + * try (Scope ignored = Context.current().with(span).makeCurrent()) {
    + *   assert Span.current().spanContext().equals(SpanContext.getInvalid());
    + * }
    + * }
    + * + *

    The above will succeed both with the {@linkplain OpenTelemetry#noop() default implementation} + * and this one, but with this implementation there will be no overhead at all. + */ +public class NoopOpenTelemetry implements OpenTelemetry { + + private static final OpenTelemetry INSTANCE = new NoopOpenTelemetry(); + + public static OpenTelemetry getInstance() { + return INSTANCE; + } + + @Override + public TracerProvider getTracerProvider() { + return NoopTracerProvider.INSTANCE; + } + + @Override + public ContextPropagators getPropagators() { + return ContextPropagators.noop(); + } + + private NoopOpenTelemetry() {} +} diff --git a/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopTracerProvider.java b/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopTracerProvider.java new file mode 100644 index 000000000..4a0e41382 --- /dev/null +++ b/opentelemetry-java/extensions/noop-api/src/main/java/io/opentelemetry/extension/noopapi/NoopTracerProvider.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.noopapi; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Context; +import java.util.concurrent.TimeUnit; + +enum NoopTracerProvider implements TracerProvider { + INSTANCE; + + @Override + public Tracer get(String instrumentationName) { + return NoopTracer.INSTANCE; + } + + @Override + public Tracer get(String instrumentationName, String instrumentationVersion) { + return NoopTracer.INSTANCE; + } + + enum NoopTracer implements Tracer { + INSTANCE; + + @Override + public SpanBuilder spanBuilder(String spanName) { + return NoopSpanBuilder.INSTANCE; + } + } + + enum NoopSpanBuilder implements SpanBuilder { + INSTANCE; + + @Override + public SpanBuilder setParent(Context context) { + return this; + } + + @Override + public SpanBuilder setNoParent() { + return this; + } + + @Override + public SpanBuilder addLink(SpanContext spanContext) { + return this; + } + + @Override + public SpanBuilder addLink(SpanContext spanContext, Attributes attributes) { + return this; + } + + @Override + public SpanBuilder setAttribute(String key, String value) { + return this; + } + + @Override + public SpanBuilder setAttribute(String key, long value) { + return this; + } + + @Override + public SpanBuilder setAttribute(String key, double value) { + return this; + } + + @Override + public SpanBuilder setAttribute(String key, boolean value) { + return this; + } + + @Override + public SpanBuilder setAttribute(AttributeKey key, T value) { + return this; + } + + @Override + public SpanBuilder setSpanKind(SpanKind spanKind) { + return this; + } + + @Override + public SpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { + return this; + } + + @Override + public Span startSpan() { + return Span.getInvalid(); + } + } +} diff --git a/opentelemetry-java/extensions/noop-api/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider b/opentelemetry-java/extensions/noop-api/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider new file mode 100644 index 000000000..978ff78ee --- /dev/null +++ b/opentelemetry-java/extensions/noop-api/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider @@ -0,0 +1 @@ +io.opentelemetry.extension.noopapi.NoopContextStorageProvider diff --git a/opentelemetry-java/extensions/trace-propagators/README.md b/opentelemetry-java/extensions/trace-propagators/README.md new file mode 100644 index 000000000..5a328451d --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/README.md @@ -0,0 +1,23 @@ +OpenTelemetry Contrib Trace Propagators +====================================================== + +[![Javadocs][javadoc-image]][javadoc-url] + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-contrib-trace-propagators.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-contrib-trace-propagators + +This repository provides several +[trace propagators](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/context/api-propagators.md), +used to propagate context across a distributed trace. + +OpenTelemetry Java provides first-party support for +[B3 (OpenZipkin)](https://github.com/openzipkin/b3-propagation) and +[Jaeger](https://github.com/jaegertracing/jaeger) propagators. Issues with those propagators +should be filed against this repo. + +--- +#### Running micro-benchmarks +From the root of the repo run `./gradlew clean :opentelemetry-extension-trace-propagators:jmh` +to run all the benchmarks +or run `./gradlew clean :opentelemetry-extension-trace-propagators:jmh -PjmhIncludeSingleClass=` +to run a specific benchmark class. diff --git a/opentelemetry-java/extensions/trace-propagators/build.gradle.kts b/opentelemetry-java/extensions/trace-propagators/build.gradle.kts new file mode 100644 index 000000000..67683e5c5 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") + id("me.champeau.jmh") +} + +description = "OpenTelemetry Extension : Trace Propagators" +extra["moduleName"] = "io.opentelemetry.extension.trace.propagation" + +dependencies { + api(project(":api:all")) + + compileOnly(project(":sdk-extensions:autoconfigure")) + + testImplementation("io.jaegertracing:jaeger-client") + testImplementation("com.google.guava:guava") + + jmhImplementation(project(":extensions:aws")) +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/jmh/java/io/opentelemetry/extension/trace/propagation/PropagatorContextExtractBenchmark.java b/opentelemetry-java/extensions/trace-propagators/src/jmh/java/io/opentelemetry/extension/trace/propagation/PropagatorContextExtractBenchmark.java new file mode 100644 index 000000000..295239f99 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/jmh/java/io/opentelemetry/extension/trace/propagation/PropagatorContextExtractBenchmark.java @@ -0,0 +1,315 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +public class PropagatorContextExtractBenchmark { + + private PropagatorContextExtractBenchmark() {} + + /** + * Abstract class containing common setup and teardown logic along with a benchmark to measure + * extracting propagated trace context. Implementing subclasses will provide the sample headers + * and the actual call to the propagator. + */ + @State(Scope.Thread) + public abstract static class AbstractContextExtractBenchmark { + + private final Map carrier = new HashMap<>(); + private Integer iteration = 0; + + @Setup + public void setup() { + carrier.putAll(getHeaders().get(0)); + } + + @Benchmark + @Measurement(iterations = 15, time = 1) + @Warmup(iterations = 5, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @BenchmarkMode(Mode.AverageTime) + @Fork(1) + public Span measureExtract() { + return Span.fromContext(doExtract()); + } + + protected abstract Context doExtract(); + + protected abstract List> getHeaders(); + + Map getCarrier() { + return carrier; + } + + @TearDown(Level.Iteration) + public void tearDown() { + this.carrier.putAll(getHeaders().get(++iteration % getHeaders().size())); + } + } + + /** Benchmark for extracting context from Jaeger headers. */ + public static class JaegerContextExtractBenchmark extends AbstractContextExtractBenchmark { + + private static final List> traceHeaders = + Arrays.asList( + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "905734c59b913b4a905734c59b913b4a:9909983295041501:0:1"), + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "21196a77f299580e21196a77f299580e:993a97ee3691eb26:0:0"), + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "2e7d0ad2390617702e7d0ad239061770:d49582a2de984b86:0:1"), + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "905734c59b913b4a905734c59b913b4a:776ff807b787538a:0:0"), + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "68ec932c33b3f2ee68ec932c33b3f2ee:68ec932c33b3f2ee:0:0")); + + private final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private final JaegerPropagator jaegerPropagator = JaegerPropagator.getInstance(); + + @Override + protected Context doExtract() { + return jaegerPropagator.extract(Context.current(), getCarrier(), getter); + } + + @Override + protected List> getHeaders() { + return traceHeaders; + } + } + + /** Benchmark for extracting context from Jaeger headers which are url encoded. */ + public static class JaegerUrlEncodedContextExtractBenchmark + extends AbstractContextExtractBenchmark { + + private static final List> traceHeaders = + Arrays.asList( + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "905734c59b913b4a905734c59b913b4a%3A9909983295041501%3A0%3A1"), + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "21196a77f299580e21196a77f299580e%3A993a97ee3691eb26%3A0%3A0"), + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "2e7d0ad2390617702e7d0ad239061770%3Ad49582a2de984b86%3A0%3A1"), + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "905734c59b913b4a905734c59b913b4a%3A776ff807b787538a%3A0%3A0"), + Collections.singletonMap( + JaegerPropagator.PROPAGATION_HEADER, + "68ec932c33b3f2ee68ec932c33b3f2ee%3A68ec932c33b3f2ee%3A0%3A0")); + + private final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private final JaegerPropagator jaegerPropagator = JaegerPropagator.getInstance(); + + @Override + protected Context doExtract() { + return jaegerPropagator.extract(Context.current(), getCarrier(), getter); + } + + @Override + protected List> getHeaders() { + return traceHeaders; + } + } + + /** Benchmark for extracting context from a single B3 header. */ + public static class B3SingleHeaderContextExtractBenchmark + extends AbstractContextExtractBenchmark { + + private static final List> traceHeaders = + Arrays.asList( + Collections.singletonMap( + B3Propagator.COMBINED_HEADER, + "905734c59b913b4a905734c59b913b4a-9909983295041501-1"), + Collections.singletonMap( + B3Propagator.COMBINED_HEADER, + "21196a77f299580e21196a77f299580e-993a97ee3691eb26-0"), + Collections.singletonMap( + B3Propagator.COMBINED_HEADER, + "2e7d0ad2390617702e7d0ad239061770-d49582a2de984b86-1"), + Collections.singletonMap( + B3Propagator.COMBINED_HEADER, + "905734c59b913b4a905734c59b913b4a-776ff807b787538a-0"), + Collections.singletonMap( + B3Propagator.COMBINED_HEADER, + "68ec932c33b3f2ee68ec932c33b3f2ee-68ec932c33b3f2ee-0")); + + private final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private final B3Propagator b3Propagator = B3Propagator.injectingSingleHeader(); + + @Override + protected Context doExtract() { + return b3Propagator.extract(Context.current(), getCarrier(), getter); + } + + @Override + protected List> getHeaders() { + return traceHeaders; + } + } + + /** Benchmark for extracting context from multiple B3 headers. */ + public static class B3MultipleHeaderContextExtractBenchmark + extends AbstractContextExtractBenchmark { + + private static final List> traceHeaders; + + static { + traceHeaders = + Arrays.asList( + createHeaders("905734c59b913b4a905734c59b913b4a", "9909983295041501", "1"), + createHeaders("21196a77f299580e21196a77f299580e", "993a97ee3691eb26", "0"), + createHeaders("2e7d0ad2390617702e7d0ad239061770", "d49582a2de984b86", "1"), + createHeaders("905734c59b913b4a905734c59b913b4a", "776ff807b787538a", "0"), + createHeaders("68ec932c33b3f2ee68ec932c33b3f2ee", "68ec932c33b3f2ee", "0")); + } + + private static Map createHeaders( + String traceId, String spanId, String sampled) { + Map headers = new HashMap<>(); + headers.put(B3Propagator.TRACE_ID_HEADER, traceId); + headers.put(B3Propagator.SPAN_ID_HEADER, spanId); + headers.put(B3Propagator.SAMPLED_HEADER, sampled); + return headers; + } + + private final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private final B3Propagator b3Propagator = B3Propagator.injectingSingleHeader(); + + @Override + protected Context doExtract() { + return b3Propagator.extract(Context.current(), getCarrier(), getter); + } + + @Override + protected List> getHeaders() { + return traceHeaders; + } + } + + /** Benchmark for extracting context from AWS X-Ray trace header. */ + public static class AwsXrayHeaderContextExtractBenchmark extends AbstractContextExtractBenchmark { + + private static final List> traceHeaders = + Arrays.asList( + Collections.singletonMap( + "X-Amzn-Trace-Id", + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1"), + Collections.singletonMap( + "X-Amzn-Trace-Id", + "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"), + Collections.singletonMap( + "X-Amzn-Trace-Id", + "Parent=53995c3f42cd8ad8;Sampled=1;Root=1-8a3c60f7-d188f8fa79d48a391a778fa6"), + Collections.singletonMap( + "X-Amzn-Trace-Id", + "Root=1-57ff426a-80c11c39b0c928905eb0828d;Parent=53995c3f42cd8ad8;Sampled=1"), + Collections.singletonMap( + "X-Amzn-Trace-Id", + "Root=1-57ff426a-80c11c39b0c928905eb0828d;Parent=12345c3f42cd8ad8;Sampled=0")); + + private final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private final AwsXrayPropagator xrayPropagator = AwsXrayPropagator.getInstance(); + + @Override + protected Context doExtract() { + return xrayPropagator.extract(Context.current(), getCarrier(), getter); + } + + @Override + protected List> getHeaders() { + return traceHeaders; + } + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/jmh/java/io/opentelemetry/extension/trace/propagation/PropagatorContextInjectBenchmark.java b/opentelemetry-java/extensions/trace-propagators/src/jmh/java/io/opentelemetry/extension/trace/propagation/PropagatorContextInjectBenchmark.java new file mode 100644 index 000000000..74f55c5c7 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/jmh/java/io/opentelemetry/extension/trace/propagation/PropagatorContextInjectBenchmark.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +public class PropagatorContextInjectBenchmark { + + private PropagatorContextInjectBenchmark() {} + + /** + * Abstract class containing common setup and teardown logic along with a benchmark to measure + * injecting trace context. Implementing subclasses will provide the actual call to the + * propagator. + */ + @State(Scope.Thread) + public abstract static class AbstractContextInjectBenchmark { + + private static final List spanContexts = + Arrays.asList( + createTestSpanContext("905734c59b913b4a905734c59b913b4a", "9909983295041501"), + createTestSpanContext("21196a77f299580e21196a77f299580e", "993a97ee3691eb26"), + createTestSpanContext("2e7d0ad2390617702e7d0ad239061770", "d49582a2de984b86"), + createTestSpanContext("905734c59b913b4a905734c59b913b4a", "776ff807b787538a"), + createTestSpanContext("68ec932c33b3f2ee68ec932c33b3f2ee", "68ec932c33b3f2ee")); + + private final Map carrier = new HashMap<>(); + + private Integer iteration = 0; + private SpanContext contextToTest = spanContexts.get(iteration); + + /** Benchmark for measuring inject with default trace state and sampled trace options. */ + @Benchmark + @Measurement(iterations = 15, time = 1) + @Warmup(iterations = 5, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @BenchmarkMode(Mode.AverageTime) + @Fork(1) + public Map measureInject() { + Context context = Context.current().with(Span.wrap(contextToTest)); + doInject(context, carrier); + return carrier; + } + + protected abstract void doInject(Context context, Map carrier); + + @TearDown(Level.Iteration) + public void tearDown() { + this.contextToTest = spanContexts.get(++iteration % spanContexts.size()); + } + + private static SpanContext createTestSpanContext(String traceId, String spanId) { + return SpanContext.create(traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()); + } + } + + /** Benchmark for injecting trace context into Jaeger headers. */ + public static class JaegerContextInjectBenchmark extends AbstractContextInjectBenchmark { + + private final JaegerPropagator jaegerPropagator = JaegerPropagator.getInstance(); + private final TextMapSetter> setter = + new TextMapSetter>() { + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } + }; + + @Override + protected void doInject(Context context, Map carrier) { + jaegerPropagator.inject(context, carrier, setter); + } + } + + /** Benchmark for injecting trace context into a single B3 header. */ + public static class B3SingleHeaderContextInjectBenchmark extends AbstractContextInjectBenchmark { + + private final B3Propagator b3Propagator = B3Propagator.injectingSingleHeader(); + private final TextMapSetter> setter = + new TextMapSetter>() { + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } + }; + + @Override + protected void doInject(Context context, Map carrier) { + b3Propagator.inject(context, carrier, setter); + } + } + + /** Benchmark for injecting trace context into multiple B3 headers. */ + public static class B3MultipleHeaderContextInjectBenchmark + extends AbstractContextInjectBenchmark { + + private final B3Propagator b3Propagator = B3Propagator.injectingMultiHeaders(); + private final TextMapSetter> setter = + new TextMapSetter>() { + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } + }; + + @Override + protected void doInject(Context context, Map carrier) { + b3Propagator.inject(context, carrier, setter); + } + } + + /** Benchmark for injecting trace context into AWS X-Ray headers. */ + public static class AwsXrayPropagatorInjectBenchmark extends AbstractContextInjectBenchmark { + private final AwsXrayPropagator xrayPropagator = AwsXrayPropagator.getInstance(); + private final TextMapSetter> setter = + new TextMapSetter>() { + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } + }; + + @Override + protected void doInject(Context context, Map carrier) { + xrayPropagator.inject(context, carrier, setter); + } + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3ConfigurablePropagator.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3ConfigurablePropagator.java new file mode 100644 index 000000000..4bbd434ae --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3ConfigurablePropagator.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +/** + * A {@link ConfigurablePropagatorProvider} which allows enabling the {@linkplain + * B3Propagator#injectingSingleHeader()} B3-single propagator} with the propagator name {@code b3}. + */ +public final class B3ConfigurablePropagator implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return B3Propagator.injectingSingleHeader(); + } + + @Override + public String getName() { + return "b3"; + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3MultiConfigurablePropagator.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3MultiConfigurablePropagator.java new file mode 100644 index 000000000..34945a16f --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3MultiConfigurablePropagator.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +/** + * A {@link ConfigurablePropagatorProvider} which allows enabling the {@linkplain + * B3Propagator#injectingMultiHeaders() B3-multi propagator} with the propagator name {@code + * b3multi}. + */ +public final class B3MultiConfigurablePropagator implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return B3Propagator.injectingMultiHeaders(); + } + + @Override + public String getName() { + return "b3multi"; + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3Propagator.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3Propagator.java new file mode 100644 index 000000000..bdaf849a0 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3Propagator.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Implementation of the B3 propagation protocol. See openzipkin/b3-propagation. + * + *

    Also see B3 + * Requirements + * + *

    To register the default B3 propagator, which injects a single header, use: + * + *

    {@code
    + * OpenTelemetry.setPropagators(
    + *   DefaultContextPropagators
    + *     .builder()
    + *     .addTextMapPropagator(B3Propagator.injectingSingleHeader())
    + *     .build());
    + * }
    + * + *

    To register a B3 propagator that injects multiple headers, use: + * + *

    {@code
    + * OpenTelemetry.setPropagators(
    + *   DefaultContextPropagators
    + *     .builder()
    + *     .addTextMapPropagator(B3Propagator.injectingMultiHeaders())
    + *     .build());
    + * }
    + */ +@Immutable +public final class B3Propagator implements TextMapPropagator { + static final String TRACE_ID_HEADER = "X-B3-TraceId"; + static final String SPAN_ID_HEADER = "X-B3-SpanId"; + static final String SAMPLED_HEADER = "X-B3-Sampled"; + static final String DEBUG_HEADER = "X-B3-Flags"; + static final String COMBINED_HEADER = "b3"; + static final String COMBINED_HEADER_DELIMITER = "-"; + static final ContextKey DEBUG_CONTEXT_KEY = ContextKey.named("b3-debug"); + static final String MULTI_HEADER_DEBUG = "1"; + static final String SINGLE_HEADER_DEBUG = "d"; + + static final char COMBINED_HEADER_DELIMITER_CHAR = '-'; + static final char IS_SAMPLED = '1'; + static final char NOT_SAMPLED = '0'; + static final char DEBUG_SAMPLED = 'd'; + + private static final B3Propagator SINGLE_HEADER_INSTANCE = + new B3Propagator(new B3PropagatorInjectorSingleHeader()); + private static final B3Propagator MULTI_HEADERS_INSTANCE = + new B3Propagator(new B3PropagatorInjectorMultipleHeaders()); + + private final B3PropagatorExtractor singleHeaderExtractor = + new B3PropagatorExtractorSingleHeader(); + private final B3PropagatorExtractor multipleHeadersExtractor = + new B3PropagatorExtractorMultipleHeaders(); + private final B3PropagatorInjector b3PropagatorInjector; + + private B3Propagator(B3PropagatorInjector b3PropagatorInjector) { + this.b3PropagatorInjector = b3PropagatorInjector; + } + + /** + * Returns an instance of the {@link B3Propagator} that injects multi headers format. + * + *

    This instance extracts both formats, in the order: single header, multi header. + * + * @return an instance of the {@link B3Propagator} that injects multi headers format. + */ + public static B3Propagator injectingMultiHeaders() { + return MULTI_HEADERS_INSTANCE; + } + + /** + * Returns an instance of the {@link B3Propagator} that injects single header format. + * + *

    This instance extracts both formats, in the order: single header, multi header. + * + *

    This is the default instance for {@link B3Propagator}. + * + * @return an instance of the {@link B3Propagator} that injects single header format. + */ + public static B3Propagator injectingSingleHeader() { + return SINGLE_HEADER_INSTANCE; + } + + @Override + public Collection fields() { + return b3PropagatorInjector.fields(); + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + b3PropagatorInjector.inject(context, carrier, setter); + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + return Stream.>>of( + () -> singleHeaderExtractor.extract(context, carrier, getter), + () -> multipleHeadersExtractor.extract(context, carrier, getter), + () -> Optional.of(context)) + .map(Supplier::get) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .get(); + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractor.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractor.java new file mode 100644 index 000000000..9f9c4aa8f --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractor.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Optional; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +interface B3PropagatorExtractor { + + Optional extract(Context context, @Nullable C carrier, TextMapGetter getter); +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractorMultipleHeaders.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractorMultipleHeaders.java new file mode 100644 index 000000000..76be1e519 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractorMultipleHeaders.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Optional; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class B3PropagatorExtractorMultipleHeaders implements B3PropagatorExtractor { + private static final Logger logger = + Logger.getLogger(B3PropagatorExtractorMultipleHeaders.class.getName()); + + @Override + public Optional extract( + Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Optional.of(Context.root()); + } + if (getter == null) { + return Optional.of(context); + } + return extractSpanContextFromMultipleHeaders(context, carrier, getter); + } + + private static Optional extractSpanContextFromMultipleHeaders( + Context context, @Nullable C carrier, TextMapGetter getter) { + String traceId = getter.get(carrier, B3Propagator.TRACE_ID_HEADER); + if (!Common.isTraceIdValid(traceId)) { + logger.fine( + "Invalid TraceId in B3 header: " + traceId + "'. Returning INVALID span context."); + return Optional.empty(); + } + + String spanId = getter.get(carrier, B3Propagator.SPAN_ID_HEADER); + if (!Common.isSpanIdValid(spanId)) { + logger.fine("Invalid SpanId in B3 header: " + spanId + "'. Returning INVALID span context."); + return Optional.empty(); + } + String heraContext = getter.get(carrier, HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + // if debug flag is set, then set sampled flag, and also set B3 debug to true in the context + // for onward use by B3 injector + if (B3Propagator.MULTI_HEADER_DEBUG.equals(getter.get(carrier, B3Propagator.DEBUG_HEADER))) { + return Optional.of( + context + .with(B3Propagator.DEBUG_CONTEXT_KEY, true) + .with(Span.wrap(Common.buildSpanContext(traceId, spanId, Common.TRUE_INT, heraContext)))); + } + + String sampled = getter.get(carrier, B3Propagator.SAMPLED_HEADER); + return Optional.of(context.with(Span.wrap(Common.buildSpanContext(traceId, spanId, sampled, heraContext)))); + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractorSingleHeader.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractorSingleHeader.java new file mode 100644 index 000000000..bf172efaa --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorExtractorSingleHeader.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Optional; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class B3PropagatorExtractorSingleHeader implements B3PropagatorExtractor { + private static final Logger logger = + Logger.getLogger(B3PropagatorExtractorSingleHeader.class.getName()); + + @Override + public Optional extract( + Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Optional.of(Context.root()); + } + if (getter == null) { + return Optional.of(context); + } + return extractSpanContextFromSingleHeader(context, carrier, getter); + } + + private static Optional extractSpanContextFromSingleHeader( + Context context, @Nullable C carrier, TextMapGetter getter) { + String value = getter.get(carrier, B3Propagator.COMBINED_HEADER); + if (StringUtils.isNullOrEmpty(value)) { + return Optional.empty(); + } + + // must have between 2 and 4 hyphen delimited parts: + // traceId-spanId-sampled-parentSpanId (last two are optional) + // NOTE: we do not use parentSpanId + String[] parts = value.split(B3Propagator.COMBINED_HEADER_DELIMITER); + if (parts.length < 2 || parts.length > 4) { + logger.fine( + "Invalid combined header '" + + B3Propagator.COMBINED_HEADER + + ". Returning INVALID span context."); + return Optional.empty(); + } + + String traceId = parts[0]; + if (!Common.isTraceIdValid(traceId)) { + logger.fine( + "Invalid TraceId in B3 header: " + + B3Propagator.COMBINED_HEADER + + ". Returning INVALID span context."); + return Optional.empty(); + } + + String spanId = parts[1]; + if (!Common.isSpanIdValid(spanId)) { + logger.fine( + "Invalid SpanId in B3 header: " + + B3Propagator.COMBINED_HEADER + + ". Returning INVALID span context."); + return Optional.empty(); + } + + String sampled = parts.length >= 3 ? parts[2] : null; + + String heraContext = getter.get(carrier, HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + // if sampled is marked as 'd'ebug, then set sampled flag, and also set B3 debug to true in + // the context for onward use by the B3 injector + if (B3Propagator.SINGLE_HEADER_DEBUG.equals(sampled)) { + return Optional.of( + context + .with(B3Propagator.DEBUG_CONTEXT_KEY, true) + .with(Span.wrap(Common.buildSpanContext(traceId, spanId, Common.TRUE_INT, heraContext)))); + } + + return Optional.of(context.with(Span.wrap(Common.buildSpanContext(traceId, spanId, sampled, heraContext)))); + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjector.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjector.java new file mode 100644 index 000000000..663c5481b --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjector.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collection; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +interface B3PropagatorInjector { + void inject(Context context, @Nullable C carrier, TextMapSetter setter); + + Collection fields(); +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjectorMultipleHeaders.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjectorMultipleHeaders.java new file mode 100644 index 000000000..468bf9be4 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjectorMultipleHeaders.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import static io.opentelemetry.extension.trace.propagation.B3Propagator.SAMPLED_HEADER; +import static io.opentelemetry.extension.trace.propagation.B3Propagator.SPAN_ID_HEADER; +import static io.opentelemetry.extension.trace.propagation.B3Propagator.TRACE_ID_HEADER; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class B3PropagatorInjectorMultipleHeaders implements B3PropagatorInjector { + private static final Collection FIELDS = + Collections.unmodifiableList(Arrays.asList(TRACE_ID_HEADER, SPAN_ID_HEADER, SAMPLED_HEADER)); + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null) { + return; + } + if (setter == null) { + return; + } + + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + if (!spanContext.isValid()) { + return; + } + + String sampled = spanContext.isSampled() ? Common.TRUE_INT : Common.FALSE_INT; + + if (Boolean.TRUE.equals(context.get(B3Propagator.DEBUG_CONTEXT_KEY))) { + setter.set(carrier, B3Propagator.DEBUG_HEADER, Common.TRUE_INT); + sampled = Common.TRUE_INT; + } + + setter.set(carrier, TRACE_ID_HEADER, spanContext.getTraceId()); + setter.set(carrier, SPAN_ID_HEADER, spanContext.getSpanId()); + setter.set(carrier, SAMPLED_HEADER, sampled); + } + + @Override + public Collection fields() { + return FIELDS; + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjectorSingleHeader.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjectorSingleHeader.java new file mode 100644 index 000000000..ea7df6231 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/B3PropagatorInjectorSingleHeader.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import static io.opentelemetry.extension.trace.propagation.B3Propagator.COMBINED_HEADER; + +import io.opentelemetry.api.internal.TemporaryBuffers; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collection; +import java.util.Collections; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +final class B3PropagatorInjectorSingleHeader implements B3PropagatorInjector { + private static final int SAMPLED_FLAG_SIZE = 1; + private static final int TRACE_ID_HEX_SIZE = TraceId.getLength(); + private static final int SPAN_ID_HEX_SIZE = SpanId.getLength(); + private static final int COMBINED_HEADER_DELIMITER_SIZE = 1; + private static final int SPAN_ID_OFFSET = TRACE_ID_HEX_SIZE + COMBINED_HEADER_DELIMITER_SIZE; + private static final int SAMPLED_FLAG_OFFSET = + SPAN_ID_OFFSET + SPAN_ID_HEX_SIZE + COMBINED_HEADER_DELIMITER_SIZE; + private static final int COMBINED_HEADER_SIZE = SAMPLED_FLAG_OFFSET + SAMPLED_FLAG_SIZE; + private static final Collection FIELDS = Collections.singletonList(COMBINED_HEADER); + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null) { + return; + } + if (setter == null) { + return; + } + + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + if (!spanContext.isValid()) { + return; + } + + char[] chars = TemporaryBuffers.chars(COMBINED_HEADER_SIZE); + String traceId = spanContext.getTraceId(); + traceId.getChars(0, traceId.length(), chars, 0); + chars[SPAN_ID_OFFSET - 1] = B3Propagator.COMBINED_HEADER_DELIMITER_CHAR; + + String spanId = spanContext.getSpanId(); + spanId.getChars(0, SpanId.getLength(), chars, SPAN_ID_OFFSET); + + chars[SAMPLED_FLAG_OFFSET - 1] = B3Propagator.COMBINED_HEADER_DELIMITER_CHAR; + if (Boolean.TRUE.equals(context.get(B3Propagator.DEBUG_CONTEXT_KEY))) { + chars[SAMPLED_FLAG_OFFSET] = B3Propagator.DEBUG_SAMPLED; + } else { + chars[SAMPLED_FLAG_OFFSET] = + spanContext.isSampled() ? B3Propagator.IS_SAMPLED : B3Propagator.NOT_SAMPLED; + } + setter.set(carrier, COMBINED_HEADER, new String(chars, 0, COMBINED_HEADER_SIZE)); + } + + @Override + public Collection fields() { + return FIELDS; + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/Common.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/Common.java new file mode 100644 index 000000000..f209be8e1 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/Common.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * This set of common propagator utils is currently only used by the OtTracePropagator and the + * B3Propagator. + */ +@Immutable +final class Common { + private static final Logger logger = Logger.getLogger(Common.class.getName()); + + static final String TRUE_INT = "1"; + static final String FALSE_INT = "0"; + static final int MAX_TRACE_ID_LENGTH = TraceId.getLength(); + static final int MIN_TRACE_ID_LENGTH = MAX_TRACE_ID_LENGTH / 2; + + private Common() {} + + static SpanContext buildSpanContext(String traceId, String spanId, String sampled, String heraContext) { + try { + TraceFlags traceFlags = + TRUE_INT.equals(sampled) || Boolean.parseBoolean(sampled) // accept either "1" or "true" + ? TraceFlags.getSampled() + : TraceFlags.getDefault(); + + return SpanContext.createFromRemoteParent( + StringUtils.padLeft(traceId, MAX_TRACE_ID_LENGTH), + spanId, + traceFlags, + TraceState.getDefault(), HeraContext.wrap(heraContext)); + } catch (RuntimeException e) { + logger.log(Level.FINE, "Error parsing header. Returning INVALID span context.", e); + return SpanContext.getInvalid(); + } + } + + static boolean isTraceIdValid(@Nullable String value) { + return !(StringUtils.isNullOrEmpty(value) + || (value.length() != MIN_TRACE_ID_LENGTH && value.length() != MAX_TRACE_ID_LENGTH) + || !TraceId.isValid(StringUtils.padLeft(value, TraceId.getLength()))); + } + + static boolean isSpanIdValid(@Nullable String value) { + return !StringUtils.isNullOrEmpty(value) && SpanId.isValid(value); + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/JaegerConfigurablePropagator.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/JaegerConfigurablePropagator.java new file mode 100644 index 000000000..720f8e7d9 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/JaegerConfigurablePropagator.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +/** + * A {@link ConfigurablePropagatorProvider} which allows enabling the {@link JaegerPropagator} with + * the propagator name {@code jaeger}. + */ +public final class JaegerConfigurablePropagator implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return JaegerPropagator.getInstance(); + } + + @Override + public String getName() { + return "jaeger"; + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/JaegerPropagator.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/JaegerPropagator.java new file mode 100644 index 000000000..a9cba6fdf --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/JaegerPropagator.java @@ -0,0 +1,291 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.internal.TemporaryBuffers; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Collection; +import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Implementation of the Jaeger propagation protocol. See Jaeger Propagation + * Format. + */ +@Immutable +public final class JaegerPropagator implements TextMapPropagator { + + private static final Logger logger = Logger.getLogger(JaegerPropagator.class.getName()); + + static final String PROPAGATION_HEADER = "uber-trace-id"; + static final String BAGGAGE_HEADER = "jaeger-baggage"; + static final String BAGGAGE_PREFIX = "uberctx-"; + // Parent span has been deprecated but Jaeger propagation protocol requires it + static final char DEPRECATED_PARENT_SPAN = '0'; + static final char PROPAGATION_HEADER_DELIMITER = ':'; + + private static final int MAX_TRACE_ID_LENGTH = TraceId.getLength(); + private static final int MAX_SPAN_ID_LENGTH = SpanId.getLength(); + private static final int MAX_FLAGS_LENGTH = 2; + + private static final char IS_SAMPLED_CHAR = '1'; + private static final char NOT_SAMPLED_CHAR = '0'; + private static final int PROPAGATION_HEADER_DELIMITER_SIZE = 1; + + private static final int TRACE_ID_HEX_SIZE = TraceId.getLength(); + private static final int SPAN_ID_HEX_SIZE = SpanId.getLength(); + private static final int PARENT_SPAN_ID_SIZE = 1; + private static final int SAMPLED_FLAG_SIZE = 1; + + private static final int SPAN_ID_OFFSET = TRACE_ID_HEX_SIZE + PROPAGATION_HEADER_DELIMITER_SIZE; + private static final int PARENT_SPAN_ID_OFFSET = + SPAN_ID_OFFSET + SPAN_ID_HEX_SIZE + PROPAGATION_HEADER_DELIMITER_SIZE; + private static final int SAMPLED_FLAG_OFFSET = + PARENT_SPAN_ID_OFFSET + PARENT_SPAN_ID_SIZE + PROPAGATION_HEADER_DELIMITER_SIZE; + private static final int PROPAGATION_HEADER_SIZE = SAMPLED_FLAG_OFFSET + SAMPLED_FLAG_SIZE; + + private static final Collection FIELDS = Collections.singletonList(PROPAGATION_HEADER); + private static final JaegerPropagator INSTANCE = new JaegerPropagator(); + + private JaegerPropagator() { + // singleton + } + + public static JaegerPropagator getInstance() { + return INSTANCE; + } + + @Override + public Collection fields() { + return FIELDS; + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null) { + return; + } + if (setter == null) { + return; + } + + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + if (spanContext.isValid()) { + injectSpan(spanContext, carrier, setter); + } + + injectBaggage(Baggage.fromContext(context), carrier, setter); + } + + private static void injectSpan( + SpanContext spanContext, @Nullable C carrier, TextMapSetter setter) { + + char[] chars = TemporaryBuffers.chars(PROPAGATION_HEADER_SIZE); + + String traceId = spanContext.getTraceId(); + traceId.getChars(0, traceId.length(), chars, 0); + + chars[SPAN_ID_OFFSET - 1] = PROPAGATION_HEADER_DELIMITER; + String spanId = spanContext.getSpanId(); + spanId.getChars(0, spanId.length(), chars, SPAN_ID_OFFSET); + + chars[PARENT_SPAN_ID_OFFSET - 1] = PROPAGATION_HEADER_DELIMITER; + chars[PARENT_SPAN_ID_OFFSET] = DEPRECATED_PARENT_SPAN; + chars[SAMPLED_FLAG_OFFSET - 1] = PROPAGATION_HEADER_DELIMITER; + chars[SAMPLED_FLAG_OFFSET] = spanContext.isSampled() ? IS_SAMPLED_CHAR : NOT_SAMPLED_CHAR; + setter.set(carrier, PROPAGATION_HEADER, new String(chars, 0, PROPAGATION_HEADER_SIZE)); + } + + private static void injectBaggage( + Baggage baggage, @Nullable C carrier, TextMapSetter setter) { + baggage.forEach( + (key, baggageEntry) -> setter.set(carrier, BAGGAGE_PREFIX + key, baggageEntry.getValue())); + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Context.root(); + } + if (getter == null) { + return context; + } + + SpanContext spanContext = getSpanContextFromHeader(carrier, getter); + if (spanContext.isValid()) { + context = context.with(Span.wrap(spanContext)); + } + + Baggage baggage = getBaggageFromHeader(carrier, getter); + if (baggage != null) { + context = context.with(baggage); + } + + return context; + } + + private static SpanContext getSpanContextFromHeader( + @Nullable C carrier, TextMapGetter getter) { + String value = getter.get(carrier, PROPAGATION_HEADER); + if (StringUtils.isNullOrEmpty(value)) { + return SpanContext.getInvalid(); + } + + // if the delimiter (:) cannot be found then the propagation value could be URL + // encoded, so we need to decode it before attempting to split it. + if (value.lastIndexOf(PROPAGATION_HEADER_DELIMITER) == -1) { + try { + // the propagation value + value = URLDecoder.decode(value, "UTF-8"); + } catch (UnsupportedEncodingException e) { + logger.fine( + "Error decoding '" + + PROPAGATION_HEADER + + "' with value " + + value + + ". Returning INVALID span context."); + return SpanContext.getInvalid(); + } + } + + String[] parts = value.split(String.valueOf(PROPAGATION_HEADER_DELIMITER)); + if (parts.length != 4) { + logger.fine( + "Invalid header '" + + PROPAGATION_HEADER + + "' with value " + + value + + ". Returning INVALID span context."); + return SpanContext.getInvalid(); + } + + String traceId = parts[0]; + if (!isTraceIdValid(traceId)) { + logger.fine( + "Invalid TraceId in Jaeger header: '" + + PROPAGATION_HEADER + + "' with traceId " + + traceId + + ". Returning INVALID span context."); + return SpanContext.getInvalid(); + } + + String spanId = parts[1]; + if (!isSpanIdValid(spanId)) { + logger.fine( + "Invalid SpanId in Jaeger header: '" + + PROPAGATION_HEADER + + "'. Returning INVALID span context."); + return SpanContext.getInvalid(); + } + + String flags = parts[3]; + if (!isFlagsValid(flags)) { + logger.fine( + "Invalid Flags in Jaeger header: '" + + PROPAGATION_HEADER + + "'. Returning INVALID span context."); + return SpanContext.getInvalid(); + } + String heraContext = getter.get(carrier, HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + return buildSpanContext(traceId, spanId, flags, heraContext); + } + + @Nullable + private static Baggage getBaggageFromHeader(@Nullable C carrier, TextMapGetter getter) { + BaggageBuilder builder = null; + + Iterable keys = carrier != null ? getter.keys(carrier) : Collections.emptyList(); + + for (String key : keys) { + if (key.startsWith(BAGGAGE_PREFIX)) { + if (key.length() == BAGGAGE_PREFIX.length()) { + continue; + } + + if (builder == null) { + builder = Baggage.builder(); + } + + String value = getter.get(carrier, key); + if (value != null) { + builder.put(key.substring(BAGGAGE_PREFIX.length()), value); + } + } else if (key.equals(BAGGAGE_HEADER)) { + String value = getter.get(carrier, key); + if (value != null) { + if (builder == null) { + builder = Baggage.builder(); + } + builder = parseBaggageHeader(value, builder); + } + } + } + return builder == null ? null : builder.build(); + } + + private static BaggageBuilder parseBaggageHeader(String header, BaggageBuilder builder) { + for (String part : header.split("\\s*,\\s*")) { + String[] kv = part.split("\\s*=\\s*"); + if (kv.length == 2) { + builder.put(kv[0], kv[1]); + } else { + logger.fine("malformed token in " + BAGGAGE_HEADER + " header: " + part); + } + } + return builder; + } + + private static SpanContext buildSpanContext(String traceId, String spanId, String flags, String heraContext) { + try { + String otelTraceId = StringUtils.padLeft(traceId, MAX_TRACE_ID_LENGTH); + String otelSpanId = StringUtils.padLeft(spanId, MAX_SPAN_ID_LENGTH); + int flagsInt = Integer.parseInt(flags); + return SpanContext.createFromRemoteParent( + otelTraceId, + otelSpanId, + ((flagsInt & 1) == 1) ? TraceFlags.getSampled() : TraceFlags.getDefault(), + TraceState.getDefault(), HeraContext.wrap(heraContext)); + } catch (RuntimeException e) { + logger.log( + Level.FINE, + "Error parsing '" + PROPAGATION_HEADER + "' header. Returning INVALID span context.", + e); + return SpanContext.getInvalid(); + } + } + + private static boolean isTraceIdValid(String value) { + return !(StringUtils.isNullOrEmpty(value) || value.length() > MAX_TRACE_ID_LENGTH); + } + + private static boolean isSpanIdValid(String value) { + return !(StringUtils.isNullOrEmpty(value) || value.length() > MAX_SPAN_ID_LENGTH); + } + + private static boolean isFlagsValid(String value) { + return !(StringUtils.isNullOrEmpty(value) || value.length() > MAX_FLAGS_LENGTH); + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/OtTraceConfigurablePropagator.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/OtTraceConfigurablePropagator.java new file mode 100644 index 000000000..943d771fe --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/OtTraceConfigurablePropagator.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +/** + * A {@link ConfigurablePropagatorProvider} which allows enabling the {@link OtTracePropagator} with + * the propagator name {@code ottrace}. + */ +public final class OtTraceConfigurablePropagator implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return OtTracePropagator.getInstance(); + } + + @Override + public String getName() { + return "ottrace"; + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/OtTracePropagator.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/OtTracePropagator.java new file mode 100644 index 000000000..aeca1f27e --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/OtTracePropagator.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import static io.opentelemetry.extension.trace.propagation.Common.MAX_TRACE_ID_LENGTH; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Implementation of the protocol used by OpenTracing Basic Tracers. Context is propagated through 3 + * headers, ot-tracer-traceid, ot-tracer-span-id, and ot-tracer-sampled. IDs are sent as hex strings + * and sampled is sent as true or false. Baggage values are propagated using the ot-baggage- prefix. + * See OT + * Python Propagation TextMapPropagator. + */ +@Immutable +public final class OtTracePropagator implements TextMapPropagator { + + static final String TRACE_ID_HEADER = "ot-tracer-traceid"; + static final String SPAN_ID_HEADER = "ot-tracer-spanid"; + static final String SAMPLED_HEADER = "ot-tracer-sampled"; + static final String PREFIX_BAGGAGE_HEADER = "ot-baggage-"; + private static final Collection FIELDS = + Collections.unmodifiableList(Arrays.asList(TRACE_ID_HEADER, SPAN_ID_HEADER, SAMPLED_HEADER)); + + private static final OtTracePropagator INSTANCE = new OtTracePropagator(); + + private OtTracePropagator() { + // singleton + } + + public static OtTracePropagator getInstance() { + return INSTANCE; + } + + @Override + public Collection fields() { + return FIELDS; + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + if (context == null || setter == null) { + return; + } + final SpanContext spanContext = Span.fromContext(context).getSpanContext(); + if (!spanContext.isValid()) { + return; + } + // Lightstep trace id MUST be 64-bits therefore OpenTelemetry trace id is truncated to 64-bits + // by retaining least significant (right-most) bits. + setter.set( + carrier, TRACE_ID_HEADER, spanContext.getTraceId().substring(TraceId.getLength() / 2)); + setter.set(carrier, SPAN_ID_HEADER, spanContext.getSpanId()); + setter.set(carrier, SAMPLED_HEADER, String.valueOf(spanContext.isSampled())); + + // Baggage is only injected if there is a valid SpanContext + Baggage baggage = Baggage.fromContext(context); + if (!baggage.isEmpty()) { + // Metadata is not supported by OpenTracing + baggage.forEach( + (key, baggageEntry) -> + setter.set(carrier, PREFIX_BAGGAGE_HEADER + key, baggageEntry.getValue())); + } + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + if (context == null) { + return Context.root(); + } + if (getter == null) { + return context; + } + String incomingTraceId = getter.get(carrier, TRACE_ID_HEADER); + String traceId = + incomingTraceId == null + ? TraceId.getInvalid() + : StringUtils.padLeft(incomingTraceId, MAX_TRACE_ID_LENGTH); + String spanId = getter.get(carrier, SPAN_ID_HEADER); + String sampled = getter.get(carrier, SAMPLED_HEADER); + String heraContext = getter.get(carrier, HeraContext.HERA_CONTEXT_PROPAGATOR_KEY); + SpanContext spanContext = buildSpanContext(traceId, spanId, sampled, heraContext); + if (!spanContext.isValid()) { + return context; + } + + Context extractedContext = context.with(Span.wrap(spanContext)); + + // Baggage is only extracted if there is a valid SpanContext + if (carrier != null) { + BaggageBuilder baggageBuilder = Baggage.builder(); + for (String key : getter.keys(carrier)) { + if (!key.startsWith(PREFIX_BAGGAGE_HEADER)) { + continue; + } + String value = getter.get(carrier, key); + if (value == null) { + continue; + } + baggageBuilder.put(key.replace(OtTracePropagator.PREFIX_BAGGAGE_HEADER, ""), value); + } + Baggage baggage = baggageBuilder.build(); + if (!baggage.isEmpty()) { + extractedContext = extractedContext.with(baggage); + } + } + + return extractedContext; + } + + static SpanContext buildSpanContext(String traceId, String spanId, String sampled, String heraContext) { + if (!Common.isTraceIdValid(traceId) || !Common.isSpanIdValid(spanId)) { + return SpanContext.getInvalid(); + } + return Common.buildSpanContext(traceId, spanId, sampled, heraContext); + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/package-info.java b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/package-info.java new file mode 100644 index 000000000..c5494a029 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/java/io/opentelemetry/extension/trace/propagation/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Implementations of {@link io.opentelemetry.context.propagation.TextMapPropagator} for various + * formats that are commonly used in distributed tracing. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.extension.trace.propagation; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/extensions/trace-propagators/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/opentelemetry-java/extensions/trace-propagators/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider new file mode 100644 index 000000000..f07ba17a1 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -0,0 +1,4 @@ +io.opentelemetry.extension.trace.propagation.B3ConfigurablePropagator +io.opentelemetry.extension.trace.propagation.B3MultiConfigurablePropagator +io.opentelemetry.extension.trace.propagation.JaegerConfigurablePropagator +io.opentelemetry.extension.trace.propagation.OtTraceConfigurablePropagator diff --git a/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/B3PropagatorTest.java b/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/B3PropagatorTest.java new file mode 100644 index 000000000..ed996b2c9 --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/B3PropagatorTest.java @@ -0,0 +1,728 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import static io.opentelemetry.extension.trace.propagation.B3Propagator.DEBUG_CONTEXT_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link B3Propagator}. */ +class B3PropagatorTest { + + private static final String TRACE_ID = "ff000000000000000000000000000041"; + private static final String EXTRA_TRACE_ID = "ff000000000000000000000000000045"; + private static final String TRACE_ID_ALL_ZERO = "00000000000000000000000000000000"; + private static final String SHORT_TRACE_ID = "ff00000000000000"; + private static final String SHORT_TRACE_ID_FULL = StringUtils.padLeft(SHORT_TRACE_ID, 32); + private static final String SPAN_ID = "ff00000000000041"; + private static final String EXTRA_SPAN_ID = "ff00000000000045"; + private static final String SPAN_ID_ALL_ZERO = "0000000000000000"; + private static final TextMapSetter> setter = Map::put; + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + private final B3Propagator b3Propagator = B3Propagator.injectingMultiHeaders(); + private final B3Propagator b3PropagatorSingleHeader = B3Propagator.injectingSingleHeader(); + + private static SpanContext getSpanContext(Context context) { + return Span.fromContext(context).getSpanContext(); + } + + private static Context withSpanContext(SpanContext spanContext, Context context) { + return context.with(Span.wrap(spanContext)); + } + + @Test + void inject_invalidContext() { + Map carrier = new LinkedHashMap<>(); + b3Propagator.inject( + withSpanContext( + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.builder().put("foo", "bar").build()), + Context.current()), + carrier, + setter); + assertThat(carrier).hasSize(0); + } + + @Test + void inject_SampledContext() { + Map carrier = new LinkedHashMap<>(); + b3Propagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + assertThat(carrier).containsEntry(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + assertThat(carrier).containsEntry(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + assertThat(carrier).containsEntry(B3Propagator.SAMPLED_HEADER, "1"); + } + + @Test + void inject_SampledContext_nullCarrierUsage() { + final Map carrier = new LinkedHashMap<>(); + b3Propagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + null, + (TextMapSetter>) (ignored, key, value) -> carrier.put(key, value)); + assertThat(carrier).containsEntry(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + assertThat(carrier).containsEntry(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + assertThat(carrier).containsEntry(B3Propagator.SAMPLED_HEADER, "1"); + } + + @Test + void inject_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + b3Propagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + assertThat(carrier).containsEntry(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + assertThat(carrier).containsEntry(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + assertThat(carrier).containsEntry(B3Propagator.SAMPLED_HEADER, "0"); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + b3Propagator.inject(null, carrier, setter); + assertThat(carrier).isEmpty(); + b3PropagatorSingleHeader.inject(null, carrier, setter); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + b3Propagator.inject(context, carrier, null); + assertThat(carrier).isEmpty(); + b3PropagatorSingleHeader.inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } + + @Test + void extract_Nothing() { + // Context remains untouched. + assertThat( + b3Propagator.extract(Context.current(), Collections.emptyMap(), getter)) + .isSameAs(Context.current()); + } + + @Test + void extract_SampledContext_Int() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Bool() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, "true"); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, Common.FALSE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Int_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, SHORT_TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Bool_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, SHORT_TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, "true"); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, SHORT_TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, Common.FALSE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_InvalidTraceId_NotHex() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, "g" + TRACE_ID.substring(1)); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_TooShort() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID.substring(2)); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_TooLong() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID + "00"); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_AllZero() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID_ALL_ZERO); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_NotHex() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, "g" + SPAN_ID.substring(1)); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_TooShort() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID.substring(2)); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_TooLong() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID + "00"); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_AllZeros() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID_ALL_ZERO); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void inject_invalidContext_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + b3PropagatorSingleHeader.inject( + withSpanContext( + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.builder().put("foo", "bar").build()), + Context.current()), + carrier, + setter); + assertThat(carrier).hasSize(0); + } + + @Test + void inject_SampledContext_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + b3PropagatorSingleHeader.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + + assertThat(carrier) + .containsEntry(B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + "1"); + } + + @Test + void inject_NotSampledContext_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + b3PropagatorSingleHeader.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + + assertThat(carrier) + .containsEntry(B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + "0"); + } + + @Test + void extract_Nothing_SingleHeader() { + // Context remains untouched. + assertThat( + b3Propagator.extract(Context.current(), Collections.emptyMap(), getter)) + .isSameAs(Context.current()); + } + + @Test + void extract_SampledContext_Int_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + Common.TRUE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_DebugFlag_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + Common.TRUE_INT + "-" + "0"); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Bool_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + "true"); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Bool_DebugFlag_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + "true" + "-" + "0"); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + Common.FALSE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Int_SingleHeader_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + B3Propagator.COMBINED_HEADER, SHORT_TRACE_ID + "-" + SPAN_ID + "-" + Common.TRUE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_DebugFlag_SingleHeader_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + B3Propagator.COMBINED_HEADER, + SHORT_TRACE_ID + "-" + SPAN_ID + "-" + Common.TRUE_INT + "-" + "0"); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Bool_SingleHeader_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.COMBINED_HEADER, SHORT_TRACE_ID + "-" + SPAN_ID + "-" + "true"); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Bool_DebugFlag_SingleHeader_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + B3Propagator.COMBINED_HEADER, SHORT_TRACE_ID + "-" + SPAN_ID + "-" + "true" + "-" + "0"); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext_SingleHeader_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + B3Propagator.COMBINED_HEADER, SHORT_TRACE_ID + "-" + SPAN_ID + "-" + Common.FALSE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_Null_SingleHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.COMBINED_HEADER, null); + + verifyInvalidBehavior(invalidHeaders); + } + + private void verifyInvalidBehavior(Map invalidHeaders) { + Context input = Context.current(); + Context extractedContext = b3Propagator.extract(input, invalidHeaders, getter); + assertThat(extractedContext).isSameAs(input); + assertThat(getSpanContext(extractedContext)).isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_Empty_SingleHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.COMBINED_HEADER, ""); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_SingleHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + B3Propagator.COMBINED_HEADER, + "abcdefghijklmnopabcdefghijklmnop" + "-" + SPAN_ID + "-" + Common.TRUE_INT); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_Size_SingleHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + B3Propagator.COMBINED_HEADER, + "abcdefghijklmnopabcdefghijklmnop" + "00" + "-" + SPAN_ID + "-" + Common.TRUE_INT); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_SingleHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + "abcdefghijklmnop" + "-" + Common.TRUE_INT); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_Size_SingleHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + B3Propagator.COMBINED_HEADER, + TRACE_ID + "-" + "abcdefghijklmnop" + "00" + "-" + Common.TRUE_INT); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_TooFewParts_SingleHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(B3Propagator.COMBINED_HEADER, TRACE_ID); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_TooManyParts_SingleHeader() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + B3Propagator.COMBINED_HEADER, + TRACE_ID + "-" + SPAN_ID + "-" + Common.TRUE_INT + "-extra-extra"); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_SampledContext_Int_From_SingleHeader_When_MultipleHeadersAlsoPresent() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + Common.TRUE_INT); + carrier.put(B3Propagator.TRACE_ID_HEADER, EXTRA_TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, EXTRA_SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Int_From_MultipleHeaders_When_InvalidSingleHeaderProvided() { + Map carrier = new LinkedHashMap<>(); + carrier.put( + B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + "abcdefghijklmnop" + "-" + Common.TRUE_INT); + carrier.put(B3Propagator.TRACE_ID_HEADER, EXTRA_TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, EXTRA_SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + EXTRA_TRACE_ID, EXTRA_SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_Invalid_When_Invalid_Single_And_MultipleHeaders_Provided() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + "abcdefghijklmnop" + "-" + Common.TRUE_INT); + invalidHeaders.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + invalidHeaders.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID + "00"); + invalidHeaders.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + + assertThat(getSpanContext(b3Propagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_nullContext() { + assertThat(b3Propagator.extract(null, Collections.emptyMap(), getter)).isSameAs(Context.root()); + assertThat(b3PropagatorSingleHeader.extract(null, Collections.emptyMap(), getter)) + .isSameAs(Context.root()); + } + + @Test + void extract_nullGetter() { + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + assertThat(b3Propagator.extract(context, Collections.emptyMap(), null)).isSameAs(context); + assertThat(b3PropagatorSingleHeader.extract(context, Collections.emptyMap(), null)) + .isSameAs(context); + } + + @Test + void fieldsList_multiInject() { + assertThat(b3Propagator.fields()) + .containsExactly( + B3Propagator.TRACE_ID_HEADER, B3Propagator.SPAN_ID_HEADER, B3Propagator.SAMPLED_HEADER); + } + + @Test + void fieldsList_singleHeader() { + assertThat(b3PropagatorSingleHeader.fields()).containsExactly(B3Propagator.COMBINED_HEADER); + } + + @Test + void headerNames() { + assertThat(B3Propagator.TRACE_ID_HEADER).isEqualTo("X-B3-TraceId"); + assertThat(B3Propagator.SPAN_ID_HEADER).isEqualTo("X-B3-SpanId"); + assertThat(B3Propagator.SAMPLED_HEADER).isEqualTo("X-B3-Sampled"); + assertThat(B3Propagator.COMBINED_HEADER).isEqualTo("b3"); + } + + @Test + void extract_emptyCarrier() { + Map emptyHeaders = new HashMap<>(); + assertThat(getSpanContext(b3Propagator.extract(Context.current(), emptyHeaders, getter))) + .isEqualTo(SpanContext.getInvalid()); + } + + @Test + void extract_DebugContext_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.COMBINED_HEADER, TRACE_ID + "-" + SPAN_ID + "-" + "d"); + + Context context = b3Propagator.extract(Context.current(), carrier, getter); + assertThat(getSpanContext(context)) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertTrue(context.get(DEBUG_CONTEXT_KEY)); + } + + @Test + void extract_DebugContext_MultipleHeaders() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.DEBUG_HEADER, Common.TRUE_INT); + + Context context = b3Propagator.extract(Context.current(), carrier, getter); + assertThat(getSpanContext(context)) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertTrue(context.get(DEBUG_CONTEXT_KEY)); + } + + @Test + void extract_DebugContext_SampledFalseDebugTrue_MultipleHeaders() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, Common.FALSE_INT); + carrier.put(B3Propagator.DEBUG_HEADER, Common.TRUE_INT); + + Context context = b3Propagator.extract(Context.current(), carrier, getter); + assertThat(getSpanContext(context)) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertTrue(context.get(DEBUG_CONTEXT_KEY)); + } + + @Test + void extract_DebugContext_SampledTrueDebugTrue_MultipleHeaders() { + Map carrier = new LinkedHashMap<>(); + carrier.put(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + carrier.put(B3Propagator.DEBUG_HEADER, Common.TRUE_INT); + + Context context = b3Propagator.extract(Context.current(), carrier, getter); + assertThat(getSpanContext(context)) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertTrue(context.get(DEBUG_CONTEXT_KEY)); + } + + @Test + void inject_DebugContext_MultipleHeaders() { + Map carrier = new LinkedHashMap<>(); + Context context = Context.current().with(DEBUG_CONTEXT_KEY, true); + b3Propagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + context), + carrier, + setter); + assertThat(carrier).containsEntry(B3Propagator.TRACE_ID_HEADER, TRACE_ID); + assertThat(carrier).containsEntry(B3Propagator.SPAN_ID_HEADER, SPAN_ID); + assertThat(carrier).containsEntry(B3Propagator.SAMPLED_HEADER, Common.TRUE_INT); + assertThat(carrier).containsEntry(B3Propagator.DEBUG_HEADER, Common.TRUE_INT); + } + + @Test + void inject_DebugContext_SingleHeader() { + Map carrier = new LinkedHashMap<>(); + Context context = Context.current().with(DEBUG_CONTEXT_KEY, true); + b3PropagatorSingleHeader.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + context), + carrier, + setter); + assertThat(carrier) + .containsEntry( + B3Propagator.COMBINED_HEADER, + TRACE_ID + "-" + SPAN_ID + "-" + B3Propagator.SINGLE_HEADER_DEBUG); + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/JaegerPropagatorTest.java b/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/JaegerPropagatorTest.java new file mode 100644 index 000000000..a518d9d3d --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/JaegerPropagatorTest.java @@ -0,0 +1,473 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import static io.opentelemetry.api.baggage.Baggage.fromContext; +import static io.opentelemetry.extension.trace.propagation.JaegerPropagator.BAGGAGE_HEADER; +import static io.opentelemetry.extension.trace.propagation.JaegerPropagator.BAGGAGE_PREFIX; +import static io.opentelemetry.extension.trace.propagation.JaegerPropagator.DEPRECATED_PARENT_SPAN; +import static io.opentelemetry.extension.trace.propagation.JaegerPropagator.PROPAGATION_HEADER; +import static io.opentelemetry.extension.trace.propagation.JaegerPropagator.PROPAGATION_HEADER_DELIMITER; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.jaegertracing.internal.JaegerSpanContext; +import io.jaegertracing.internal.propagation.TextMapCodec; +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageEntryMetadata; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link io.opentelemetry.extension.trace.propagation.JaegerPropagator}. */ +class JaegerPropagatorTest { + + private static final long TRACE_ID_HI = 77L; + private static final long TRACE_ID_LOW = 22L; + private static final String TRACE_ID = "000000000000004d0000000000000016"; + private static final long SHORT_TRACE_ID_HI = 0L; + private static final long SHORT_TRACE_ID_LOW = 2322222L; + private static final String SHORT_TRACE_ID = "00000000000000000000000000236f2e"; + private static final String SPAN_ID = "0000000000017c29"; + private static final long SPAN_ID_LONG = 97321L; + private static final long DEPRECATED_PARENT_SPAN_LONG = 0L; + private static final TextMapSetter> setter = Map::put; + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private final JaegerPropagator jaegerPropagator = JaegerPropagator.getInstance(); + + private static SpanContext getSpanContext(Context context) { + return Span.fromContext(context).getSpanContext(); + } + + private static Context withSpanContext(SpanContext spanContext, Context context) { + return context.with(Span.wrap(spanContext)); + } + + @Test + void inject_invalidContext() { + Map carrier = new LinkedHashMap<>(); + jaegerPropagator.inject( + withSpanContext( + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.builder().put("foo", "bar").build()), + Context.current()), + carrier, + setter); + assertThat(carrier).hasSize(0); + } + + @Test + void inject_SampledContext() { + Map carrier = new LinkedHashMap<>(); + jaegerPropagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + + assertThat(carrier) + .containsEntry( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, SPAN_ID, DEPRECATED_PARENT_SPAN, "1")); + } + + @Test + void inject_SampledContext_nullCarrierUsage() { + final Map carrier = new LinkedHashMap<>(); + + jaegerPropagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + null, + (TextMapSetter>) (ignored, key, value) -> carrier.put(key, value)); + + assertThat(carrier) + .containsEntry( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, SPAN_ID, DEPRECATED_PARENT_SPAN, "1")); + } + + @Test + void inject_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + jaegerPropagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + assertThat(carrier) + .containsEntry( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, SPAN_ID, DEPRECATED_PARENT_SPAN, "0")); + } + + @Test + void inject_SampledContext_withBaggage() { + Map carrier = new LinkedHashMap<>(); + Context context = + Context.current() + .with( + Span.wrap( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()))) + .with(Baggage.builder().put("foo", "bar").build()); + + jaegerPropagator.inject(context, carrier, setter); + assertThat(carrier) + .containsEntry( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, SPAN_ID, DEPRECATED_PARENT_SPAN, "1")); + assertThat(carrier).containsEntry(BAGGAGE_PREFIX + "foo", "bar"); + } + + @Test + void inject_baggageOnly() { + // Metadata won't be propagated, but it MUST NOT cause ay problem. + Baggage baggage = + Baggage.builder() + .put("nometa", "nometa-value") + .put("meta", "meta-value", BaggageEntryMetadata.create("somemetadata; someother=foo")) + .build(); + Map carrier = new LinkedHashMap<>(); + jaegerPropagator.inject(Context.root().with(baggage), carrier, Map::put); + assertThat(carrier) + .containsExactlyInAnyOrderEntriesOf( + ImmutableMap.of( + BAGGAGE_PREFIX + "nometa", "nometa-value", + BAGGAGE_PREFIX + "meta", "meta-value")); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + jaegerPropagator.inject(null, carrier, setter); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + jaegerPropagator.inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } + + @Test + void extract_Nothing() { + // Context remains untouched. + assertThat( + jaegerPropagator.extract( + Context.current(), Collections.emptyMap(), getter)) + .isSameAs(Context.current()); + } + + @Test + void extract_EmptyHeaderValue() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(PROPAGATION_HEADER, ""); + + assertThat(getSpanContext(jaegerPropagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_NotEnoughParts() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(PROPAGATION_HEADER, "aa:bb:cc"); + + verifyInvalidBehavior(invalidHeaders); + } + + private void verifyInvalidBehavior(Map invalidHeaders) { + Context input = Context.current(); + Context result = jaegerPropagator.extract(input, invalidHeaders, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_TooManyParts() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(PROPAGATION_HEADER, "aa:bb:cc:dd:ee"); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + PROPAGATION_HEADER, + generateTraceIdHeaderValue( + "abcdefghijklmnopabcdefghijklmnop", SPAN_ID, DEPRECATED_PARENT_SPAN, "0")); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidTraceId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID + "00", SPAN_ID, DEPRECATED_PARENT_SPAN, "0")); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, "abcdefghijklmnop", DEPRECATED_PARENT_SPAN, "0")); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, SPAN_ID + "00", DEPRECATED_PARENT_SPAN, "0")); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, SPAN_ID, DEPRECATED_PARENT_SPAN, "")); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, SPAN_ID, DEPRECATED_PARENT_SPAN, "10220")); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidFlags_NonNumeric() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put( + PROPAGATION_HEADER, + generateTraceIdHeaderValue(TRACE_ID, SPAN_ID, DEPRECATED_PARENT_SPAN, "abcdefr")); + + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_SampledContext() { + Map carrier = new LinkedHashMap<>(); + JaegerSpanContext context = + new JaegerSpanContext( + TRACE_ID_HI, TRACE_ID_LOW, SPAN_ID_LONG, DEPRECATED_PARENT_SPAN_LONG, (byte) 5); + carrier.put(PROPAGATION_HEADER, TextMapCodec.contextAsString(context)); + + assertThat(getSpanContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + JaegerSpanContext context = + new JaegerSpanContext( + TRACE_ID_HI, TRACE_ID_LOW, SPAN_ID_LONG, DEPRECATED_PARENT_SPAN_LONG, (byte) 0); + carrier.put(PROPAGATION_HEADER, TextMapCodec.contextAsString(context)); + + assertThat(getSpanContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + JaegerSpanContext context = + new JaegerSpanContext( + SHORT_TRACE_ID_HI, + SHORT_TRACE_ID_LOW, + SPAN_ID_LONG, + DEPRECATED_PARENT_SPAN_LONG, + (byte) 1); + carrier.put(PROPAGATION_HEADER, TextMapCodec.contextAsString(context)); + + assertThat(getSpanContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_UrlEncodedContext() throws UnsupportedEncodingException { + Map carrier = new LinkedHashMap<>(); + JaegerSpanContext context = + new JaegerSpanContext( + TRACE_ID_HI, TRACE_ID_LOW, SPAN_ID_LONG, DEPRECATED_PARENT_SPAN_LONG, (byte) 5); + carrier.put( + PROPAGATION_HEADER, URLEncoder.encode(TextMapCodec.contextAsString(context), "UTF-8")); + + assertThat(getSpanContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_withBaggage() { + Map carrier = new LinkedHashMap<>(); + JaegerSpanContext context = + new JaegerSpanContext( + TRACE_ID_HI, TRACE_ID_LOW, SPAN_ID_LONG, DEPRECATED_PARENT_SPAN_LONG, (byte) 5); + carrier.put(PROPAGATION_HEADER, TextMapCodec.contextAsString(context)); + carrier.put(BAGGAGE_PREFIX + "foo", "bar"); + + assertThat(getSpanContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + assertThat(fromContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo(Baggage.builder().put("foo", "bar").build()); + } + + @Test + void extract_baggageOnly_withPrefix() { + Map carrier = new LinkedHashMap<>(); + carrier.put(BAGGAGE_PREFIX + "nometa", "nometa-value"); + carrier.put(BAGGAGE_PREFIX + "meta", "meta-value"); + carrier.put("another", "value"); + + assertThat(fromContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + Baggage.builder().put("nometa", "nometa-value").put("meta", "meta-value").build()); + } + + @Test + void extract_baggageOnly_withPrefix_emptyKey() { + Map carrier = new LinkedHashMap<>(); + carrier.put(BAGGAGE_PREFIX, "value"); // Not really a valid key. + + assertThat(fromContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo(Baggage.empty()); + } + + @Test + void extract_baggageOnly_withHeader() { + Map carrier = new LinkedHashMap<>(); + carrier.put(BAGGAGE_HEADER, "nometa=nometa-value,meta=meta-value"); + + assertThat(fromContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + Baggage.builder().put("nometa", "nometa-value").put("meta", "meta-value").build()); + } + + @Test + void extract_baggageOnly_withHeader_andSpaces() { + Map carrier = new LinkedHashMap<>(); + carrier.put(BAGGAGE_HEADER, "nometa = nometa-value , meta = meta-value"); + + assertThat(fromContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + Baggage.builder().put("nometa", "nometa-value").put("meta", "meta-value").build()); + } + + @Test + void extract_baggageOnly_withHeader_invalid() { + Map carrier = new LinkedHashMap<>(); + carrier.put(BAGGAGE_HEADER, "nometa+novalue"); + + assertThat(fromContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo(Baggage.empty()); + } + + @Test + void extract_baggageOnly_withHeader_andPrefix() { + Map carrier = new LinkedHashMap<>(); + carrier.put(BAGGAGE_HEADER, "nometa=nometa-value,meta=meta-value"); + carrier.put(BAGGAGE_PREFIX + "foo", "bar"); + + assertThat(fromContext(jaegerPropagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + Baggage.builder() + .put("nometa", "nometa-value") + .put("meta", "meta-value") + .put("foo", "bar") + .build()); + } + + @Test + void extract_nullContext() { + assertThat(jaegerPropagator.extract(null, Collections.emptyMap(), getter)) + .isSameAs(Context.root()); + } + + @Test + void extract_nullGetter() { + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + assertThat(jaegerPropagator.extract(context, Collections.emptyMap(), null)).isSameAs(context); + } + + private static String generateTraceIdHeaderValue( + String traceId, String spanId, char parentSpan, String sampled) { + return traceId + + PROPAGATION_HEADER_DELIMITER + + spanId + + PROPAGATION_HEADER_DELIMITER + + parentSpan + + PROPAGATION_HEADER_DELIMITER + + sampled; + } +} diff --git a/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/OtTracePropagatorTest.java b/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/OtTracePropagatorTest.java new file mode 100644 index 000000000..2da3a970e --- /dev/null +++ b/opentelemetry-java/extensions/trace-propagators/src/test/java/io/opentelemetry/extension/trace/propagation/OtTracePropagatorTest.java @@ -0,0 +1,343 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.extension.trace.propagation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class OtTracePropagatorTest { + + private static final String TRACE_ID = "ff000000000000000000000000000041"; + private static final String TRACE_ID_RIGHT_PART = "0000000000000041"; + private static final String SHORT_TRACE_ID = "ff00000000000000"; + private static final String SHORT_TRACE_ID_FULL = "0000000000000000ff00000000000000"; + private static final String SPAN_ID = "ff00000000000041"; + private static final TextMapSetter> setter = Map::put; + private static final TextMapGetter> getter = + new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + private final OtTracePropagator propagator = OtTracePropagator.getInstance(); + + private static SpanContext getSpanContext(Context context) { + return Span.fromContext(context).getSpanContext(); + } + + private static Context withSpanContext(SpanContext spanContext, Context context) { + return context.with(Span.wrap(spanContext)); + } + + @Test + void inject_invalidContext() { + Map carrier = new LinkedHashMap<>(); + propagator.inject( + withSpanContext( + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.builder().put("foo", "bar").build()), + Context.current()), + carrier, + setter); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_SampledContext() { + Map carrier = new LinkedHashMap<>(); + propagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + assertThat(carrier).containsEntry(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID_RIGHT_PART); + assertThat(carrier).containsEntry(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + assertThat(carrier).containsEntry(OtTracePropagator.SAMPLED_HEADER, "true"); + } + + @Test + void inject_SampledContext_nullCarrierUsage() { + final Map carrier = new LinkedHashMap<>(); + propagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current()), + null, + (TextMapSetter>) (ignored, key, value) -> carrier.put(key, value)); + assertThat(carrier).containsEntry(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID_RIGHT_PART); + assertThat(carrier).containsEntry(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + assertThat(carrier).containsEntry(OtTracePropagator.SAMPLED_HEADER, "true"); + } + + @Test + void inject_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + propagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()), + carrier, + setter); + assertThat(carrier).containsEntry(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID_RIGHT_PART); + assertThat(carrier).containsEntry(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + assertThat(carrier).containsEntry(OtTracePropagator.SAMPLED_HEADER, "false"); + } + + @Test + void inject_Baggage() { + Map carrier = new LinkedHashMap<>(); + Baggage baggage = Baggage.builder().put("foo", "bar").put("key", "value").build(); + propagator.inject( + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault()), + Context.current().with(baggage)), + carrier, + setter); + assertThat(carrier).containsEntry(OtTracePropagator.PREFIX_BAGGAGE_HEADER + "foo", "bar"); + assertThat(carrier).containsEntry(OtTracePropagator.PREFIX_BAGGAGE_HEADER + "key", "value"); + } + + @Test + void inject_Baggage_InvalidContext() { + Map carrier = new LinkedHashMap<>(); + Baggage baggage = Baggage.builder().put("foo", "bar").put("key", "value").build(); + propagator.inject( + withSpanContext( + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.getDefault()), + Context.current().with(baggage)), + carrier, + setter); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullContext() { + Map carrier = new LinkedHashMap<>(); + propagator.inject(null, carrier, setter); + assertThat(carrier).isEmpty(); + } + + @Test + void inject_nullSetter() { + Map carrier = new LinkedHashMap<>(); + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + propagator.inject(context, carrier, null); + assertThat(carrier).isEmpty(); + } + + @Test + void extract_Nothing() { + // Context remains untouched. + assertThat( + propagator.extract(Context.current(), Collections.emptyMap(), getter)) + .isSameAs(Context.current()); + } + + @Test + void extract_SampledContext_Int() { + Map carrier = new LinkedHashMap<>(); + carrier.put(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(OtTracePropagator.SAMPLED_HEADER, Common.TRUE_INT); + + assertThat(getSpanContext(propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Bool() { + Map carrier = new LinkedHashMap<>(); + carrier.put(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(OtTracePropagator.SAMPLED_HEADER, "true"); + + assertThat(getSpanContext(propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(OtTracePropagator.SAMPLED_HEADER, Common.FALSE_INT); + + assertThat(getSpanContext(propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Int_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put(OtTracePropagator.TRACE_ID_HEADER, SHORT_TRACE_ID); + carrier.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(OtTracePropagator.SAMPLED_HEADER, Common.TRUE_INT); + + assertThat(getSpanContext(propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_SampledContext_Bool_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put(OtTracePropagator.TRACE_ID_HEADER, SHORT_TRACE_ID); + carrier.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(OtTracePropagator.SAMPLED_HEADER, "true"); + + assertThat(getSpanContext(propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())); + } + + @Test + void extract_NotSampledContext_Short_TraceId() { + Map carrier = new LinkedHashMap<>(); + carrier.put(OtTracePropagator.TRACE_ID_HEADER, SHORT_TRACE_ID); + carrier.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(OtTracePropagator.SAMPLED_HEADER, Common.FALSE_INT); + + assertThat(getSpanContext(propagator.extract(Context.current(), carrier, getter))) + .isEqualTo( + SpanContext.createFromRemoteParent( + SHORT_TRACE_ID_FULL, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())); + } + + @Test + void extract_InvalidTraceId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(OtTracePropagator.TRACE_ID_HEADER, "abcdefghijklmnopabcdefghijklmnop"); + invalidHeaders.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + invalidHeaders.put(OtTracePropagator.SAMPLED_HEADER, Common.TRUE_INT); + assertThat(getSpanContext(propagator.extract(Context.current(), invalidHeaders, getter))) + .isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_InvalidTraceId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID + "00"); + invalidHeaders.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + invalidHeaders.put(OtTracePropagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID); + invalidHeaders.put(OtTracePropagator.SPAN_ID_HEADER, "abcdefghijklmnop"); + invalidHeaders.put(OtTracePropagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + @Test + void extract_InvalidSpanId_Size() { + Map invalidHeaders = new LinkedHashMap<>(); + invalidHeaders.put(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID); + invalidHeaders.put(OtTracePropagator.SPAN_ID_HEADER, "abcdefghijklmnop" + "00"); + invalidHeaders.put(OtTracePropagator.SAMPLED_HEADER, Common.TRUE_INT); + verifyInvalidBehavior(invalidHeaders); + } + + private void verifyInvalidBehavior(Map invalidHeaders) { + Context input = Context.current(); + Context result = propagator.extract(input, invalidHeaders, getter); + assertThat(result).isSameAs(input); + assertThat(getSpanContext(result)).isSameAs(SpanContext.getInvalid()); + } + + @Test + void extract_emptyCarrier() { + Map emptyHeaders = new HashMap<>(); + verifyInvalidBehavior(emptyHeaders); + } + + @Test + void extract_Baggage() { + Map carrier = new LinkedHashMap<>(); + carrier.put(OtTracePropagator.TRACE_ID_HEADER, TRACE_ID); + carrier.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(OtTracePropagator.SAMPLED_HEADER, Common.TRUE_INT); + carrier.put(OtTracePropagator.PREFIX_BAGGAGE_HEADER + "foo", "bar"); + carrier.put(OtTracePropagator.PREFIX_BAGGAGE_HEADER + "key", "value"); + + Context context = propagator.extract(Context.current(), carrier, getter); + + Baggage expectedBaggage = Baggage.builder().put("foo", "bar").put("key", "value").build(); + assertThat(Baggage.fromContext(context)).isEqualTo(expectedBaggage); + } + + @Test + void extract_Baggage_InvalidContext() { + Map carrier = new LinkedHashMap<>(); + carrier.put(OtTracePropagator.TRACE_ID_HEADER, TraceId.getInvalid()); + carrier.put(OtTracePropagator.SPAN_ID_HEADER, SPAN_ID); + carrier.put(OtTracePropagator.SAMPLED_HEADER, Common.TRUE_INT); + carrier.put(OtTracePropagator.PREFIX_BAGGAGE_HEADER + "foo", "bar"); + carrier.put(OtTracePropagator.PREFIX_BAGGAGE_HEADER + "key", "value"); + + Context context = propagator.extract(Context.current(), carrier, getter); + + assertThat(Baggage.fromContext(context).isEmpty()).isTrue(); + } + + @Test + void extract_nullContext() { + assertThat(propagator.extract(null, Collections.emptyMap(), getter)).isSameAs(Context.root()); + } + + @Test + void extract_nullGetter() { + Context context = + withSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()), + Context.current()); + assertThat(propagator.extract(context, Collections.emptyMap(), null)).isSameAs(context); + } +} diff --git a/opentelemetry-java/gradle.properties b/opentelemetry-java/gradle.properties new file mode 100644 index 000000000..0b5e135ed --- /dev/null +++ b/opentelemetry-java/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.parallel=true +org.gradle.caching=true + +org.gradle.priority=low + +# Gradle default is 256m which causes issues with our build - https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory +org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m diff --git a/opentelemetry-java/gradle/wrapper/gradle-wrapper.jar b/opentelemetry-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..c9d55ea1c --- /dev/null +++ b/opentelemetry-java/gradle/wrapper/gradle-wrapper.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637 +size 59203 diff --git a/opentelemetry-java/gradle/wrapper/gradle-wrapper.properties b/opentelemetry-java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..dbdefee81 --- /dev/null +++ b/opentelemetry-java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionSha256Sum=0e46229820205440b48a5501122002842b82886e76af35f0f3a069243dca4b3c +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/opentelemetry-java/gradlew b/opentelemetry-java/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/opentelemetry-java/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/opentelemetry-java/gradlew.bat b/opentelemetry-java/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/opentelemetry-java/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/opentelemetry-java/integration-tests/README.md b/opentelemetry-java/integration-tests/README.md new file mode 100644 index 000000000..fab3dc99a --- /dev/null +++ b/opentelemetry-java/integration-tests/README.md @@ -0,0 +1,4 @@ +# OpenTelemetry Integration Tests + + +* Integration Test code lives here \ No newline at end of file diff --git a/opentelemetry-java/integration-tests/build.gradle.kts b/opentelemetry-java/integration-tests/build.gradle.kts new file mode 100644 index 000000000..7880d24fb --- /dev/null +++ b/opentelemetry-java/integration-tests/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + java +} + +description = "OpenTelemetry Integration Tests" +extra["moduleName"] = "io.opentelemetry.integration.tests" + +dependencies { + implementation(project(":sdk:all")) + implementation(project(":exporters:jaeger")) + implementation(project(":semconv")) + + implementation("io.grpc:grpc-protobuf") + implementation("com.google.protobuf:protobuf-java") + implementation("io.grpc:grpc-netty-shaded") + + testImplementation(project(":extensions:trace-propagators")) + testImplementation(project(":sdk:testing")) + testImplementation("org.junit.jupiter:junit-jupiter-params") + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("com.squareup.okhttp3:okhttp") + testImplementation("org.slf4j:slf4j-simple") + testImplementation("com.sparkjava:spark-core") +} diff --git a/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/B3PropagationIntegrationTest.java b/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/B3PropagationIntegrationTest.java new file mode 100644 index 000000000..2b6ab2266 --- /dev/null +++ b/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/B3PropagationIntegrationTest.java @@ -0,0 +1,254 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.io.IOException; +import java.util.List; +import java.util.Random; +import java.util.stream.Stream; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.testcontainers.shaded.okhttp3.Call; +import org.testcontainers.shaded.okhttp3.OkHttpClient; +import org.testcontainers.shaded.okhttp3.Request; +import org.testcontainers.shaded.okhttp3.Response; +import org.testcontainers.shaded.okhttp3.ResponseBody; +import spark.Service; + +/** Integration tests for the B3 propagators, in various configurations. */ +public class B3PropagationIntegrationTest { + + private static final int server1Port = new Random().nextInt(40000) + 1024; + private static final int server2Port = new Random().nextInt(40000) + 1024; + private static final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + + private Service server1; + private Service server2; + + private void setup(TextMapPropagator propagator) { + setupServer1(propagator); + setupServer2(propagator); + } + + @AfterEach + void shutdown() { + if (server2 != null) { + server2.stop(); + } + if (server1 != null) { + server1.stop(); + } + spanExporter.reset(); + } + + @ParameterizedTest + @ArgumentsSource(PropagatorArgumentSupplier.class) + void propagation(TextMapPropagator propagator) throws IOException { + setup(propagator); + OpenTelemetrySdk clientSdk = setupClient(propagator); + + OkHttpClient httpClient = createPropagatingClient(clientSdk); + + Span span = clientSdk.getTracer("testTracer").spanBuilder("clientSpan").startSpan(); + try (Scope ignored = span.makeCurrent()) { + Call call = + httpClient.newCall( + new Request.Builder().get().url("http://localhost:" + server1Port + "/test").build()); + + try (Response r = call.execute()) { + ResponseBody body = r.body(); + assertThat(body.string()).isEqualTo("OK"); + } + } finally { + span.end(); + } + + List finishedSpanItems = spanExporter.getFinishedSpanItems(); + // 3 spans, one from the client, and one from each of the servers. + assertThat(finishedSpanItems).hasSize(3); + String traceId = finishedSpanItems.get(0).getTraceId(); + + assertThat(finishedSpanItems) + .allSatisfy(spanData -> assertThat(spanData.getTraceId()).isEqualTo(traceId)); + } + + @ParameterizedTest + @ArgumentsSource(PropagatorArgumentSupplier.class) + void noClientTracing(TextMapPropagator propagator) throws IOException { + setup(propagator); + OkHttpClient httpClient = new OkHttpClient(); + + Call call = + httpClient.newCall( + new Request.Builder().get().url("http://localhost:" + server1Port + "/test").build()); + + try (Response r = call.execute()) { + ResponseBody body = r.body(); + assertThat(body.string()).isEqualTo("OK"); + } + + List finishedSpanItems = spanExporter.getFinishedSpanItems(); + // 2 spans, one from each of the servers + assertThat(finishedSpanItems).hasSize(2); + String traceId = finishedSpanItems.get(0).getTraceId(); + + assertThat(finishedSpanItems) + .allSatisfy(spanData -> assertThat(spanData.getTraceId()).isEqualTo(traceId)); + } + + private static OkHttpClient createPropagatingClient(OpenTelemetrySdk sdk) { + return new OkHttpClient.Builder() + .addNetworkInterceptor( + chain -> { + Request request = chain.request(); + + Request.Builder requestBuilder = request.newBuilder(); + + sdk.getPropagators() + .getTextMapPropagator() + .inject(Context.current(), requestBuilder, Request.Builder::header); + + request = requestBuilder.build(); + return chain.proceed(request); + }) + .build(); + } + + private static OpenTelemetrySdk setupClient(TextMapPropagator propagator) { + OpenTelemetrySdk clientSdk = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(propagator)) + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build()) + .build(); + return clientSdk; + } + + private void setupServer1(TextMapPropagator propagator) { + OpenTelemetrySdk serverSdk = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(propagator)) + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build()) + .build(); + + OkHttpClient serverClient = createPropagatingClient(serverSdk); + server1 = Service.ignite().port(server1Port); + server1.get( + "/test", + (request, response) -> { + Context incomingContext = extract(request, serverSdk); + + Span span = + serverSdk + .getTracer("server1Tracer") + .spanBuilder("server1Span") + .setParent(incomingContext) + .startSpan(); + try (Scope ignored = span.makeCurrent()) { + Call call = + serverClient.newCall( + new Request.Builder() + .get() + .url("http://localhost:" + server2Port + "/test2") + .build()); + + try (Response r = call.execute()) { + assertThat(r.code()).isEqualTo(200); + } + + return "OK"; + } finally { + span.end(); + } + }); + server1.awaitInitialization(); + } + + private void setupServer2(TextMapPropagator propagator) { + OpenTelemetrySdk serverSdk = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(propagator)) + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build()) + .build(); + + server2 = Service.ignite().port(server2Port); + server2.get( + "/test2", + (request, response) -> { + Context incomingContext = extract(request, serverSdk); + + Span span = + serverSdk + .getTracer("server2Tracer") + .spanBuilder("server2Span") + .setParent(incomingContext) + .startSpan(); + try (Scope ignored = span.makeCurrent()) { + return "OK"; + } finally { + span.end(); + } + }); + server2.awaitInitialization(); + } + + private static Context extract(spark.Request request, OpenTelemetrySdk sdk) { + return sdk.getPropagators() + .getTextMapPropagator() + .extract( + Context.root(), + request, + new TextMapGetter() { + @Override + public Iterable keys(spark.Request carrier) { + return carrier.headers(); + } + + @Nullable + @Override + public String get(@Nullable spark.Request carrier, String key) { + return carrier.headers(key); + } + }); + } + + public static class PropagatorArgumentSupplier implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(B3Propagator.injectingMultiHeaders()), + Arguments.of(B3Propagator.injectingSingleHeader())); + } + } +} diff --git a/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/JaegerExporterIntegrationTest.java b/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/JaegerExporterIntegrationTest.java new file mode 100644 index 000000000..8d57baf82 --- /dev/null +++ b/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/JaegerExporterIntegrationTest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.time.Duration; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +/** Integration test to verify that the Jaeger GRPC exporter works. */ +@Testcontainers(disabledWithoutDocker = true) +class JaegerExporterIntegrationTest { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final OkHttpClient client = new OkHttpClient(); + + private static final int QUERY_PORT = 16686; + private static final int JAEGER_API_PORT = 14250; + private static final int HEALTH_PORT = 14269; + private static final String SERVICE_NAME = "integration test"; + private static final String JAEGER_URL = "http://localhost"; + + @Container + public static GenericContainer jaegerContainer = + new GenericContainer<>( + DockerImageName.parse("ghcr.io/open-telemetry/java-test-containers:jaeger")) + .withExposedPorts(JAEGER_API_PORT, QUERY_PORT, HEALTH_PORT) + .waitingFor(Wait.forHttp("/").forPort(HEALTH_PORT)); + + @Test + void testJaegerExampleAppIntegration() { + OpenTelemetry openTelemetry = + initOpenTelemetry( + jaegerContainer.getHost(), jaegerContainer.getMappedPort(JAEGER_API_PORT)); + myWonderfulUseCase(openTelemetry); + + Awaitility.await() + .atMost(Duration.ofSeconds(30)) + .until(JaegerExporterIntegrationTest::assertJaegerHasTheTrace); + } + + private static Boolean assertJaegerHasTheTrace() { + try { + String url = + String.format( + "%s/api/traces?service=%s", + String.format(JAEGER_URL + ":%d", jaegerContainer.getMappedPort(QUERY_PORT)), + SERVICE_NAME); + + Request request = + new Request.Builder() + .url(url) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .build(); + + final JsonNode json; + try (Response response = client.newCall(request).execute()) { + json = objectMapper.readTree(response.body().byteStream()); + } + + return json.get("data").get(0).get("traceID") != null; + } catch (Exception e) { + return false; + } + } + + private static OpenTelemetry initOpenTelemetry(String ip, int port) { + // Create a channel for the Jaeger endpoint + ManagedChannel jaegerChannel = + ManagedChannelBuilder.forAddress(ip, port).usePlaintext().build(); + // Export traces to Jaeger + JaegerGrpcSpanExporter jaegerExporter = + JaegerGrpcSpanExporter.builder() + .setChannel(jaegerChannel) + .setTimeout(Duration.ofSeconds(30)) + .build(); + + // Set to process the spans by the Jaeger Exporter + return OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(jaegerExporter)) + .setResource( + Resource.getDefault().toBuilder() + .put(ResourceAttributes.SERVICE_NAME, "integration test") + .build()) + .build()) + .buildAndRegisterGlobal(); + } + + private static void myWonderfulUseCase(OpenTelemetry openTelemetry) { + // Generate a span + Span span = + openTelemetry + .getTracer("io.opentelemetry.SendTraceToJaeger") + .spanBuilder("Start my wonderful use case") + .startSpan(); + span.addEvent("Event 0"); + // execute my use case - here we simulate a wait + doWait(); + span.addEvent("Event 1"); + span.end(); + } + + private static void doWait() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // catch + } + } +} diff --git a/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/package-info.java b/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/package-info.java new file mode 100644 index 000000000..4b15eeb3e --- /dev/null +++ b/opentelemetry-java/integration-tests/src/test/java/io/opentelemetry/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/integration-tests/tracecontext/README.md b/opentelemetry-java/integration-tests/tracecontext/README.md new file mode 100644 index 000000000..f62ad0af7 --- /dev/null +++ b/opentelemetry-java/integration-tests/tracecontext/README.md @@ -0,0 +1,12 @@ +# OpenTelemetry W3C Context Propagation tests + +Based on https://github.com/w3c/trace-context/tree/master/test + +Environmental requirements: +- System with bourne shell e.g. linux, macOS +- Python version >= 3.6.0 +- pip3 + +```shell script +./tracecontext-integration-test.sh +``` diff --git a/opentelemetry-java/integration-tests/tracecontext/build.gradle.kts b/opentelemetry-java/integration-tests/tracecontext/build.gradle.kts new file mode 100644 index 000000000..ac944a0cf --- /dev/null +++ b/opentelemetry-java/integration-tests/tracecontext/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + java + + id("com.github.johnrengelman.shadow") +} + +description = "OpenTelemetry W3C Context Propagation Integration Tests" +extra["moduleName"] = "io.opentelemetry.tracecontext.integration.tests" + +dependencies { + implementation(project(":sdk:all")) + implementation(project(":extensions:trace-propagators")) + + implementation("com.linecorp.armeria:armeria") + implementation("org.slf4j:slf4j-simple") + + testImplementation("org.testcontainers:junit-jupiter") +} + +tasks { + val shadowJar by existing(Jar::class) { + archiveFileName.set("tracecontext-tests.jar") + + manifest { + attributes("Main-Class" to "io.opentelemetry.Application") + } + } + + withType(Test::class) { + dependsOn(shadowJar) + + jvmArgs("-Dio.opentelemetry.testArchive=${shadowJar.get().archiveFile.get().asFile.absolutePath}") + } +} diff --git a/opentelemetry-java/integration-tests/tracecontext/docker/Dockerfile b/opentelemetry-java/integration-tests/tracecontext/docker/Dockerfile new file mode 100644 index 000000000..fc6dd4e7c --- /dev/null +++ b/opentelemetry-java/integration-tests/tracecontext/docker/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3 AS build + +# Main branch SHA as of April-1-2021 +ARG TRACECONTEXT_GIT_TAG="dcd3ad9b7d6ac36f70ff3739874b73c11b0302a1" + +WORKDIR /workspace + +ADD https://github.com/w3c/trace-context/archive/${TRACECONTEXT_GIT_TAG}.zip /workspace/trace-context.zip +# Unzips to folder +RUN unzip trace-context.zip +RUN rm trace-context.zip +RUN mv trace-context-${TRACECONTEXT_GIT_TAG}/test /tracecontext-testsuite + +FROM python:3-slim + +RUN pip install aiohttp + +WORKDIR /tracecontext-testsuite +COPY --from=build /tracecontext-testsuite /tracecontext-testsuite + +ENTRYPOINT ["python", "test.py"] diff --git a/opentelemetry-java/integration-tests/tracecontext/src/main/java/io/opentelemetry/Application.java b/opentelemetry-java/integration-tests/tracecontext/src/main/java/io/opentelemetry/Application.java new file mode 100644 index 000000000..a0054f32c --- /dev/null +++ b/opentelemetry-java/integration-tests/tracecontext/src/main/java/io/opentelemetry/Application.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; +import com.linecorp.armeria.common.HttpMethod; +import com.linecorp.armeria.common.MediaType; +import com.linecorp.armeria.common.RequestHeaders; +import com.linecorp.armeria.common.RequestHeadersBuilder; +import com.linecorp.armeria.server.Server; +import com.linecorp.armeria.server.annotation.Blocking; +import com.linecorp.armeria.server.annotation.Post; +import com.linecorp.armeria.server.healthcheck.HealthCheckService; +import com.linecorp.armeria.server.logging.LoggingService; +import io.netty.util.AsciiString; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +public final class Application { + private static final Logger logger = Logger.getLogger(Application.class.getName()); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final OpenTelemetry openTelemetry; + + static { + openTelemetry = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build(); + } + + private enum ArmeriaGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(RequestHeaders carrier) { + return carrier.names().stream().map(AsciiString::toString).collect(Collectors.toList()); + } + + @Nullable + @Override + public String get(@Nullable RequestHeaders carrier, String key) { + if (carrier == null) { + return null; + } + return carrier.get(key); + } + } + + private enum ArmeriaSetter implements TextMapSetter { + INSTANCE; + + @Override + public void set(@Nullable RequestHeadersBuilder carrier, String key, String value) { + if (carrier == null) { + return; + } + carrier.set(key, value); + } + } + + private static class Service { + private final WebClient client = WebClient.of(); + + @Post("/verify-tracecontext") + @Blocking + public String serve(RequestHeaders headers, List requests) { + Context context = + openTelemetry + .getPropagators() + .getTextMapPropagator() + .extract(Context.current(), headers, ArmeriaGetter.INSTANCE); + + for (io.opentelemetry.Request req : requests) { + Span span = + openTelemetry + .getTracer("validation-server") + .spanBuilder("Entering Validation Server") + .setParent(context) + .startSpan(); + + Context withSpanContext = context.with(span); + + RequestHeadersBuilder outHeaders = + RequestHeaders.builder(HttpMethod.POST, req.getUrl()).contentType(MediaType.JSON_UTF_8); + openTelemetry + .getPropagators() + .getTextMapPropagator() + .inject(withSpanContext, outHeaders, ArmeriaSetter.INSTANCE); + + try { + AggregatedHttpResponse response = + client + .execute(outHeaders.build(), objectMapper.writeValueAsBytes(req.getArguments())) + .aggregate() + .join(); + logger.info("response: " + response.status()); + } catch (Throwable t) { + logger.log(Level.SEVERE, "failed to send", t); + } + span.end(); + } + return "Done"; + } + } + + private Application() {} + + /** Entry point. */ + public static void main(String[] args) { + Server server = + Server.builder() + .http(5000) + .annotatedService(new Service()) + .service("/health", HealthCheckService.of()) + .decorator(LoggingService.newDecorator()) + .build(); + server.start().join(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> server.stop().join())); + } +} diff --git a/opentelemetry-java/integration-tests/tracecontext/src/main/java/io/opentelemetry/Request.java b/opentelemetry-java/integration-tests/tracecontext/src/main/java/io/opentelemetry/Request.java new file mode 100644 index 000000000..6f90d4100 --- /dev/null +++ b/opentelemetry-java/integration-tests/tracecontext/src/main/java/io/opentelemetry/Request.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry; + +import java.util.Arrays; + +public final class Request { + private String url; + private Request[] arguments; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Request[] getArguments() { + return arguments; + } + + public void setArguments(Request[] arguments) { + this.arguments = arguments; + } + + @Override + public String toString() { + return "Request{" + "url='" + url + '\'' + ", arguments=" + Arrays.toString(arguments) + '}'; + } +} diff --git a/opentelemetry-java/integration-tests/tracecontext/src/main/resources/simplelogger.properties b/opentelemetry-java/integration-tests/tracecontext/src/main/resources/simplelogger.properties new file mode 100644 index 000000000..7c21e58a5 --- /dev/null +++ b/opentelemetry-java/integration-tests/tracecontext/src/main/resources/simplelogger.properties @@ -0,0 +1,2 @@ +org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.log.org.eclipse.jetty=warn \ No newline at end of file diff --git a/opentelemetry-java/integration-tests/tracecontext/src/test/java/io/opentelemetry/integrationtests/tracecontext/TraceContextIntegrationTest.java b/opentelemetry-java/integration-tests/tracecontext/src/test/java/io/opentelemetry/integrationtests/tracecontext/TraceContextIntegrationTest.java new file mode 100644 index 000000000..a7856fa1d --- /dev/null +++ b/opentelemetry-java/integration-tests/tracecontext/src/test/java/io/opentelemetry/integrationtests/tracecontext/TraceContextIntegrationTest.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtests.tracecontext; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +// TODO(anuraaga): https://github.com/open-telemetry/opentelemetry-java/issues/3117 +@Disabled +@Testcontainers(disabledWithoutDocker = true) +class TraceContextIntegrationTest { + + @Container + private static final GenericContainer appContainer = + new GenericContainer<>( + DockerImageName.parse("ghcr.io/open-telemetry/java-test-containers:openjdk8")) + .withExposedPorts(5000) + .withNetwork(Network.SHARED) + .withNetworkAliases("app") + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("app"))) + .withCommand("java", "-jar", "/opt/app.jar") + .waitingFor(Wait.forHttp("/health")) + .withCopyFileToContainer( + MountableFile.forHostPath(System.getProperty("io.opentelemetry.testArchive")), + "/opt/app.jar"); + + @Container + private static final GenericContainer testSuiteContainer = + new GenericContainer<>( + DockerImageName.parse( + "ghcr.io/open-telemetry/java-test-containers:w3c-tracecontext-testsuite")) + .withNetwork(Network.SHARED) + .withNetworkAliases("testsuite") + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("testsuite"))) + .withCommand("http://app:5000/verify-tracecontext") + .withEnv("HARNESS_HOST", "testsuite") + .waitingFor(Wait.forLogMessage(".*Ran \\d+ tests in \\d+\\.\\d+s.*", 1)) + .dependsOn(appContainer); + + @Test + void run() { + // TODO(anuraaga): We currently run all tests to print logs of our compatibility, including many + // failing tests. If we are ever able to fix the tests we can add an assertion here that the + // test succeeded. + } +} diff --git a/opentelemetry-java/integration-tests/tracecontext/tracecontext-integration-test.sh b/opentelemetry-java/integration-tests/tracecontext/tracecontext-integration-test.sh new file mode 100755 index 000000000..ef86e2d8d --- /dev/null +++ b/opentelemetry-java/integration-tests/tracecontext/tracecontext-integration-test.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e +# hard-coding the git tag to ensure stable builds. +TRACECONTEXT_GIT_TAG="98f210efd89c63593dce90e2bae0a1bdcb986f51" +# clone w3c tracecontext tests +mkdir -p target +rm -rf ./target/trace-context +git clone https://github.com/w3c/trace-context ./target/trace-context +cd ./target/trace-context && git checkout $TRACECONTEXT_GIT_TAG && cd - +pip3 install setuptools; +pip3 install aiohttp; +../../gradlew shadowJar +java -jar build/libs/tracecontext-tests.jar 1>&2 & +EXAMPLE_SERVER_PID=$! +# give the app server a little time to start up. Not adding some sort +# of delay would cause many of the tracecontext tests to fail being +# unable to connect. +sleep 5 +onshutdown() +{ + # sleep for printing test results + sleep 2 + kill $EXAMPLE_SERVER_PID +} +trap onshutdown EXIT +export STRICT_LEVEL=1 +cd ./target/trace-context/test +python3 test.py http://127.0.0.1:5000/verify-tracecontext \ No newline at end of file diff --git a/opentelemetry-java/opencensus-shim/README.md b/opentelemetry-java/opencensus-shim/README.md new file mode 100644 index 000000000..347d59579 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/README.md @@ -0,0 +1,51 @@ + +# OpenTelemetry OpenCensus Shim + +The OpenCensus shim allows applications and libraries that are instrumented +with OpenTelemetry, but depend on other libraries instrumented with OpenCensus, +to export trace spans from both OpenTelemetry and OpenCensus with the correct +parent-child relationship. It also allows OpenCensus metrics to be exported by +any configured OpenTelemetry metric exporter. + +## Usage + +### Traces + +To allow the shim to work for traces, add the shim as a dependency. + +Nothing else needs to be added to libraries for this to work. + +Applications only need to set up OpenTelemetry exporters, not OpenCensus. + +### Metrics + +To allow the shim to work for metrics, add the shim as a dependency. + +Applications also need to pass the configured metric exporter to the shim: + +``` +OpenTelemetryMetricsExporter exporter = + OpenTelemetryMetricsExporter.createAndRegister(metricExporter); +``` + +For example, if a logging exporter were configured, the following would be +added: + +``` +LoggingMetricExporter metricExporter = new LoggingMetricExporter(); +OpenTelemetryMetricsExporter exporter = + OpenTelemetryMetricsExporter.createAndRegister(metricExporter); +``` + +The export interval can also be set: + +``` +OpenTelemetryMetricsExporter exporter = + OpenTelemetryMetricsExporter.createAndRegister(metricExporter, + Duration.create(0, 500)); +``` + +## Known Problems + +* OpenCensus links added after an OpenCensus span is created will not be +exported, as OpenTelemetry only supports links added when a span is created. diff --git a/opentelemetry-java/opencensus-shim/build.gradle.kts b/opentelemetry-java/opencensus-shim/build.gradle.kts new file mode 100644 index 000000000..1716b45c2 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `java-library` + `maven-publish` +} + +description = "OpenTelemetry OpenCensus Shim" +extra["moduleName"] = "io.opentelemetry.opencensusshim" + +dependencies { + api(project(":api:all")) + api(project(":extensions:trace-propagators")) + api(project(":sdk:all")) + api(project(":sdk:metrics")) + + api("io.opencensus:opencensus-api") + api("io.opencensus:opencensus-impl-core") + api("io.opencensus:opencensus-exporter-metrics-util") + + testImplementation(project(":sdk:all")) + + testImplementation("org.slf4j:slf4j-simple") + testImplementation("io.opencensus:opencensus-impl") +} diff --git a/opentelemetry-java/opencensus-shim/gradle.properties b/opentelemetry-java/opencensus-shim/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/opencensus-shim/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryContextManager.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryContextManager.java new file mode 100644 index 000000000..d73e41ab5 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryContextManager.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import io.opencensus.trace.ContextHandle; +import io.opencensus.trace.ContextManager; +import io.opencensus.trace.Span; +import io.opentelemetry.context.Context; +import java.util.logging.Logger; + +/** + * This is a context manager implementation that overrides the default OpenCensus context manager + * {@link io.opencensus.trace.unsafe.ContextManagerImpl}. It is loaded by OpenCensus via reflection + * automatically in {@link io.opencensus.trace.unsafe.ContextHandleUtils} when the OpenCensus shim + * library exists as a dependency. + */ +public final class OpenTelemetryContextManager implements ContextManager { + + private static final Logger LOGGER = + Logger.getLogger(OpenTelemetryContextManager.class.getName()); + + public OpenTelemetryContextManager() {} + + @Override + public ContextHandle currentContext() { + return wrapContext(Context.current()); + } + + @Override + public ContextHandle withValue(ContextHandle ctx, Span span) { + if (!(ctx instanceof OpenTelemetryCtx)) { + LOGGER.warning( + "ContextHandle is not an instance of OpenTelemetryCtx. " + + "There should not be a local implementation of ContextHandle " + + "other than OpenTelemetryCtx."); + } + OpenTelemetryCtx openTelemetryCtx = (OpenTelemetryCtx) ctx; + if (span instanceof OpenTelemetrySpanImpl) { + return wrapContext(unwrapContext(openTelemetryCtx).with((OpenTelemetrySpanImpl) span)); + } + return wrapContext( + unwrapContext(openTelemetryCtx).with((OpenTelemetryNoRecordEventsSpanImpl) span)); + } + + @Override + public Span getValue(ContextHandle ctx) { + io.opentelemetry.api.trace.Span span = + io.opentelemetry.api.trace.Span.fromContext(unwrapContext(ctx)); + if (span instanceof OpenTelemetrySpanImpl) { + return (OpenTelemetrySpanImpl) span; + } + return SpanConverter.fromOtelSpan(span); + } + + private static ContextHandle wrapContext(Context context) { + return new OpenTelemetryCtx(context); + } + + private static Context unwrapContext(ContextHandle ctx) { + return ((OpenTelemetryCtx) ctx).getContext(); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryCtx.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryCtx.java new file mode 100644 index 000000000..ae94f6307 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryCtx.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import io.opencensus.trace.ContextHandle; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import javax.annotation.Nullable; + +class OpenTelemetryCtx implements ContextHandle { + + private final Context context; + + @Nullable private Scope scope; + + public OpenTelemetryCtx(Context context) { + this.context = context; + } + + Context getContext() { + return context; + } + + @Override + @SuppressWarnings("MustBeClosedChecker") + public ContextHandle attach() { + scope = context.makeCurrent(); + return this; + } + + @Override + public void detach(ContextHandle ctx) { + OpenTelemetryCtx impl = (OpenTelemetryCtx) ctx; + if (impl.scope != null) { + impl.scope.close(); + } + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryMetricsExporter.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryMetricsExporter.java new file mode 100644 index 000000000..3d615445e --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryMetricsExporter.java @@ -0,0 +1,261 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import com.google.common.base.Joiner; +import io.opencensus.common.Duration; +import io.opencensus.exporter.metrics.util.IntervalMetricReader; +import io.opencensus.exporter.metrics.util.MetricExporter; +import io.opencensus.exporter.metrics.util.MetricReader; +import io.opencensus.metrics.Metrics; +import io.opencensus.metrics.export.Metric; +import io.opencensus.metrics.export.MetricDescriptor; +import io.opencensus.metrics.export.Point; +import io.opencensus.metrics.export.Summary; +import io.opencensus.metrics.export.Summary.Snapshot; +import io.opencensus.metrics.export.TimeSeries; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.metrics.common.LabelsBuilder; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class OpenTelemetryMetricsExporter extends MetricExporter { + private static final Logger LOGGER = + Logger.getLogger(OpenTelemetryMetricsExporter.class.getName()); + + private static final String EXPORTER_NAME = "OpenTelemetryMetricExporter"; + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create("io.opentelemetry.opencensusshim", null); + + private final IntervalMetricReader intervalMetricReader; + private final io.opentelemetry.sdk.metrics.export.MetricExporter otelExporter; + + public static OpenTelemetryMetricsExporter createAndRegister( + io.opentelemetry.sdk.metrics.export.MetricExporter otelExporter) { + return new OpenTelemetryMetricsExporter(otelExporter, Duration.create(60, 0)); + } + + public static OpenTelemetryMetricsExporter createAndRegister( + io.opentelemetry.sdk.metrics.export.MetricExporter otelExporter, Duration exportInterval) { + return new OpenTelemetryMetricsExporter(otelExporter, exportInterval); + } + + private OpenTelemetryMetricsExporter( + io.opentelemetry.sdk.metrics.export.MetricExporter otelExporter, Duration exportInterval) { + this.otelExporter = otelExporter; + IntervalMetricReader.Options.Builder options = IntervalMetricReader.Options.builder(); + MetricReader reader = + MetricReader.create( + MetricReader.Options.builder() + .setMetricProducerManager(Metrics.getExportComponent().getMetricProducerManager()) + .setSpanName(EXPORTER_NAME) + .build()); + intervalMetricReader = + IntervalMetricReader.create( + this, reader, options.setExportInterval(exportInterval).build()); + } + + @Override + public void export(Collection metrics) { + List metricData = new ArrayList<>(); + Set unsupportedTypes = new HashSet<>(); + for (Metric metric : metrics) { + for (TimeSeries timeSeries : metric.getTimeSeriesList()) { + LabelsBuilder labelsBuilder = Labels.builder(); + for (int i = 0; i < metric.getMetricDescriptor().getLabelKeys().size(); i++) { + if (timeSeries.getLabelValues().get(i).getValue() != null) { + labelsBuilder.put( + metric.getMetricDescriptor().getLabelKeys().get(i).getKey(), + timeSeries.getLabelValues().get(i).getValue()); + } + } + Labels labels = labelsBuilder.build(); + List points = new ArrayList<>(); + MetricDescriptor.Type type = null; + for (Point point : timeSeries.getPoints()) { + type = mapAndAddPoint(unsupportedTypes, metric, labels, points, point); + } + MetricData md = toMetricData(type, metric.getMetricDescriptor(), points); + if (md != null) { + metricData.add(md); + } + } + } + if (!unsupportedTypes.isEmpty()) { + LOGGER.warning( + Joiner.on(",").join(unsupportedTypes) + + " not supported by OpenCensus to OpenTelemetry migrator."); + } + if (!metricData.isEmpty()) { + otelExporter.export(metricData); + } + } + + @Nonnull + private static MetricDescriptor.Type mapAndAddPoint( + Set unsupportedTypes, + Metric metric, + Labels labels, + List points, + Point point) { + long timestampNanos = + TimeUnit.SECONDS.toNanos(point.getTimestamp().getSeconds()) + + point.getTimestamp().getNanos(); + MetricDescriptor.Type type = metric.getMetricDescriptor().getType(); + switch (type) { + case GAUGE_INT64: + case CUMULATIVE_INT64: + points.add(mapLongPoint(labels, point, timestampNanos)); + break; + case GAUGE_DOUBLE: + case CUMULATIVE_DOUBLE: + points.add(mapDoublePoint(labels, point, timestampNanos)); + break; + case SUMMARY: + points.add(mapSummaryPoint(labels, point, timestampNanos)); + break; + default: + unsupportedTypes.add(type); + break; + } + return type; + } + + public void stop() { + intervalMetricReader.stop(); + } + + @Nonnull + private static DoubleSummaryPointData mapSummaryPoint( + Labels labels, Point point, long timestampNanos) { + return DoubleSummaryPointData.create( + timestampNanos, + timestampNanos, + labels, + point + .getValue() + .match(arg -> null, arg -> null, arg -> null, Summary::getCount, arg -> null), + point.getValue().match(arg -> null, arg -> null, arg -> null, Summary::getSum, arg -> null), + point + .getValue() + .match( + arg -> null, + arg -> null, + arg -> null, + OpenTelemetryMetricsExporter::mapPercentiles, + arg -> null)); + } + + private static List mapPercentiles(Summary arg) { + List percentiles = new ArrayList<>(); + for (Snapshot.ValueAtPercentile percentile : arg.getSnapshot().getValueAtPercentiles()) { + percentiles.add(ValueAtPercentile.create(percentile.getPercentile(), percentile.getValue())); + } + return percentiles; + } + + @Nonnull + private static DoublePointData mapDoublePoint(Labels labels, Point point, long timestampNanos) { + return DoublePointData.create( + timestampNanos, + timestampNanos, + labels, + point + .getValue() + .match(arg -> arg, Long::doubleValue, arg -> null, arg -> null, arg -> null)); + } + + @Nonnull + private static LongPointData mapLongPoint(Labels labels, Point point, long timestampNanos) { + return LongPointData.create( + timestampNanos, + timestampNanos, + labels, + point + .getValue() + .match(Double::longValue, arg -> arg, arg -> null, arg -> null, arg -> null)); + } + + @Nullable + @SuppressWarnings("unchecked") + private static MetricData toMetricData( + MetricDescriptor.Type type, + MetricDescriptor metricDescriptor, + List points) { + if (metricDescriptor.getType() == null) { + return null; + } + switch (type) { + case GAUGE_INT64: + return MetricData.createLongGauge( + Resource.getDefault(), + INSTRUMENTATION_LIBRARY_INFO, + metricDescriptor.getName(), + metricDescriptor.getDescription(), + metricDescriptor.getUnit(), + LongGaugeData.create((List) points)); + + case GAUGE_DOUBLE: + return MetricData.createDoubleGauge( + Resource.getDefault(), + INSTRUMENTATION_LIBRARY_INFO, + metricDescriptor.getName(), + metricDescriptor.getDescription(), + metricDescriptor.getUnit(), + DoubleGaugeData.create((List) points)); + + case CUMULATIVE_INT64: + return MetricData.createLongSum( + Resource.getDefault(), + INSTRUMENTATION_LIBRARY_INFO, + metricDescriptor.getName(), + metricDescriptor.getDescription(), + metricDescriptor.getUnit(), + LongSumData.create( + true, AggregationTemporality.CUMULATIVE, (List) points)); + case CUMULATIVE_DOUBLE: + return MetricData.createDoubleSum( + Resource.getDefault(), + INSTRUMENTATION_LIBRARY_INFO, + metricDescriptor.getName(), + metricDescriptor.getDescription(), + metricDescriptor.getUnit(), + DoubleSumData.create( + true, AggregationTemporality.CUMULATIVE, (List) points)); + case SUMMARY: + return MetricData.createDoubleSummary( + Resource.getDefault(), + INSTRUMENTATION_LIBRARY_INFO, + metricDescriptor.getName(), + metricDescriptor.getDescription(), + metricDescriptor.getUnit(), + DoubleSummaryData.create((List) points)); + default: + return null; + } + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryNoRecordEventsSpanImpl.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryNoRecordEventsSpanImpl.java new file mode 100644 index 000000000..b6f3e8eec --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryNoRecordEventsSpanImpl.java @@ -0,0 +1,182 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.opencensusshim; + +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.MessageEvent; +import io.opencensus.trace.Span; +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.Status; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.StatusCode; +import java.util.EnumSet; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; + +class OpenTelemetryNoRecordEventsSpanImpl extends Span implements io.opentelemetry.api.trace.Span { + private static final EnumSet NOT_RECORD_EVENTS_SPAN_OPTIONS = + EnumSet.noneOf(Options.class); + + private OpenTelemetryNoRecordEventsSpanImpl(SpanContext context) { + super(context, NOT_RECORD_EVENTS_SPAN_OPTIONS); + } + + static OpenTelemetryNoRecordEventsSpanImpl create(SpanContext context) { + return new OpenTelemetryNoRecordEventsSpanImpl(context); + } + + @Override + public void addAnnotation(String description, Map 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 attributes) { + Preconditions.checkNotNull(attributes, "attributes"); + } + + @Override + public void addMessageEvent(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 io.opentelemetry.api.trace.Span setStatus(StatusCode canonicalCode) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span setStatus(StatusCode canonicalCode, String description) { + return this; + } + + @Override + public void end(EndSpanOptions options) { + Preconditions.checkNotNull(options, "options"); + } + + @Override + public void end(long timestamp, TimeUnit unit) { + // do nothing + } + + @Override + public io.opentelemetry.api.trace.Span setAttribute(String key, @Nonnull String value) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span setAttribute(String key, long value) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span setAttribute(String key, double value) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span setAttribute(String key, boolean value) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span setAttribute(AttributeKey key, @Nonnull T value) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span addEvent(String name) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span addEvent(String name, long timestamp, TimeUnit unit) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span addEvent(String name, Attributes attributes) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span addEvent( + String name, Attributes attributes, long timestamp, TimeUnit unit) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span recordException(Throwable exception) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span recordException( + Throwable exception, Attributes additionalAttributes) { + return this; + } + + @Override + public io.opentelemetry.api.trace.Span updateName(String name) { + return this; + } + + @Override + public io.opentelemetry.api.trace.SpanContext getSpanContext() { + return SpanConverter.mapSpanContext(getContext()); + } + + @Override + public boolean isRecording() { + return false; + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryPropagationComponentImpl.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryPropagationComponentImpl.java new file mode 100644 index 000000000..6898345eb --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryPropagationComponentImpl.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import io.opencensus.implcore.trace.propagation.PropagationComponentImpl; +import io.opencensus.trace.propagation.TextFormat; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.extension.trace.propagation.B3Propagator; + +class OpenTelemetryPropagationComponentImpl extends PropagationComponentImpl { + + private final TextFormat b3Format = + new OpenTelemetryTextFormatImpl(B3Propagator.injectingMultiHeaders()); + private final TextFormat traceContextFormat = + new OpenTelemetryTextFormatImpl(W3CTraceContextPropagator.getInstance()); + + @Override + public TextFormat getB3Format() { + return b3Format; + } + + @Override + public TextFormat getTraceContextFormat() { + return traceContextFormat; + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetrySpanBuilderImpl.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetrySpanBuilderImpl.java new file mode 100644 index 000000000..fbf4b423e --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetrySpanBuilderImpl.java @@ -0,0 +1,221 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.opencensusshim; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.opentelemetry.opencensusshim.SpanConverter.mapKind; + +import io.opencensus.implcore.trace.internal.RandomHandler; +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 io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import javax.annotation.Nullable; + +class OpenTelemetrySpanBuilderImpl extends SpanBuilder { + private static final Tracer OTEL_TRACER = + GlobalOpenTelemetry.getTracer("io.opentelemetry.opencensusshim"); + private static final Tracestate OC_TRACESTATE_DEFAULT = Tracestate.builder().build(); + private static final TraceOptions OC_SAMPLED_TRACE_OPTIONS = + TraceOptions.builder().setIsSampled(true).build(); + private static final TraceOptions OC_NOT_SAMPLED_TRACE_OPTIONS = + TraceOptions.builder().setIsSampled(false).build(); + + private final String name; + private final Options options; + + private List ocParentLinks = Collections.emptyList(); + private final List otelParentLinks = new ArrayList<>(); + @Nullable private final Span ocParent; + @Nullable private final SpanContext ocRemoteParentSpanContext; + @Nullable private Sampler ocSampler; + @Nullable private SpanKind otelKind; + + @Override + public SpanBuilder setSampler(Sampler sampler) { + this.ocSampler = checkNotNull(sampler, "sampler"); + return this; + } + + @Override + public SpanBuilder setParentLinks(List parentLinks) { + this.ocParentLinks = checkNotNull(parentLinks, "parentLinks"); + for (Span parent : parentLinks) { + this.otelParentLinks.add(SpanConverter.mapSpanContext(parent.getContext())); + } + return this; + } + + @Override + public SpanBuilder setRecordEvents(boolean recordEvents) { + return this; + } + + @Override + public SpanBuilder setSpanKind(@Nullable Kind kind) { + this.otelKind = mapKind(kind); + return this; + } + + @Override + public Span startSpan() { + // To determine whether to sample this span + TraceParams ocActiveTraceParams = options.traceConfig.getActiveTraceParams(); + Random random = options.randomHandler.current(); + TraceId ocTraceId; + SpanId ocSpanId = SpanId.generateRandomId(random); + Tracestate ocTracestate = OC_TRACESTATE_DEFAULT; + SpanContext ocParentContext = null; + Boolean hasRemoteParent = null; + if (ocRemoteParentSpanContext != null && ocRemoteParentSpanContext.isValid()) { + ocParentContext = ocRemoteParentSpanContext; + hasRemoteParent = Boolean.TRUE; + ocTraceId = ocParentContext.getTraceId(); + ocTracestate = ocParentContext.getTracestate(); + } else if (ocParent != null && ocParent.getContext().isValid()) { + ocParentContext = ocParent.getContext(); + hasRemoteParent = Boolean.FALSE; + ocTraceId = ocParentContext.getTraceId(); + ocTracestate = ocParentContext.getTracestate(); + } else { + // New root span. + ocTraceId = TraceId.generateRandomId(random); + } + TraceOptions ocTraceOptions = + makeSamplingDecision( + ocParentContext, + hasRemoteParent, + name, + ocSampler, + ocParentLinks, + ocTraceId, + ocSpanId, + ocActiveTraceParams) + ? OC_SAMPLED_TRACE_OPTIONS + : OC_NOT_SAMPLED_TRACE_OPTIONS; + if (!ocTraceOptions.isSampled()) { + return OpenTelemetryNoRecordEventsSpanImpl.create( + SpanContext.create(ocTraceId, ocSpanId, ocTraceOptions, ocTracestate)); + } + + // If sampled + io.opentelemetry.api.trace.SpanBuilder otelSpanBuilder = OTEL_TRACER.spanBuilder(name); + if (ocParent != null && ocParent instanceof OpenTelemetrySpanImpl) { + otelSpanBuilder.setParent(Context.current().with((OpenTelemetrySpanImpl) ocParent)); + } + if (ocRemoteParentSpanContext != null) { + otelSpanBuilder.addLink(SpanConverter.mapSpanContext(ocRemoteParentSpanContext)); + } + if (otelKind != null) { + otelSpanBuilder.setSpanKind(otelKind); + } + if (!otelParentLinks.isEmpty()) { + for (io.opentelemetry.api.trace.SpanContext spanContext : otelParentLinks) { + otelSpanBuilder.addLink(spanContext); + } + } + io.opentelemetry.api.trace.Span otSpan = otelSpanBuilder.startSpan(); + return new OpenTelemetrySpanImpl(otSpan); + } + + private OpenTelemetrySpanBuilderImpl( + String name, + @Nullable SpanContext ocRemoteParentSpanContext, + @Nullable Span ocParent, + OpenTelemetrySpanBuilderImpl.Options options) { + this.name = checkNotNull(name, "name"); + this.ocParent = ocParent; + this.ocRemoteParentSpanContext = ocRemoteParentSpanContext; + this.options = options; + } + + static OpenTelemetrySpanBuilderImpl createWithParent( + String spanName, @Nullable Span parent, OpenTelemetrySpanBuilderImpl.Options options) { + return new OpenTelemetrySpanBuilderImpl(spanName, null, parent, options); + } + + static OpenTelemetrySpanBuilderImpl createWithRemoteParent( + String spanName, + @Nullable SpanContext remoteParentSpanContext, + OpenTelemetrySpanBuilderImpl.Options options) { + return new OpenTelemetrySpanBuilderImpl(spanName, remoteParentSpanContext, null, options); + } + + private static boolean makeSamplingDecision( + @Nullable SpanContext parent, + @Nullable Boolean hasRemoteParent, + String name, + @Nullable Sampler sampler, + List 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 parentLinks) { + for (Span parentLink : parentLinks) { + if (parentLink.getContext().getTraceOptions().isSampled()) { + return true; + } + } + return false; + } + + static final class Options { + private final RandomHandler randomHandler; + private final TraceConfig traceConfig; + + Options(RandomHandler randomHandler, TraceConfig traceConfig) { + this.randomHandler = checkNotNull(randomHandler, "randomHandler"); + this.traceConfig = checkNotNull(traceConfig, "traceConfig"); + } + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetrySpanImpl.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetrySpanImpl.java new file mode 100644 index 000000000..6d0e39501 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetrySpanImpl.java @@ -0,0 +1,200 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.opencensusshim; + +import static io.opentelemetry.opencensusshim.SpanConverter.MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED; +import static io.opentelemetry.opencensusshim.SpanConverter.MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED; +import static io.opentelemetry.opencensusshim.SpanConverter.MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE; +import static io.opentelemetry.opencensusshim.SpanConverter.mapSpanContext; +import static io.opentelemetry.opencensusshim.SpanConverter.setBooleanAttribute; +import static io.opentelemetry.opencensusshim.SpanConverter.setDoubleAttribute; +import static io.opentelemetry.opencensusshim.SpanConverter.setLongAttribute; +import static io.opentelemetry.opencensusshim.SpanConverter.setStringAttribute; + +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.MessageEvent; +import io.opencensus.trace.Span; +import io.opencensus.trace.Status; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.StatusCode; +import java.util.EnumSet; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import javax.annotation.Nonnull; + +class OpenTelemetrySpanImpl extends Span implements io.opentelemetry.api.trace.Span { + private static final Logger LOGGER = Logger.getLogger(OpenTelemetrySpanImpl.class.getName()); + private static final EnumSet RECORD_EVENTS_SPAN_OPTIONS = + EnumSet.of(Span.Options.RECORD_EVENTS); + + private final io.opentelemetry.api.trace.Span otelSpan; + + OpenTelemetrySpanImpl(io.opentelemetry.api.trace.Span otelSpan) { + super(mapSpanContext(otelSpan.getSpanContext()), RECORD_EVENTS_SPAN_OPTIONS); + this.otelSpan = otelSpan; + } + + @Override + public void putAttribute(String key, AttributeValue value) { + Preconditions.checkNotNull(key, "key"); + Preconditions.checkNotNull(value, "value"); + value.match( + arg -> otelSpan.setAttribute(key, arg), + arg -> otelSpan.setAttribute(key, arg), + arg -> otelSpan.setAttribute(key, arg), + arg -> otelSpan.setAttribute(key, arg), + arg -> null); + } + + @Override + public void putAttributes(Map attributes) { + Preconditions.checkNotNull(attributes, "attributes"); + attributes.forEach(this::putAttribute); + } + + @Override + public void addAnnotation(String description, Map attributes) { + AttributesBuilder attributesBuilder = Attributes.builder(); + mapAttributes(attributes, attributesBuilder); + otelSpan.addEvent(description, attributesBuilder.build()); + } + + @Override + public void addAnnotation(Annotation annotation) { + AttributesBuilder attributesBuilder = Attributes.builder(); + mapAttributes(annotation.getAttributes(), attributesBuilder); + otelSpan.addEvent(annotation.getDescription(), attributesBuilder.build()); + } + + @Override + public void addLink(Link link) { + LOGGER.warning("OpenTelemetry does not support links added after a span is created."); + } + + @Override + public void addMessageEvent(MessageEvent messageEvent) { + otelSpan.addEvent( + String.valueOf(messageEvent.getMessageId()), + Attributes.of( + AttributeKey.stringKey(MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE), + messageEvent.getType().toString(), + AttributeKey.longKey(MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED), + messageEvent.getUncompressedMessageSize(), + AttributeKey.longKey(MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED), + messageEvent.getCompressedMessageSize())); + } + + @Override + public void setStatus(Status status) { + Preconditions.checkNotNull(status, "status"); + otelSpan.setStatus(status.isOk() ? StatusCode.OK : StatusCode.ERROR); + } + + @Override + public io.opentelemetry.api.trace.Span setStatus(StatusCode canonicalCode, String description) { + return otelSpan.setStatus(canonicalCode, description); + } + + @Override + public void end(EndSpanOptions options) { + otelSpan.end(); + } + + @Override + @SuppressWarnings("ParameterPackage") + public void end(long timestamp, TimeUnit unit) { + otelSpan.end(timestamp, unit); + } + + @Override + public io.opentelemetry.api.trace.Span setAttribute(AttributeKey key, @Nonnull T value) { + return otelSpan.setAttribute(key, value); + } + + @Override + public io.opentelemetry.api.trace.Span addEvent(String name) { + return otelSpan.addEvent(name); + } + + @Override + public io.opentelemetry.api.trace.Span addEvent(String name, long timestamp, TimeUnit unit) { + return otelSpan.addEvent(name, timestamp, unit); + } + + @Override + public io.opentelemetry.api.trace.Span addEvent(String name, Attributes attributes) { + return otelSpan.addEvent(name, attributes); + } + + @Override + public io.opentelemetry.api.trace.Span addEvent( + String name, Attributes attributes, long timestamp, TimeUnit unit) { + return otelSpan.addEvent(name, attributes, timestamp, unit); + } + + @Override + public io.opentelemetry.api.trace.Span recordException(Throwable exception) { + return otelSpan.recordException(exception); + } + + @Override + public io.opentelemetry.api.trace.Span recordException( + Throwable exception, Attributes additionalAttributes) { + return otelSpan.recordException(exception, additionalAttributes); + } + + @Override + public io.opentelemetry.api.trace.Span updateName(String name) { + return otelSpan.updateName(name); + } + + @Override + public SpanContext getSpanContext() { + return otelSpan.getSpanContext(); + } + + @Override + public boolean isRecording() { + return true; + } + + private static void mapAttributes( + Map attributes, AttributesBuilder attributesBuilder) { + attributes.forEach( + (s, attributeValue) -> + attributeValue.match( + setStringAttribute(attributesBuilder, s), + setBooleanAttribute(attributesBuilder, s), + setLongAttribute(attributesBuilder, s), + setDoubleAttribute(attributesBuilder, s), + arg -> null)); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTextFormatImpl.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTextFormatImpl.java new file mode 100644 index 000000000..d9ed3adf5 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTextFormatImpl.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import static io.opentelemetry.opencensusshim.SpanConverter.mapSpanContext; + +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.propagation.TextFormat; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +class OpenTelemetryTextFormatImpl extends TextFormat { + + private final TextMapPropagator propagator; + + OpenTelemetryTextFormatImpl(TextMapPropagator propagator) { + this.propagator = propagator; + } + + @Override + public List fields() { + return new ArrayList<>(propagator.fields()); + } + + @Override + public void inject(SpanContext spanContext, C carrier, Setter setter) { + io.opentelemetry.api.trace.SpanContext otelSpanContext = mapSpanContext(spanContext); + Context otelContext = Context.current().with(Span.wrap(otelSpanContext)); + propagator.inject(otelContext, carrier, setter::put); + } + + @Override + public SpanContext extract(C carrier, Getter getter) { + Context context = + propagator.extract( + Context.current(), + carrier, + new TextMapGetter() { + // OC Getter cannot return keys for an object, but users should not need it either. + @Override + public Iterable keys(C carrier) { + return Collections.emptyList(); + } + + @Nullable + @Override + public String get(@Nullable C carrier, String key) { + return getter.get(carrier, key); + } + }); + io.opentelemetry.api.trace.SpanContext spanContext = Span.fromContext(context).getSpanContext(); + return mapSpanContext(spanContext); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTraceComponentImpl.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTraceComponentImpl.java new file mode 100644 index 000000000..b35e0092c --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTraceComponentImpl.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import io.opencensus.common.Clock; +import io.opencensus.implcore.common.MillisClock; +import io.opencensus.implcore.trace.config.TraceConfigImpl; +import io.opencensus.implcore.trace.internal.RandomHandler; +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; + +/** + * Implementation of the {@link TraceComponent} for OpenTelemetry migration, which uses the + * OpenTelemetry migration StartEndHandler. This class is loaded by reflection in {@link + * io.opencensus.trace.Tracing} and overrides the OpenCensus default implementation when present. + */ +public final class OpenTelemetryTraceComponentImpl extends TraceComponent { + private final PropagationComponent propagationComponent = + new OpenTelemetryPropagationComponentImpl(); + private final ExportComponent noopExportComponent = ExportComponent.newNoopExportComponent(); + private final Clock clock; + private final TraceConfig traceConfig = new TraceConfigImpl(); + private final Tracer tracer; + + /** Public constructor to be used with reflection loading. */ + public OpenTelemetryTraceComponentImpl() { + clock = MillisClock.getInstance(); + RandomHandler randomHandler = new ThreadLocalRandomHandler(); + tracer = new OpenTelemetryTracerImpl(randomHandler, traceConfig); + } + + @Override + public Tracer getTracer() { + return tracer; + } + + @Override + public PropagationComponent getPropagationComponent() { + return propagationComponent; + } + + @Override + public final Clock getClock() { + return clock; + } + + @Override + public ExportComponent getExportComponent() { + // Drop all OC spans, we will export converted OT spans instead + return noopExportComponent; + } + + @Override + public TraceConfig getTraceConfig() { + return traceConfig; + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTracerImpl.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTracerImpl.java new file mode 100644 index 000000000..c3ba98b67 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/OpenTelemetryTracerImpl.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.opencensusshim; + +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; + +class OpenTelemetryTracerImpl extends Tracer { + private final OpenTelemetrySpanBuilderImpl.Options spanBuilderOptions; + + public OpenTelemetryTracerImpl(RandomHandler randomHandler, TraceConfig traceConfig) { + spanBuilderOptions = new OpenTelemetrySpanBuilderImpl.Options(randomHandler, traceConfig); + } + + @Override + public SpanBuilder spanBuilderWithExplicitParent(String spanName, @Nullable Span parent) { + return OpenTelemetrySpanBuilderImpl.createWithParent(spanName, parent, spanBuilderOptions); + } + + @Override + public SpanBuilder spanBuilderWithRemoteParent( + String spanName, @Nullable SpanContext remoteParentSpanContext) { + return OpenTelemetrySpanBuilderImpl.createWithRemoteParent( + spanName, remoteParentSpanContext, spanBuilderOptions); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java new file mode 100644 index 000000000..11d41b369 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import io.opencensus.common.Function; +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.Tracestate; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.TraceStateBuilder; +import javax.annotation.Nullable; + +final class SpanConverter { + static final String MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE = "message.event.type"; + static final String MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED = + "message.event.size.uncompressed"; + static final String MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED = "message.event.size.compressed"; + + private SpanConverter() {} + + static SpanKind mapKind(@Nullable io.opencensus.trace.Span.Kind ocKind) { + if (ocKind == null) { + return SpanKind.INTERNAL; + } + switch (ocKind) { + case CLIENT: + return SpanKind.CLIENT; + case SERVER: + return SpanKind.SERVER; + } + return SpanKind.INTERNAL; + } + + static Span fromOtelSpan(io.opentelemetry.api.trace.Span otSpan) { + if (otSpan == null) { + return null; + } + return new OpenTelemetrySpanImpl(otSpan); + } + + static SpanContext mapSpanContext(io.opentelemetry.api.trace.SpanContext otelSpanContext) { + return SpanContext.create( + TraceId.fromLowerBase16(otelSpanContext.getTraceId()), + SpanId.fromLowerBase16(otelSpanContext.getSpanId()), + TraceOptions.builder().setIsSampled(otelSpanContext.isSampled()).build(), + mapTracestate(otelSpanContext.getTraceState())); + } + + static io.opentelemetry.api.trace.SpanContext mapSpanContext(SpanContext ocSpanContext) { + return io.opentelemetry.api.trace.SpanContext.create( + ocSpanContext.getTraceId().toLowerBase16(), + ocSpanContext.getSpanId().toLowerBase16(), + ocSpanContext.getTraceOptions().isSampled() + ? TraceFlags.getSampled() + : TraceFlags.getDefault(), + mapTracestate(ocSpanContext.getTracestate()), HeraContext.getDefault()); + } + + private static TraceState mapTracestate(Tracestate tracestate) { + TraceStateBuilder builder = TraceState.builder(); + tracestate.getEntries().forEach(entry -> builder.put(entry.getKey(), entry.getValue())); + return builder.build(); + } + + static Tracestate mapTracestate(TraceState traceState) { + Tracestate.Builder tracestateBuilder = Tracestate.builder(); + traceState.forEach(tracestateBuilder::set); + return tracestateBuilder.build(); + } + + static Function setStringAttribute(AttributesBuilder builder, String key) { + return arg -> { + builder.put(key, arg); + return null; + }; + } + + static Function setBooleanAttribute(AttributesBuilder builder, String key) { + return arg -> { + builder.put(key, arg); + return null; + }; + } + + static Function setLongAttribute(AttributesBuilder builder, String key) { + return arg -> { + builder.put(key, arg); + return null; + }; + } + + static Function setDoubleAttribute(AttributesBuilder builder, String key) { + return arg -> { + builder.put(key, arg); + return null; + }; + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/ThreadLocalRandomHandler.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/ThreadLocalRandomHandler.java new file mode 100644 index 000000000..76b1567d2 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/ThreadLocalRandomHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +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}. There is an existing + * implementation in opencensus-impl, however we do not want to depend on opencensus-impl here. + */ +@ThreadSafe +public final class ThreadLocalRandomHandler extends RandomHandler { + + /** Constructs a new {@code ThreadLocalRandomHandler}. */ + public ThreadLocalRandomHandler() {} + + @Override + public Random current() { + return ThreadLocalRandom.current(); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/package-info.java b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/package-info.java new file mode 100644 index 000000000..36b5be41e --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** The OpenCensus to OpenTelemetry shim. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.opencensusshim; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/FakeMetricExporter.java b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/FakeMetricExporter.java new file mode 100644 index 000000000..185747212 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/FakeMetricExporter.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.opencensusshim; + +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; + +class FakeMetricExporter implements MetricExporter { + + private final Object monitor = new Object(); + + @GuardedBy("monitor") + private List> exportedMetrics = new ArrayList<>(); + + /** + * Waits until export is called for numberOfExports times. Returns the list of exported lists of + * metrics + */ + @Nullable + List> waitForNumberOfExports(int numberOfExports) { + List> ret; + synchronized (monitor) { + while (exportedMetrics.size() < numberOfExports) { + try { + monitor.wait(); + } catch (InterruptedException e) { + // Preserve the interruption status as per guidance. + Thread.currentThread().interrupt(); + return null; + } + } + ret = exportedMetrics; + exportedMetrics = new ArrayList<>(); + } + return ret; + } + + @Override + public CompletableResultCode export(Collection metrics) { + synchronized (monitor) { + this.exportedMetrics.add(new ArrayList<>(metrics)); + monitor.notifyAll(); + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return null; + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/InteroperabilityTest.java b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/InteroperabilityTest.java new file mode 100644 index 000000000..91fde1b2d --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/InteroperabilityTest.java @@ -0,0 +1,551 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.opencensus.common.Duration; +import io.opencensus.stats.Aggregation; +import io.opencensus.stats.BucketBoundaries; +import io.opencensus.stats.Measure; +import io.opencensus.stats.Stats; +import io.opencensus.stats.StatsRecorder; +import io.opencensus.stats.View; +import io.opencensus.stats.ViewManager; +import io.opencensus.tags.TagContext; +import io.opencensus.tags.TagKey; +import io.opencensus.tags.TagMetadata; +import io.opencensus.tags.TagValue; +import io.opencensus.tags.Tagger; +import io.opencensus.tags.Tags; +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.Kind; +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.Status; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.samplers.Samplers; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class InteroperabilityTest { + + private static final String NULL_SPAN_ID = "0000000000000000"; + + // Initialize OpenTelemetry statically because OpenCensus is. + private static final SpanExporter spanExporter; + private static final OpenTelemetry openTelemetry; + + static { + spanExporter = spy(SpanExporter.class); + when(spanExporter.export(anyList())).thenReturn(CompletableResultCode.ofSuccess()); + + SpanProcessor spanProcessor = SimpleSpanProcessor.create(spanExporter); + openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build()) + .buildAndRegisterGlobal(); + } + + @Captor private ArgumentCaptor> spanDataCaptor; + + @BeforeEach + void resetMocks() { + reset(spanExporter); + } + + @Test + void testParentChildRelationshipsAreExportedCorrectly() { + Tracer tracer = openTelemetry.getTracer("io.opentelemetry.test.scoped.span.1"); + Span span = tracer.spanBuilder("OpenTelemetrySpan").startSpan(); + try (Scope scope = Context.current().with(span).makeCurrent()) { + span.addEvent("OpenTelemetry: Event 1"); + createOpenCensusScopedSpanWithChildSpan( + /* withInnerOpenTelemetrySpan= */ true, /* withInnerOpenCensusSpan= */ false); + span.addEvent("OpenTelemetry: Event 2"); + } finally { + span.end(); + } + + verify(spanExporter, times(3)).export(spanDataCaptor.capture()); + Collection export1 = spanDataCaptor.getAllValues().get(0); + Collection export2 = spanDataCaptor.getAllValues().get(1); + Collection export3 = spanDataCaptor.getAllValues().get(2); + + assertThat(export1.size()).isEqualTo(1); + SpanData spanData1 = export1.iterator().next(); + assertThat(spanData1.getName()).isEqualTo("OpenTelemetrySpan2"); + assertThat(spanData1.getTotalRecordedEvents()).isEqualTo(2); + assertThat(spanData1.getEvents().get(0).getName()).isEqualTo("OpenTelemetry2: Event 1"); + assertThat(spanData1.getEvents().get(1).getName()).isEqualTo("OpenTelemetry2: Event 2"); + + assertThat(export2.size()).isEqualTo(1); + SpanData spanData2 = export2.iterator().next(); + assertThat(spanData2.getName()).isEqualTo("OpenCensusSpan1"); + assertThat(spanData2.getTotalRecordedEvents()).isEqualTo(2); + assertThat(spanData2.getEvents().get(0).getName()).isEqualTo("OpenCensus1: Event 1"); + assertThat(spanData2.getEvents().get(1).getName()).isEqualTo("OpenCensus1: Event 2"); + + assertThat(export3.size()).isEqualTo(1); + SpanData spanData3 = export3.iterator().next(); + assertThat(spanData3.getName()).isEqualTo("OpenTelemetrySpan"); + assertThat(spanData3.getTotalRecordedEvents()).isEqualTo(2); + assertThat(spanData3.getEvents().get(0).getName()).isEqualTo("OpenTelemetry: Event 1"); + assertThat(spanData3.getEvents().get(1).getName()).isEqualTo("OpenTelemetry: Event 2"); + + assertThat(spanData1.getParentSpanId()).isEqualTo(spanData2.getSpanId()); + assertThat(spanData2.getParentSpanId()).isEqualTo(spanData3.getSpanId()); + assertThat(spanData3.getParentSpanId()).isEqualTo(NULL_SPAN_ID); + } + + @Test + void testRemoteParent() { + io.opencensus.trace.Tracer tracer = Tracing.getTracer(); + io.opencensus.trace.Span remoteParentSpan = + tracer.spanBuilder("remote parent span").startSpan(); + try (io.opencensus.common.Scope scope = + tracer + .spanBuilderWithRemoteParent("OpenCensusSpan", remoteParentSpan.getContext()) + .setSpanKind(Kind.SERVER) + .setRecordEvents(true) + .setSampler(Samplers.alwaysSample()) + .startScopedSpan()) { + remoteParentSpan.addAnnotation("test"); + } + Tracing.getExportComponent().shutdown(); + + verify(spanExporter, times(1)).export(spanDataCaptor.capture()); + Collection export1 = spanDataCaptor.getAllValues().get(0); + + assertThat(export1.size()).isEqualTo(1); + SpanData spanData1 = export1.iterator().next(); + assertThat(spanData1.getName()).isEqualTo("OpenCensusSpan"); + assertThat(spanData1.getLinks().get(0).getSpanContext().getSpanId()) + .isEqualTo(remoteParentSpan.getContext().getSpanId().toLowerBase16()); + } + + @Test + void testParentChildRelationshipsAreExportedCorrectlyForOpenCensusOnly() { + io.opencensus.trace.Tracer tracer = Tracing.getTracer(); + io.opencensus.trace.Span parentLinkSpan = tracer.spanBuilder("parent link span").startSpan(); + try (io.opencensus.common.Scope scope = + tracer + .spanBuilder("OpenCensusSpan") + .setSpanKind(Kind.SERVER) + .setRecordEvents(true) + .setSampler(Samplers.alwaysSample()) + .setParentLinks(ImmutableList.of(parentLinkSpan)) + .startScopedSpan()) { + io.opencensus.trace.Span span = tracer.getCurrentSpan(); + span.putAttributes( + ImmutableMap.of( + "testKey", + AttributeValue.doubleAttributeValue(2.5), + "testKey2", + AttributeValue.booleanAttributeValue(false), + "testKey3", + AttributeValue.longAttributeValue(3))); + span.addMessageEvent( + MessageEvent.builder(MessageEvent.Type.SENT, 12345) + .setUncompressedMessageSize(10) + .setCompressedMessageSize(8) + .build()); + span.addAnnotation( + "OpenCensus: Event 1", + ImmutableMap.of( + "testKey", + AttributeValue.doubleAttributeValue(123), + "testKey2", + AttributeValue.booleanAttributeValue(true))); + span.addLink(Link.fromSpanContext(SpanContext.INVALID, Link.Type.PARENT_LINKED_SPAN)); + createOpenCensusScopedSpanWithChildSpan( + /* withInnerOpenTelemetrySpan= */ false, /* withInnerOpenCensusSpan= */ true); + span.addAnnotation(Annotation.fromDescription("OpenCensus: Event 2")); + span.setStatus(Status.OK); + } + Tracing.getExportComponent().shutdown(); + + verify(spanExporter, times(3)).export(spanDataCaptor.capture()); + Collection export1 = spanDataCaptor.getAllValues().get(0); + Collection export2 = spanDataCaptor.getAllValues().get(1); + Collection export3 = spanDataCaptor.getAllValues().get(2); + + assertThat(export1.size()).isEqualTo(1); + SpanData spanData1 = export1.iterator().next(); + assertThat(spanData1.getName()).isEqualTo("OpenCensusSpan2"); + assertThat(spanData1.getTotalRecordedEvents()).isEqualTo(2); + assertThat(spanData1.getEvents().get(0).getName()).isEqualTo("OpenCensus2: Event 1"); + assertThat(spanData1.getEvents().get(1).getName()).isEqualTo("OpenCensus2: Event 2"); + + assertThat(export2.size()).isEqualTo(1); + SpanData spanData2 = export2.iterator().next(); + assertThat(spanData2.getName()).isEqualTo("OpenCensusSpan1"); + assertThat(spanData2.getTotalRecordedEvents()).isEqualTo(2); + assertThat(spanData2.getEvents().get(0).getName()).isEqualTo("OpenCensus1: Event 1"); + assertThat(spanData2.getEvents().get(1).getName()).isEqualTo("OpenCensus1: Event 2"); + + assertThat(export3.size()).isEqualTo(1); + SpanData spanData3 = export3.iterator().next(); + assertThat(spanData3.getName()).isEqualTo("OpenCensusSpan"); + assertThat(spanData3.getLinks().get(0).getSpanContext().getSpanId()) + .isEqualTo(parentLinkSpan.getContext().getSpanId().toLowerBase16()); + assertThat(spanData3.getKind()).isEqualTo(SpanKind.SERVER); + assertThat(spanData3.getStatus()).isEqualTo(StatusData.ok()); + assertThat(spanData3.getAttributes().get(AttributeKey.doubleKey("testKey"))).isEqualTo(2.5); + assertThat(spanData3.getAttributes().get(AttributeKey.booleanKey("testKey2"))).isEqualTo(false); + assertThat(spanData3.getAttributes().get(AttributeKey.longKey("testKey3"))).isEqualTo(3); + assertThat(spanData3.getTotalRecordedEvents()).isEqualTo(3); + assertThat(spanData3.getEvents().get(0).getName()).isEqualTo("12345"); + assertThat( + spanData3 + .getEvents() + .get(0) + .getAttributes() + .get(AttributeKey.longKey("message.event.size.compressed"))) + .isEqualTo(8); + assertThat( + spanData3 + .getEvents() + .get(0) + .getAttributes() + .get(AttributeKey.longKey("message.event.size.uncompressed"))) + .isEqualTo(10); + assertThat( + spanData3 + .getEvents() + .get(0) + .getAttributes() + .get(AttributeKey.stringKey("message.event.type"))) + .isEqualTo("SENT"); + assertThat(spanData3.getEvents().get(1).getName()).isEqualTo("OpenCensus: Event 1"); + assertThat(spanData3.getEvents().get(1).getAttributes().get(AttributeKey.doubleKey("testKey"))) + .isEqualTo(123); + assertThat( + spanData3.getEvents().get(1).getAttributes().get(AttributeKey.booleanKey("testKey2"))) + .isEqualTo(true); + assertThat(spanData3.getEvents().get(2).getName()).isEqualTo("OpenCensus: Event 2"); + + assertThat(spanData1.getParentSpanId()).isEqualTo(spanData2.getSpanId()); + assertThat(spanData2.getParentSpanId()).isEqualTo(spanData3.getSpanId()); + assertThat(spanData3.getParentSpanId()).isEqualTo(NULL_SPAN_ID); + } + + @Test + void testOpenTelemetryMethodsOnOpenCensusSpans() { + io.opencensus.trace.Tracer tracer = Tracing.getTracer(); + try (io.opencensus.common.Scope scope = + tracer + .spanBuilder("OpenCensusSpan") + .setRecordEvents(true) + .setSampler(Samplers.alwaysSample()) + .startScopedSpan()) { + OpenTelemetrySpanImpl span = (OpenTelemetrySpanImpl) tracer.getCurrentSpan(); + span.setStatus(StatusCode.ERROR); + span.setAttribute("testKey", "testValue"); + span.addEvent("OpenCensus span: Event 1"); + span.addEvent( + "OpenCensus span: Event 2", + Attributes.of(AttributeKey.doubleKey("key2"), 3.5), + 0, + TimeUnit.HOURS); + span.updateName("OpenCensus Span renamed"); + span.addEvent("OpenCensus span: Event 3", 5, TimeUnit.MILLISECONDS); + span.updateName("OpenCensus Span renamed"); + span.addEvent("OpenCensus span: Event 4", Attributes.of(AttributeKey.longKey("key3"), 4L)); + span.updateName("OpenCensus Span renamed"); + } + Tracing.getExportComponent().shutdown(); + + verify(spanExporter, times(1)).export(spanDataCaptor.capture()); + Collection export1 = spanDataCaptor.getAllValues().get(0); + + assertThat(export1.size()).isEqualTo(1); + SpanData spanData1 = export1.iterator().next(); + assertThat(spanData1.getName()).isEqualTo("OpenCensus Span renamed"); + assertThat(spanData1.getTotalRecordedEvents()).isEqualTo(4); + assertThat(spanData1.getEvents().get(0).getName()).isEqualTo("OpenCensus span: Event 1"); + assertThat(spanData1.getEvents().get(1).getName()).isEqualTo("OpenCensus span: Event 2"); + assertThat(spanData1.getEvents().get(1).getAttributes().get(AttributeKey.doubleKey("key2"))) + .isEqualTo(3.5); + assertThat(spanData1.getEvents().get(1).getEpochNanos()).isEqualTo(0); + assertThat(spanData1.getEvents().get(2).getName()).isEqualTo("OpenCensus span: Event 3"); + assertThat(spanData1.getEvents().get(2).getEpochNanos()).isEqualTo((long) 5e6); + assertThat(spanData1.getEvents().get(3).getName()).isEqualTo("OpenCensus span: Event 4"); + assertThat(spanData1.getEvents().get(3).getAttributes().get(AttributeKey.longKey("key3"))) + .isEqualTo(4L); + assertThat(spanData1.getAttributes().get(AttributeKey.stringKey("testKey"))) + .isEqualTo("testValue"); + } + + @Test + public void testNoSampleDoesNotExport() { + io.opencensus.trace.Tracer tracer = Tracing.getTracer(); + try (io.opencensus.common.Scope scope = + tracer.spanBuilder("OpenCensusSpan").setSampler(Samplers.neverSample()).startScopedSpan()) { + io.opencensus.trace.Span span = tracer.getCurrentSpan(); + span.addAnnotation("OpenCensus: Event 1"); + span.addAnnotation("OpenCensus: Event 2"); + span.addAnnotation(Annotation.fromDescription("OpenCensus: Event 2")); + span.setStatus(Status.RESOURCE_EXHAUSTED); + span.putAttribute("testKey", AttributeValue.stringAttributeValue("testValue")); + } + Tracing.getExportComponent().shutdown(); + verify(spanExporter, never()).export(anyCollection()); + } + + @Test + public void testNoRecordDoesNotExport() { + io.opencensus.trace.Tracer tracer = Tracing.getTracer(); + try (io.opencensus.common.Scope scope = + tracer.spanBuilder("OpenCensusSpan").setRecordEvents(false).startScopedSpan()) { + io.opencensus.trace.Span span = tracer.getCurrentSpan(); + span.addAnnotation("OpenCensus: Event 1"); + span.addAnnotation(Annotation.fromDescription("OpenCensus: Event 2")); + span.setStatus(Status.RESOURCE_EXHAUSTED); + span.putAttribute("testKey", AttributeValue.stringAttributeValue("testValue")); + } + Tracing.getExportComponent().shutdown(); + verify(spanExporter, never()).export(anyCollection()); + } + + @Test + @SuppressWarnings("deprecation") // Summary is deprecated in census + void testSupportedMetricsExportedCorrectly() { + Tagger tagger = Tags.getTagger(); + Measure.MeasureLong latency = + Measure.MeasureLong.create("task_latency", "The task latency in milliseconds", "ms"); + Measure.MeasureDouble latency2 = + Measure.MeasureDouble.create("task_latency_2", "The task latency in milliseconds 2", "ms"); + StatsRecorder statsRecorder = Stats.getStatsRecorder(); + TagKey tagKey = TagKey.create("tagKey"); + TagValue tagValue = TagValue.create("tagValue"); + View longSumView = + View.create( + View.Name.create("long_sum"), + "long sum", + latency, + Aggregation.Sum.create(), + ImmutableList.of(tagKey)); + View longGaugeView = + View.create( + View.Name.create("long_gauge"), + "long gauge", + latency, + Aggregation.LastValue.create(), + ImmutableList.of(tagKey)); + View doubleSumView = + View.create( + View.Name.create("double_sum"), + "double sum", + latency2, + Aggregation.Sum.create(), + ImmutableList.of()); + View doubleGaugeView = + View.create( + View.Name.create("double_gauge"), + "double gauge", + latency2, + Aggregation.LastValue.create(), + ImmutableList.of()); + ViewManager viewManager = Stats.getViewManager(); + viewManager.registerView(longSumView); + viewManager.registerView(longGaugeView); + viewManager.registerView(doubleSumView); + viewManager.registerView(doubleGaugeView); + FakeMetricExporter metricExporter = new FakeMetricExporter(); + OpenTelemetryMetricsExporter.createAndRegister(metricExporter, Duration.create(0, 5000)); + + TagContext tagContext = + tagger + .emptyBuilder() + .put(tagKey, tagValue, TagMetadata.create(TagMetadata.TagTtl.UNLIMITED_PROPAGATION)) + .build(); + try (io.opencensus.common.Scope ss = tagger.withTagContext(tagContext)) { + statsRecorder.newMeasureMap().put(latency, 50).record(); + statsRecorder.newMeasureMap().put(latency2, 60).record(); + } + List> exported = metricExporter.waitForNumberOfExports(3); + List metricData = + exported.get(2).stream() + .sorted(Comparator.comparing(MetricData::getName)) + .collect(Collectors.toList()); + assertThat(metricData.size()).isEqualTo(4); + + MetricData metric = metricData.get(0); + assertThat(metric.getName()).isEqualTo("double_gauge"); + assertThat(metric.getDescription()).isEqualTo("double gauge"); + assertThat(metric.getUnit()).isEqualTo("ms"); + assertThat(metric.getType()).isEqualTo(MetricDataType.DOUBLE_GAUGE); + assertThat(metric.getDoubleGaugeData().getPoints().size()).isEqualTo(1); + PointData pointData = metric.getDoubleGaugeData().getPoints().iterator().next(); + assertThat(((DoublePointData) pointData).getValue()).isEqualTo(60); + assertThat(pointData.getLabels().size()).isEqualTo(0); + + metric = metricData.get(1); + assertThat(metric.getName()).isEqualTo("double_sum"); + assertThat(metric.getDescription()).isEqualTo("double sum"); + assertThat(metric.getUnit()).isEqualTo("ms"); + assertThat(metric.getType()).isEqualTo(MetricDataType.DOUBLE_SUM); + assertThat(metric.getDoubleSumData().getPoints().size()).isEqualTo(1); + pointData = metric.getDoubleSumData().getPoints().iterator().next(); + assertThat(((DoublePointData) pointData).getValue()).isEqualTo(60); + assertThat(pointData.getLabels().size()).isEqualTo(0); + + metric = metricData.get(2); + assertThat(metric.getName()).isEqualTo("long_gauge"); + assertThat(metric.getDescription()).isEqualTo("long gauge"); + assertThat(metric.getUnit()).isEqualTo("ms"); + assertThat(metric.getType()).isEqualTo(MetricDataType.LONG_GAUGE); + assertThat(metric.getLongGaugeData().getPoints().size()).isEqualTo(1); + + metric = metricData.get(3); + assertThat(metric.getName()).isEqualTo("long_sum"); + assertThat(metric.getDescription()).isEqualTo("long sum"); + assertThat(metric.getUnit()).isEqualTo("ms"); + assertThat(metric.getType()).isEqualTo(MetricDataType.LONG_SUM); + assertThat(metric.getLongSumData().getPoints().size()).isEqualTo(1); + pointData = metric.getLongSumData().getPoints().iterator().next(); + assertThat(((LongPointData) pointData).getValue()).isEqualTo(50); + assertThat(pointData.getLabels().size()).isEqualTo(1); + assertThat(pointData.getLabels().get(tagKey.getName())).isEqualTo(tagValue.asString()); + } + + @Test + void testUnsupportedMetricsDoesNotGetExported() throws InterruptedException { + Tagger tagger = Tags.getTagger(); + Measure.MeasureLong latency = + Measure.MeasureLong.create( + "task_latency_distribution", "The task latency in milliseconds", "ms"); + StatsRecorder statsRecorder = Stats.getStatsRecorder(); + TagKey tagKey = TagKey.create("tagKey"); + TagValue tagValue = TagValue.create("tagValue"); + View view = + View.create( + View.Name.create("task_latency_distribution"), + "The distribution of the task latencies.", + latency, + Aggregation.Distribution.create( + BucketBoundaries.create(ImmutableList.of(100.0, 150.0, 200.0))), + ImmutableList.of(tagKey)); + ViewManager viewManager = Stats.getViewManager(); + viewManager.registerView(view); + FakeMetricExporter metricExporter = new FakeMetricExporter(); + OpenTelemetryMetricsExporter.createAndRegister(metricExporter, Duration.create(0, 500)); + + TagContext tagContext = + tagger + .emptyBuilder() + .put(tagKey, tagValue, TagMetadata.create(TagMetadata.TagTtl.UNLIMITED_PROPAGATION)) + .build(); + try (io.opencensus.common.Scope ss = tagger.withTagContext(tagContext)) { + statsRecorder.newMeasureMap().put(latency, 50).record(); + } + // Sleep so that there is time for export() to be called. + Thread.sleep(2); + // This is 0 in case this test gets run first, or by itself. + // If other views have already been registered in other tests, they will produce metric data, so + // we are testing for the absence of this particular view's metric data. + List> allExports = metricExporter.waitForNumberOfExports(0); + if (!allExports.isEmpty()) { + for (MetricData metricData : allExports.get(allExports.size() - 1)) { + assertThat(metricData.getName()).isNotEqualTo("task_latency_distribution"); + } + } + } + + private static void createOpenCensusScopedSpanWithChildSpan( + boolean withInnerOpenTelemetrySpan, boolean withInnerOpenCensusSpan) { + io.opencensus.trace.Tracer tracer = Tracing.getTracer(); + try (io.opencensus.common.Scope scope = + tracer + .spanBuilder("OpenCensusSpan1") + .setRecordEvents(true) + .setSampler(Samplers.alwaysSample()) + .startScopedSpan()) { + io.opencensus.trace.Span span = tracer.getCurrentSpan(); + span.addAnnotation("OpenCensus1: Event 1"); + if (withInnerOpenTelemetrySpan) { + createOpenTelemetryScopedSpan(); + } + if (withInnerOpenCensusSpan) { + createOpenCensusScopedSpan(); + } + span.addAnnotation("OpenCensus1: Event 2"); + } + } + + private static void createOpenCensusScopedSpan() { + io.opencensus.trace.Tracer tracer = Tracing.getTracer(); + try (io.opencensus.common.Scope scope = + tracer + .spanBuilder("OpenCensusSpan2") + .setRecordEvents(true) + .setSampler(Samplers.alwaysSample()) + .startScopedSpan()) { + io.opencensus.trace.Span span = tracer.getCurrentSpan(); + span.addAnnotation("OpenCensus2: Event 1"); + span.addAnnotation("OpenCensus2: Event 2"); + } + } + + private static void createOpenTelemetryScopedSpan() { + Tracer tracer = openTelemetry.getTracer("io.opentelemetry.test.scoped.span.2"); + Span span = tracer.spanBuilder("OpenTelemetrySpan2").startSpan(); + try (Scope scope = Context.current().with(span).makeCurrent()) { + span.addEvent("OpenTelemetry2: Event 1"); + span.addEvent("OpenTelemetry2: Event 2"); + } finally { + span.end(); + } + } +} diff --git a/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryNoRecordEventsSpanImplTest.java b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryNoRecordEventsSpanImplTest.java new file mode 100644 index 000000000..8f6d67e5e --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryNoRecordEventsSpanImplTest.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.opencensusshim; + +import static org.assertj.core.api.Assertions.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.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 io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +public class OpenTelemetryNoRecordEventsSpanImplTest { + 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 OpenTelemetryNoRecordEventsSpanImpl noRecordEventsSpan = + OpenTelemetryNoRecordEventsSpanImpl.create(spanContext); + + @Test + public void propagatesSpanContext() { + assertThat(noRecordEventsSpan.getContext()).isEqualTo(spanContext); + assertThat(noRecordEventsSpan.getSpanContext()) + .isEqualTo(SpanConverter.mapSpanContext(spanContext)); + } + + @Test + public void isNotRecording() { + assertThat(noRecordEventsSpan.getOptions()).doesNotContain(Options.RECORD_EVENTS); + assertThat(noRecordEventsSpan.isRecording()).isFalse(); + } + + @Test + public void doNotCrash() { + Map attributes = new HashMap(); + attributes.put( + "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue")); + Map multipleAttributes = new HashMap(); + 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.addAnnotation("MyAnnotation"); + noRecordEventsSpan.addAnnotation("MyAnnotation", attributes); + noRecordEventsSpan.addAnnotation("MyAnnotation", multipleAttributes); + noRecordEventsSpan.addAnnotation(Annotation.fromDescription("MyAnnotation")); + 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.setAttribute("OTelAttributeKeyString", "OTelAttributeValue"); + noRecordEventsSpan.setAttribute("OTelAttributeKeyLong", 123); + noRecordEventsSpan.setAttribute("OTelAttributeKeyDouble", 123.45); + noRecordEventsSpan.setAttribute("OTelAttributeKeyBoolean", true); + noRecordEventsSpan.addEvent("OTel event 1"); + noRecordEventsSpan.addEvent("OTel event 2", 29922310, TimeUnit.HOURS); + noRecordEventsSpan.addEvent( + "OTel event 2", Attributes.of(AttributeKey.booleanKey("OTelAttributeKey"), true)); + noRecordEventsSpan.updateName("OTel name"); + noRecordEventsSpan.end(EndSpanOptions.DEFAULT); + noRecordEventsSpan.end(); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryPropagationComponentImplTest.java b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryPropagationComponentImplTest.java new file mode 100644 index 000000000..4aa727f73 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryPropagationComponentImplTest.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.opencensusshim; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opencensus.trace.propagation.PropagationComponent; +import org.junit.jupiter.api.Test; + +public class OpenTelemetryPropagationComponentImplTest { + private final PropagationComponent propagationComponent = + new OpenTelemetryPropagationComponentImpl(); + + @Test + public void implementationOfBinary() { + assertThat(propagationComponent.getBinaryFormat().getClass().getName()) + .isEqualTo("io.opencensus.implcore.trace.propagation.BinaryFormatImpl"); + } + + @Test + public void implementationOfB3Format() { + assertThat(propagationComponent.getB3Format()).isInstanceOf(OpenTelemetryTextFormatImpl.class); + } + + @Test + public void implementationOfTraceContextFormat() { + assertThat(propagationComponent.getTraceContextFormat()) + .isInstanceOf(OpenTelemetryTextFormatImpl.class); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryTextFormatImplTest.java b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryTextFormatImplTest.java new file mode 100644 index 000000000..4416d23d1 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/OpenTelemetryTextFormatImplTest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +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.TextFormat.Getter; +import io.opencensus.trace.propagation.TextFormat.Setter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class OpenTelemetryTextFormatImplTest { + private static final Setter> SETTER = + new Setter>() { + @Override + public void put(Map carrier, String key, String value) { + carrier.put(key, value); + } + }; + private static final Getter> GETTER = + new Getter>() { + @Nullable + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + private static final Random RANDOM = new Random(); + private static final SpanContext SPAN_CONTEXT = + SpanContext.create( + TraceId.generateRandomId(RANDOM), + SpanId.generateRandomId(RANDOM), + TraceOptions.builder().setIsSampled(true).build(), + Tracestate.builder().set("key", "value").build()); + + @Test + void testInject() { + TextMapPropagator propagator = spy(TextMapPropagator.class); + OpenTelemetryTextFormatImpl textFormatImpl = new OpenTelemetryTextFormatImpl(propagator); + Map carrier = new LinkedHashMap<>(); + + textFormatImpl.inject(SPAN_CONTEXT, carrier, SETTER); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(Context.class); + verify(propagator, times(1)).inject(contextCaptor.capture(), any(), any()); + assertThat(Span.fromContext(contextCaptor.getValue()).getSpanContext()) + .isEqualTo(SpanConverter.mapSpanContext(SPAN_CONTEXT)); + } + + @Test + void testInjectWithNotSampledContext() { + TextMapPropagator propagator = spy(TextMapPropagator.class); + OpenTelemetryTextFormatImpl textFormatImpl = new OpenTelemetryTextFormatImpl(propagator); + SpanContext spanContext = + SpanContext.create( + TraceId.generateRandomId(RANDOM), + SpanId.generateRandomId(RANDOM), + TraceOptions.builder().setIsSampled(false).build(), + Tracestate.builder().build()); + Map carrier = new LinkedHashMap<>(); + + textFormatImpl.inject(spanContext, carrier, SETTER); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(Context.class); + verify(propagator, times(1)).inject(contextCaptor.capture(), any(), any()); + assertThat(Span.fromContext(contextCaptor.getValue()).getSpanContext()) + .isEqualTo(SpanConverter.mapSpanContext(spanContext)); + } + + @Test + void testInjectWithDefaultOptions() { + TextMapPropagator propagator = spy(TextMapPropagator.class); + OpenTelemetryTextFormatImpl textFormatImpl = new OpenTelemetryTextFormatImpl(propagator); + SpanContext spanContext = + SpanContext.create( + TraceId.generateRandomId(RANDOM), + SpanId.generateRandomId(RANDOM), + TraceOptions.DEFAULT, + Tracestate.builder().build()); + Map carrier = new LinkedHashMap<>(); + + textFormatImpl.inject(spanContext, carrier, SETTER); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(Context.class); + verify(propagator, times(1)).inject(contextCaptor.capture(), any(), any()); + assertThat(Span.fromContext(contextCaptor.getValue()).getSpanContext()) + .isEqualTo(SpanConverter.mapSpanContext(spanContext)); + } + + @Test + void testInjectAndExtractWithB3() { + OpenTelemetryTextFormatImpl textFormatImpl = + new OpenTelemetryTextFormatImpl(B3Propagator.injectingMultiHeaders()); + Map carrier = new LinkedHashMap<>(); + + textFormatImpl.inject(SPAN_CONTEXT, carrier, SETTER); + + SpanContext extractedSpanContext = textFormatImpl.extract(carrier, GETTER); + assertThat(extractedSpanContext).isEqualTo(SPAN_CONTEXT); + } + + @Test + void testInjectAndExtractWithW3c() { + OpenTelemetryTextFormatImpl textFormatImpl = + new OpenTelemetryTextFormatImpl(W3CTraceContextPropagator.getInstance()); + Map carrier = new LinkedHashMap<>(); + + textFormatImpl.inject(SPAN_CONTEXT, carrier, SETTER); + + SpanContext extractedSpanContext = textFormatImpl.extract(carrier, GETTER); + assertThat(extractedSpanContext).isEqualTo(SPAN_CONTEXT); + } +} diff --git a/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/SpanConverterTest.java b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/SpanConverterTest.java new file mode 100644 index 000000000..d49cab982 --- /dev/null +++ b/opentelemetry-java/opencensus-shim/src/test/java/io/opentelemetry/opencensusshim/SpanConverterTest.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opencensusshim; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import io.opencensus.trace.SpanContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.trace.IdGenerator; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class SpanConverterTest { + private static final IdGenerator RANDOM_IDS_GENERATOR = IdGenerator.random(); + + @Test + void testFromOtelSpan() { + String traceIdHex = RANDOM_IDS_GENERATOR.generateTraceId(); + String spanIdHex = RANDOM_IDS_GENERATOR.generateSpanId(); + String traceStateKey = "key123"; + String traceStateValue = "value123"; + + Span otelSpan = Mockito.mock(Span.class); + when(otelSpan.getSpanContext()) + .thenReturn( + io.opentelemetry.api.trace.SpanContext.create( + traceIdHex, + spanIdHex, + TraceFlags.getSampled(), + TraceState.builder().put(traceStateKey, traceStateValue).build())); + + io.opencensus.trace.Span ocSPan = SpanConverter.fromOtelSpan(otelSpan); + + SpanContext context = ocSPan.getContext(); + assertThat(context.getTraceId().toLowerBase16()).isEqualTo(traceIdHex); + assertThat(context.getSpanId().toLowerBase16()).isEqualTo(spanIdHex); + assertThat(context.getTraceOptions().isSampled()).isTrue(); + assertThat(context.getTracestate().getEntries().size()).isEqualTo(1); + assertThat(context.getTracestate().get(traceStateKey)).isEqualTo(traceStateValue); + } +} diff --git a/opentelemetry-java/opentracing-shim/README.md b/opentelemetry-java/opentracing-shim/README.md new file mode 100644 index 000000000..3900f13b9 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/README.md @@ -0,0 +1,24 @@ +[![Javadocs][javadoc-image]][javadoc-url] +# OpenTelemetry - OpenTracing Shim +The OpenTracing shim is a bridge layer from OpenTelemetry to the OpenTracing API. +It takes OpenTelemetry Tracer and exposes it as an implementation of an OpenTracing Tracer. + +## Usage + +There are 2 ways to expose an OpenTracing tracer: +1. From the global OpenTelemetry configuration: + ```java + Tracer tracer = OpenTracingShim.createTracerShim(); + ``` +1. From a provided `OpenTelemetry` instance: + ```java + Tracer tracer = OpenTracingShim.createTracerShim(openTelemetry); + ``` + +Optionally register the tracer as the OpenTracing GlobalTracer: +```java +GlobalTracer.registerIfAbsent(tracer); +``` + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-opentracing-shim.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-opentracing-shim diff --git a/opentelemetry-java/opentracing-shim/build.gradle.kts b/opentelemetry-java/opentracing-shim/build.gradle.kts new file mode 100644 index 000000000..7f9d2f562 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `java-library` + `maven-publish` +} + +description = "OpenTelemetry OpenTracing Bridge" +extra["moduleName"] = "io.opentelemetry.opentracingshim" + +dependencies { + api(project(":api:all")) + + api("io.opentracing:opentracing-api") + implementation(project(":semconv")) + + testImplementation(project(":sdk:testing")) + + testImplementation("org.slf4j:slf4j-simple") +} + +tasks { + withType(Test::class) { + testLogging { + showStandardStreams = true + } + } +} diff --git a/opentelemetry-java/opentracing-shim/gradle.properties b/opentelemetry-java/opentracing-shim/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/opentracing-shim/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/BaseShimObject.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/BaseShimObject.java new file mode 100644 index 000000000..3a5f3cce4 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/BaseShimObject.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.api.trace.Tracer; + +abstract class BaseShimObject { + + final TelemetryInfo telemetryInfo; + + BaseShimObject(TelemetryInfo telemetryInfo) { + this.telemetryInfo = telemetryInfo; + } + + TelemetryInfo telemetryInfo() { + return telemetryInfo; + } + + Tracer tracer() { + return telemetryInfo.tracer(); + } + + SpanContextShimTable spanContextTable() { + return telemetryInfo.spanContextTable(); + } + + OpenTracingPropagators propagators() { + return telemetryInfo.propagators(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/NoopSpanBuilderShim.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/NoopSpanBuilderShim.java new file mode 100644 index 000000000..07d8d3fdc --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/NoopSpanBuilderShim.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.tag.Tag; + +final class NoopSpanBuilderShim extends BaseShimObject implements SpanBuilder { + + private static final Tracer TRACER = + TracerProvider.noop().get("io.opentelemetry.opentracingshim"); + + private final String spanName; + + public NoopSpanBuilderShim(TelemetryInfo telemetryInfo, String spanName) { + super(telemetryInfo); + this.spanName = spanName == null ? "" : spanName; // OT is more permissive than OTel. + } + + @Override + public SpanBuilder asChildOf(Span parent) { + return this; + } + + @Override + public SpanBuilder asChildOf(SpanContext parent) { + return this; + } + + @Override + public SpanBuilder addReference(String referenceType, SpanContext referencedContext) { + return this; + } + + @Override + public SpanBuilder ignoreActiveSpan() { + return this; + } + + @Override + public SpanBuilder withTag(String key, String value) { + return this; + } + + @Override + public SpanBuilder withTag(String key, boolean value) { + return this; + } + + @Override + public SpanBuilder withTag(String key, Number number) { + return this; + } + + @Override + public SpanBuilder withTag(Tag tag, T value) { + return this; + } + + @Override + public SpanBuilder withStartTimestamp(long microseconds) { + return this; + } + + @Override + public Span start() { + return new SpanShim(telemetryInfo, TRACER.spanBuilder(spanName).startSpan()); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingPropagators.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingPropagators.java new file mode 100644 index 000000000..1c2fc3ec5 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingPropagators.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.context.propagation.TextMapPropagator; + +/** + * Container for {@link io.opentracing.propagation.Format.Builtin#TEXT_MAP} and {@link + * io.opentracing.propagation.Format.Builtin#HTTP_HEADERS} format propagators. + * + * @since 1.1.0 + */ +public class OpenTracingPropagators { + private final TextMapPropagator textMapPropagator; + private final TextMapPropagator httpHeadersPropagator; + + OpenTracingPropagators( + TextMapPropagator textMapPropagator, TextMapPropagator httpHeadersPropagator) { + this.textMapPropagator = textMapPropagator; + this.httpHeadersPropagator = httpHeadersPropagator; + } + + /** + * Returns a new builder instance for {@link OpenTracingPropagators}. + * + * @return a new builder instance for {@link OpenTracingPropagators}. + */ + public static OpenTracingPropagatorsBuilder builder() { + return new OpenTracingPropagatorsBuilder(); + } + + /** + * Returns the propagator for {@link io.opentracing.propagation.Format.Builtin#TEXT_MAP} format. + * + * @return the propagator for {@link io.opentracing.propagation.Format.Builtin#TEXT_MAP} format. + */ + public TextMapPropagator textMapPropagator() { + return textMapPropagator; + } + + /** + * Returns the propagator for {@link io.opentracing.propagation.Format.Builtin#HTTP_HEADERS} + * format. + * + * @return the propagator for {@link io.opentracing.propagation.Format.Builtin#HTTP_HEADERS} + * format. + */ + public TextMapPropagator httpHeadersPropagator() { + return httpHeadersPropagator; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingPropagatorsBuilder.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingPropagatorsBuilder.java new file mode 100644 index 000000000..06ceabe37 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingPropagatorsBuilder.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.Objects; + +/** Builder for {@link OpenTracingPropagators}. */ +public class OpenTracingPropagatorsBuilder { + + private TextMapPropagator textMapPropagator = + GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); + private TextMapPropagator httpHeadersPropagator = + GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); + + /** Set propagator for {@link io.opentracing.propagation.Format.Builtin#TEXT_MAP} format. */ + public OpenTracingPropagatorsBuilder setTextMap(TextMapPropagator textMapPropagator) { + Objects.requireNonNull(textMapPropagator, "textMapPropagator"); + this.textMapPropagator = textMapPropagator; + return this; + } + + /** Set propagator for {@link io.opentracing.propagation.Format.Builtin#HTTP_HEADERS} format. */ + public OpenTracingPropagatorsBuilder setHttpHeaders(TextMapPropagator httpHeadersPropagator) { + Objects.requireNonNull(httpHeadersPropagator, "httpHeadersPropagator"); + this.httpHeadersPropagator = httpHeadersPropagator; + return this; + } + + /** + * Constructs a new instance of the {@link OpenTracingPropagators} based on the builder's values. + * If propagators are not set then {@code GlobalOpenTelemetry.getPropagators()} is used. + * + * @return a new Propagators instance. + */ + public OpenTracingPropagators build() { + return new OpenTracingPropagators(textMapPropagator, httpHeadersPropagator); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingShim.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingShim.java new file mode 100644 index 000000000..db8152aa1 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/OpenTracingShim.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; + +/** + * Factory for creating an OpenTracing {@link io.opentracing.Tracer} that is implemented using the + * OpenTelemetry APIs. + */ +public final class OpenTracingShim { + private OpenTracingShim() {} + + /** + * Creates a {@code io.opentracing.Tracer} shim out of {@code + * GlobalOpenTelemetry.getTracerProvider()} and {@code GlobalOpenTelemetry.getPropagators()}. + * + * @return a {@code io.opentracing.Tracer}. + */ + public static io.opentracing.Tracer createTracerShim() { + return createTracerShim(getTracer(GlobalOpenTelemetry.getTracerProvider())); + } + + /** + * Creates a {@code io.opentracing.Tracer} shim using provided Tracer instance and {@code + * GlobalOpenTelemetry.getPropagators()}. + * + * @return a {@code io.opentracing.Tracer}. + */ + public static io.opentracing.Tracer createTracerShim(Tracer tracer) { + return createTracerShim(tracer, OpenTracingPropagators.builder().build()); + } + + /** + * Creates a {@code io.opentracing.Tracer} shim using provided Tracer instance and {@code + * OpenTracingPropagators} instance. + * + * @return a {@code io.opentracing.Tracer}. + * @since 1.1.0 + */ + public static io.opentracing.Tracer createTracerShim( + Tracer tracer, OpenTracingPropagators propagators) { + return new TracerShim(new TelemetryInfo(tracer, propagators)); + } + + /** + * Creates a {@code io.opentracing.Tracer} shim using the provided OpenTelemetry instance. + * + * @param openTelemetry the {@code OpenTelemetry} instance used to create this shim. + * @return a {@code io.opentracing.Tracer}. + */ + public static io.opentracing.Tracer createTracerShim(OpenTelemetry openTelemetry) { + return createTracerShim( + getTracer(openTelemetry.getTracerProvider()), + OpenTracingPropagators.builder() + .setTextMap(openTelemetry.getPropagators().getTextMapPropagator()) + .setHttpHeaders(openTelemetry.getPropagators().getTextMapPropagator()) + .build()); + } + + private static Tracer getTracer(TracerProvider tracerProvider) { + return tracerProvider.get("opentracingshim"); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/Propagation.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/Propagation.java new file mode 100644 index 000000000..94969dd78 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/Propagation.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMapExtract; +import io.opentracing.propagation.TextMapInject; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +final class Propagation extends BaseShimObject { + private static final TextMapSetter SETTER_INSTANCE = new TextMapSetter(); + private static final TextMapGetter GETTER_INSTANCE = new TextMapGetter(); + + Propagation(TelemetryInfo telemetryInfo) { + super(telemetryInfo); + } + + void injectTextMap(SpanContextShim contextShim, Format format, TextMapInject carrier) { + Context context = Context.current().with(Span.wrap(contextShim.getSpanContext())); + context = context.with(contextShim.getBaggage()); + + getPropagator(format).inject(context, carrier, SETTER_INSTANCE); + } + + @Nullable + SpanContextShim extractTextMap(Format format, TextMapExtract carrier) { + Map carrierMap = new HashMap<>(); + for (Map.Entry entry : carrier) { + carrierMap.put(entry.getKey(), entry.getValue()); + } + + Context context = getPropagator(format).extract(Context.current(), carrierMap, GETTER_INSTANCE); + + Span span = Span.fromContext(context); + if (!span.getSpanContext().isValid()) { + return null; + } + + return new SpanContextShim(telemetryInfo, span.getSpanContext(), Baggage.fromContext(context)); + } + + private TextMapPropagator getPropagator(Format format) { + if (format == Format.Builtin.HTTP_HEADERS) { + return propagators().httpHeadersPropagator(); + } + return propagators().textMapPropagator(); + } + + static final class TextMapSetter + implements io.opentelemetry.context.propagation.TextMapSetter { + private TextMapSetter() {} + + @Override + public void set(TextMapInject carrier, String key, String value) { + carrier.put(key, value); + } + } + + // We use Map<> instead of TextMap as we need to query a specified key, and iterating over + // *all* values per key-query *might* be a bad idea. + static final class TextMapGetter + implements io.opentelemetry.context.propagation.TextMapGetter> { + private TextMapGetter() {} + + @Nullable + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + for (Map.Entry entry : carrier.entrySet()) { + if (key.equalsIgnoreCase(entry.getKey())) { + return entry.getValue(); + } + } + return null; + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/ScopeManagerShim.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/ScopeManagerShim.java new file mode 100644 index 000000000..c50acf967 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/ScopeManagerShim.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentracing.Scope; +import io.opentracing.ScopeManager; +import io.opentracing.Span; + +final class ScopeManagerShim extends BaseShimObject implements ScopeManager { + + public ScopeManagerShim(TelemetryInfo telemetryInfo) { + super(telemetryInfo); + } + + @Override + @SuppressWarnings("ReturnMissingNullable") + public Span activeSpan() { + // As OpenTracing simply returns null when no active instance is available, + // we need to do map an invalid OpenTelemetry span to null here. + io.opentelemetry.api.trace.Span span = io.opentelemetry.api.trace.Span.current(); + if (!span.getSpanContext().isValid()) { + return null; + } + + // TODO: Properly include the bagagge/distributedContext. + return new SpanShim(telemetryInfo(), span); + } + + @Override + @SuppressWarnings("MustBeClosedChecker") + public Scope activate(Span span) { + io.opentelemetry.api.trace.Span actualSpan = getActualSpan(span); + return new ScopeShim(actualSpan.makeCurrent()); + } + + static io.opentelemetry.api.trace.Span getActualSpan(Span span) { + if (!(span instanceof SpanShim)) { + throw new IllegalArgumentException("span is not a valid SpanShim object"); + } + + return ((SpanShim) span).getSpan(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/ScopeShim.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/ScopeShim.java new file mode 100644 index 000000000..e51d77bae --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/ScopeShim.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentracing.Scope; + +final class ScopeShim implements Scope { + final io.opentelemetry.context.Scope scope; + + public ScopeShim(io.opentelemetry.context.Scope scope) { + this.scope = scope; + } + + @Override + public void close() { + scope.close(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanBuilderShim.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanBuilderShim.java new file mode 100644 index 000000000..011707ae0 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanBuilderShim.java @@ -0,0 +1,251 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.tag.Tag; +import io.opentracing.tag.Tags; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +final class SpanBuilderShim extends BaseShimObject implements SpanBuilder { + private final String spanName; + + // The parent will be either a Span or a SpanContext. + // Inherited baggage is supported only for the main parent. + private SpanShim parentSpan; + private SpanContextShim parentSpanContext; + private boolean ignoreActiveSpan; + + private final List parentLinks = new ArrayList<>(); + + @SuppressWarnings("rawtypes") + private final List spanBuilderAttributeKeys = new ArrayList<>(); + + private final List spanBuilderAttributeValues = new ArrayList<>(); + private SpanKind spanKind; + private boolean error; + private long startTimestampMicros; + + public SpanBuilderShim(TelemetryInfo telemetryInfo, String spanName) { + super(telemetryInfo); + this.spanName = spanName; + } + + @Override + public SpanBuilder asChildOf(Span parent) { + if (parent == null) { + return this; + } + + // TODO - Verify we handle a no-op Span + SpanShim spanShim = getSpanShim(parent); + + if (parentSpan == null && parentSpanContext == null) { + parentSpan = spanShim; + } else { + parentLinks.add(spanShim.getSpan().getSpanContext()); + } + + return this; + } + + @Override + public SpanBuilder asChildOf(SpanContext parent) { + return addReference(null, parent); + } + + @Override + public SpanBuilder addReference(String referenceType, SpanContext referencedContext) { + if (referencedContext == null) { + return this; + } + + // TODO - Use referenceType + SpanContextShim contextShim = getContextShim(referencedContext); + + if (parentSpan == null && parentSpanContext == null) { + parentSpanContext = contextShim; + } else { + parentLinks.add(contextShim.getSpanContext()); + } + + return this; + } + + @Override + public SpanBuilder ignoreActiveSpan() { + ignoreActiveSpan = true; + return this; + } + + @Override + public SpanBuilder withTag(String key, String value) { + if (Tags.SPAN_KIND.getKey().equals(key)) { + switch (value) { + case Tags.SPAN_KIND_CLIENT: + spanKind = SpanKind.CLIENT; + break; + case Tags.SPAN_KIND_SERVER: + spanKind = SpanKind.SERVER; + break; + case Tags.SPAN_KIND_PRODUCER: + spanKind = SpanKind.PRODUCER; + break; + case Tags.SPAN_KIND_CONSUMER: + spanKind = SpanKind.CONSUMER; + break; + default: + spanKind = SpanKind.INTERNAL; + break; + } + } else if (Tags.ERROR.getKey().equals(key)) { + error = Boolean.parseBoolean(value); + } else { + this.spanBuilderAttributeKeys.add(stringKey(key)); + this.spanBuilderAttributeValues.add(value); + } + + return this; + } + + @Override + public SpanBuilder withTag(String key, boolean value) { + if (Tags.ERROR.getKey().equals(key)) { + error = value; + } else { + this.spanBuilderAttributeKeys.add(booleanKey(key)); + this.spanBuilderAttributeValues.add(value); + } + return this; + } + + @Override + public SpanBuilder withTag(String key, Number value) { + if (value == null) { + return this; + } + // TODO - Verify only the 'basic' types are supported/used. + if (value instanceof Integer + || value instanceof Long + || value instanceof Short + || value instanceof Byte) { + this.spanBuilderAttributeKeys.add(longKey(key)); + this.spanBuilderAttributeValues.add(value.longValue()); + } else if (value instanceof Float || value instanceof Double) { + this.spanBuilderAttributeKeys.add(doubleKey(key)); + this.spanBuilderAttributeValues.add(value.doubleValue()); + } else { + throw new IllegalArgumentException("Number type not supported"); + } + + return this; + } + + @Override + public SpanBuilder withTag(Tag tag, T value) { + if (tag == null) { + return this; + } + if (value instanceof String) { + this.withTag(tag.getKey(), (String) value); + } else if (value instanceof Boolean) { + this.withTag(tag.getKey(), (Boolean) value); + } else if (value instanceof Number) { + this.withTag(tag.getKey(), (Number) value); + } else { + this.withTag(tag.getKey(), value.toString()); + } + + return this; + } + + @Override + public SpanBuilder withStartTimestamp(long microseconds) { + this.startTimestampMicros = microseconds; + return this; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Span start() { + Baggage baggage = null; + io.opentelemetry.api.trace.SpanBuilder builder = tracer().spanBuilder(spanName); + + if (ignoreActiveSpan && parentSpan == null && parentSpanContext == null) { + builder.setNoParent(); + } else if (parentSpan != null) { + builder.setParent(Context.root().with(parentSpan.getSpan())); + SpanContextShim contextShim = spanContextTable().get(parentSpan); + baggage = contextShim == null ? null : contextShim.getBaggage(); + } else if (parentSpanContext != null) { + builder.setParent( + Context.root() + .with(io.opentelemetry.api.trace.Span.wrap(parentSpanContext.getSpanContext()))); + baggage = parentSpanContext.getBaggage(); + } + + for (io.opentelemetry.api.trace.SpanContext link : parentLinks) { + builder.addLink(link); + } + + if (spanKind != null) { + builder.setSpanKind(spanKind); + } + + if (startTimestampMicros > 0) { + builder.setStartTimestamp(startTimestampMicros, TimeUnit.MICROSECONDS); + } + + io.opentelemetry.api.trace.Span span = builder.startSpan(); + + for (int i = 0; i < this.spanBuilderAttributeKeys.size(); i++) { + AttributeKey key = this.spanBuilderAttributeKeys.get(i); + Object value = this.spanBuilderAttributeValues.get(i); + span.setAttribute(key, value); + } + if (error) { + span.setStatus(StatusCode.ERROR); + } + + SpanShim spanShim = new SpanShim(telemetryInfo(), span); + + if (baggage != null && baggage != telemetryInfo().emptyBaggage()) { + spanContextTable().create(spanShim, baggage); + } + + return spanShim; + } + + private static SpanShim getSpanShim(Span span) { + if (!(span instanceof SpanShim)) { + throw new IllegalArgumentException("span is not a valid SpanShim object"); + } + + return (SpanShim) span; + } + + private static SpanContextShim getContextShim(SpanContext context) { + if (!(context instanceof SpanContextShim)) { + throw new IllegalArgumentException("context is not a valid SpanContextShim object"); + } + + return (SpanContextShim) context; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanContextShim.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanContextShim.java new file mode 100644 index 000000000..62d31fe45 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanContextShim.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.baggage.BaggageEntryMetadata; +import io.opentracing.SpanContext; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +final class SpanContextShim extends BaseShimObject implements SpanContext { + + private final io.opentelemetry.api.trace.SpanContext context; + private final Baggage baggage; + + public SpanContextShim(SpanShim spanShim) { + this( + spanShim.telemetryInfo(), + spanShim.getSpan().getSpanContext(), + spanShim.telemetryInfo().emptyBaggage()); + } + + public SpanContextShim( + TelemetryInfo telemetryInfo, io.opentelemetry.api.trace.SpanContext context) { + this(telemetryInfo, context, telemetryInfo.emptyBaggage()); + } + + public SpanContextShim( + TelemetryInfo telemetryInfo, + io.opentelemetry.api.trace.SpanContext context, + Baggage baggage) { + super(telemetryInfo); + this.context = context; + this.baggage = baggage; + } + + SpanContextShim newWithKeyValue(String key, String value) { + BaggageBuilder builder = baggage.toBuilder(); + builder.put(key, value, BaggageEntryMetadata.empty()); + + return new SpanContextShim(telemetryInfo(), context, builder.build()); + } + + io.opentelemetry.api.trace.SpanContext getSpanContext() { + return context; + } + + Baggage getBaggage() { + return baggage; + } + + @Override + public String toTraceId() { + return context.getTraceId(); + } + + @Override + public String toSpanId() { + return context.getSpanId(); + } + + @Override + public Iterable> baggageItems() { + List> items = new ArrayList<>(baggage.size()); + baggage.forEach( + (key, baggageEntry) -> + items.add(new AbstractMap.SimpleImmutableEntry<>(key, baggageEntry.getValue()))); + return items; + } + + @SuppressWarnings("ReturnMissingNullable") + String getBaggageItem(String key) { + return baggage.getEntryValue(key); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanContextShimTable.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanContextShimTable.java new file mode 100644 index 000000000..ded13f909 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanContextShimTable.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.Nullable; + +/* + * SpanContextShimTable stores and manages OpenTracing SpanContext instances, + * which are expected to a unmodfiable union of SpanContext and Baggage + * (Baggage/TagMap under OpenTelemetry). + * + * This requires that changes on a given Span and its (new) SpanContext + * are visible in all threads at *any* moment. The current approach uses + * a weak map synchronized through a read-write lock to get and set both + * SpanContext and any Baggage content related to its owner Span. + * + * Observe that, because of this design, a global read or write lock + * will be taken for ALL operations involving OT SpanContext/Baggage. + * When/if performance becomes an issue in the OT Shim layer, consider + * adding an extra slot in io.opentelemetry.trace.Span, so: + * 1) The current SpanContextShim is directly stored in Span. + * 2) The lock for this operation can be on a per-Span basis. + * + * For more information, see: + * https://github.com/opentracing/specification/blob/master/specification.md#set-a-baggage-item + */ +final class SpanContextShimTable { + private final Map shimsMap = new WeakHashMap<>(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public void setBaggageItem(SpanShim spanShim, String key, String value) { + lock.writeLock().lock(); + try { + SpanContextShim contextShim = shimsMap.get(spanShim.getSpan()); + if (contextShim == null) { + contextShim = new SpanContextShim(spanShim); + } + + contextShim = contextShim.newWithKeyValue(key, value); + shimsMap.put(spanShim.getSpan(), contextShim); + } finally { + lock.writeLock().unlock(); + } + } + + @Nullable + public String getBaggageItem(SpanShim spanShim, String key) { + lock.readLock().lock(); + try { + SpanContextShim contextShim = shimsMap.get(spanShim.getSpan()); + return contextShim == null ? null : contextShim.getBaggageItem(key); + } finally { + lock.readLock().unlock(); + } + } + + @Nullable + public SpanContextShim get(SpanShim spanShim) { + lock.readLock().lock(); + try { + return shimsMap.get(spanShim.getSpan()); + } finally { + lock.readLock().unlock(); + } + } + + public SpanContextShim create(SpanShim spanShim) { + return create(spanShim, spanShim.telemetryInfo().emptyBaggage()); + } + + public SpanContextShim create(SpanShim spanShim, Baggage baggage) { + lock.writeLock().lock(); + try { + SpanContextShim contextShim = shimsMap.get(spanShim.getSpan()); + if (contextShim != null) { + return contextShim; + } + + contextShim = + new SpanContextShim( + spanShim.telemetryInfo(), spanShim.getSpan().getSpanContext(), baggage); + shimsMap.put(spanShim.getSpan(), contextShim); + return contextShim; + + } finally { + lock.writeLock().unlock(); + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanShim.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanShim.java new file mode 100644 index 000000000..a38b5f771 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/SpanShim.java @@ -0,0 +1,273 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.log.Fields; +import io.opentracing.tag.Tag; +import io.opentracing.tag.Tags; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +/* + * SpanContextShim is not directly stored in the SpanShim, + * as its changes need to be visible in all threads at *any* moment. + * By default, the related SpanContextShim will not be created + * in order to avoid overhead (which would require taking a write lock + * at creation time). + * + * Calling context() or setBaggageItem() will effectively force the creation + * of SpanContextShim object if none existed yet. + */ +final class SpanShim extends BaseShimObject implements Span { + private static final String DEFAULT_EVENT_NAME = "log"; + private static final String ERROR = "error"; + + private final io.opentelemetry.api.trace.Span span; + + public SpanShim(TelemetryInfo telemetryInfo, io.opentelemetry.api.trace.Span span) { + super(telemetryInfo); + this.span = span; + } + + io.opentelemetry.api.trace.Span getSpan() { + return span; + } + + @Override + public SpanContext context() { + /* Read the value using the read lock first. */ + SpanContextShim contextShim = spanContextTable().get(this); + + /* Switch to the write lock *only* for the relatively exceptional case + * of no context being created. + * (as we cannot upgrade read->write lock sadly).*/ + if (contextShim == null) { + contextShim = spanContextTable().create(this); + } + + return contextShim; + } + + @Override + public Span setTag(String key, String value) { + if (Tags.SPAN_KIND.getKey().equals(key)) { + // TODO: confirm we can safely ignore span.kind after Span was created + // https://github.com/bogdandrutu/opentelemetry/issues/42 + } else if (Tags.ERROR.getKey().equals(key)) { + StatusCode canonicalCode = Boolean.parseBoolean(value) ? StatusCode.ERROR : StatusCode.UNSET; + span.setStatus(canonicalCode); + } else { + span.setAttribute(key, value); + } + + return this; + } + + @Override + public Span setTag(String key, boolean value) { + if (Tags.ERROR.getKey().equals(key)) { + StatusCode canonicalCode = value ? StatusCode.ERROR : StatusCode.UNSET; + span.setStatus(canonicalCode); + } else { + span.setAttribute(key, value); + } + + return this; + } + + @Override + public Span setTag(String key, Number value) { + if (value == null) { + return this; + } + // TODO - Verify only the 'basic' types are supported/used. + if (value instanceof Integer + || value instanceof Long + || value instanceof Short + || value instanceof Byte) { + span.setAttribute(key, value.longValue()); + } else if (value instanceof Float || value instanceof Double) { + span.setAttribute(key, value.doubleValue()); + } else { + throw new IllegalArgumentException("Number type not supported"); + } + + return this; + } + + @Override + public Span setTag(Tag tag, T value) { + if (tag == null) { + return this; + } + tag.set(this, value); + return this; + } + + @Override + public Span log(Map fields) { + logInternal(-1, fields); + return this; + } + + @Override + public Span log(long timestampMicroseconds, Map fields) { + logInternal(timestampMicroseconds, fields); + return this; + } + + @Override + public Span log(String event) { + span.addEvent(event); + return this; + } + + @Override + public Span log(long timestampMicroseconds, String event) { + span.addEvent(event, timestampMicroseconds, TimeUnit.MICROSECONDS); + return this; + } + + @Override + public Span setBaggageItem(String key, String value) { + // TagKey nor TagValue can be created with null values. + if (key == null || value == null) { + return this; + } + + spanContextTable().setBaggageItem(this, key, value); + + return this; + } + + @Nullable + @Override + public String getBaggageItem(String key) { + if (key == null) { + return null; + } + + return spanContextTable().getBaggageItem(this, key); + } + + @Override + public Span setOperationName(String operationName) { + span.updateName(operationName); + return this; + } + + @Override + public void finish() { + span.end(); + } + + @Override + public void finish(long finishMicros) { + span.end(finishMicros, TimeUnit.MICROSECONDS); + } + + private void logInternal(long timestampMicroseconds, Map fields) { + String name = getEventNameFromFields(fields); + Throwable throwable = null; + boolean isError = false; + if (name.equals(ERROR)) { + throwable = findThrowable(fields); + isError = true; + if (throwable == null) { + name = SemanticAttributes.EXCEPTION_EVENT_NAME; + } + } + Attributes attributes = convertToAttributes(fields, isError, throwable != null); + + if (throwable != null) { + // timestamp is not recorded if specified + span.recordException(throwable, attributes); + } else if (timestampMicroseconds != -1) { + span.addEvent(name, attributes, timestampMicroseconds, TimeUnit.MICROSECONDS); + } else { + span.addEvent(name, attributes); + } + } + + private static String getEventNameFromFields(Map fields) { + Object eventValue = fields == null ? null : fields.get(Fields.EVENT); + if (eventValue != null) { + return eventValue.toString(); + } + + return DEFAULT_EVENT_NAME; + } + + private static Attributes convertToAttributes( + Map fields, boolean isError, boolean isRecordingException) { + AttributesBuilder attributesBuilder = Attributes.builder(); + + for (Map.Entry entry : fields.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // TODO - verify null values are NOT allowed. + if (value == null) { + continue; + } + + if (value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + attributesBuilder.put(longKey(key), ((Number) value).longValue()); + } else if (value instanceof Float || value instanceof Double) { + attributesBuilder.put(doubleKey(key), ((Number) value).doubleValue()); + } else if (value instanceof Boolean) { + attributesBuilder.put(booleanKey(key), (Boolean) value); + } else { + AttributeKey attributeKey = null; + if (isError && !isRecordingException) { + if (key.equals(Fields.ERROR_KIND)) { + attributeKey = SemanticAttributes.EXCEPTION_TYPE; + } else if (key.equals(Fields.MESSAGE)) { + attributeKey = SemanticAttributes.EXCEPTION_MESSAGE; + } else if (key.equals(Fields.STACK)) { + attributeKey = SemanticAttributes.EXCEPTION_STACKTRACE; + } + } + if (isRecordingException && key.equals(Fields.ERROR_OBJECT)) { + // Already recorded as the exception itself so don't add as attribute. + continue; + } + + if (attributeKey == null) { + attributeKey = stringKey(key); + } + attributesBuilder.put(attributeKey, value.toString()); + } + } + + return attributesBuilder.build(); + } + + @Nullable + private static Throwable findThrowable(Map fields) { + Object value = fields.get(Fields.ERROR_OBJECT); + if (value instanceof Throwable) { + return (Throwable) value; + } + return null; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/TelemetryInfo.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/TelemetryInfo.java new file mode 100644 index 000000000..9617f1b32 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/TelemetryInfo.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Tracer; + +/** + * Utility class that holds a Tracer, a BaggageManager, and related objects that are core part of + * the OT Shim layer. + */ +final class TelemetryInfo { + + private final Tracer tracer; + private final Baggage emptyBaggage; + private final OpenTracingPropagators openTracingPropagators; + private final SpanContextShimTable spanContextTable; + + TelemetryInfo(Tracer tracer, OpenTracingPropagators openTracingPropagators) { + this.tracer = tracer; + this.openTracingPropagators = openTracingPropagators; + this.emptyBaggage = Baggage.empty(); + this.spanContextTable = new SpanContextShimTable(); + } + + Tracer tracer() { + return tracer; + } + + SpanContextShimTable spanContextTable() { + return spanContextTable; + } + + Baggage emptyBaggage() { + return emptyBaggage; + } + + OpenTracingPropagators propagators() { + return openTracingPropagators; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/TracerShim.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/TracerShim.java new file mode 100644 index 000000000..660807285 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/TracerShim.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentracing.Scope; +import io.opentracing.ScopeManager; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMapExtract; +import io.opentracing.propagation.TextMapInject; +import java.util.logging.Level; +import java.util.logging.Logger; + +final class TracerShim extends BaseShimObject implements Tracer { + private static final Logger logger = Logger.getLogger(TracerShim.class.getName()); + + private final ScopeManager scopeManagerShim; + private final Propagation propagation; + private volatile boolean isClosed; + + TracerShim(TelemetryInfo telemetryInfo) { + super(telemetryInfo); + this.scopeManagerShim = new ScopeManagerShim(telemetryInfo); + this.propagation = new Propagation(telemetryInfo); + } + + @Override + public ScopeManager scopeManager() { + return scopeManagerShim; + } + + @Override + public Span activeSpan() { + return scopeManagerShim.activeSpan(); + } + + @Override + public Scope activateSpan(Span span) { + return scopeManagerShim.activate(span); + } + + @Override + public SpanBuilder buildSpan(String operationName) { + if (isClosed) { + return new NoopSpanBuilderShim(telemetryInfo(), operationName); + } + + return new SpanBuilderShim(telemetryInfo, operationName); + } + + @Override + public void inject(SpanContext context, Format format, C carrier) { + if (context == null) { + logger.log(Level.INFO, "Cannot inject a null span context."); + return; + } + + SpanContextShim contextShim = getContextShim(context); + + if (format == Format.Builtin.TEXT_MAP + || format == Format.Builtin.TEXT_MAP_INJECT + || format == Format.Builtin.HTTP_HEADERS) { + propagation.injectTextMap(contextShim, format, (TextMapInject) carrier); + } + } + + @SuppressWarnings("ReturnMissingNullable") + @Override + public SpanContext extract(Format format, C carrier) { + try { + if (format == Format.Builtin.TEXT_MAP + || format == Format.Builtin.TEXT_MAP_EXTRACT + || format == Format.Builtin.HTTP_HEADERS) { + return propagation.extractTextMap(format, (TextMapExtract) carrier); + } + } catch (RuntimeException e) { + logger.log( + Level.INFO, + "Exception caught while extracting span context; returning null. " + + "Exception: [{0}] Message: [{1}]", + new String[] {e.getClass().getName(), e.getMessage()}); + } + + return null; + } + + @Override + public void close() { + isClosed = true; + } + + static SpanContextShim getContextShim(SpanContext context) { + if (!(context instanceof SpanContextShim)) { + throw new IllegalArgumentException("context is not a valid SpanContextShim object"); + } + + return (SpanContextShim) context; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/package-info.java b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/package-info.java new file mode 100644 index 000000000..53177c39a --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/java/io/opentelemetry/opentracingshim/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * An OpenTracing implementation that delegates to the OpenTelemetry SDK. Use {@link + * io.opentelemetry.opentracingshim.OpenTracingShim} to create tracers using this implementation. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.opentracingshim; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/opentracing-shim/src/main/resources/simplelogger.properties b/opentelemetry-java/opentracing-shim/src/main/resources/simplelogger.properties new file mode 100644 index 000000000..cd90c2acb --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/main/resources/simplelogger.properties @@ -0,0 +1 @@ +org.slf4j.simpleLogger.defaultLogLevel=warn diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/CustomTextMapPropagator.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/CustomTextMapPropagator.java new file mode 100644 index 000000000..4d3aed3c0 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/CustomTextMapPropagator.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collection; +import java.util.Collections; +import javax.annotation.Nullable; + +class CustomTextMapPropagator implements TextMapPropagator { + private boolean extracted; + private boolean injected; + + @Override + public Collection fields() { + return Collections.emptyList(); + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + injected = true; + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + extracted = true; + return context; + } + + public boolean isExtracted() { + return extracted; + } + + public boolean isInjected() { + return injected; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/OpenTracingShimTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/OpenTracingShimTest.java new file mode 100644 index 000000000..901977fb4 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/OpenTracingShimTest.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class OpenTracingShimTest { + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + void createTracerShim_default() { + TracerShim tracerShim = (TracerShim) OpenTracingShim.createTracerShim(); + assertThat(tracerShim.tracer()).isEqualTo(GlobalOpenTelemetry.getTracer("opentracingshim")); + } + + @Test + void createTracerShim_fromOpenTelemetryInstance() { + OpenTelemetry openTelemetry = mock(OpenTelemetry.class); + SdkTracerProvider sdk = SdkTracerProvider.builder().build(); + when(openTelemetry.getTracerProvider()).thenReturn(sdk); + ContextPropagators contextPropagators = mock(ContextPropagators.class); + when(contextPropagators.getTextMapPropagator()).thenReturn(mock(TextMapPropagator.class)); + when(openTelemetry.getPropagators()).thenReturn(contextPropagators); + + TracerShim tracerShim = (TracerShim) OpenTracingShim.createTracerShim(openTelemetry); + assertThat(tracerShim.tracer()).isEqualTo(sdk.get("opentracingshim")); + } + + @Test + void createTracerShim_withPropagators() { + Tracer tracer = mock(Tracer.class); + + TextMapPropagator textMapPropagator = new CustomTextMapPropagator(); + TextMapPropagator httpHeadersPropagator = new CustomTextMapPropagator(); + + TracerShim tracerShim = + (TracerShim) + OpenTracingShim.createTracerShim( + tracer, + OpenTracingPropagators.builder() + .setTextMap(textMapPropagator) + .setHttpHeaders(httpHeadersPropagator) + .build()); + + assertThat(tracerShim.propagators().textMapPropagator()).isSameAs(textMapPropagator); + assertThat(tracerShim.propagators().httpHeadersPropagator()).isSameAs(httpHeadersPropagator); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/PropagationTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/PropagationTest.java new file mode 100644 index 000000000..e0ac84480 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/PropagationTest.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PropagationTest { + private final Tracer tracer = SdkTracerProvider.builder().build().get("PropagationTest"); + + @BeforeEach + @AfterEach + void setUp() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + public void defaultPropagators() { + Propagation propagation = + new Propagation(new TelemetryInfo(tracer, OpenTracingPropagators.builder().build())); + assertThat(propagation.propagators().textMapPropagator()) + .isSameAs(propagation.propagators().httpHeadersPropagator()); + } + + @Test + public void textMapPropagator() { + TextMapPropagator textMapPropagator = new CustomTextMapPropagator(); + + Propagation propagation = + new Propagation( + new TelemetryInfo( + tracer, OpenTracingPropagators.builder().setTextMap(textMapPropagator).build())); + + assertThat(propagation.propagators().textMapPropagator()) + .isNotSameAs(propagation.propagators().httpHeadersPropagator()); + + assertThat(propagation.propagators().textMapPropagator()).isSameAs(textMapPropagator); + } + + @Test + public void httpHeadersPropagator() { + TextMapPropagator httpHeadersPropagator = new CustomTextMapPropagator(); + + Propagation propagation = + new Propagation( + new TelemetryInfo( + tracer, + OpenTracingPropagators.builder().setHttpHeaders(httpHeadersPropagator).build())); + + assertThat(propagation.propagators().textMapPropagator()) + .isNotSameAs(propagation.propagators().httpHeadersPropagator()); + + assertThat(propagation.propagators().httpHeadersPropagator()).isSameAs(httpHeadersPropagator); + } + + @Test + public void bothCustomPropagator() { + TextMapPropagator textMapPropagator = new CustomTextMapPropagator(); + TextMapPropagator httpHeadersPropagator = new CustomTextMapPropagator(); + + Propagation propagation = + new Propagation( + new TelemetryInfo( + tracer, + OpenTracingPropagators.builder() + .setTextMap(textMapPropagator) + .setHttpHeaders(httpHeadersPropagator) + .build())); + + assertThat(propagation.propagators().textMapPropagator()) + .isNotSameAs(propagation.propagators().httpHeadersPropagator()); + + assertThat(propagation.propagators().textMapPropagator()).isSameAs(textMapPropagator); + assertThat(propagation.propagators().httpHeadersPropagator()).isSameAs(httpHeadersPropagator); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/SpanBuilderShimTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/SpanBuilderShimTest.java new file mode 100644 index 000000000..df6b9356a --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/SpanBuilderShimTest.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import static io.opentelemetry.opentracingshim.TestUtils.getBaggageMap; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SpanBuilderShimTest { + + private final SdkTracerProvider tracerSdkFactory = SdkTracerProvider.builder().build(); + private final Tracer tracer = tracerSdkFactory.get("SpanShimTest"); + private final TelemetryInfo telemetryInfo = + new TelemetryInfo(tracer, OpenTracingPropagators.builder().build()); + + private static final String SPAN_NAME = "Span"; + + @BeforeEach + void setUp() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + void baggage_parent() { + SpanShim parentSpan = (SpanShim) new SpanBuilderShim(telemetryInfo, SPAN_NAME).start(); + try { + parentSpan.setBaggageItem("key1", "value1"); + + SpanShim childSpan = + (SpanShim) new SpanBuilderShim(telemetryInfo, SPAN_NAME).asChildOf(parentSpan).start(); + try { + assertThat("value1").isEqualTo(childSpan.getBaggageItem("key1")); + assertThat(getBaggageMap(parentSpan.context().baggageItems())) + .isEqualTo(getBaggageMap(childSpan.context().baggageItems())); + } finally { + childSpan.finish(); + } + } finally { + parentSpan.finish(); + } + } + + @Test + void baggage_parentContext() { + SpanShim parentSpan = (SpanShim) new SpanBuilderShim(telemetryInfo, SPAN_NAME).start(); + try { + parentSpan.setBaggageItem("key1", "value1"); + + SpanShim childSpan = + (SpanShim) + new SpanBuilderShim(telemetryInfo, SPAN_NAME).asChildOf(parentSpan.context()).start(); + try { + assertThat("value1").isEqualTo(childSpan.getBaggageItem("key1")); + assertThat(getBaggageMap(parentSpan.context().baggageItems())) + .isEqualTo(getBaggageMap(childSpan.context().baggageItems())); + } finally { + childSpan.finish(); + } + } finally { + parentSpan.finish(); + } + } + + @Test + void parent_NullContextShim() { + /* SpanContextShim is null until Span.context() or Span.getBaggageItem() are called. + * Verify a null SpanContextShim in the parent is handled properly. */ + SpanShim parentSpan = (SpanShim) new SpanBuilderShim(telemetryInfo, SPAN_NAME).start(); + try { + SpanShim childSpan = + (SpanShim) new SpanBuilderShim(telemetryInfo, SPAN_NAME).asChildOf(parentSpan).start(); + try { + assertThat(childSpan.context().baggageItems().iterator().hasNext()).isFalse(); + } finally { + childSpan.finish(); + } + } finally { + parentSpan.finish(); + } + } + + @Test + void withStartTimestamp() { + long micros = 123447307984L; + SpanShim spanShim = + (SpanShim) new SpanBuilderShim(telemetryInfo, SPAN_NAME).withStartTimestamp(micros).start(); + SpanData spanData = ((ReadableSpan) spanShim.getSpan()).toSpanData(); + assertThat(spanData.getStartEpochNanos()).isEqualTo(micros * 1000L); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/SpanShimTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/SpanShimTest.java new file mode 100644 index 000000000..f724b3f45 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/SpanShimTest.java @@ -0,0 +1,262 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import static io.opentelemetry.opentracingshim.TestUtils.getBaggageMap; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentracing.log.Fields; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SpanShimTest { + + private final SdkTracerProvider tracerSdkFactory = SdkTracerProvider.builder().build(); + private final Tracer tracer = tracerSdkFactory.get("SpanShimTest"); + private final TelemetryInfo telemetryInfo = + new TelemetryInfo(tracer, OpenTracingPropagators.builder().build()); + private Span span; + + private static final String SPAN_NAME = "Span"; + + @BeforeEach + void setUp() { + span = telemetryInfo.tracer().spanBuilder(SPAN_NAME).startSpan(); + } + + @AfterEach + void tearDown() { + span.end(); + } + + @Test + void context_simple() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + + SpanContextShim contextShim = (SpanContextShim) spanShim.context(); + assertThat(contextShim).isNotNull(); + assertThat(span.getSpanContext()).isEqualTo(contextShim.getSpanContext()); + assertThat(span.getSpanContext().getTraceId().toString()).isEqualTo(contextShim.toTraceId()); + assertThat(span.getSpanContext().getSpanId().toString()).isEqualTo(contextShim.toSpanId()); + assertThat(contextShim.baggageItems().iterator().hasNext()).isFalse(); + } + + @Test + void baggage() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + + spanShim.setBaggageItem("key1", "value1"); + spanShim.setBaggageItem("key2", "value2"); + assertThat("value1").isEqualTo(spanShim.getBaggageItem("key1")); + assertThat("value2").isEqualTo(spanShim.getBaggageItem("key2")); + + SpanContextShim contextShim = (SpanContextShim) spanShim.context(); + assertThat(contextShim).isNotNull(); + Map baggageMap = getBaggageMap(contextShim.baggageItems()); + assertThat(baggageMap.size()).isEqualTo(2); + assertThat("value1").isEqualTo(baggageMap.get("key1")); + assertThat("value2").isEqualTo(baggageMap.get("key2")); + } + + @Test + void baggage_replacement() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + SpanContextShim contextShim1 = (SpanContextShim) spanShim.context(); + + spanShim.setBaggageItem("key1", "value1"); + SpanContextShim contextShim2 = (SpanContextShim) spanShim.context(); + assertThat(contextShim2).isNotEqualTo(contextShim1); + assertThat(contextShim1.baggageItems().iterator().hasNext()).isFalse(); /* original, empty */ + assertThat(contextShim2.baggageItems().iterator()).hasNext(); /* updated, with values */ + } + + @Test + void baggage_differentShimObjs() { + SpanShim spanShim1 = new SpanShim(telemetryInfo, span); + spanShim1.setBaggageItem("key1", "value1"); + + /* Baggage should be synchronized among different SpanShim objects + * referring to the same Span.*/ + SpanShim spanShim2 = new SpanShim(telemetryInfo, span); + spanShim2.setBaggageItem("key1", "value2"); + assertThat(spanShim1.getBaggageItem("key1")).isEqualTo("value2"); + assertThat(spanShim2.getBaggageItem("key1")).isEqualTo("value2"); + assertThat(getBaggageMap(spanShim2.context().baggageItems())) + .isEqualTo(getBaggageMap(spanShim1.context().baggageItems())); + } + + @Test + void finish_micros() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + long micros = 123447307984L; + spanShim.finish(micros); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + assertThat(spanData.getEndEpochNanos()).isEqualTo(micros * 1000L); + } + + @Test + public void log_error() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + Map fields = createErrorFields(); + spanShim.log(fields); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + verifyErrorEvent(spanData); + } + + @Test + public void log_error_with_timestamp() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + Map fields = createErrorFields(); + long micros = 123447307984L; + spanShim.log(micros, fields); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + verifyErrorEvent(spanData); + } + + @Test + public void log_exception() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + Map fields = createExceptionFields(); + spanShim.log(fields); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + assertThat(spanData.getEvents()).hasSize(1); + + verifyExceptionEvent(spanData); + } + + @Test + public void log_error_with_exception() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + final Map fields = createExceptionFields(); + fields.putAll(createErrorFields()); + + long micros = 123447307984L; + spanShim.log(micros, fields); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + verifyErrorEvent(spanData); + } + + @Test + public void log_exception_with_timestamp() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + Map fields = createExceptionFields(); + long micros = 123447307984L; + spanShim.log(micros, fields); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + + verifyExceptionEvent(spanData); + assertThat(spanData.getEvents().get(0).getEpochNanos()).isEqualTo(micros * 1000L); + } + + @Test + public void log_fields() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + spanShim.log(putKeyValuePairsToMap(new HashMap<>())); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + verifyAttributes(spanData.getEvents().get(0)); + } + + @Test + void log_micros() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + long micros = 123447307984L; + spanShim.log(micros, "event"); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + assertThat(spanData.getEvents().get(0).getEpochNanos()).isEqualTo(micros * 1000L); + } + + @Test + void log_fields_micros() { + SpanShim spanShim = new SpanShim(telemetryInfo, span); + long micros = 123447307984L; + spanShim.log(micros, putKeyValuePairsToMap(new HashMap<>())); + SpanData spanData = ((ReadableSpan) span).toSpanData(); + EventData eventData = spanData.getEvents().get(0); + assertThat(eventData.getEpochNanos()).isEqualTo(micros * 1000L); + verifyAttributes(eventData); + } + + private static Map createErrorFields() { + Map fields = new HashMap<>(); + fields.put(Fields.EVENT, "error"); + fields.put(Fields.ERROR_OBJECT, new RuntimeException()); + + putKeyValuePairsToMap(fields); + return fields; + } + + private static void verifyErrorEvent(SpanData spanData) { + assertThat(spanData.getEvents()).hasSize(1); + + EventData eventData = spanData.getEvents().get(0); + assertThat(eventData.getName()).isEqualTo("exception"); + + verifyAttributes(eventData); + } + + private static Map createExceptionFields() { + Map fields = new HashMap<>(); + fields.put(Fields.EVENT, "error"); + fields.put(Fields.ERROR_KIND, "kind"); + fields.put(Fields.MESSAGE, "message"); + fields.put(Fields.STACK, "stack"); + + putKeyValuePairsToMap(fields); + return fields; + } + + private static Map putKeyValuePairsToMap(Map fields) { + fields.put("keyForString", "value"); + fields.put("keyForInt", 1); + fields.put("keyForDouble", 1.0); + fields.put("keyForBoolean", true); + return fields; + } + + private static void verifyExceptionEvent(SpanData spanData) { + assertThat(spanData.getEvents()).hasSize(1); + + EventData eventData = spanData.getEvents().get(0); + assertThat(eventData.getName()).isEqualTo("exception"); + assertThat( + eventData + .getAttributes() + .get(AttributeKey.stringKey(SemanticAttributes.EXCEPTION_TYPE.getKey()))) + .isEqualTo("kind"); + assertThat( + eventData + .getAttributes() + .get(AttributeKey.stringKey(SemanticAttributes.EXCEPTION_MESSAGE.getKey()))) + .isEqualTo("message"); + assertThat( + eventData + .getAttributes() + .get(AttributeKey.stringKey(SemanticAttributes.EXCEPTION_STACKTRACE.getKey()))) + .isEqualTo("stack"); + + verifyAttributes(eventData); + } + + private static void verifyAttributes(EventData eventData) { + assertThat(eventData.getAttributes().get(AttributeKey.stringKey("keyForString"))) + .isEqualTo("value"); + assertThat(eventData.getAttributes().get(AttributeKey.longKey("keyForInt"))).isEqualTo(1); + assertThat(eventData.getAttributes().get(AttributeKey.doubleKey("keyForDouble"))) + .isEqualTo(1.0); + assertThat(eventData.getAttributes().get(AttributeKey.booleanKey("keyForBoolean"))).isTrue(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/TestUtils.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/TestUtils.java new file mode 100644 index 000000000..fb8a52e26 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/TestUtils.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import java.util.HashMap; +import java.util.Map; + +final class TestUtils { + private TestUtils() {} + + static Map getBaggageMap(Iterable> baggage) { + Map baggageMap = new HashMap<>(); + for (Map.Entry entry : baggage) { + baggageMap.put(entry.getKey(), entry.getValue()); + } + + return baggageMap; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/TracerShimTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/TracerShimTest.java new file mode 100644 index 000000000..28729c8d7 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/TracerShimTest.java @@ -0,0 +1,230 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMapAdapter; +import io.opentracing.tag.BooleanTag; +import io.opentracing.tag.IntTag; +import io.opentracing.tag.StringTag; +import io.opentracing.tag.Tag; +import io.opentracing.tag.Tags; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TracerShimTest { + + @RegisterExtension public OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + TracerShim tracerShim; + Tracer tracer; + + @BeforeEach + void setUp() { + tracer = otelTesting.getOpenTelemetry().getTracer("opentracingshim"); + tracerShim = + new TracerShim(new TelemetryInfo(tracer, OpenTracingPropagators.builder().build())); + } + + @Test + void defaultTracer() { + assertThat(tracerShim.buildSpan("one")).isNotNull(); + assertThat(tracerShim.scopeManager()).isNotNull(); + assertThat(tracerShim.activeSpan()).isNull(); + assertThat(tracerShim.scopeManager().activeSpan()).isNull(); + } + + @Test + void activateSpan() { + Span otSpan = tracerShim.buildSpan("one").start(); + io.opentelemetry.api.trace.Span span = ((SpanShim) otSpan).getSpan(); + + assertThat(tracerShim.activeSpan()).isNull(); + assertThat(tracerShim.scopeManager().activeSpan()).isNull(); + + try (Scope scope = tracerShim.activateSpan(otSpan)) { + assertThat(tracerShim.activeSpan()).isNotNull(); + assertThat(tracerShim.scopeManager().activeSpan()).isNotNull(); + assertThat(((SpanShim) tracerShim.activeSpan()).getSpan()).isEqualTo(span); + assertThat(((SpanShim) tracerShim.scopeManager().activeSpan()).getSpan()).isEqualTo(span); + } + + assertThat(tracerShim.activeSpan()).isNull(); + assertThat(tracerShim.scopeManager().activeSpan()).isNull(); + } + + @Test + void extract_nullContext() { + SpanContext result = + tracerShim.extract(Format.Builtin.TEXT_MAP, new TextMapAdapter(Collections.emptyMap())); + assertThat(result).isNull(); + } + + @Test + void inject_nullContext() { + Map map = new HashMap<>(); + tracerShim.inject(null, Format.Builtin.TEXT_MAP, new TextMapAdapter(map)); + assertThat(map).isEmpty(); + } + + @Test + void inject_textMap() { + Map map = new HashMap<>(); + CustomTextMapPropagator textMapPropagator = new CustomTextMapPropagator(); + CustomTextMapPropagator httpHeadersPropagator = new CustomTextMapPropagator(); + TelemetryInfo telemetryInfo = + new TelemetryInfo( + tracer, + OpenTracingPropagators.builder() + .setTextMap(textMapPropagator) + .setHttpHeaders(httpHeadersPropagator) + .build()); + tracerShim = new TracerShim(telemetryInfo); + io.opentelemetry.api.trace.Span span = telemetryInfo.tracer().spanBuilder("span").startSpan(); + SpanContext context = new SpanShim(telemetryInfo, span).context(); + + tracerShim.inject(context, Format.Builtin.TEXT_MAP, new TextMapAdapter(map)); + assertThat(textMapPropagator.isInjected()).isTrue(); + assertThat(httpHeadersPropagator.isInjected()).isFalse(); + } + + @Test + void inject_httpHeaders() { + Map map = new HashMap<>(); + CustomTextMapPropagator textMapPropagator = new CustomTextMapPropagator(); + CustomTextMapPropagator httpHeadersPropagator = new CustomTextMapPropagator(); + TelemetryInfo telemetryInfo = + new TelemetryInfo( + tracer, + OpenTracingPropagators.builder() + .setTextMap(textMapPropagator) + .setHttpHeaders(httpHeadersPropagator) + .build()); + tracerShim = new TracerShim(telemetryInfo); + io.opentelemetry.api.trace.Span span = telemetryInfo.tracer().spanBuilder("span").startSpan(); + SpanContext context = new SpanShim(telemetryInfo, span).context(); + + tracerShim.inject(context, Format.Builtin.HTTP_HEADERS, new TextMapAdapter(map)); + assertThat(textMapPropagator.isInjected()).isFalse(); + assertThat(httpHeadersPropagator.isInjected()).isTrue(); + } + + @Test + void extract_textMap() { + Map map = new HashMap<>(); + CustomTextMapPropagator textMapPropagator = new CustomTextMapPropagator(); + CustomTextMapPropagator httpHeadersPropagator = new CustomTextMapPropagator(); + tracerShim = + new TracerShim( + new TelemetryInfo( + tracer, + OpenTracingPropagators.builder() + .setTextMap(textMapPropagator) + .setHttpHeaders(httpHeadersPropagator) + .build())); + + tracerShim.extract(Format.Builtin.TEXT_MAP, new TextMapAdapter(map)); + assertThat(textMapPropagator.isExtracted()).isTrue(); + assertThat(httpHeadersPropagator.isExtracted()).isFalse(); + } + + @Test + void extract_httpHeaders() { + Map map = new HashMap<>(); + CustomTextMapPropagator textMapPropagator = new CustomTextMapPropagator(); + CustomTextMapPropagator httpHeadersPropagator = new CustomTextMapPropagator(); + tracerShim = + new TracerShim( + new TelemetryInfo( + tracer, + OpenTracingPropagators.builder() + .setTextMap(textMapPropagator) + .setHttpHeaders(httpHeadersPropagator) + .build())); + + tracerShim.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(map)); + assertThat(textMapPropagator.isExtracted()).isFalse(); + assertThat(httpHeadersPropagator.isExtracted()).isTrue(); + } + + @Test + void close() { + tracerShim.close(); + Span otSpan = tracerShim.buildSpan(null).start(); + io.opentelemetry.api.trace.Span span = ((SpanShim) otSpan).getSpan(); + assertThat(span.getSpanContext().isValid()).isFalse(); + } + + @Test + void doesNotCrash() { + Span span = + tracerShim + .buildSpan("test") + .asChildOf((Span) null) + .asChildOf((SpanContext) null) + .addReference(null, null) + .addReference("parent", tracerShim.buildSpan("parent").start().context()) + .ignoreActiveSpan() + .withTag((Tag) null, null) + .withTag("foo", (String) null) + .withTag("bar", false) + .withTag("cat", (Number) null) + .withTag("dog", 0.0f) + .withTag("bear", 10) + .withTag(new StringTag("string"), "string") + .withTag(new BooleanTag("boolean"), false) + .withTag(new IntTag("int"), 10) + .start(); + + span.setTag((Tag) null, null) + .setTag("foo", (String) null) + .setTag("bar", false) + .setTag("cat", (Number) null) + .setTag("dog", 0.0f) + .setTag("bear", 10) + .setTag(new StringTag("string"), "string") + .setTag(new BooleanTag("boolean"), false) + .setTag(new IntTag("int"), 10) + .log(10, new HashMap<>()) + .log(20, "foo") + .setBaggageItem(null, null) + .setOperationName("name") + .setTag(Tags.ERROR.getKey(), "true"); + + assertThat(((SpanShim) span).getSpan().isRecording()).isTrue(); + } + + @Test + void noopDoesNotCrash() { + tracerShim.close(); + Span span = + tracerShim + .buildSpan("test") + .asChildOf((Span) null) + .asChildOf((SpanContext) null) + .addReference(null, null) + .ignoreActiveSpan() + .withTag((Tag) null, null) + .withTag("foo", (String) null) + .withTag("bar", false) + .withTag("cat", (Number) null) + .withStartTimestamp(0) + .start(); + + assertThat(((SpanShim) span).getSpan().isRecording()).isFalse(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/OpenTelemetryInteroperabilityTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/OpenTelemetryInteroperabilityTest.java new file mode 100644 index 000000000..9b233349a --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/OpenTelemetryInteroperabilityTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OpenTelemetryInteroperabilityTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final io.opentelemetry.api.trace.Tracer tracer = + otelTesting.getOpenTelemetry().getTracer("opentracingshim"); + + private final Tracer otTracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + + @Test + void sdkContinuesOpenTracingTrace() { + Span otSpan = otTracer.buildSpan("ot_span").start(); + try (Scope scope = otTracer.scopeManager().activate(otSpan)) { + tracer.spanBuilder("otel_span").startSpan().end(); + } finally { + otSpan.finish(); + } + assertThat(io.opentelemetry.api.trace.Span.current().getSpanContext().isValid()).isFalse(); + assertThat(otTracer.activeSpan()).isNull(); + + List finishedSpans = otelTesting.getSpans(); + assertThat(finishedSpans).hasSize(2); + TestUtils.assertSameTrace(finishedSpans); + } + + @Test + void openTracingContinuesSdkTrace() { + io.opentelemetry.api.trace.Span otelSpan = tracer.spanBuilder("otel_span").startSpan(); + try (io.opentelemetry.context.Scope scope = otelSpan.makeCurrent()) { + otTracer.buildSpan("ot_span").start().finish(); + } finally { + otelSpan.end(); + } + + assertThat(io.opentelemetry.api.trace.Span.current().getSpanContext().isValid()).isFalse(); + assertThat(otTracer.activeSpan()).isNull(); + + List finishedSpans = otelTesting.getSpans(); + assertThat(finishedSpans).hasSize(2); + TestUtils.assertSameTrace(finishedSpans); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/TestUtils.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/TestUtils.java new file mode 100644 index 000000000..8ad2a0e00 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/TestUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +public final class TestUtils { + private TestUtils() {} + + /** Returns the number of finished {@code Span}s in the specified {@code InMemorySpanExporter}. */ + public static Callable finishedSpansSize(final OpenTelemetryExtension otelTesting) { + return () -> otelTesting.getSpans().size(); + } + + /** Returns a {@code List} with the {@code Span} matching the specified attribute. */ + public static List getByAttr( + List spans, final AttributeKey key, final T value) { + return getByCondition( + spans, + spanData -> { + T attrValue = spanData.getAttributes().get(key); + if (attrValue == null) { + return false; + } + + return value.equals(attrValue); + }); + } + + /** Returns a {@code List} with the {@code Span} matching the specified kind. */ + public static List getByKind(List spans, final SpanKind kind) { + return getByCondition(spans, span -> span.getKind() == kind); + } + + /** + * Returns one {@code Span} instance matching the specified kind. In case of more than one + * instance being matched, an {@code IllegalArgumentException} will be thrown. + */ + @Nullable + public static SpanData getOneByKind(List spans, final SpanKind kind) { + + List found = getByKind(spans, kind); + if (found.size() > 1) { + throw new IllegalArgumentException("there is more than one span with kind '" + kind + "'"); + } + + return found.isEmpty() ? null : found.get(0); + } + + /** Returns a {@code List} with the {@code Span} matching the specified name. */ + private static List getByName(List spans, final String name) { + return getByCondition(spans, span -> span.getName().equals(name)); + } + + /** + * Returns one {@code Span} instance matching the specified name. In case of more than one + * instance being matched, an {@code IllegalArgumentException} will be thrown. + */ + @Nullable + public static SpanData getOneByName(List spans, final String name) { + + List found = getByName(spans, name); + if (found.size() > 1) { + throw new IllegalArgumentException("there is more than one span with name '" + name + "'"); + } + + return found.isEmpty() ? null : found.get(0); + } + + interface Condition { + boolean check(SpanData span); + } + + private static List getByCondition(List spans, Condition cond) { + List found = new ArrayList<>(); + for (SpanData span : spans) { + if (cond.check(span)) { + found.add(span); + } + } + + return found; + } + + /** Sleeps for a random period of time, expected to be under 1 second. */ + public static void sleep() { + try { + TimeUnit.MILLISECONDS.sleep(new Random().nextInt(500)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted", e); + } + } + + /** Sleeps the specified milliseconds. */ + public static void sleep(long milliseconds) { + try { + TimeUnit.MILLISECONDS.sleep(milliseconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted", e); + } + } + + /** + * Sorts the specified {@code List} of {@code Span} by their start epoch timestamp values, + * returning it as a new {@code List}. + */ + public static List sortByStartTime(List spans) { + List sortedSpans = new ArrayList<>(spans); + Collections.sort( + sortedSpans, (o1, o2) -> Long.compare(o1.getStartEpochNanos(), o2.getStartEpochNanos())); + return sortedSpans; + } + + /** Asserts the specified {@code List} of {@code Span} belongs to the same trace. */ + public static void assertSameTrace(List spans) { + for (int i = 0; i < spans.size() - 1; i++) { + // TODO - Include nanos in this comparison. + assertThat(spans.get(i).getEndEpochNanos()) + .isLessThanOrEqualTo(spans.get(spans.size() - 1).getEndEpochNanos()); + assertThat(spans.get(i).getTraceId()).isEqualTo(spans.get(spans.size() - 1).getTraceId()); + assertThat(spans.get(i).getParentSpanId()).isEqualTo(spans.get(spans.size() - 1).getSpanId()); + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/activespanreplacement/ActiveSpanReplacementTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/activespanreplacement/ActiveSpanReplacementTest.java new file mode 100644 index 000000000..18e2aecd9 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/activespanreplacement/ActiveSpanReplacementTest.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.activespanreplacement; + +import static io.opentelemetry.api.trace.SpanId.isValid; +import static io.opentelemetry.opentracingshim.testbed.TestUtils.finishedSpansSize; +import static io.opentelemetry.opentracingshim.testbed.TestUtils.sleep; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("FutureReturnValueIgnored") +class ActiveSpanReplacementTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Test + void test() throws Exception { + // Start an isolated task and query for its result in another task/thread + Span span = tracer.buildSpan("initial").start(); + try (Scope scope = tracer.scopeManager().activate(span)) { + // Explicitly pass a Span to be finished once a late calculation is done. + submitAnotherTask(span); + } + + await().atMost(Duration.ofSeconds(15)).until(finishedSpansSize(otelTesting), equalTo(3)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(3); + assertThat(spans.get(0).getName()).isEqualTo("initial"); // Isolated task + assertThat(spans.get(1).getName()).isEqualTo("subtask"); + assertThat(spans.get(2).getName()).isEqualTo("task"); + + // task/subtask are part of the same trace, and subtask is a child of task + assertThat(spans.get(2).getTraceId()).isEqualTo(spans.get(1).getTraceId()); + assertThat(spans.get(1).getParentSpanId()).isEqualTo(spans.get(2).getSpanId()); + + // initial task is not related in any way to those two tasks + assertThat(spans.get(1).getTraceId()).isNotEqualTo(spans.get(0).getTraceId()); + assertThat(isValid(spans.get(0).getParentSpanId())).isFalse(); + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } + + private void submitAnotherTask(final Span initialSpan) { + + executor.submit( + () -> { + // Create a new Span for this task + Span taskSpan = tracer.buildSpan("task").start(); + try (Scope scope = tracer.scopeManager().activate(taskSpan)) { + + // Simulate work strictly related to the initial Span + // and finish it. + try (Scope initialScope = tracer.scopeManager().activate(initialSpan)) { + sleep(50); + } finally { + initialSpan.finish(); + } + + // Restore the span for this task and create a subspan + Span subTaskSpan = tracer.buildSpan("subtask").start(); + try (Scope subTaskScope = tracer.scopeManager().activate(subTaskSpan)) { + sleep(50); + } finally { + subTaskSpan.finish(); + } + } finally { + taskSpan.finish(); + } + }); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/actorpropagation/Actor.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/actorpropagation/Actor.java new file mode 100644 index 000000000..ccaa5d66f --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/actorpropagation/Actor.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.actorpropagation; + +import io.opentracing.References; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Phaser; + +final class Actor implements AutoCloseable { + private final ExecutorService executor; + private final Tracer tracer; + private final Phaser phaser; + + public Actor(Tracer tracer, Phaser phaser) { + // Passed along here for testing. Normally should be referenced via GlobalTracer.get(). + this.tracer = tracer; + + this.phaser = phaser; + executor = Executors.newFixedThreadPool(2); + } + + @Override + public void close() { + executor.shutdown(); + } + + public Future tell(final String message) { + final Span parent = tracer.scopeManager().activeSpan(); + phaser.register(); + return executor.submit( + () -> { + Span child = + tracer + .buildSpan("received") + .addReference(References.FOLLOWS_FROM, parent.context()) + .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CONSUMER) + .start(); + try (Scope scope = tracer.activateSpan(child)) { + phaser.arriveAndAwaitAdvance(); // child tracer started + child.log("received " + message); + phaser.arriveAndAwaitAdvance(); // assert size + } finally { + child.finish(); + } + + phaser.arriveAndAwaitAdvance(); // child tracer finished + phaser.arriveAndAwaitAdvance(); // assert size + }); + } + + public Future ask(final String message) { + final Span parent = tracer.scopeManager().activeSpan(); + phaser.register(); + Future future = + executor.submit( + () -> { + Span span = + tracer + .buildSpan("received") + .addReference(References.FOLLOWS_FROM, parent.context()) + .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CONSUMER) + .start(); + try { + phaser.arriveAndAwaitAdvance(); // child tracer started + phaser.arriveAndAwaitAdvance(); // assert size + return "received " + message; + } finally { + span.finish(); + + phaser.arriveAndAwaitAdvance(); // child tracer finished + phaser.arriveAndAwaitAdvance(); // assert size + } + }); + return future; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/actorpropagation/ActorPropagationTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/actorpropagation/ActorPropagationTest.java new file mode 100644 index 000000000..64f0595f2 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/actorpropagation/ActorPropagationTest.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.actorpropagation; + +import static io.opentelemetry.opentracingshim.testbed.TestUtils.getByKind; +import static io.opentelemetry.opentracingshim.testbed.TestUtils.getOneByKind; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.Phaser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * These tests are intended to simulate the kind of async models that are common in java async + * frameworks. + * + *

    For improved readability, ignore the phaser lines as those are there to ensure deterministic + * execution for the tests without sleeps. + */ +@SuppressWarnings("FutureReturnValueIgnored") +class ActorPropagationTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private Phaser phaser; + + @BeforeEach + void before() { + phaser = new Phaser(); + } + + @Test + void testActorTell() { + try (Actor actor = new Actor(tracer, phaser)) { + phaser.register(); + Span parent = + tracer + .buildSpan("actorTell") + .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_PRODUCER) + .withTag(Tags.COMPONENT.getKey(), "example-actor") + .start(); + try (Scope scope = tracer.activateSpan(parent)) { + actor.tell("my message 1"); + actor.tell("my message 2"); + } finally { + parent.finish(); + } + + phaser.arriveAndAwaitAdvance(); // child tracer started + assertThat(otelTesting.getSpans().size()).isEqualTo(1); + phaser.arriveAndAwaitAdvance(); // continue... + phaser.arriveAndAwaitAdvance(); // child tracer finished + assertThat(otelTesting.getSpans().size()).isEqualTo(3); + assertThat(getByKind(otelTesting.getSpans(), SpanKind.CONSUMER)).hasSize(2); + phaser.arriveAndDeregister(); // continue... + + List finished = otelTesting.getSpans(); + assertThat(finished.size()).isEqualTo(3); + assertThat(finished.get(0).getTraceId()).isEqualTo(finished.get(1).getTraceId()); + assertThat(getByKind(finished, SpanKind.CONSUMER)).hasSize(2); + assertThat(getOneByKind(finished, SpanKind.PRODUCER)).isNotNull(); + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } + } + + @Test + void testActorAsk() throws ExecutionException, InterruptedException { + try (Actor actor = new Actor(tracer, phaser)) { + phaser.register(); + Future future1; + Future future2; + Span span = + tracer + .buildSpan("actorAsk") + .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_PRODUCER) + .withTag(Tags.COMPONENT.getKey(), "example-actor") + .start(); + try (Scope scope = tracer.activateSpan(span)) { + future1 = actor.ask("my message 1"); + future2 = actor.ask("my message 2"); + } finally { + span.finish(); + } + phaser.arriveAndAwaitAdvance(); // child tracer started + assertThat(otelTesting.getSpans().size()).isEqualTo(1); + phaser.arriveAndAwaitAdvance(); // continue... + phaser.arriveAndAwaitAdvance(); // child tracer finished + assertThat(otelTesting.getSpans().size()).isEqualTo(3); + assertThat(getByKind(otelTesting.getSpans(), SpanKind.CONSUMER)).hasSize(2); + phaser.arriveAndDeregister(); // continue... + + List finished = otelTesting.getSpans(); + String message1 = future1.get(); // This really should be a non-blocking callback... + String message2 = future2.get(); // This really should be a non-blocking callback... + assertThat(message1).isEqualTo("received my message 1"); + assertThat(message2).isEqualTo("received my message 2"); + + assertThat(finished.size()).isEqualTo(3); + assertThat(finished.get(0).getTraceId()).isEqualTo(finished.get(1).getTraceId()); + assertThat(getByKind(finished, SpanKind.CONSUMER)).hasSize(2); + assertThat(getOneByKind(finished, SpanKind.PRODUCER)).isNotNull(); + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/baggagehandling/BaggageHandlingTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/baggagehandling/BaggageHandlingTest.java new file mode 100644 index 000000000..69440bba8 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/baggagehandling/BaggageHandlingTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.baggagehandling; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentracing.Span; +import io.opentracing.Tracer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public final class BaggageHandlingTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Test + void test_multithreaded() throws Exception { + final Span span = tracer.buildSpan("one").start(); + span.setBaggageItem("key1", "value1"); + + Future f = + executor.submit( + () -> { + /* Override the previous value... */ + span.setBaggageItem("key1", "value2"); + + /* add a new baggage item... */ + span.setBaggageItem("newkey", "newvalue"); + + /* have a child that updates its own baggage + * (should not be reflected in the original Span). */ + Span childSpan = tracer.buildSpan("child").start(); + try { + childSpan.setBaggageItem("key1", "childvalue"); + } finally { + childSpan.finish(); + } + + /* and finish the Span. */ + span.finish(); + }); + + /* Single call, no need to use await() */ + f.get(5, TimeUnit.SECONDS); + + assertThat(otelTesting.getSpans()).hasSize(2); + assertThat("value2").isEqualTo(span.getBaggageItem("key1")); + assertThat("newvalue").isEqualTo(span.getBaggageItem("newkey")); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Client.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Client.java new file mode 100644 index 000000000..4da5ca155 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Client.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.clientserver; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.propagation.Format.Builtin; +import io.opentracing.propagation.TextMapInjectAdapter; +import io.opentracing.tag.Tags; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; + +final class Client { + + private final ArrayBlockingQueue queue; + private final Tracer tracer; + + public Client(ArrayBlockingQueue queue, Tracer tracer) { + this.queue = queue; + this.tracer = tracer; + } + + public void send(boolean convertKeysToUpperCase) throws InterruptedException { + Message message = new Message(); + + Span span = + tracer + .buildSpan("send") + .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT) + .withTag(Tags.COMPONENT.getKey(), "example-client") + .start(); + try (Scope scope = tracer.activateSpan(span)) { + tracer.inject(span.context(), Builtin.TEXT_MAP_INJECT, new TextMapInjectAdapter(message)); + if (convertKeysToUpperCase) { + Message newMessage = new Message(); + for (Map.Entry entry : message.entrySet()) { + newMessage.put(entry.getKey().toUpperCase(), entry.getValue()); + } + message = newMessage; + } + queue.put(message); + } finally { + span.finish(); + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Message.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Message.java new file mode 100644 index 000000000..75bd6d060 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Message.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.clientserver; + +import java.util.HashMap; + +final class Message extends HashMap {} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Server.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Server.java new file mode 100644 index 000000000..f38203925 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/Server.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.clientserver; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.propagation.Format.Builtin; +import io.opentracing.propagation.TextMapExtractAdapter; +import io.opentracing.tag.Tags; +import java.util.concurrent.ArrayBlockingQueue; + +final class Server extends Thread { + + private final ArrayBlockingQueue queue; + private final Tracer tracer; + + public Server(ArrayBlockingQueue queue, Tracer tracer) { + this.queue = queue; + this.tracer = tracer; + } + + private void process(Message message) { + SpanContext context = + tracer.extract(Builtin.TEXT_MAP_EXTRACT, new TextMapExtractAdapter(message)); + Span span = + tracer + .buildSpan("receive") + .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER) + .withTag(Tags.COMPONENT.getKey(), "example-server") + .asChildOf(context) + .start(); + + try (Scope scope = tracer.activateSpan(span)) { + // Simulate work. + } finally { + span.finish(); + } + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + + try { + process(queue.take()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/TestClientServerTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/TestClientServerTest.java new file mode 100644 index 000000000..b88d780bb --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/clientserver/TestClientServerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.clientserver; + +import static io.opentelemetry.opentracingshim.testbed.TestUtils.finishedSpansSize; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Tracer; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TestClientServerTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(10); + private Server server; + + @BeforeEach + void before() { + server = new Server(queue, tracer); + server.start(); + } + + @AfterEach + void after() throws InterruptedException { + server.interrupt(); + server.join(5_000L); + } + + @Test + void test() throws Exception { + Client client = new Client(queue, tracer); + client.send(false); + verify(); + } + + @Test + public void testUpperCaseKeys() throws Exception { + Client client = new Client(queue, tracer); + client.send(true); + verify(); + } + + private void verify() { + await().atMost(Duration.ofSeconds(15)).until(finishedSpansSize(otelTesting), equalTo(2)); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(2); + + assertThat(finished.get(1).getTraceId()).isEqualTo(finished.get(0).getTraceId()); + SpanKind firstSpanKind = finished.get(0).getKind(); + switch (firstSpanKind) { + case CLIENT: + assertThat(finished.get(1).getKind()).isEqualTo(SpanKind.SERVER); + break; + case SERVER: + assertThat(finished.get(1).getKind()).isEqualTo(SpanKind.CLIENT); + break; + default: + fail("Unexpected first span kind: " + firstSpanKind); + } + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/Client.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/Client.java new file mode 100644 index 000000000..5718176d0 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/Client.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.concurrentcommonrequesthandler; + +import io.opentelemetry.opentracingshim.testbed.TestUtils; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class Client { + + private static final Logger logger = LoggerFactory.getLogger(Client.class); + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + private final RequestHandler requestHandler; + + public Client(RequestHandler requestHandler) { + this.requestHandler = requestHandler; + } + + public Future send(final Object message) { + final Context context = new Context(); + return executor.submit( + () -> { + logger.info("send {}", message); + TestUtils.sleep(); + executor + .submit( + () -> { + TestUtils.sleep(); + requestHandler.beforeRequest(message, context); + }) + .get(); + + executor + .submit( + () -> { + TestUtils.sleep(); + requestHandler.afterResponse(message, context); + }) + .get(); + + return message + ":response"; + }); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/Context.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/Context.java new file mode 100644 index 000000000..a8763a78a --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/Context.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.concurrentcommonrequesthandler; + +import java.util.HashMap; + +final class Context extends HashMap {} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/HandlerTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/HandlerTest.java new file mode 100644 index 000000000..8dd7aa180 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/HandlerTest.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.concurrentcommonrequesthandler; + +import static io.opentelemetry.opentracingshim.testbed.TestUtils.getOneByName; +import static io.opentelemetry.opentracingshim.testbed.TestUtils.sortByStartTime; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * There is only one instance of 'RequestHandler' per 'Client'. Methods of 'RequestHandler' are + * executed concurrently in different threads which are reused (common pool). Therefore we cannot + * use current active span and activate span. So one issue here is setting correct parent span. + */ +class HandlerTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private final Client client = new Client(new RequestHandler(tracer)); + + @Test + void two_requests() throws Exception { + Future responseFuture = client.send("message"); + Future responseFuture2 = client.send("message2"); + + assertThat(responseFuture.get(15, TimeUnit.SECONDS)).isEqualTo("message:response"); + assertThat(responseFuture2.get(15, TimeUnit.SECONDS)).isEqualTo("message2:response"); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(2); + + for (SpanData spanData : finished) { + assertThat(spanData.getKind()).isEqualTo(SpanKind.CLIENT); + } + + assertThat(finished.get(1).getTraceId()).isNotEqualTo(finished.get(0).getTraceId()); + assertThat(SpanId.isValid(finished.get(0).getParentSpanId())).isFalse(); + assertThat(SpanId.isValid(finished.get(1).getParentSpanId())).isFalse(); + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } + + /** Active parent is not picked up by child. */ + @Test + void parent_not_picked_up() throws Exception { + Span parentSpan = tracer.buildSpan("parent").start(); + try (Scope parentScope = tracer.activateSpan(parentSpan)) { + String response = client.send("no_parent").get(15, TimeUnit.SECONDS); + assertThat(response).isEqualTo("no_parent:response"); + } finally { + parentSpan.finish(); + } + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(2); + + SpanData child = getOneByName(finished, RequestHandler.OPERATION_NAME); + assertThat(child).isNotNull(); + + SpanData parent = getOneByName(finished, "parent"); + assertThat(parent).isNotNull(); + + // Here check that there is no parent-child relation although it should be because child is + // created when parent is active + assertThat(child.getParentSpanId()).isNotEqualTo(parent.getSpanId()); + } + + /** + * Solution is bad because parent is per client (we don't have better choice). Therefore all + * client requests will have the same parent. But if client is long living and injected/reused in + * different places then initial parent will not be correct. + */ + @Test + void bad_solution_to_set_parent() throws Exception { + Client client; + Span parentSpan = tracer.buildSpan("parent").start(); + try (Scope parentScope = tracer.activateSpan(parentSpan)) { + client = new Client(new RequestHandler(tracer, parentSpan.context())); + String response = client.send("correct_parent").get(15, TimeUnit.SECONDS); + assertThat(response).isEqualTo("correct_parent:response"); + } finally { + parentSpan.finish(); + } + + // Send second request, now there is no active parent, but it will be set, ups + String response = client.send("wrong_parent").get(15, TimeUnit.SECONDS); + assertThat(response).isEqualTo("wrong_parent:response"); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(3); + + finished = sortByStartTime(finished); + + SpanData parent = getOneByName(finished, "parent"); + assertThat(parent).isNotNull(); + + // now there is parent/child relation between first and second span: + assertThat(finished.get(1).getParentSpanId()).isEqualTo(parent.getSpanId()); + + // third span should not have parent, but it has, damn it + assertThat(finished.get(2).getParentSpanId()).isEqualTo(parent.getSpanId()); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/RequestHandler.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/RequestHandler.java new file mode 100644 index 000000000..07808a5a9 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/concurrentcommonrequesthandler/RequestHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.concurrentcommonrequesthandler; + +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.tag.Tags; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * One instance per Client. Executed concurrently for all requests of one client. 'beforeRequest' + * and 'afterResponse' are executed in different threads for one 'send' + */ +final class RequestHandler { + + private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); + + static final String OPERATION_NAME = "send"; + + private final Tracer tracer; + + private final SpanContext parentContext; + + public RequestHandler(Tracer tracer) { + this(tracer, null); + } + + public RequestHandler(Tracer tracer, SpanContext parentContext) { + this.tracer = tracer; + this.parentContext = parentContext; + } + + public void beforeRequest(Object request, Context context) { + logger.info("before send {}", request); + + // we cannot use active span because we don't know in which thread it is executed + // and we cannot therefore activate span. thread can come from common thread pool. + SpanBuilder spanBuilder = + tracer + .buildSpan(OPERATION_NAME) + .ignoreActiveSpan() + .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT); + + if (parentContext != null) { + spanBuilder.asChildOf(parentContext); + } + + context.put("span", spanBuilder.start()); + } + + public void afterResponse(Object response, Context context) { + logger.info("after response {}", response); + + Object spanObject = context.get("span"); + if (spanObject instanceof Span) { + Span span = (Span) spanObject; + span.finish(); + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/errorreporting/ErrorReportingTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/errorreporting/ErrorReportingTest.java new file mode 100644 index 000000000..2353d88a1 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/errorreporting/ErrorReportingTest.java @@ -0,0 +1,166 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.errorreporting; + +import static io.opentelemetry.opentracingshim.testbed.TestUtils.finishedSpansSize; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.log.Fields; +import io.opentracing.tag.Tags; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("FutureReturnValueIgnored") +public final class ErrorReportingTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + /* Very simple error handling **/ + @Test + void testSimpleError() { + Span span = tracer.buildSpan("one").start(); + try (Scope scope = tracer.activateSpan(span)) { + throw new RuntimeException("Invalid state"); + } catch (RuntimeException e) { + Tags.ERROR.set(span, true); + } finally { + span.finish(); + } + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(StatusCode.ERROR).isEqualTo(spans.get(0).getStatus().getStatusCode()); + } + + /* Error handling in a callback capturing/activating the Span */ + @Test + void testCallbackError() { + final Span span = tracer.buildSpan("one").start(); + executor.submit( + () -> { + try (Scope scope = tracer.activateSpan(span)) { + throw new RuntimeException("Invalid state"); + } catch (RuntimeException exc) { + Tags.ERROR.set(span, true); + } finally { + span.finish(); + } + }); + + await().atMost(Duration.ofSeconds(5)).until(finishedSpansSize(otelTesting), equalTo(1)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(StatusCode.ERROR).isEqualTo(spans.get(0).getStatus().getStatusCode()); + } + + /* Error handling for a max-retries task (such as url fetching). + * We log the Exception at each retry. */ + @Test + void testErrorRecovery() { + final int maxRetries = 1; + int retries = 0; + + Span span = tracer.buildSpan("one").start(); + try (Scope scope = tracer.activateSpan(span)) { + while (retries++ < maxRetries) { + try { + throw new RuntimeException("No url could be fetched"); + } catch (RuntimeException exc) { + Map errorMap = new HashMap<>(); + errorMap.put(Fields.EVENT, Tags.ERROR.getKey()); + errorMap.put(Fields.ERROR_OBJECT, exc); + span.log(errorMap); + } + } + } + + Tags.ERROR.set(span, true); // Could not fetch anything. + span.finish(); + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(StatusCode.ERROR).isEqualTo(spans.get(0).getStatus().getStatusCode()); + + List events = spans.get(0).getEvents(); + assertThat(events).hasSize(maxRetries); + assertThat("exception").isEqualTo(events.get(0).getName()); + /* TODO: Handle actual objects being passed to log/events. */ + /*assertNotNull(events.get(0).getEvent().getAttributes().get(Fields.ERROR_OBJECT));*/ + } + + /* Error handling for a mocked layer automatically capturing/activating + * the Span for a submitted Runnable. */ + @Test + void testInstrumentationLayer() { + Span span = tracer.buildSpan("one").start(); + try (Scope scope = tracer.activateSpan(span)) { + + // ScopedRunnable captures the active Span at this time. + executor.submit( + new ScopedRunnable( + () -> { + try { + throw new RuntimeException("Invalid state"); + } catch (RuntimeException exc) { + Tags.ERROR.set(tracer.activeSpan(), true); + } finally { + tracer.activeSpan().finish(); + } + }, + tracer)); + } + + await().atMost(Duration.ofSeconds(5)).until(finishedSpansSize(otelTesting), equalTo(1)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(StatusCode.ERROR).isEqualTo(spans.get(0).getStatus().getStatusCode()); + } + + static class ScopedRunnable implements Runnable { + Runnable runnable; + Tracer tracer; + Span span; + + public ScopedRunnable(Runnable runnable, Tracer tracer) { + this.runnable = runnable; + this.tracer = tracer; + this.span = tracer.activeSpan(); + } + + @Override + public void run() { + // No error reporting is done, as we are a simple wrapper. + try (Scope scope = tracer.activateSpan(span)) { + runnable.run(); + } + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/latespanfinish/LateSpanFinishTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/latespanfinish/LateSpanFinishTest.java new file mode 100644 index 000000000..6c39cbc35 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/latespanfinish/LateSpanFinishTest.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.latespanfinish; + +import static io.opentelemetry.opentracingshim.testbed.TestUtils.assertSameTrace; +import static io.opentelemetry.opentracingshim.testbed.TestUtils.sleep; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("FutureReturnValueIgnored") +public final class LateSpanFinishTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Test + void test() throws Exception { + // Create a Span manually and use it as parent of a pair of subtasks + Span parentSpan = tracer.buildSpan("parent").start(); + submitTasks(parentSpan); + + // Wait for the threadpool to be done first, instead of polling/waiting + executor.shutdown(); + executor.awaitTermination(15, TimeUnit.SECONDS); + + // Late-finish the parent Span now + parentSpan.finish(); + + // Children finish order is not guaranteed, but parent should finish *last*. + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(3); + assertThat(spans.get(0).getName()).startsWith("task"); + assertThat(spans.get(0).getName()).startsWith("task"); + assertThat(spans.get(1).getName()).startsWith("task"); + assertThat(spans.get(2).getName()).isEqualTo("parent"); + + assertSameTrace(spans); + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } + + /* + * Fire away a few subtasks, passing a parent Span whose lifetime + * is not tied at-all to the children + */ + private void submitTasks(final Span parentSpan) { + + executor.submit( + () -> { + /* Alternative to calling activate() is to pass it manually to asChildOf() for each + * created Span. */ + try (Scope scope = tracer.scopeManager().activate(parentSpan)) { + Span childSpan = tracer.buildSpan("task1").start(); + try (Scope childScope = tracer.scopeManager().activate(childSpan)) { + sleep(55); + } finally { + childSpan.finish(); + } + } + }); + + executor.submit( + () -> { + try (Scope scope = tracer.scopeManager().activate(parentSpan)) { + Span childSpan = tracer.buildSpan("task2").start(); + try (Scope childScope = tracer.scopeManager().activate(childSpan)) { + sleep(85); + } finally { + childSpan.finish(); + } + } + }); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/Client.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/Client.java new file mode 100644 index 000000000..197962152 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/Client.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.listenerperrequest; + +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +final class Client { + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final Tracer tracer; + + public Client(Tracer tracer) { + this.tracer = tracer; + } + + /** Async execution. */ + private Future execute(final Object message, final ResponseListener responseListener) { + return executor.submit( + () -> { + // send via wire and get response + Object response = message + ":response"; + responseListener.onResponse(response); + return response; + }); + } + + public Future send(final Object message) { + Span span = + tracer.buildSpan("send").withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).start(); + return execute(message, new ResponseListener(span)); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/ListenerTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/ListenerTest.java new file mode 100644 index 000000000..77378bbdc --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/ListenerTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.listenerperrequest; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Tracer; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Each request has own instance of ResponseListener. */ +class ListenerTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + + @Test + void test() throws Exception { + Client client = new Client(tracer); + Object response = client.send("message").get(); + assertThat(response).isEqualTo("message:response"); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(1); + assertThat(CLIENT).isEqualTo(finished.get(0).getKind()); + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/ResponseListener.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/ResponseListener.java new file mode 100644 index 000000000..dea4957c2 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/listenerperrequest/ResponseListener.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.listenerperrequest; + +import io.opentracing.Span; + +/** Response listener per request. Executed in a thread different from 'send' thread */ +final class ResponseListener { + private final Span span; + + public ResponseListener(Span span) { + this.span = span; + } + + /** executed when response is received from server. Any thread. */ + public void onResponse(Object response) { + span.finish(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/multiplecallbacks/Client.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/multiplecallbacks/Client.java new file mode 100644 index 000000000..e5eeccf0c --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/multiplecallbacks/Client.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.multiplecallbacks; + +import io.opentracing.References; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class Client { + private static final Logger logger = LoggerFactory.getLogger(Client.class); + + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final CountDownLatch parentDoneLatch; + private final Tracer tracer; + + public Client(Tracer tracer, CountDownLatch parentDoneLatch) { + this.tracer = tracer; + this.parentDoneLatch = parentDoneLatch; + } + + public Future send(final Object message) { + final SpanContext parentSpanContext = tracer.activeSpan().context(); + + return executor.submit( + () -> { + logger.info("Child thread with message '{}' started", message); + + Span span = + tracer + .buildSpan("subtask") + .addReference(References.FOLLOWS_FROM, parentSpanContext) + .start(); + try (Scope subtaskScope = tracer.activateSpan(span)) { + // Simulate work - make sure we finish *after* the parent Span. + parentDoneLatch.await(); + } finally { + span.finish(); + } + + logger.info("Child thread with message '{}' finished", message); + return message + "::response"; + }); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/multiplecallbacks/MultipleCallbacksTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/multiplecallbacks/MultipleCallbacksTest.java new file mode 100644 index 000000000..08e43060d --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/multiplecallbacks/MultipleCallbacksTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.multiplecallbacks; + +import static io.opentelemetry.opentracingshim.testbed.TestUtils.finishedSpansSize; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * These tests are intended to simulate a task with independent, asynchronous callbacks. + * + *

    For improved readability, ignore the CountDownLatch lines as those are there to ensure + * deterministic execution for the tests without sleeps. + */ +@SuppressWarnings("FutureReturnValueIgnored") +class MultipleCallbacksTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + + @Test + void test() { + CountDownLatch parentDoneLatch = new CountDownLatch(1); + Client client = new Client(tracer, parentDoneLatch); + + Span span = tracer.buildSpan("parent").start(); + try (Scope scope = tracer.activateSpan(span)) { + client.send("task1"); + client.send("task2"); + client.send("task3"); + } finally { + span.finish(); + parentDoneLatch.countDown(); + } + + await().atMost(Duration.ofSeconds(15)).until(finishedSpansSize(otelTesting), equalTo(4)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(4); + assertThat(spans.get(0).getName()).isEqualTo("parent"); + + SpanData parentSpan = spans.get(0); + for (int i = 1; i < 4; i++) { + assertThat(spans.get(i).getTraceId()).isEqualTo(parentSpan.getTraceId()); + assertThat(spans.get(i).getParentSpanId()).isEqualTo(parentSpan.getSpanId()); + } + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/nestedcallbacks/NestedCallbacksTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/nestedcallbacks/NestedCallbacksTest.java new file mode 100644 index 000000000..c12d038bc --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/nestedcallbacks/NestedCallbacksTest.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.nestedcallbacks; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.opentracingshim.testbed.TestUtils.finishedSpansSize; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("FutureReturnValueIgnored") +public final class NestedCallbacksTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Test + void test() { + + Span span = tracer.buildSpan("one").start(); + submitCallbacks(span); + + await().atMost(Duration.ofSeconds(15)).until(finishedSpansSize(otelTesting), equalTo(1)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(spans.get(0).getName()).isEqualTo("one"); + + Attributes attrs = spans.get(0).getAttributes(); + assertThat(attrs.size()).isEqualTo(3); + for (int i = 1; i <= 3; i++) { + assertThat(spans.get(0).getAttributes().get(stringKey("key" + i))) + .isEqualTo(Integer.toString(i)); + } + + assertThat(tracer.scopeManager().activeSpan()).isNull(); + } + + private void submitCallbacks(final Span span) { + + executor.submit( + () -> { + try (Scope scope = tracer.scopeManager().activate(span)) { + span.setTag("key1", "1"); + + executor.submit( + () -> { + try (Scope scope12 = tracer.scopeManager().activate(span)) { + span.setTag("key2", "2"); + + executor.submit( + () -> { + try (Scope scope1 = tracer.scopeManager().activate(span)) { + span.setTag("key3", "3"); + } finally { + span.finish(); + } + }); + } + }); + } + }); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/Promise.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/Promise.java new file mode 100644 index 000000000..35679dae0 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/Promise.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.promisepropagation; + +import io.opentracing.References; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import java.util.ArrayList; +import java.util.Collection; + +final class Promise { + private final PromiseContext context; + private final Tracer tracer; + private final Span parentSpan; + + private final Collection> successCallbacks = new ArrayList<>(); + private final Collection errorCallbacks = new ArrayList<>(); + + public Promise(PromiseContext context, Tracer tracer) { + this.context = context; + + // Passed along here for testing. Normally should be referenced via GlobalTracer.get(). + this.tracer = tracer; + parentSpan = tracer.scopeManager().activeSpan(); + } + + public void onSuccess(SuccessCallback successCallback) { + successCallbacks.add(successCallback); + } + + public void onError(ErrorCallback errorCallback) { + errorCallbacks.add(errorCallback); + } + + @SuppressWarnings("FutureReturnValueIgnored") + public void success(final T result) { + for (final SuccessCallback callback : successCallbacks) { + context.submit( + () -> { + Span childSpan = + tracer + .buildSpan("success") + .addReference(References.FOLLOWS_FROM, parentSpan.context()) + .withTag(Tags.COMPONENT.getKey(), "success") + .start(); + try (Scope childScope = tracer.activateSpan(childSpan)) { + callback.accept(result); + } finally { + childSpan.finish(); + } + context.getPhaser().arriveAndAwaitAdvance(); // trace reported + }); + } + } + + @SuppressWarnings("FutureReturnValueIgnored") + public void error(final Throwable error) { + for (final ErrorCallback callback : errorCallbacks) { + context.submit( + () -> { + Span childSpan = + tracer + .buildSpan("error") + .addReference(References.FOLLOWS_FROM, parentSpan.context()) + .withTag(Tags.COMPONENT.getKey(), "error") + .start(); + try (Scope childScope = tracer.activateSpan(childSpan)) { + callback.accept(error); + } finally { + childSpan.finish(); + } + context.getPhaser().arriveAndAwaitAdvance(); // trace reported + }); + } + } + + public interface SuccessCallback { + void accept(T t); + } + + public interface ErrorCallback { + void accept(Throwable t); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/PromiseContext.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/PromiseContext.java new file mode 100644 index 000000000..81c59638a --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/PromiseContext.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.promisepropagation; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Phaser; + +final class PromiseContext implements AutoCloseable { + private final Phaser phaser; + private final ExecutorService executor; + + public PromiseContext(Phaser phaser, int concurrency) { + this.phaser = phaser; + executor = Executors.newFixedThreadPool(concurrency); + } + + @Override + public void close() { + executor.shutdown(); + } + + public Future submit(Runnable runnable) { + phaser.register(); // register the work to be done on the executor + return executor.submit(runnable); + } + + public Phaser getPhaser() { + return phaser; + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/PromisePropagationTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/PromisePropagationTest.java new file mode 100644 index 000000000..d4a8e1e1d --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/promisepropagation/PromisePropagationTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.promisepropagation; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.opentracingshim.testbed.TestUtils.getByAttr; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import java.util.List; +import java.util.concurrent.Phaser; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * These tests are intended to simulate the kind of async models that are common in java async + * frameworks. + * + *

    For improved readability, ignore the phaser lines as those are there to ensure deterministic + * execution for the tests without sleeps. + */ +class PromisePropagationTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private Phaser phaser; + + @BeforeEach + void before() { + phaser = new Phaser(); + } + + @Test + void testPromiseCallback() { + phaser.register(); // register test thread + final AtomicReference successResult1 = new AtomicReference<>(); + final AtomicReference successResult2 = new AtomicReference<>(); + final AtomicReference errorResult = new AtomicReference<>(); + + try (PromiseContext context = new PromiseContext(phaser, 3)) { + Span parentSpan = + tracer.buildSpan("promises").withTag(Tags.COMPONENT.getKey(), "example-promises").start(); + + try (Scope parentScope = tracer.activateSpan(parentSpan)) { + Promise successPromise = new Promise<>(context, tracer); + + successPromise.onSuccess( + s -> { + tracer.activeSpan().log("Promised 1 " + s); + successResult1.set(s); + phaser.arriveAndAwaitAdvance(); // result set + }); + successPromise.onSuccess( + s -> { + tracer.activeSpan().log("Promised 2 " + s); + successResult2.set(s); + phaser.arriveAndAwaitAdvance(); // result set + }); + + Promise errorPromise = new Promise<>(context, tracer); + + errorPromise.onError( + t -> { + errorResult.set(t); + phaser.arriveAndAwaitAdvance(); // result set + }); + + assertThat(otelTesting.getSpans().size()).isEqualTo(0); + successPromise.success("success!"); + errorPromise.error(new Exception("some error.")); + } finally { + parentSpan.finish(); + } + + phaser.arriveAndAwaitAdvance(); // wait for results to be set + assertThat(successResult1.get()).isEqualTo("success!"); + assertThat(successResult2.get()).isEqualTo("success!"); + assertThat(errorResult.get()).hasMessage("some error."); + + phaser.arriveAndAwaitAdvance(); // wait for traces to be reported + + List finished = otelTesting.getSpans(); + assertThat(finished.size()).isEqualTo(4); + + AttributeKey component = stringKey(Tags.COMPONENT.getKey()); + List spanExamplePromise = getByAttr(finished, component, "example-promises"); + assertThat(spanExamplePromise).hasSize(1); + assertThat(spanExamplePromise.get(0).getParentSpanId()).isEqualTo(SpanId.getInvalid()); + + assertThat(getByAttr(finished, component, "success")).hasSize(2); + + CharSequence parentId = spanExamplePromise.get(0).getSpanId(); + for (SpanData span : getByAttr(finished, component, "success")) { + assertThat(span.getParentSpanId()).isEqualTo(parentId.toString()); + } + + List spanError = getByAttr(finished, component, "error"); + assertThat(spanError).hasSize(1); + assertThat(spanError.get(0).getParentSpanId()).isEqualTo(parentId.toString()); + } + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/Client.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/Client.java new file mode 100644 index 000000000..5f9fb074e --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/Client.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.statelesscommonrequesthandler; + +import io.opentelemetry.opentracingshim.testbed.TestUtils; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class Client { + private static final Logger logger = LoggerFactory.getLogger(Client.class); + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + private final RequestHandler requestHandler; + + public Client(RequestHandler requestHandler) { + this.requestHandler = requestHandler; + } + + /** Send a request....... */ + public Future send(final Object message) { + + return executor.submit( + () -> { + logger.info("send {}", message); + TestUtils.sleep(); + requestHandler.beforeRequest(message); + + TestUtils.sleep(); + requestHandler.afterResponse(message); + + return message + ":response"; + }); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/HandlerTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/HandlerTest.java new file mode 100644 index 000000000..785a6d628 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/HandlerTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.statelesscommonrequesthandler; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Tracer; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * There is only one instance of 'RequestHandler' per 'Client'. Methods of 'RequestHandler' are + * executed in the same thread (beforeRequest() and its resulting afterRequest(), that is). + */ +public final class HandlerTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + private final Client client = new Client(new RequestHandler(tracer)); + + @Test + void test_requests() throws Exception { + Future responseFuture = client.send("message"); + Future responseFuture2 = client.send("message2"); + Future responseFuture3 = client.send("message3"); + + assertThat(responseFuture3.get(5, TimeUnit.SECONDS)).isEqualTo("message3:response"); + assertThat(responseFuture2.get(5, TimeUnit.SECONDS)).isEqualTo("message2:response"); + assertThat(responseFuture.get(5, TimeUnit.SECONDS)).isEqualTo("message:response"); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(3); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/RequestHandler.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/RequestHandler.java new file mode 100644 index 000000000..9bd405d87 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/statelesscommonrequesthandler/RequestHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.statelesscommonrequesthandler; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * One instance per Client. 'beforeRequest' and 'afterResponse' are executed in the same thread for + * one 'send', but as these methods do not expose any object storing state, a thread-local field in + * 'RequestHandler' itself is used to contain the Scope related to Span activation. + */ +final class RequestHandler { + + private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); + + static final String OPERATION_NAME = "send"; + + private final Tracer tracer; + + private static final ThreadLocal tlsScope = new ThreadLocal<>(); + + public RequestHandler(Tracer tracer) { + this.tracer = tracer; + } + + /** beforeRequest handler....... */ + public void beforeRequest(Object request) { + logger.info("before send {}", request); + + Span span = tracer.buildSpan(OPERATION_NAME).start(); + tlsScope.set(tracer.activateSpan(span)); + } + + /** afterResponse handler....... */ + public void afterResponse(Object response) { + logger.info("after response {}", response); + + // Finish the Span + tracer.scopeManager().activeSpan().finish(); + + // Deactivate the Span + tlsScope.get().close(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/suspendresumepropagation/SuspendResume.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/suspendresumepropagation/SuspendResume.java new file mode 100644 index 000000000..9e9235eb6 --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/suspendresumepropagation/SuspendResume.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.suspendresumepropagation; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; + +final class SuspendResume { + private final Tracer tracer; + private final Span span; + + public SuspendResume(int id, Tracer tracer) { + // Passed along here for testing. Normally should be referenced via GlobalTracer.get(). + this.tracer = tracer; + + Span span = + tracer.buildSpan("job " + id).withTag(Tags.COMPONENT.getKey(), "suspend-resume").start(); + try (Scope scope = tracer.scopeManager().activate(span)) { + this.span = span; + } + } + + public void doPart(String name) { + try (Scope scope = tracer.scopeManager().activate(span)) { + span.log("part: " + name); + } + } + + public void done() { + span.finish(); + } +} diff --git a/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/suspendresumepropagation/SuspendResumePropagationTest.java b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/suspendresumepropagation/SuspendResumePropagationTest.java new file mode 100644 index 000000000..16221ea7b --- /dev/null +++ b/opentelemetry-java/opentracing-shim/src/test/java/io/opentelemetry/opentracingshim/testbed/suspendresumepropagation/SuspendResumePropagationTest.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.opentracingshim.testbed.suspendresumepropagation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentracing.Tracer; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * These tests are intended to simulate the kind of async models that are common in java async + * frameworks. + */ +class SuspendResumePropagationTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = OpenTracingShim.createTracerShim(otelTesting.getOpenTelemetry()); + + @BeforeEach + void before() {} + + @Test + void testContinuationInterleaving() { + SuspendResume job1 = new SuspendResume(1, tracer); + SuspendResume job2 = new SuspendResume(2, tracer); + + // Pretend that the framework is controlling actual execution here. + job1.doPart("some work for 1"); + job2.doPart("some work for 2"); + job1.doPart("other work for 1"); + job2.doPart("other work for 2"); + job2.doPart("more work for 2"); + job1.doPart("more work for 1"); + + job1.done(); + job2.done(); + + List finished = otelTesting.getSpans(); + assertThat(finished.size()).isEqualTo(2); + + assertThat(finished.get(0).getName()).isEqualTo("job 1"); + assertThat(finished.get(1).getName()).isEqualTo("job 2"); + + assertThat(SpanId.isValid(finished.get(0).getParentSpanId())).isFalse(); + assertThat(SpanId.isValid(finished.get(1).getParentSpanId())).isFalse(); + } +} diff --git a/opentelemetry-java/perf-harness/build.gradle.kts b/opentelemetry-java/perf-harness/build.gradle.kts new file mode 100644 index 000000000..186775beb --- /dev/null +++ b/opentelemetry-java/perf-harness/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + java +} + +description = "Performance Testing Harness" +extra["moduleName"] = "io.opentelemetry.perf-harness" + +dependencies { + implementation(project(":api:all")) + implementation(project(":sdk:all")) + implementation(project(":sdk:testing")) + implementation(project(":exporters:otlp:trace")) + implementation(project(":exporters:logging")) + implementation(project(":semconv")) + + implementation("eu.rekawek.toxiproxy:toxiproxy-java") + implementation("org.testcontainers:junit-jupiter") + + runtimeOnly("io.grpc:grpc-netty-shaded") +} diff --git a/opentelemetry-java/perf-harness/src/test/java/io/opentelemetry/perf/OtlpPipelineStressTest.java b/opentelemetry-java/perf-harness/src/test/java/io/opentelemetry/perf/OtlpPipelineStressTest.java new file mode 100644 index 000000000..8748e73a0 --- /dev/null +++ b/opentelemetry-java/perf-harness/src/test/java/io/opentelemetry/perf/OtlpPipelineStressTest.java @@ -0,0 +1,285 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.perf; + +import eu.rekawek.toxiproxy.Proxy; +import eu.rekawek.toxiproxy.ToxiproxyClient; +import eu.rekawek.toxiproxy.model.ToxicDirection; +import eu.rekawek.toxiproxy.model.ToxicList; +import eu.rekawek.toxiproxy.model.toxic.Timeout; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import io.opentelemetry.sdk.metrics.testing.InMemoryMetricExporter; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +@Testcontainers(disabledWithoutDocker = true) +@SuppressWarnings({"FutureReturnValueIgnored", "CatchAndPrintStackTrace"}) +public class OtlpPipelineStressTest { + + public static final int OTLP_RECEIVER_PORT = 4317; + public static final int COLLECTOR_PROXY_PORT = 44444; + public static final int TOXIPROXY_CONTROL_PORT = 8474; + public static Network network = Network.newNetwork(); + public static AtomicLong totalSpansReceivedByCollector = new AtomicLong(); + + private static final Logger logger = LoggerFactory.getLogger(OtlpPipelineStressTest.class); + + @Container + public static GenericContainer collectorContainer = + new GenericContainer<>( + DockerImageName.parse( + "ghcr.io/open-telemetry/java-test-containers:otel-collector-dev")) + .withNetwork(network) + .withNetworkAliases("otel-collector") + .withExposedPorts(OTLP_RECEIVER_PORT) + .withCommand("--config=/etc/otel-collector-config-perf.yaml") + .withCopyFileToContainer( + MountableFile.forClasspathResource("otel-collector-config-perf.yaml"), + "/etc/otel-collector-config-perf.yaml") + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("otel-collector"))) + .withLogConsumer( + outputFrame -> { + String logline = outputFrame.getUtf8String(); + String spanExportPrefix = "TracesExporter\t{\"#spans\": "; + int start = logline.indexOf(spanExportPrefix); + int end = logline.indexOf("}"); + if (start > 0) { + String substring = logline.substring(start + spanExportPrefix.length(), end); + totalSpansReceivedByCollector.addAndGet(Long.parseLong(substring)); + } + }) + .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Everything is ready.*")); + + @Container + public static GenericContainer toxiproxyContainer = + new GenericContainer<>( + DockerImageName.parse("ghcr.io/open-telemetry/java-test-containers:toxiproxy")) + .withNetwork(network) + .withNetworkAliases("toxiproxy") + .withExposedPorts(TOXIPROXY_CONTROL_PORT, COLLECTOR_PROXY_PORT) + .dependsOn(collectorContainer) + // .withLogConsumer(outputFrame -> System.out.print(outputFrame.getUtf8String())) + .waitingFor(new LogMessageWaitStrategy().withRegEx(".*API HTTP server starting.*")); + + private final InMemoryMetricExporter metricExporter = InMemoryMetricExporter.create(); + + private SdkTracerProvider sdkTracerProvider; + private OpenTelemetry openTelemetry; + private IntervalMetricReader intervalMetricReader; + private Proxy collectorProxy; + private ToxiproxyClient toxiproxyClient; + + @BeforeEach + void setUp() throws IOException { + toxiproxyClient = + new ToxiproxyClient( + toxiproxyContainer.getHost(), toxiproxyContainer.getMappedPort(TOXIPROXY_CONTROL_PORT)); + toxiproxyClient.reset(); + collectorProxy = toxiproxyClient.getProxyOrNull("collector"); + + if (collectorProxy == null) { + collectorProxy = + toxiproxyClient.createProxy( + "collector", + "0.0.0.0:" + COLLECTOR_PROXY_PORT, + "otel-collector" + ":" + OTLP_RECEIVER_PORT); + } + collectorProxy.enable(); + + setupSdk(); + } + + @AfterEach + void tearDown() throws IOException { + intervalMetricReader.shutdown(); + sdkTracerProvider.shutdown(); + + toxiproxyClient.reset(); + collectorProxy.delete(); + logger.info("totalSpansReceivedByCollector = {}", totalSpansReceivedByCollector); + } + + @Test + @Disabled("we don't want to run this with every build.") + void oltpExportWithFlakyCollector() throws IOException, InterruptedException { + ToxicList toxics = collectorProxy.toxics(); + // Latency latency = toxics.latency("jittery_latency", ToxicDirection.UPSTREAM, 500); + // latency.setJitter(1000); + // latency.setToxicity(0.4f); + // for (Toxic toxic : toxiproxyClient.getProxy("collector").toxics().getAll()) { + // System.out.println("toxic = " + toxic.getName()); + // } + + // warm up with a fixed 1000 spans + runOnce(1000, 0); + Thread.sleep(2000); + metricExporter.reset(); + + // spawn threads that will each run for an interval of time + int numberOfThreads = 8; + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(1); + for (int i = 0; i < numberOfThreads; i++) { + executorService.submit( + () -> { + try { + latch.await(); + runOnce(null, 30_000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted", e); + } + }); + } + latch.countDown(); + + // timeout the connection after 5s, then allow reconnecting + Thread.sleep(5000); + Timeout timeout = toxics.timeout("timeout_connection", ToxicDirection.UPSTREAM, 1000); + // wait a second before allowing new connections + Thread.sleep(1000); + timeout.remove(); + + executorService.shutdown(); + executorService.awaitTermination(1, TimeUnit.MINUTES); + + Thread.sleep(10000); + List finishedMetricItems = metricExporter.getFinishedMetricItems(); + intervalMetricReader.shutdown(); + Thread.sleep(1000); + reportMetrics(finishedMetricItems); + Thread.sleep(10000); + } + + private static void reportMetrics(List finishedMetricItems) { + Map> metricsByName = + finishedMetricItems.stream().collect(Collectors.groupingBy(MetricData::getName)); + metricsByName.forEach( + (name, metricData) -> { + Stream longPointStream = + metricData.stream().flatMap(md -> md.getLongSumData().getPoints().stream()); + Map> pointsByLabelset = + longPointStream.collect(Collectors.groupingBy(PointData::getLabels)); + pointsByLabelset.forEach( + (labels, longPoints) -> { + long total = longPoints.get(longPoints.size() - 1).getValue(); + logger.info("{} : {} : {}", name, labels, total); + }); + }); + } + + private void runOnce(Integer numberOfSpans, int numberOfMillisToRunFor) + throws InterruptedException { + Tracer tracer = openTelemetry.getTracer("io.opentelemetry.perf"); + long start = System.currentTimeMillis(); + int i = 0; + while (numberOfSpans == null + ? System.currentTimeMillis() - start < numberOfMillisToRunFor + : i < numberOfSpans) { + // for (int i = 0; i < 10000; i++) { + Span exampleSpan = tracer.spanBuilder("exampleSpan").startSpan(); + try (Scope scope = exampleSpan.makeCurrent()) { + exampleSpan.setAttribute("exampleNumber", i++); + exampleSpan.setAttribute("attribute0", "attvalue-0"); + exampleSpan.setAttribute("attribute1", "attvalue-1"); + exampleSpan.setAttribute("attribute2", "attvalue-2"); + exampleSpan.setAttribute("attribute3", "attvalue-3"); + exampleSpan.setAttribute("attribute4", "attvalue-4"); + exampleSpan.setAttribute("attribute5", "attvalue-5"); + exampleSpan.setAttribute("attribute6", "attvalue-6"); + exampleSpan.setAttribute("attribute7", "attvalue-7"); + exampleSpan.setAttribute("attribute8", "attvalue-8"); + exampleSpan.setAttribute("attribute9", "attvalue-9"); + exampleSpan.addEvent("pre-sleep"); + Thread.sleep(1); + } finally { + exampleSpan.end(); + } + } + } + + private void setupSdk() { + Resource resource = + Resource.create( + Attributes.builder() + .put(ResourceAttributes.SERVICE_NAME, "PerfTester") + .put(ResourceAttributes.SERVICE_VERSION, "1.0.1-RC-1") + .build()); + + // set up the metric exporter and wire it into the SDK and a timed reader. + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().setResource(resource).buildAndRegisterGlobal(); + + intervalMetricReader = + IntervalMetricReader.builder() + .setMetricExporter(metricExporter) + .setMetricProducers(Collections.singleton(meterProvider)) + .setExportIntervalMillis(1000) + .buildAndStart(); + + // set up the span exporter and wire it into the SDK + OtlpGrpcSpanExporter spanExporter = + OtlpGrpcSpanExporter.builder() + .setEndpoint( + "http://" + + toxiproxyContainer.getHost() + + ":" + + toxiproxyContainer.getMappedPort(COLLECTOR_PROXY_PORT)) + // .setDeadlineMs(1000) + .build(); + BatchSpanProcessor spanProcessor = + BatchSpanProcessor.builder(spanExporter) + // .setMaxQueueSize(1000) + // .setMaxExportBatchSize(1024) + // .setScheduleDelayMillis(1000) + .build(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); + openTelemetry = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + sdkTracerProvider = tracerProvider; + } +} diff --git a/opentelemetry-java/perf-harness/src/test/resources/otel-collector-config-perf.yaml b/opentelemetry-java/perf-harness/src/test/resources/otel-collector-config-perf.yaml new file mode 100644 index 000000000..0aa39a4d9 --- /dev/null +++ b/opentelemetry-java/perf-harness/src/test/resources/otel-collector-config-perf.yaml @@ -0,0 +1,32 @@ +receivers: + otlp: + protocols: + grpc: + +exporters: + logging: + loglevel: info + sampling_initial: 1 + sampling_thereafter: 1 + +processors: + batch: + +extensions: + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 + +service: + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [logging] diff --git a/opentelemetry-java/proto/README.md b/opentelemetry-java/proto/README.md new file mode 100644 index 000000000..2d8c54342 --- /dev/null +++ b/opentelemetry-java/proto/README.md @@ -0,0 +1,6 @@ +# OpenTelemetry - Protobuf messages + +[![Javadocs][javadoc-image]][javadoc-url] + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-proto.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-proto \ No newline at end of file diff --git a/opentelemetry-java/proto/build.gradle.kts b/opentelemetry-java/proto/build.gradle.kts new file mode 100644 index 000000000..3bb6c592c --- /dev/null +++ b/opentelemetry-java/proto/build.gradle.kts @@ -0,0 +1,70 @@ +import de.undercouch.gradle.tasks.download.Download +import de.undercouch.gradle.tasks.download.Verify + +plugins { + id("java-library") + id("maven-publish") + + id("com.google.protobuf") + id("de.undercouch.download") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Proto" +extra["moduleName"] = "io.opentelemetry.proto" + +dependencies { + api("com.google.protobuf:protobuf-java") + api("io.grpc:grpc-api") + api("io.grpc:grpc-protobuf") + api("io.grpc:grpc-stub") +} + +val protoVersion = "0.9.0" +// To generate checksum, download the file and run "shasum -a 256 ~/path/to/vfoo.zip" +val protoChecksum = "5e4131064e9471eb09294374db0d55028fdb73898b08aa07a835d17d61e5f017" +val protoArchive = file("$buildDir/archives/opentelemetry-proto-${protoVersion}.zip") + +tasks { + val downloadProtoArchive by registering(Download::class) { + onlyIf { !protoArchive.exists() } + src("https://github.com/open-telemetry/opentelemetry-proto/archive/v${protoVersion}.zip") + dest(protoArchive) + } + + val verifyProtoArchive by registering(Verify::class) { + dependsOn(downloadProtoArchive) + src(protoArchive) + algorithm("SHA-256") + checksum(protoChecksum) + } + + val unzipProtoArchive by registering(Copy::class) { + dependsOn(verifyProtoArchive) + from(zipTree(protoArchive)) + into("$buildDir/protos") + } + + afterEvaluate { + named("generateProto") { + dependsOn(unzipProtoArchive) + } + } +} + +sourceSets { + main { + proto { + srcDir("$buildDir/protos/opentelemetry-proto-${protoVersion}") + } + } +} + +// IntelliJ complains that the generated classes are not found, ask IntelliJ to include the +// generated Java directories as source folders. +idea { + module { + sourceDirs.add(file("build/generated/source/proto/main/java")) + // If you have additional sourceSets and/or codegen plugins, add all of them + } +} diff --git a/opentelemetry-java/proto/gradle.properties b/opentelemetry-java/proto/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/proto/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/sdk-extensions/async-processor/README.md b/opentelemetry-java/sdk-extensions/async-processor/README.md new file mode 100644 index 000000000..b76920ce2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/async-processor/README.md @@ -0,0 +1,10 @@ +# OpenTelemetry SDK Contrib Async Processor + +[![Javadocs][javadoc-image]][javadoc-url] + +An implementation of the trace `SpanProcessors` that uses +[Disruptor](https://github.com/LMAX-Exchange/disruptor) to make all the `SpanProcessors` hooks run +async. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-sdk-contrib-async-processor.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-contrib-async-processor \ No newline at end of file diff --git a/opentelemetry-java/sdk-extensions/async-processor/build.gradle.kts b/opentelemetry-java/sdk-extensions/async-processor/build.gradle.kts new file mode 100644 index 000000000..ae7d6a755 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/async-processor/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry SDK Extension: Async SpanProcessor" +extra["moduleName"] = "io.opentelemetry.sdk.extension.trace.export" + +dependencies { + api(project(":api:all")) + api(project(":sdk:all")) + + implementation("com.google.guava:guava") + implementation("com.lmax:disruptor") +} diff --git a/opentelemetry-java/sdk-extensions/async-processor/gradle.properties b/opentelemetry-java/sdk-extensions/async-processor/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/async-processor/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessor.java b/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessor.java new file mode 100644 index 000000000..e3640bc29 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessor.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.export; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import java.util.Objects; +import javax.annotation.concurrent.ThreadSafe; + +/** + * A {@link SpanProcessor} implementation that uses {@code Disruptor} to execute all the hooks on an + * async thread. + */ +@ThreadSafe +public final class DisruptorAsyncSpanProcessor implements SpanProcessor { + + private final DisruptorEventQueue disruptorEventQueue; + private final boolean startRequired; + private final boolean endRequired; + + // TODO: Add metrics for dropped spans. + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + if (!startRequired) { + return; + } + disruptorEventQueue.enqueueStartEvent(span, parentContext); + } + + @Override + public boolean isStartRequired() { + return startRequired; + } + + @Override + public void onEnd(ReadableSpan span) { + if (!endRequired) { + return; + } + disruptorEventQueue.enqueueEndEvent(span); + } + + @Override + public boolean isEndRequired() { + return endRequired; + } + + @Override + public CompletableResultCode shutdown() { + return disruptorEventQueue.shutdown(); + } + + @Override + public CompletableResultCode forceFlush() { + return disruptorEventQueue.forceFlush(); + } + + /** + * Returns a new Builder for {@link DisruptorAsyncSpanProcessor}. + * + * @param spanProcessor the {@code List} to where the Span's events are pushed. + * @return a new {@link DisruptorAsyncSpanProcessor}. + * @throws NullPointerException if the {@code spanProcessor} is {@code null}. + */ + public static DisruptorAsyncSpanProcessorBuilder builder(SpanProcessor spanProcessor) { + return new DisruptorAsyncSpanProcessorBuilder(Objects.requireNonNull(spanProcessor)); + } + + DisruptorAsyncSpanProcessor( + DisruptorEventQueue disruptorEventQueue, boolean startRequired, boolean endRequired) { + this.disruptorEventQueue = disruptorEventQueue; + this.startRequired = startRequired; + this.endRequired = endRequired; + } +} diff --git a/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessorBuilder.java b/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessorBuilder.java new file mode 100644 index 000000000..d8e4c3661 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessorBuilder.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.export; + +import com.lmax.disruptor.SleepingWaitStrategy; +import com.lmax.disruptor.WaitStrategy; +import io.opentelemetry.sdk.trace.SpanProcessor; + +/** Builder for {@link DisruptorAsyncSpanProcessor}. */ +public final class DisruptorAsyncSpanProcessorBuilder { + + // 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. + private static final int DEFAULT_DISRUPTOR_BUFFER_SIZE = 8192; + // The default value of the Disruptor behavior, blocks when no space available. + private static final boolean DEFAULT_BLOCKING = true; + // The default number of retries for the SleepingWaitingStrategy. + private static final int DEFAULT_NUM_RETRIES = 0; + // The default waiting time in ns for the SleepingWaitingStrategy. + private static final long DEFAULT_SLEEPING_TIME_NS = 1000 * 1000L; + + private final SpanProcessor spanProcessor; + private int bufferSize = DEFAULT_DISRUPTOR_BUFFER_SIZE; + private boolean blocking = DEFAULT_BLOCKING; + private WaitStrategy waitStrategy = + new SleepingWaitStrategy(DEFAULT_NUM_RETRIES, DEFAULT_SLEEPING_TIME_NS); + + DisruptorAsyncSpanProcessorBuilder(SpanProcessor spanProcessor) { + this.spanProcessor = spanProcessor; + } + + /** + * If {@code true} blocks when the Disruptor's ring buffer is full. + * + * @param blocking {@code true} blocks when the Disruptor's ring buffer is full. + * @return this. + */ + public DisruptorAsyncSpanProcessorBuilder setBlocking(boolean blocking) { + this.blocking = blocking; + return this; + } + + /** + * Sets the buffer size for the Disruptor's ring buffer. + * + * @param bufferSize the buffer size for the Disruptor ring buffer. + * @return this. + */ + public DisruptorAsyncSpanProcessorBuilder setBufferSize(int bufferSize) { + if (bufferSize <= 0) { + throw new IllegalArgumentException("bufferSize must be positive"); + } + this.bufferSize = bufferSize; + return this; + } + + /** + * Sets the {@code WaitStrategy} for the Disruptor's worker thread. + * + * @param waitingStrategy the {@code WaitStrategy} for the Disruptor's worker thread. + * @return this. + */ + public DisruptorAsyncSpanProcessorBuilder setWaitingStrategy(WaitStrategy waitingStrategy) { + this.waitStrategy = waitingStrategy; + return this; + } + + /** + * Returns a new {@link DisruptorAsyncSpanProcessor}. + * + * @return a new {@link DisruptorAsyncSpanProcessor}. + */ + public DisruptorAsyncSpanProcessor build() { + return new DisruptorAsyncSpanProcessor( + new DisruptorEventQueue(bufferSize, waitStrategy, spanProcessor, blocking), + spanProcessor.isStartRequired(), + spanProcessor.isEndRequired()); + } +} diff --git a/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorEventQueue.java b/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorEventQueue.java new file mode 100644 index 000000000..5d1047d08 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/DisruptorEventQueue.java @@ -0,0 +1,243 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.export; + +import com.lmax.disruptor.EventFactory; +import com.lmax.disruptor.EventHandler; +import com.lmax.disruptor.EventTranslatorThreeArg; +import com.lmax.disruptor.RingBuffer; +import com.lmax.disruptor.WaitStrategy; +import com.lmax.disruptor.dsl.Disruptor; +import com.lmax.disruptor.dsl.ProducerType; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import java.util.AbstractMap; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.concurrent.atomic.AtomicBoolean; +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. + */ +@ThreadSafe +final class DisruptorEventQueue { + private static final Logger logger = Logger.getLogger(DisruptorEventQueue.class.getName()); + private static final String WORKER_THREAD_NAME = "DisruptorEventQueue_WorkerThread"; + + private static final EventTranslatorThreeArg< + DisruptorEvent, EventType, Object, CompletableResultCode> + TRANSLATOR_THREE_ARG = + new EventTranslatorThreeArg() { + @Override + public void translateTo( + DisruptorEvent event, + long sequence, + EventType eventType, + Object span, + CompletableResultCode result) { + event.setEntry(eventType, span, result); + } + }; + private static final EventFactory EVENT_FACTORY = + new EventFactory() { + @Override + public DisruptorEvent newInstance() { + return new DisruptorEvent(); + } + }; + + private final RingBuffer ringBuffer; + private final AtomicBoolean loggedShutdownMessage = new AtomicBoolean(false); + private volatile boolean isShutdown = false; + private final boolean blocking; + + private enum EventType { + ON_START, + ON_END, + ON_SHUTDOWN, + ON_FORCE_FLUSH + } + + // Creates a new EventQueue. Private to prevent creation of non-singleton instance. + DisruptorEventQueue( + int bufferSize, WaitStrategy waitStrategy, SpanProcessor spanProcessor, boolean blocking) { + // 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 disruptor = + new Disruptor<>( + EVENT_FACTORY, + bufferSize, + new DaemonThreadFactory(WORKER_THREAD_NAME), + ProducerType.MULTI, + waitStrategy); + disruptor.handleEventsWith(new DisruptorEventHandler(spanProcessor)); + this.ringBuffer = disruptor.start(); + this.blocking = blocking; + } + + void enqueueStartEvent(ReadWriteSpan span, Context parentContext) { + if (isShutdown) { + if (!loggedShutdownMessage.getAndSet(true)) { + logger.info("Attempted to enqueue start event after Disruptor shutdown."); + } + return; + } + enqueue(EventType.ON_START, new AbstractMap.SimpleImmutableEntry<>(span, parentContext), null); + } + + void enqueueEndEvent(ReadableSpan span) { + if (isShutdown) { + if (!loggedShutdownMessage.getAndSet(true)) { + logger.info("Attempted to enqueue end event after Disruptor shutdown."); + } + return; + } + enqueue(EventType.ON_END, span, null); + } + + // Shuts down the underlying disruptor. Ensures that when this method returns the disruptor is + // shutdown. + CompletableResultCode shutdown() { + synchronized (this) { + if (isShutdown) { + // Race condition between two calls to shutdown. The other call already finished. + return CompletableResultCode.ofSuccess(); + } + isShutdown = true; + return enqueueWithResult(EventType.ON_SHUTDOWN); + } + } + + // Force to publish the ended spans to the SpanProcessor + CompletableResultCode forceFlush() { + if (isShutdown) { + if (!loggedShutdownMessage.getAndSet(true)) { + logger.info("Attempted to flush after Disruptor shutdown."); + } + return CompletableResultCode.ofFailure(); + } + return enqueueWithResult(EventType.ON_FORCE_FLUSH); + } + + private CompletableResultCode enqueueWithResult(EventType event) { + CompletableResultCode result = new CompletableResultCode(); + enqueue(event, null, result); + return result; + } + + // Enqueues an event on the {@link DisruptorEventQueue}. + private void enqueue(EventType eventType, Object span, CompletableResultCode result) { + if (blocking) { + ringBuffer.publishEvent(TRANSLATOR_THREE_ARG, eventType, span, result); + } else { + // TODO: Record metrics if element not added. + ringBuffer.tryPublishEvent(TRANSLATOR_THREE_ARG, eventType, span, result); + } + } + + // An event in the {@link EventQueue}. Just holds a reference to an EventQueue.Entry. + private static final class DisruptorEvent { + @Nullable private Object eventArgs = null; + @Nullable private EventType eventType = null; + @Nullable private CompletableResultCode result = null; + + void setEntry( + @Nullable EventType eventType, + @Nullable Object span, + @Nullable CompletableResultCode result) { + this.eventArgs = span; + this.eventType = eventType; + this.result = result; + } + + @Nullable + Object getEventArgs() { + return eventArgs; + } + + @Nullable + EventType getEventType() { + return eventType; + } + + void succeed() { + if (result != null) { + result.succeed(); + } + } + + void fail() { + if (result != null) { + result.fail(); + } + } + } + + private static final class DisruptorEventHandler implements EventHandler { + private final SpanProcessor spanProcessor; + + private DisruptorEventHandler(SpanProcessor spanProcessor) { + this.spanProcessor = spanProcessor; + } + + @Override + public void onEvent(final DisruptorEvent event, long sequence, boolean endOfBatch) { + final Object readableSpan = event.getEventArgs(); + final EventType eventType = event.getEventType(); + if (eventType == null) { + logger.warning("Disruptor enqueued null element type."); + return; + } + try { + switch (eventType) { + case ON_START: + @SuppressWarnings("unchecked") + final SimpleImmutableEntry eventArgs = + (SimpleImmutableEntry) readableSpan; + spanProcessor.onStart(eventArgs.getValue(), eventArgs.getKey()); + break; + case ON_END: + spanProcessor.onEnd((ReadableSpan) readableSpan); + break; + case ON_SHUTDOWN: + propagateResult(spanProcessor.shutdown(), event); + break; + case ON_FORCE_FLUSH: + propagateResult(spanProcessor.forceFlush(), event); + + break; + } + } finally { + // Remove the reference to the previous entry to allow the memory to be gc'ed. + event.setEntry(null, null, null); + } + } + } + + private static void propagateResult( + final CompletableResultCode result, final DisruptorEvent event) { + result.whenComplete( + new Runnable() { + @Override + public void run() { + if (result.isSuccess()) { + event.succeed(); + } else { + event.fail(); + } + } + }); + } +} diff --git a/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/package-info.java b/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/package-info.java new file mode 100644 index 000000000..c5c2871eb --- /dev/null +++ b/opentelemetry-java/sdk-extensions/async-processor/src/main/java/io/opentelemetry/sdk/extension/trace/export/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * An exporter that uses the LMAX Disruptor + * for processing exported spans. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.trace.export; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/async-processor/src/test/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessorTest.java b/opentelemetry-java/sdk-extensions/async-processor/src/test/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessorTest.java new file mode 100644 index 000000000..fbc2de155 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/async-processor/src/test/java/io/opentelemetry/sdk/extension/trace/export/DisruptorAsyncSpanProcessorTest.java @@ -0,0 +1,259 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.export; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Unit tests for {@link DisruptorAsyncSpanProcessor}. */ +@ExtendWith(MockitoExtension.class) +class DisruptorAsyncSpanProcessorTest { + private static final boolean REQUIRED = true; + private static final boolean NOT_REQUIRED = false; + + @Mock private ReadableSpan readableSpan; + @Mock private ReadWriteSpan readWriteSpan; + + // EventQueueEntry for incrementing a Counter. + private static class IncrementSpanProcessor implements SpanProcessor { + private final AtomicInteger counterOnStart = new AtomicInteger(0); + private final AtomicInteger counterOnEnd = new AtomicInteger(0); + private final AtomicInteger counterEndSpans = new AtomicInteger(0); + private final AtomicInteger counterOnShutdown = new AtomicInteger(0); + private final AtomicInteger counterOnForceFlush = new AtomicInteger(0); + private final AtomicInteger counterOnExportedForceFlushSpans = new AtomicInteger(0); + private final AtomicInteger deltaExportedForceFlushSpans = new AtomicInteger(0); + private final boolean startRequired; + private final boolean endRequired; + + private IncrementSpanProcessor(boolean startRequired, boolean endRequired) { + this.startRequired = startRequired; + this.endRequired = endRequired; + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + counterOnStart.incrementAndGet(); + } + + @Override + public boolean isStartRequired() { + return startRequired; + } + + @Override + public void onEnd(ReadableSpan span) { + counterOnEnd.incrementAndGet(); + counterEndSpans.incrementAndGet(); + } + + @Override + public boolean isEndRequired() { + return endRequired; + } + + @Override + public CompletableResultCode shutdown() { + counterOnShutdown.incrementAndGet(); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode forceFlush() { + counterOnForceFlush.incrementAndGet(); + deltaExportedForceFlushSpans.set(counterEndSpans.getAndSet(0)); + counterOnExportedForceFlushSpans.addAndGet(deltaExportedForceFlushSpans.get()); + return CompletableResultCode.ofSuccess(); + } + + private int getCounterOnStart() { + return counterOnStart.get(); + } + + private int getCounterOnEnd() { + return counterOnEnd.get(); + } + + private int getCounterOnShutdown() { + return counterOnShutdown.get(); + } + + private int getCounterOnForceFlush() { + return counterOnForceFlush.get(); + } + + public int getCounterOnExportedForceFlushSpans() { + return counterOnExportedForceFlushSpans.get(); + } + + public int getDeltaExportedForceFlushSpans() { + return deltaExportedForceFlushSpans.get(); + } + } + + @Test + void incrementOnce() { + IncrementSpanProcessor incrementSpanProcessor = new IncrementSpanProcessor(REQUIRED, REQUIRED); + DisruptorAsyncSpanProcessor disruptorAsyncSpanProcessor = + DisruptorAsyncSpanProcessor.builder(incrementSpanProcessor).build(); + assertThat(disruptorAsyncSpanProcessor.isStartRequired()).isTrue(); + assertThat(disruptorAsyncSpanProcessor.isEndRequired()).isTrue(); + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(0); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(0); + disruptorAsyncSpanProcessor.onStart(Context.root(), readWriteSpan); + disruptorAsyncSpanProcessor.onEnd(readableSpan); + disruptorAsyncSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(1); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(1); + assertThat(incrementSpanProcessor.getCounterOnForceFlush()).isEqualTo(1); + assertThat(incrementSpanProcessor.getCounterOnShutdown()).isEqualTo(1); + } + + @Test + void incrementOnce_NoStart() { + IncrementSpanProcessor incrementSpanProcessor = + new IncrementSpanProcessor(NOT_REQUIRED, REQUIRED); + DisruptorAsyncSpanProcessor disruptorAsyncSpanProcessor = + DisruptorAsyncSpanProcessor.builder(incrementSpanProcessor).build(); + assertThat(disruptorAsyncSpanProcessor.isStartRequired()).isFalse(); + assertThat(disruptorAsyncSpanProcessor.isEndRequired()).isTrue(); + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(0); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(0); + disruptorAsyncSpanProcessor.onStart(Context.root(), readWriteSpan); + disruptorAsyncSpanProcessor.onEnd(readableSpan); + disruptorAsyncSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(0); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(1); + assertThat(incrementSpanProcessor.getCounterOnForceFlush()).isEqualTo(1); + assertThat(incrementSpanProcessor.getCounterOnShutdown()).isEqualTo(1); + } + + @Test + void incrementOnce_NoEnd() { + IncrementSpanProcessor incrementSpanProcessor = + new IncrementSpanProcessor(REQUIRED, NOT_REQUIRED); + DisruptorAsyncSpanProcessor disruptorAsyncSpanProcessor = + DisruptorAsyncSpanProcessor.builder(incrementSpanProcessor).build(); + assertThat(disruptorAsyncSpanProcessor.isStartRequired()).isTrue(); + assertThat(disruptorAsyncSpanProcessor.isEndRequired()).isFalse(); + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(0); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(0); + disruptorAsyncSpanProcessor.onStart(Context.root(), readWriteSpan); + disruptorAsyncSpanProcessor.onEnd(readableSpan); + disruptorAsyncSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(1); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(0); + assertThat(incrementSpanProcessor.getCounterOnForceFlush()).isEqualTo(1); + assertThat(incrementSpanProcessor.getCounterOnShutdown()).isEqualTo(1); + } + + @Test + void shutdownIsCalledOnlyOnce() { + IncrementSpanProcessor incrementSpanProcessor = new IncrementSpanProcessor(REQUIRED, REQUIRED); + DisruptorAsyncSpanProcessor disruptorAsyncSpanProcessor = + DisruptorAsyncSpanProcessor.builder(incrementSpanProcessor).build(); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getCounterOnShutdown()).isEqualTo(1); + } + + @Test + void incrementAfterShutdown() { + IncrementSpanProcessor incrementSpanProcessor = new IncrementSpanProcessor(REQUIRED, REQUIRED); + DisruptorAsyncSpanProcessor disruptorAsyncSpanProcessor = + DisruptorAsyncSpanProcessor.builder(incrementSpanProcessor).build(); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + disruptorAsyncSpanProcessor.onStart(Context.root(), readWriteSpan); + disruptorAsyncSpanProcessor.onEnd(readableSpan); + disruptorAsyncSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(0); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(0); + assertThat(incrementSpanProcessor.getCounterOnForceFlush()).isEqualTo(0); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getCounterOnShutdown()).isEqualTo(1); + } + + @Test + void incrementTenK() { + final int tenK = 10000; + IncrementSpanProcessor incrementSpanProcessor = new IncrementSpanProcessor(REQUIRED, REQUIRED); + DisruptorAsyncSpanProcessor disruptorAsyncSpanProcessor = + DisruptorAsyncSpanProcessor.builder(incrementSpanProcessor).build(); + for (int i = 1; i <= tenK; i++) { + disruptorAsyncSpanProcessor.onStart(Context.root(), readWriteSpan); + disruptorAsyncSpanProcessor.onEnd(readableSpan); + if (i % 10 == 0) { + disruptorAsyncSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS); + } + } + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(tenK); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(tenK); + assertThat(incrementSpanProcessor.getCounterOnForceFlush()).isEqualTo(tenK / 10); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getCounterOnShutdown()).isEqualTo(1); + } + + @Test + void incrementMultiSpanProcessor() { + IncrementSpanProcessor incrementSpanProcessor1 = new IncrementSpanProcessor(REQUIRED, REQUIRED); + IncrementSpanProcessor incrementSpanProcessor2 = new IncrementSpanProcessor(REQUIRED, REQUIRED); + DisruptorAsyncSpanProcessor disruptorAsyncSpanProcessor = + DisruptorAsyncSpanProcessor.builder( + SpanProcessor.composite( + Arrays.asList(incrementSpanProcessor1, incrementSpanProcessor2))) + .build(); + disruptorAsyncSpanProcessor.onStart(Context.root(), readWriteSpan); + disruptorAsyncSpanProcessor.onEnd(readableSpan); + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor1.getCounterOnStart()).isEqualTo(1); + assertThat(incrementSpanProcessor1.getCounterOnEnd()).isEqualTo(1); + assertThat(incrementSpanProcessor1.getCounterOnShutdown()).isEqualTo(1); + assertThat(incrementSpanProcessor1.getCounterOnForceFlush()).isEqualTo(0); + assertThat(incrementSpanProcessor2.getCounterOnStart()).isEqualTo(1); + assertThat(incrementSpanProcessor2.getCounterOnEnd()).isEqualTo(1); + assertThat(incrementSpanProcessor2.getCounterOnShutdown()).isEqualTo(1); + assertThat(incrementSpanProcessor2.getCounterOnForceFlush()).isEqualTo(0); + } + + @Test + void multipleForceFlush() { + final int tenK = 10000; + IncrementSpanProcessor incrementSpanProcessor = new IncrementSpanProcessor(REQUIRED, REQUIRED); + DisruptorAsyncSpanProcessor disruptorAsyncSpanProcessor = + DisruptorAsyncSpanProcessor.builder(incrementSpanProcessor).build(); + for (int i = 1; i <= tenK; i++) { + disruptorAsyncSpanProcessor.onStart(Context.root(), readWriteSpan); + disruptorAsyncSpanProcessor.onEnd(readableSpan); + if (i % 100 == 0) { + disruptorAsyncSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getDeltaExportedForceFlushSpans()).isEqualTo(100); + } + } + disruptorAsyncSpanProcessor.shutdown().join(10, TimeUnit.SECONDS); + assertThat(incrementSpanProcessor.getCounterOnStart()).isEqualTo(tenK); + assertThat(incrementSpanProcessor.getCounterOnEnd()).isEqualTo(tenK); + assertThat(incrementSpanProcessor.getCounterOnExportedForceFlushSpans()).isEqualTo(tenK); + assertThat(incrementSpanProcessor.getCounterOnShutdown()).isEqualTo(1); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/README.md b/opentelemetry-java/sdk-extensions/autoconfigure/README.md new file mode 100644 index 000000000..af13c88f0 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/README.md @@ -0,0 +1,180 @@ +# OpenTelemetry SDK Autoconfigure + +This artifact implements environment-based autoconfiguration of the OpenTelemetry SDK. This can be +an alternative to programmatic configuration using the normal SDK builders. + +All options support being passed as Java system properties, e.g., `-Dotel.traces.exporter=zipkin` or +environment variables, e.g., `OTEL_TRACES_EXPORTER=zipkin`. + +## Contents + +* [General notes](#general-notes) +* [Exporters](#exporters) + + [OTLP exporter (both span and metric exporters)](#otlp-exporter-both-span-and-metric-exporters) + + [Jaeger exporter](#jaeger-exporter) + + [Zipkin exporter](#zipkin-exporter) + + [Prometheus exporter](#prometheus-exporter) + + [Logging exporter](#logging-exporter) +* [Trace context propagation](#propagator) +* [OpenTelemetry Resource](#opentelemetry-resource) +* [Batch span processor](#batch-span-processor) +* [Sampler](#sampler) +* [Span limits](#span-limits) +* [Interval metric reader](#interval-metric-reader) +* [Customizing the OpenTelemetry SDK](#customizing-the-opentelemetry-sdk) + +## General notes + +- The autoconfigure module registers Java shutdown hooks to shut down the SDK when appropriate. Please note that since this project uses +java.util.logging for all of it's logging, some of that logging may be suppressed during shutdown hooks. This is a bug in the JDK itself, +and not something we can control. If you require logging during shutdown hooks, please consider using `System.out` rather than a logging framework +that might shut itself down in a shutdown hook, thus suppressing your log messages. See this [JDK bug](https://bugs.openjdk.java.net/browse/JDK-8161253) +for more details. + +## Exporters + +The following configuration properties are common to all exporters: + +| System property | Environment variable | Purpose | +|-----------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| otel.traces.exporter | OTEL_TRACES_EXPORTER | The exporter to be used for tracing. Default is `otlp`. `none` means no autoconfigured exporter. | +| otel.metrics.exporter | OTEL_METRICS_EXPORTER | The exporter to be used for metrics. Default is `otlp`. `none` means no autoconfigured exporter. | + +### OTLP exporter (both span and metric exporters) + +The [OpenTelemetry Protocol (OTLP)](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md) span and metric exporters + +| System property | Environment variable | Description | +|------------------------------|-----------------------------|---------------------------------------------------------------------------| +| otel.traces.exporter=otlp (default) | OTEL_TRACES_EXPORTER=otlp | Select the OpenTelemetry exporter for tracing (default) | +| otel.metrics.exporter=otlp (default) | OTEL_METRICS_EXPORTER=otlp | Select the OpenTelemetry exporter for metrics (default) | +| otel.exporter.otlp.endpoint | OTEL_EXPORTER_OTLP_ENDPOINT | The OTLP traces and metrics endpoint to connect to. Must be a URL with a scheme of either `http` or `https` based on the use of TLS. Default is `http://localhost:4317`. | +| otel.exporter.otlp.traces.endpoint | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | The OTLP traces endpoint to connect to. Must be a URL with a scheme of either `http` or `https` based on the use of TLS. Default is `http://localhost:4317`. | +| otel.exporter.otlp.metrics.endpoint | OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | The OTLP metrics endpoint to connect to. Must be a URL with a scheme of either `http` or `https` based on the use of TLS. Default is `http://localhost:4317`. | +| otel.exporter.otlp.headers | OTEL_EXPORTER_OTLP_HEADERS | Key-value pairs separated by commas to pass as request headers. | +| otel.exporter.otlp.timeout | OTEL_EXPORTER_OTLP_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each batch. Default is `10000`. | + +To configure the service name for the OTLP exporter, add the `service.name` key +to the OpenTelemetry Resource ([see below](#opentelemetry-resource)), e.g. `OTEL_RESOURCE_ATTRIBUTES=service.name=myservice`. + +### Jaeger exporter + +The [Jaeger](https://www.jaegertracing.io/docs/1.21/apis/#protobuf-via-grpc-stable) exporter. This exporter uses gRPC for its communications protocol. + +| System property | Environment variable | Description | +|-----------------------------------|-----------------------------------|----------------------------------------------------------------------------------------------------| +| otel.traces.exporter=jaeger | OTEL_TRACES_EXPORTER=jaeger | Select the Jaeger exporter | +| otel.exporter.jaeger.endpoint | OTEL_EXPORTER_JAEGER_ENDPOINT | The Jaeger gRPC endpoint to connect to. Default is `http://localhost:14250`. | +| otel.exporter.jaeger.timeout | OTEL_EXPORTER_JAEGER_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each batch. Default is `10000`. | + +### Zipkin exporter + +The [Zipkin](https://zipkin.io/zipkin-api/) exporter. It sends JSON in [Zipkin format](https://zipkin.io/zipkin-api/#/default/post_spans) to a specified HTTP URL. + +| System property | Environment variable | Description | +|-----------------------------------|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| otel.traces.exporter=zipkin | OTEL_TRACES_EXPORTER=zipkin | Select the Zipkin exporter | +| otel.exporter.zipkin.endpoint | OTEL_EXPORTER_ZIPKIN_ENDPOINT | The Zipkin endpoint to connect to. Default is `http://localhost:9411/api/v2/spans`. Currently only HTTP is supported. | + +### Prometheus exporter + +The [Prometheus](https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md) exporter. + +| System property | Environment variable | Description | +|-------------------------------|-------------------------------|------------------------------------------------------------------------------------| +| otel.metrics.exporter=prometheus | OTEL_METRICS_EXPORTER=prometheus | Select the Prometheus exporter | +| otel.exporter.prometheus.port | OTEL_EXPORTER_PROMETHEUS_PORT | The local port used to bind the prometheus metric server. Default is `9464`. | +| otel.exporter.prometheus.host | OTEL_EXPORTER_PROMETHEUS_HOST | The local address used to bind the prometheus metric server. Default is `0.0.0.0`. | + +Note that this is a pull exporter - it opens up a server on the local process listening on the specified host and port, which +a Prometheus server scrapes from. + +### Logging exporter + +The logging exporter prints the name of the span along with its attributes to stdout. It's mainly used for testing and debugging. + +| System property | Environment variable | Description | +|------------------------------|------------------------------|------------------------------------------------------------------------------| +| otel.traces.exporter=logging | OTEL_TRACES_EXPORTER=logging | Select the logging exporter for tracing | +| otel.metrics.exporter=logging | OTEL_METRICS_EXPORTER=logging | Select the logging exporter for metrics | +| otel.exporter.logging.prefix | OTEL_EXPORTER_LOGGING_PREFIX | An optional string printed in front of the span name and attributes. | + +## Propagator + +The propagators determine which distributed tracing header formats are used, and which baggage propagation header formats are used. + +| System property | Environment variable | Description | +|------------------|----------------------|-----------------------------------------------------------------------------------------------------------------| +| otel.propagators | OTEL_PROPAGATORS | The propagators to be used. Use a comma-separated list for multiple propagators. Default is `tracecontext,baggage` (W3C). | + +Supported values are + +- `"tracecontext"`: [W3C Trace Context](https://www.w3.org/TR/trace-context/) (add `baggage` as well to include W3C baggage) +- `"baggage"`: [W3C Baggage](https://www.w3.org/TR/baggage/) +- `"b3"`: [B3 Single](https://github.com/openzipkin/b3-propagation#single-header) +- `"b3multi"`: [B3 Multi](https://github.com/openzipkin/b3-propagation#multiple-headers) +- `"jaeger"`: [Jaeger](https://www.jaegertracing.io/docs/1.21/client-libraries/#propagation-format) (includes Jaeger baggage) +- `"xray"`: [AWS X-Ray](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader) +- `"ottrace"`: [OT Trace](https://github.com/opentracing?q=basic&type=&language=) + +## OpenTelemetry Resource + +The [OpenTelemetry Resource](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/sdk.md) +is a representation of the entity producing telemetry. + +| System property | Environment variable | Description | +|--------------------------|--------------------------|------------------------------------------------------------------------------------| +| otel.resource.attributes | OTEL_RESOURCE_ATTRIBUTES | Specify resource attributes in the following format: key1=val1,key2=val2,key3=val3 | + +You almost always want to specify the [`service.name`](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions#service) for your application. +It corresponds to how you describe the application, for example `authservice` could be an application that authenticates requests, and `cats` could be an application that returns information about [cats](https://en.wikipedia.org/wiki/Cat). +This means you would specify resource attributes something like `OTEL_RESOURCE_ATTRIBUTES=service.name=authservice`, or `-Dotel.resource.attributes=service.name=cats,service.namespace=mammals`. + +## Batch span processor + +| System property | Environment variable | Description | +|---------------------------|---------------------------|------------------------------------------------------------------------------------| +| otel.bsp.schedule.delay | OTEL_BSP_SCHEDULE_DELAY | The interval, in milliseconds, between two consecutive exports. Default is `5000`. | +| otel.bsp.max.queue.size | OTEL_BSP_MAX_QUEUE_SIZE | The maximum queue size. Default is `2048`. | +| otel.bsp.max.export.batch.size | OTEL_BSP_MAX_EXPORT_BATCH_SIZE | The maximum batch size. Default is `512`. | +| otel.bsp.export.timeout | OTEL_BSP_EXPORT_TIMEOUT | The maximum allowed time, in milliseconds, to export data. Default is `30000`. | + +## Sampler + +The sampler configures whether spans will be recorded for any call to `SpanBuilder.startSpan`. + +| System property | Environment variable | Description | +|---------------------------------|---------------------------------|--------------------------------------------------------------| +| otel.traces.sampler | OTEL_TRACES_SAMPLER | The sampler to use for tracing. Defaults to `parentbased_always_on` | +| otel.traces.sampler.arg | OTEL_TRACES_SAMPLER_ARG | An argument to the configured tracer if supported, for example a ratio. | + +Supported values for `otel.traces.sampler` are + +- "always_on": AlwaysOnSampler +- "always_off": AlwaysOffSampler +- "traceidratio": TraceIdRatioBased. `otel.traces.sampler.arg` sets the ratio. +- "parentbased_always_on": ParentBased(root=AlwaysOnSampler) +- "parentbased_always_off": ParentBased(root=AlwaysOffSampler) +- "parentbased_traceidratio": ParentBased(root=TraceIdRatioBased). `otel.traces.sampler.arg` sets the ratio. + +## Span limits + +These properties can be used to control the maximum size of recordings per span. + +| System property | Environment variable | Description | +|---------------------------------|---------------------------------|--------------------------------------------------------------| +| otel.span.attribute.count.limit | OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT | The maximum number of attributes per span. Default is `128`. | +| otel.span.event.count.limit | OTEL_SPAN_EVENT_COUNT_LIMIT | The maximum number of events per span. Default is `128`. | +| otel.span.link.count.limit | OTEL_SPAN_LINK_COUNT_LIMIT | The maximum number of links per span. Default is `128` | + +## Interval metric reader + +| System property | Environment variable | Description | +|--------------------------|--------------------------|-----------------------------------------------------------------------------------| +| otel.imr.export.interval | OTEL_IMR_EXPORT_INTERVAL | The interval, in milliseconds, between pushes to the exporter. Default is `60000`.| + +## Customizing the OpenTelemetry SDK + +Autoconfiguration exposes SPI [hooks](./src/main/java/io/opentelemetry/sdk/autoconfigure/spi) for customizing behavior programmatically as needed. +It's recommended to use the above configuration properties where possible, only implementing the SPI to add functionality not found in the +SDK by default. diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/build.gradle.kts b/opentelemetry-java/sdk-extensions/autoconfigure/build.gradle.kts new file mode 100644 index 000000000..d7815decb --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/build.gradle.kts @@ -0,0 +1,145 @@ +plugins { + `java-library` + `maven-publish` + + id("org.unbroken-dome.test-sets") +} + +description = "OpenTelemetry SDK Auto-configuration" +extra["moduleName"] = "io.opentelemetry.sdk.autoconfigure" + +testSets { + create("testConfigError") + create("testFullConfig") + create("testInitializeRegistersGlobal") + create("testJaeger") + create("testPrometheus") + create("testOtlpTls") + create("testResourceDisabledByProperty") + create("testResourceDisabledByEnv") + create("testZipkin") +} + +dependencies { + api(project(":sdk:all")) + api(project(":sdk:metrics")) + + implementation(project(":semconv")) + implementation("io.micrometer:micrometer-registry-prometheus:1.1.7") + implementation("run.mone:nacos-client:1.2.1-mone-v3-SNAPSHOT") + + + compileOnly(project(":exporters:jaeger")) + compileOnly(project(":exporters:logging")) + compileOnly(project(":exporters:otlp:all")) + compileOnly(project(":exporters:otlp:metrics")) + compileOnly(project(":exporters:prometheus")) + compileOnly("io.prometheus:simpleclient_httpserver") + compileOnly(project(":exporters:zipkin")) + + + + testImplementation(project(path=":sdk:trace-shaded-deps")) + + testImplementation(project(":proto")) + testImplementation(project(":sdk:testing")) + testImplementation("com.linecorp.armeria:armeria-junit5") + testImplementation("com.linecorp.armeria:armeria-grpc") + testRuntimeOnly("io.grpc:grpc-netty-shaded") + testRuntimeOnly("org.slf4j:slf4j-simple") + + add("testFullConfigImplementation", project(":extensions:aws")) + add("testFullConfigImplementation", project(":extensions:trace-propagators")) + add("testFullConfigImplementation", project(":exporters:jaeger")) + add("testFullConfigImplementation", project(":exporters:logging")) + add("testFullConfigImplementation", project(":exporters:otlp:all")) + add("testFullConfigImplementation", project(":exporters:otlp:metrics")) + add("testFullConfigImplementation", project(":exporters:prometheus")) + add("testFullConfigImplementation", "io.prometheus:simpleclient_httpserver") + add("testFullConfigImplementation", project(":exporters:zipkin")) + add("testFullConfigImplementation", project(":sdk-extensions:resources")) + + add("testOtlpTlsImplementation", project(":exporters:otlp:all")) + + add("testJaegerImplementation", project(":exporters:jaeger")) + + add("testZipkinImplementation", project(":exporters:zipkin")) + + add("testConfigErrorImplementation", project(":extensions:trace-propagators")) + add("testConfigErrorImplementation", project(":exporters:jaeger")) + add("testConfigErrorImplementation", project(":exporters:logging")) + add("testConfigErrorImplementation", project(":exporters:otlp:all")) + add("testConfigErrorImplementation", project(":exporters:otlp:metrics")) + add("testConfigErrorImplementation", project(":exporters:prometheus")) + add("testConfigErrorImplementation", "io.prometheus:simpleclient_httpserver") + add("testConfigErrorImplementation", project(":exporters:zipkin")) + add("testConfigErrorImplementation", "org.junit-pioneer:junit-pioneer") + + add("testPrometheusImplementation", project(":exporters:prometheus")) + add("testPrometheusImplementation", "io.prometheus:simpleclient_httpserver") + + add("testResourceDisabledByPropertyImplementation", project(":sdk-extensions:resources")) + add("testResourceDisabledByEnvImplementation", project(":sdk-extensions:resources")) +} + +tasks { + val testConfigError by existing(Test::class) { + } + + val testFullConfig by existing(Test::class) { + environment("OTEL_RESOURCE_ATTRIBUTES", "service.name=test,cat=meow") + environment("OTEL_PROPAGATORS", "tracecontext,baggage,b3,b3multi,jaeger,ottrace,xray,test") + environment("OTEL_BSP_SCHEDULE_DELAY", "10") + environment("OTEL_IMR_EXPORT_INTERVAL", "10") + environment("OTEL_EXPORTER_OTLP_HEADERS", "cat=meow,dog=bark") + environment("OTEL_EXPORTER_OTLP_TIMEOUT", "5000") + environment("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "2") + } + + val testJaeger by existing(Test::class) { + environment("OTEL_TRACES_EXPORTER", "jaeger") + environment("OTEL_METRICS_EXPORTER", "none") + environment("OTEL_BSP_SCHEDULE_DELAY", "10") + } + + val testOtlpTls by existing(Test::class) { + environment("OTEL_RESOURCE_ATTRIBUTES", "service.name=test,cat=meow") + environment("OTEL_TRACES_EXPORTER", "otlp") + environment("OTEL_METRICS_EXPORTER", "none") + environment("OTEL_BSP_SCHEDULE_DELAY", "10") + } + + val testZipkin by existing(Test::class) { + environment("OTEL_TRACES_EXPORTER", "zipkin") + environment("OTEL_METRICS_EXPORTER", "none") + environment("OTEL_BSP_SCHEDULE_DELAY", "10") + } + + val testPrometheus by existing(Test::class) { + environment("OTEL_TRACES_EXPORTER", "none") + environment("OTEL_METRICS_EXPORTER", "prometheus") + environment("OTEL_IMR_EXPORT_INTERVAL", "10") + } + + val testResourceDisabledByProperty by existing(Test::class) { + jvmArgs("-Dotel.java.disabled.resource-providers=io.opentelemetry.sdk.extension.resources.OsResourceProvider,io.opentelemetry.sdk.extension.resources.ProcessResourceProvider") + // Properties win, this is ignored. + environment("OTEL_JAVA_DISABLED_RESOURCE_PROVIDERS", "io.opentelemetry.sdk.extension.resources.ProcessRuntimeResourceProvider") + } + + val testResourceDisabledByEnv by existing(Test::class) { + environment("OTEL_JAVA_DISABLED_RESOURCE_PROVIDERS", "io.opentelemetry.sdk.extension.resources.OsResourceProvider,io.opentelemetry.sdk.extension.resources.ProcessResourceProvider") + } + + val check by existing { + dependsOn( + testConfigError, + testFullConfig, + testJaeger, + testPrometheus, + testZipkin, + testResourceDisabledByProperty, + testResourceDisabledByEnv + ) + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/gradle.properties b/opentelemetry-java/sdk-extensions/autoconfigure/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ClasspathUtil.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ClasspathUtil.java new file mode 100644 index 000000000..891407c4f --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ClasspathUtil.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +final class ClasspathUtil { + + @SuppressWarnings("UnusedException") + static void checkClassExists(String className, String featureName, String requiredLibrary) { + try { + Class.forName(className); + } catch (ClassNotFoundException unused) { + throw new ConfigurationException( + featureName + + " enabled but " + + requiredLibrary + + " not found on classpath. " + + "Make sure to add it as a dependency to enable this feature."); + } + } + + private ClasspathUtil() {} +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ConfigProperties.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ConfigProperties.java new file mode 100644 index 000000000..80e70239e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ConfigProperties.java @@ -0,0 +1,270 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import java.time.Duration; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * Properties to be used for auto-configuration of the OpenTelemetry SDK components. These + * properties will be a combination of system properties and environment variables. The properties + * for both of these will be normalized to be all lower case, and underscores will be replaced with + * periods. + */ +public final class ConfigProperties { + + private final Map config; + + static ConfigProperties get() { + return new ConfigProperties(System.getProperties(), System.getenv()); + } + + // Visible for testing + static ConfigProperties createForTest(Map properties) { + return new ConfigProperties(properties, Collections.emptyMap()); + } + + private ConfigProperties(Map systemProperties, Map environmentVariables) { + Map config = new HashMap<>(); + environmentVariables.forEach( + (name, value) -> config.put(name.toLowerCase(Locale.ROOT).replace('_', '.'), value)); + systemProperties.forEach( + (key, value) -> + config.put(((String) key).toLowerCase(Locale.ROOT).replace('-', '.'), (String) value)); + + this.config = config; + } + + /** + * Returns a string-valued configuration property. + * + * @return null if the property has not been configured. + */ + @Nullable + public String getString(String name) { + return config.get(name); + } + + /** + * Returns a integer-valued configuration property. + * + * @return null if the property has not been configured. + * @throws NumberFormatException if the property is not a valid integer. + */ + @Nullable + @SuppressWarnings("UnusedException") + public Integer getInt(String name) { + String value = config.get(name); + if (value == null || value.isEmpty()) { + return null; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException ex) { + throw newInvalidPropertyException(name, value, "integer"); + } + } + + /** + * Returns a long-valued configuration property. + * + * @return null if the property has not been configured. + * @throws NumberFormatException if the property is not a valid long. + */ + @Nullable + @SuppressWarnings("UnusedException") + public Long getLong(String name) { + String value = config.get(name); + if (value == null || value.isEmpty()) { + return null; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException ex) { + throw newInvalidPropertyException(name, value, "long"); + } + } + + /** + * Returns a double-valued configuration property. + * + * @return null if the property has not been configured. + * @throws NumberFormatException if the property is not a valid double. + */ + @Nullable + @SuppressWarnings("UnusedException") + public Double getDouble(String name) { + String value = config.get(name); + if (value == null || value.isEmpty()) { + return null; + } + try { + return Double.parseDouble(value); + } catch (NumberFormatException ex) { + throw newInvalidPropertyException(name, value, "double"); + } + } + + /** + * Returns a list-valued configuration property. The format of the original value must be + * comma-separated. Empty values will be removed. + * + * @return an empty list if the property has not been configured. + */ + public List getCommaSeparatedValues(String name) { + String value = config.get(name); + if (value == null) { + return Collections.emptyList(); + } + return filterBlanksAndNulls(value.split(",")); + } + + /** + * Returns a duration property from the map, or {@code null} if it cannot be found or it has a + * wrong type. + * + *

    Durations can be of the form "{number}{unit}", where unit is one of: + * + *

      + *
    • ms + *
    • s + *
    • m + *
    • h + *
    • d + *
    + * + *

    If no unit is specified, milliseconds is the assumed duration unit. + * + * @param name The property name + * @return the {@link Duration} value of the property, {@code null} if the property cannot be + * found. + * @throws ConfigurationException for malformed duration strings. + */ + @Nullable + @SuppressWarnings("UnusedException") + public Duration getDuration(String name) { + String value = config.get(name); + if (value == null || value.isEmpty()) { + return null; + } + String unitString = getUnitString(value); + // TODO: Environment variables have unknown encoding. `trim()` may cut codepoints oddly + // but likely we'll fail for malformed unit string either way. + String numberString = value.substring(0, value.length() - unitString.length()); + try { + long rawNumber = Long.parseLong(numberString.trim()); + TimeUnit unit = getDurationUnit(unitString.trim()); + return Duration.ofMillis(TimeUnit.MILLISECONDS.convert(rawNumber, unit)); + } catch (NumberFormatException ex) { + throw new ConfigurationException( + "Invalid duration property " + + name + + "=" + + value + + ". Expected number, found: " + + numberString); + } catch (ConfigurationException ex) { + throw new ConfigurationException( + "Invalid duration property " + name + "=" + value + ". " + ex.getMessage()); + } + } + + /** + * Returns a map-valued configuration property. The format of the original value must be + * comma-separated for each key, with an '=' separating the key and value. For instance, + * service.name=Greatest Service,host.name=localhost Empty values will be removed. + * + * @return an empty list if the property has not been configured. + */ + public Map getCommaSeparatedMap(String name) { + return getCommaSeparatedValues(name).stream() + .map(keyValuePair -> filterBlanksAndNulls(keyValuePair.split("=", 2))) + .map( + splitKeyValuePairs -> { + if (splitKeyValuePairs.size() != 2) { + throw new ConfigurationException( + "Invalid map property: " + name + "=" + config.get(name)); + } + return new AbstractMap.SimpleImmutableEntry<>( + splitKeyValuePairs.get(0), splitKeyValuePairs.get(1)); + }) + // If duplicate keys, prioritize later ones similar to duplicate system properties on a + // Java command line. + .collect( + Collectors.toMap( + Map.Entry::getKey, Map.Entry::getValue, (first, next) -> next, LinkedHashMap::new)); + } + + /** + * Returns a boolean-valued configuration property. Uses the same rules as {@link + * Boolean#parseBoolean(String)} for handling the values. + * + * @return false if the property has not been configured. + */ + public boolean getBoolean(String name) { + return Boolean.parseBoolean(config.get(name)); + } + + private static ConfigurationException newInvalidPropertyException( + String name, String value, String type) { + throw new ConfigurationException( + "Invalid value for property " + name + "=" + value + ". Must be a " + type + "."); + } + + private static List filterBlanksAndNulls(String[] values) { + return Arrays.stream(values) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + } + + /** Returns the TimeUnit associated with a unit string. Defaults to milliseconds. */ + private static TimeUnit getDurationUnit(String unitString) { + switch (unitString) { + case "": // Falllthrough expected + case "ms": + return TimeUnit.MILLISECONDS; + case "s": + return TimeUnit.SECONDS; + case "m": + return TimeUnit.MINUTES; + case "h": + return TimeUnit.HOURS; + case "d": + return TimeUnit.DAYS; + default: + throw new ConfigurationException("Invalid duration string, found: " + unitString); + } + } + + /** + * Fragments the 'units' portion of a config value from the 'value' portion. + * + *

    E.g. "1ms" would return the string "ms". + */ + private static String getUnitString(String rawValue) { + int lastDigitIndex = rawValue.length() - 1; + while (lastDigitIndex >= 0) { + char c = rawValue.charAt(lastDigitIndex); + if (Character.isDigit(c)) { + break; + } + lastDigitIndex -= 1; + } + // Pull everything after the last digit. + return rawValue.substring(lastDigitIndex + 1); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ConfigurationException.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ConfigurationException.java new file mode 100644 index 000000000..43208abb7 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ConfigurationException.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +/** An exception that is thrown if the user-provided configuration is invalid. */ +public final class ConfigurationException extends RuntimeException { + + private static final long serialVersionUID = 4717640118051490483L; + + ConfigurationException(String message) { + super(message); + } + + ConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java new file mode 100644 index 000000000..b02009b42 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResource.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import io.opentelemetry.sdk.common.SystemCommon; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; + +/** + * Factory for a {@link Resource} which parses the standard "otel.resource.attributes" system + * property or OTEL_RESOURCE_ATTRIBUTES environment variable. Will also use + * OTEL_SERVICE_NAME/otel.service.name to specifically set the service name. + */ +public final class EnvironmentResource { + + // Visible for testing + static final String ATTRIBUTE_PROPERTY = "otel.resource.attributes"; + static final String SERVICE_NAME_PROPERTY = "otel.service.name"; + + /** + * Returns a {@link Resource} which contains information from the standard + * "otel.resource.attributes"/"otel.service.name" system properties or + * OTEL_RESOURCE_ATTRIBUTES/OTEL_SERVICE_NAME environment variables. + */ + public static Resource get() { + return create(ConfigProperties.get()); + } + + static Resource create(ConfigProperties config) { + return Resource.create(getAttributes(config)); + } + + // visible for testing + static Attributes getAttributes(ConfigProperties configProperties) { + AttributesBuilder resourceAttributes = Attributes.builder(); + configProperties.getCommaSeparatedMap(ATTRIBUTE_PROPERTY).forEach(resourceAttributes::put); + String serviceName = configProperties.getString(SERVICE_NAME_PROPERTY); + if (serviceName == null) { + serviceName = SystemCommon.getEnvOrProperties(EnvOrJvmProperties.MIONE_PROJECT_NAME.getKey()); + } + + if(serviceName != null) { + resourceAttributes.put(ResourceAttributes.SERVICE_NAME, serviceName); + } + + return resourceAttributes.build(); + } + + private EnvironmentResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/JcommonHTTPServer.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/JcommonHTTPServer.java new file mode 100644 index 000000000..c64dae2c3 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/JcommonHTTPServer.java @@ -0,0 +1,201 @@ +package io.opentelemetry.sdk.autoconfigure; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URLDecoder; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@SuppressWarnings({"SystemOut","CatchAndPrintStackTrace","DefaultCharset"}) +public class JcommonHTTPServer { + + /** + * Handles Metrics collections from the given registry. + */ + static class HTTPMetricHandler implements HttpHandler { + + private final Map registryMap; + + private final static byte[] HEALTHY_RESPONSE = "ok".getBytes(); + + HTTPMetricHandler(Map registryMap) { + this.registryMap = registryMap; + } + + private byte[] data = new byte[]{}; + + private long lastTime; + + private synchronized byte[] getData(String contentType, CollectorRegistry registry, String query) { + long now = System.currentTimeMillis(); + if (now - lastTime > 5000L) { + this.lastTime = now; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(baos)) { + TextFormat.writeFormat(contentType, writer, registry.filteredMetricFamilySamples(parseQuery(query))); + writer.flush(); + this.data = baos.toByteArray(); + return this.data; + } catch (Throwable ex) { + ex.printStackTrace(); + return new byte[]{}; + } + } + return this.data; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + long a = System.currentTimeMillis(); + String path = exchange.getRequestURI().getPath(); + try (OutputStream os = exchange.getResponseBody()) { + String query = exchange.getRequestURI().getRawQuery(); + if ("/-/healthy".equals(path)) { + exchange.sendResponseHeaders(200, HEALTHY_RESPONSE.length); + os.write(HEALTHY_RESPONSE); + os.flush(); + return; + } else { + String contentType = TextFormat.chooseContentType(exchange.getRequestHeaders().getFirst("Accept")); + exchange.getResponseHeaders().set("Content-Type", contentType); + CollectorRegistry registry = this.registryMap.get("default"); + if ("/jvm".equals(path)) { + registry = this.registryMap.get("jvm"); + } + byte[] data = getData(contentType, registry, query); + exchange.sendResponseHeaders(200, data.length); + os.write(data); + os.flush(); + } + } catch (Throwable ex) { + ex.printStackTrace(); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().write(new byte[]{}); + } finally { + long b = System.currentTimeMillis(); + System.out.println("prometheus request uri:" + path + " duration:" + (b - a)); + } + } + + } + + protected static Set parseQuery(String query) throws IOException { + Set names = new HashSet(); + if (query != null) { + String[] pairs = query.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals("name[]")) { + names.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); + } + } + } + return names; + } + + + static class NamedDaemonThreadFactory implements ThreadFactory { + private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); + + private final int poolNumber = POOL_NUMBER.getAndIncrement(); + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final ThreadFactory delegate; + private final boolean daemon; + + NamedDaemonThreadFactory(ThreadFactory delegate, boolean daemon) { + this.delegate = delegate; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = delegate.newThread(r); + t.setName(String.format("prometheus-http-%d-%d", poolNumber, threadNumber.getAndIncrement())); + t.setDaemon(daemon); + return t; + } + + static ThreadFactory defaultThreadFactory(boolean daemon) { + return new NamedDaemonThreadFactory(Executors.defaultThreadFactory(), daemon); + } + } + + protected final HttpServer server; + protected final ExecutorService executorService; + + /** + * Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}. + * The {@code httpServer} is expected to already be bound to an address + */ + public JcommonHTTPServer(HttpServer httpServer, Map registryMap, boolean daemon) { + server = httpServer; + HttpHandler mHandler = new HTTPMetricHandler(registryMap); + server.createContext("/", mHandler); + server.createContext("/metrics", mHandler); + server.createContext("/-/healthy", mHandler); + executorService = new ThreadPoolExecutor(10, 10, + 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(1000)); + server.setExecutor(executorService); + start(daemon); + } + + /** + * Start a HTTP server by making sure that its background thread inherit proper daemon flag. + */ + private void start(boolean daemon) { + if (daemon == Thread.currentThread().isDaemon()) { + server.start(); + } else { + FutureTask startTask = new FutureTask(new Runnable() { + @Override + public void run() { + server.start(); + } + }, null); + NamedDaemonThreadFactory.defaultThreadFactory(daemon).newThread(startTask).start(); + try { + startTask.get(); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + // This is possible only if the current tread has been interrupted, + // but in real use cases this should not happen. + // In any case, there is nothing to do, except to propagate interrupted flag. + Thread.currentThread().interrupt(); + } + } + } + + /** + * Stop the HTTP server. + */ + public void stop() { + server.stop(0); + executorService.shutdown(); // Free any (parked/idle) threads in pool + } + + /** + * Gets the port number. + */ + public int getPort() { + return server.getAddress().getPort(); + } +} + diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfiguration.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfiguration.java new file mode 100644 index 000000000..0d9acd95b --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfiguration.java @@ -0,0 +1,286 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.client.naming.NacosNamingService; +import com.sun.net.httpserver.HttpServer; +import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics; +import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.core.instrument.binder.system.UptimeMetrics; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.opentelemetry.exporter.logging.LoggingMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import io.opentelemetry.sdk.common.SystemCommon; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReader; +import io.opentelemetry.sdk.metrics.export.IntervalMetricReaderBuilder; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.prometheus.client.CollectorRegistry; +import org.apache.commons.lang3.StringUtils; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +final class MetricExporterConfiguration { + + private static final ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(MetricExporterConfiguration.class.getName())); + + private static String applicationName; + private static String serverIp = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_HOST_IP.getKey()); + private static final String projectEnv = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_NAME.getKey()); + private static final String BUILDIN_K8S = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_HERA_BUILD_K8S.getKey()); + private static final String NODE_IP = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_NODE_IP.getKey()); + private static final String ENV_ID = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_MIONE_PROJECT_ENV_ID.getKey()); + private static final String ENV_DEFAULT = "default_env"; + private static final String LOG_AGENT_NACOS_KET = "prometheus_server_10010_log_agent"; + private static final String LOG_AGENT_ENV_ID = "1"; + + static void configureExporter( + String name, ConfigProperties config, SdkMeterProvider meterProvider) { + applicationName = config.getString(EnvOrJvmProperties.JVM_OTEL_RESOURCE_ATTRIBUTES.getKey()); + if (StringUtils.isNotEmpty(applicationName)) { + applicationName = applicationName.split("=")[1]; + } else { + applicationName = + SystemCommon.getEnvOrProperties(EnvOrJvmProperties.MIONE_PROJECT_NAME.getKey()) == null + ? EnvOrJvmProperties.MIONE_PROJECT_NAME.getDefaultValue() + : SystemCommon.getEnvOrProperties(EnvOrJvmProperties.MIONE_PROJECT_NAME.getKey()); + } + // Replace the "-" with "_" in the project name. + applicationName = applicationName.replaceAll("-", "_"); + if (StringUtils.isEmpty(serverIp)) { + serverIp = config.getString(EnvOrJvmProperties.JVM_OTEL_SERVICE_IP.getKey()); + } + if (StringUtils.isEmpty(name)) { + name = "default"; + } + switch (name) { + case "otlp": + configureOtlpMetrics(config, meterProvider); + return; + case "prometheus": + configureJcommonPrometheusMetrics(config); + return; + case "logging": + ClasspathUtil.checkClassExists( + "io.opentelemetry.exporter.logging.LoggingMetricExporter", + "Logging Metrics Exporter", + "opentelemetry-exporter-logging"); + configureLoggingMetrics(config, meterProvider); + return; + default: + return; + } + } + + private static void configureLoggingMetrics( + ConfigProperties config, SdkMeterProvider meterProvider) { + MetricExporter exporter = new LoggingMetricExporter(); + configureIntervalMetricReader(config, meterProvider, exporter); + } + + // Visible for testing + @Nullable + static OtlpGrpcMetricExporter configureOtlpMetrics( + ConfigProperties config, SdkMeterProvider meterProvider) { + try { + ClasspathUtil.checkClassExists( + "io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter", + "OTLP Metrics Exporter", + "opentelemetry-exporter-otlp-metrics"); + } catch (ConfigurationException e) { + // Squash this for now, until metrics are stable and included in the `exporter-otlp` artifact + // by default, + return null; + } + OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder(); + + String endpoint = config.getString("otel.exporter.otlp.metrics.endpoint"); + if (endpoint == null) { + endpoint = config.getString("otel.exporter.otlp.endpoint"); + } + if (endpoint != null) { + builder.setEndpoint(endpoint); + } + + config.getCommaSeparatedMap("otel.exporter.otlp.headers").forEach(builder::addHeader); + + Duration timeout = config.getDuration("otel.exporter.otlp.timeout"); + if (timeout != null) { + builder.setTimeout(timeout); + } + + OtlpGrpcMetricExporter exporter = builder.build(); + + configureIntervalMetricReader(config, meterProvider, exporter); + + return exporter; + } + + private static void configureIntervalMetricReader( + ConfigProperties config, SdkMeterProvider meterProvider, MetricExporter exporter) { + IntervalMetricReaderBuilder readerBuilder = + IntervalMetricReader.builder() + .setMetricProducers(Collections.singleton(meterProvider)) + .setMetricExporter(exporter); + Duration exportInterval = config.getDuration("otel.imr.export.interval"); + if (exportInterval != null) { + readerBuilder.setExportIntervalMillis(exportInterval.toMillis()); + } + IntervalMetricReader reader = readerBuilder.buildAndStart(); + Runtime.getRuntime().addShutdownHook(new Thread(reader::shutdown)); + } + +// private static void configurePrometheusMetrics( +// ConfigProperties config, SdkMeterProvider meterProvider) { +// ClasspathUtil.checkClassExists( +// "io.opentelemetry.exporter.prometheus.PrometheusCollector", +// "Prometheus Metrics Server", +// "opentelemetry-exporter-prometheus"); +// PrometheusCollector.builder().setMetricProducer(meterProvider).buildAndRegister(); +// Integer port = config.getInt("otel.exporter.prometheus.port"); +// if (port == null) { +// port = 9464; +// } +// String host = config.getString("otel.exporter.prometheus.host"); +// if (host == null) { +// host = "0.0.0.0"; +// } +// final HTTPServer server; +// try { +// server = new HTTPServer(host, port, true); +// } catch (IOException e) { +// throw new IllegalStateException("Failed to create Prometheus server", e); +// } +// Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); +// } + + @SuppressWarnings({"BooleanParameter", "UnnecessaryParentheses", "UnusedVariable"}) + private static void configureJcommonPrometheusMetrics(ConfigProperties config) { + // regist nacos for prometheus port + String javaagentPrometheusPort = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.ENV_JAVAAGENT_PROMETHEUS_PORT.getKey()); + if (StringUtils.isEmpty(javaagentPrometheusPort)) { + javaagentPrometheusPort = SystemCommon.getEnvOrProperties( + EnvOrJvmProperties.JVM_OTEL_METRICS_PROMETHEUS_PORT.getKey()); + } + String nacosAddr = SystemCommon.getEnvOrProperties(EnvOrJvmProperties.JVM_OTEL_NACOS_ADDRESS.getKey()); + registJvmNacos(javaagentPrometheusPort, nacosAddr); + registLogAgentNacos(nacosAddr); + + PrometheusMeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + if ("1".equals(BUILDIN_K8S)) { + registry.config().commonTags( + new String[] {"application", applicationName, "serverIp", serverIp, "jumpIp", NODE_IP, + "serverEnv", projectEnv, "serverEnvId", ENV_ID}); + } else { + registry.config().commonTags( + new String[] {"application", applicationName, "serverIp", serverIp, "jumpIp", serverIp, + "serverEnv", projectEnv, "serverEnvId", ENV_ID}); + } + (new ClassLoaderMetrics()).bindTo(registry); + (new JvmMemoryMetrics()).bindTo(registry); + (new JvmGcMetrics()).bindTo(registry); + (new ProcessorMetrics()).bindTo(registry); + (new JvmThreadMetrics()).bindTo(registry); + (new UptimeMetrics()).bindTo(registry); + (new FileDescriptorMetrics()).bindTo(registry); + String finalValue = javaagentPrometheusPort; + new Thread(() -> { + try { + InetSocketAddress addr = new InetSocketAddress(Integer.valueOf(finalValue)); + Map map = new HashMap<>(5); + map.put("default", CollectorRegistry.defaultRegistry); + map.put("jvm", registry.getPrometheusRegistry()); + new JcommonHTTPServer(HttpServer.create(addr, 3), map, false); + } catch (Exception e) { + throw new ConfigurationException("Prometheus export metrics exception: " + e.getMessage()); + } + }).start(); + } + + @SuppressWarnings("SystemOut") + private static void registJvmNacos(String prometheusPort, String nacosServerAddr) { + try { + String appName = "prometheus_server_" + applicationName; + NacosNamingService nacosNamingService = new NacosNamingService(nacosServerAddr); + Instance instance = new Instance(); + instance.setIp(serverIp); + instance.setPort(55255); + Map map = new HashMap<>(); + map.put("javaagent_prometheus_port", prometheusPort); + if (StringUtils.isNotEmpty(ENV_ID)) { + map.put("env_id", ENV_ID); + } + if (StringUtils.isNotEmpty(projectEnv)) { + map.put("env_name", projectEnv); + } + instance.setMetadata(map); + nacosNamingService.registerInstance(appName, instance); + // deregister + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + logger.log(Level.INFO, "nacos shutdown hook deregister instance"); + try { + nacosNamingService.deregisterInstance(appName, instance); + } catch (Exception e) { + logger.log(Level.WARNING, "nacos shutdown hook error : " + e.getMessage()); + } + })); + } catch (Exception e) { + e.printStackTrace(); + throw new ConfigurationException( + "Prometheus export regist nacos exception: " + e.getMessage()); + } + } + + private static void registLogAgentNacos(String nacosAddr) { + try { + NacosNamingService nacosNamingService = new NacosNamingService(nacosAddr); + Instance instance = new Instance(); + instance.setIp(serverIp); + instance.setPort(55256); + Map map = new HashMap<>(); + map.put("env_id", LOG_AGENT_ENV_ID); + map.put("env_name", ENV_DEFAULT); + instance.setMetadata(map); + nacosNamingService.registerInstance(LOG_AGENT_NACOS_KET, instance); + // deregister + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + logger.log(Level.INFO, "nacos shutdown hook deregister instance"); + try { + nacosNamingService.deregisterInstance(LOG_AGENT_NACOS_KET, instance); + } catch (Exception e) { + logger.log(Level.WARNING, "nacos shutdown hook error : " + e.getMessage()); + } + })); + } catch (Exception e) { + throw new ConfigurationException( + "Prometheus export regist nacos exception: " + e.getMessage()); + } + } + + private MetricExporterConfiguration() {} +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfiguration.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfiguration.java new file mode 100644 index 000000000..74d9aee4c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfiguration.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.autoconfigure.spi.SdkMeterProviderConfigurer; +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.HashSet; +import java.util.ServiceLoader; +import java.util.Set; + +/** + * Auto-configuration for the OpenTelemetry SDK. As an alternative to programmatically configuring + * the SDK using {@link OpenTelemetrySdk#builder()}, this package can be used to automatically + * configure the SDK using environment properties specified by OpenTelemetry. + */ +public final class OpenTelemetrySdkAutoConfiguration { + + private static final Resource RESOURCE = buildResource(); + + /** Returns the automatically configured {@link Resource}. */ + public static Resource getResource() { + return RESOURCE; + } + + /** + * Returns an {@link OpenTelemetrySdk} automatically initialized through recognized system + * properties and environment variables. + */ + public static OpenTelemetrySdk initialize() { + ConfigProperties config = ConfigProperties.get(); + ContextPropagators propagators = PropagatorConfiguration.configurePropagators(config); + + Resource resource = getResource(); + + configureMeterProvider(resource, config); + + SdkTracerProvider tracerProvider = + TracerProviderConfiguration.configureTracerProvider(resource, config); + + return OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .setPropagators(propagators) + .buildAndRegisterGlobal(); + } + + private static void configureMeterProvider(Resource resource, ConfigProperties config) { + SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder().setResource(resource); + + for (SdkMeterProviderConfigurer configurer : + ServiceLoader.load(SdkMeterProviderConfigurer.class)) { + configurer.configure(meterProviderBuilder); + } + + SdkMeterProvider meterProvider = meterProviderBuilder.buildAndRegisterGlobal(); + + String exporterName = config.getString(EnvOrJvmProperties.JVM_OTEL_METRICS_EXPORTER.getKey()); + MetricExporterConfiguration.configureExporter(exporterName, config, meterProvider); + } + + private static Resource buildResource() { + ConfigProperties config = ConfigProperties.get(); + Resource result = Resource.getDefault(); + + // TODO(anuraaga): We use a hyphen only once in this artifact, for + // otel.java.disabled.resource-providers. But fetching by the dot version is the simplest way + // to implement it for now. + Set disabledProviders = + new HashSet<>(config.getCommaSeparatedValues("otel.java.disabled.resource.providers")); + for (ResourceProvider resourceProvider : ServiceLoader.load(ResourceProvider.class)) { + if (disabledProviders.contains(resourceProvider.getClass().getName())) { + continue; + } + result = result.merge(resourceProvider.createResource(config)); + } + + result = result.merge(EnvironmentResource.create(config)); + + return result; + } + + private OpenTelemetrySdkAutoConfiguration() {} +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/PropagatorConfiguration.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/PropagatorConfiguration.java new file mode 100644 index 000000000..137a9fa70 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/PropagatorConfiguration.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +final class PropagatorConfiguration { + + static ContextPropagators configurePropagators(ConfigProperties config) { + Map spiPropagators = + StreamSupport.stream( + ServiceLoader.load(ConfigurablePropagatorProvider.class).spliterator(), false) + .collect( + Collectors.toMap( + ConfigurablePropagatorProvider::getName, + ConfigurablePropagatorProvider::getPropagator)); + + Set propagators = new LinkedHashSet<>(); + List requestedPropagators = config.getCommaSeparatedValues("otel.propagators"); + if (requestedPropagators.isEmpty()) { + requestedPropagators = Arrays.asList("tracecontext", "baggage"); + } + for (String propagatorName : requestedPropagators) { + propagators.add(getPropagator(propagatorName, spiPropagators)); + } + + return ContextPropagators.create(TextMapPropagator.composite(propagators)); + } + + private static TextMapPropagator getPropagator( + String name, Map spiPropagators) { + if (name.equals("tracecontext")) { + return W3CTraceContextPropagator.getInstance(); + } + if (name.equals("baggage")) { + return W3CBaggagePropagator.getInstance(); + } + + TextMapPropagator spiPropagator = spiPropagators.get(name); + if (spiPropagator != null) { + return spiPropagator; + } + throw new ConfigurationException( + "Unrecognized value for otel.propagators: " + + name + + ". Make sure the artifact including the propagator is on the classpath."); + } + + private PropagatorConfiguration() {} +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfiguration.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfiguration.java new file mode 100644 index 000000000..cb7c137d5 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfiguration.java @@ -0,0 +1,163 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter; +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporterBuilder; +import io.opentelemetry.exporter.logging.Log4j2SpanExporter; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import javax.annotation.Nullable; + +@SuppressWarnings("SystemOut") +final class SpanExporterConfiguration { + + @Nullable + static SpanExporter configureExporter(String name, ConfigProperties config) { + Map spiExporters = + StreamSupport.stream( + ServiceLoader.load(ConfigurableSpanExporterProvider.class).spliterator(), false) + .collect( + Collectors.toMap( + ConfigurableSpanExporterProvider::getName, + configurableSpanExporterProvider -> + configurableSpanExporterProvider.createExporter(config))); + + switch (name) { + case "otlp": + return configureOtlpSpans(config); + case "jaeger": + return configureJaeger(config); + case "zipkin": + return configureZipkin(config); + case "logging": + ClasspathUtil.checkClassExists( + "io.opentelemetry.exporter.logging.LoggingSpanExporter", + "Logging Trace Exporter", + "opentelemetry-exporter-logging"); + return new LoggingSpanExporter(); +// case "logback": +// return configureLogbackExporter(config); + case "log4j2": + ClasspathUtil.checkClassExists( + "io.opentelemetry.exporter.logging.Log4j2SpanExporter", + "Log4j2 Trace Exporter", + "opentelemetry-exporter-logging"); + return new Log4j2SpanExporter(); +// case "doceanlog": +// ClasspathUtil.checkClassExists( +// "io.opentelemetry.exporter.logging.DoceanLogSpanExporter", +// "DoceanLog Trace Exporter", +// "opentelemetry-exporter-logging"); +// return new DoceanLogSpanExporter(); + case "none": + return null; + default: + SpanExporter spiExporter = spiExporters.get(name); + if (spiExporter == null) { + throw new ConfigurationException("Unrecognized value for otel.traces.exporter: " + name); + } + return spiExporter; + } + } + + // Visible for testing + static OtlpGrpcSpanExporter configureOtlpSpans(ConfigProperties config) { + ClasspathUtil.checkClassExists( + "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter", + "OTLP Trace Exporter", + "opentelemetry-exporter-otlp"); + OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder(); + + String endpoint = config.getString("otel.exporter.otlp.traces.endpoint"); + if (endpoint == null) { + endpoint = config.getString("otel.exporter.otlp.endpoint"); + } + if (endpoint != null) { + builder.setEndpoint(endpoint); + } + + config.getCommaSeparatedMap("otel.exporter.otlp.headers").forEach(builder::addHeader); + + Duration timeout = config.getDuration("otel.exporter.otlp.timeout"); + if (timeout != null) { + builder.setTimeout(timeout); + } + + String certificate = config.getString("otel.exporter.otlp.certificate"); + if (certificate != null) { + Path path = Paths.get(certificate); + if (!Files.exists(path)) { + throw new ConfigurationException("Invalid OTLP certificate path: " + path); + } + final byte[] certificateBytes; + try { + certificateBytes = Files.readAllBytes(path); + } catch (IOException e) { + throw new ConfigurationException("Error reading OTLP certificate.", e); + } + builder.setTrustedCertificates(certificateBytes); + } + + return builder.build(); + } + + private static SpanExporter configureJaeger(ConfigProperties config) { + ClasspathUtil.checkClassExists( + "io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter", + "Jaeger gRPC Exporter", + "opentelemetry-exporter-jaeger"); + JaegerGrpcSpanExporterBuilder builder = JaegerGrpcSpanExporter.builder(); + + String endpoint = config.getString("otel.exporter.jaeger.endpoint"); + if (endpoint != null) { + builder.setEndpoint(endpoint); + } + + Duration timeout = config.getDuration("otel.exporter.jaeger.timeout"); + if (timeout != null) { + builder.setTimeout(timeout); + } + + return builder.build(); + } + + private static SpanExporter configureZipkin(ConfigProperties config) { + ClasspathUtil.checkClassExists( + "io.opentelemetry.exporter.zipkin.ZipkinSpanExporter", + "Zipkin Exporter", + "opentelemetry-exporter-zipkin"); + ZipkinSpanExporterBuilder builder = ZipkinSpanExporter.builder(); + + String endpoint = config.getString("otel.exporter.zipkin.endpoint"); + if (endpoint != null) { + builder.setEndpoint(endpoint); + } + + Duration timeout = config.getDuration("otel.exporter.zipkin.timeout"); + if (timeout != null) { + builder.setReadTimeout(timeout); + } + + return builder.build(); + } + + private SpanExporterConfiguration() {} +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/TracerProviderConfiguration.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/TracerProviderConfiguration.java new file mode 100644 index 000000000..b7426e459 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/TracerProviderConfiguration.java @@ -0,0 +1,167 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSamplerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer; +import io.opentelemetry.sdk.common.EnvOrJvmProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.SpanLimitsBuilder; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.time.Duration; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +final class TracerProviderConfiguration { + + static SdkTracerProvider configureTracerProvider(Resource resource, ConfigProperties config) { + SdkTracerProviderBuilder tracerProviderBuilder = + SdkTracerProvider.builder() + .setResource(resource) + .setSpanLimits(configureSpanLimits(config)); + + String sampler = config.getString("otel.traces.sampler"); + if (sampler != null) { + tracerProviderBuilder.setSampler(configureSampler(sampler, config)); + } + + // Run user configuration before setting exporters from environment to allow user span + // processors to effect export. + for (SdkTracerProviderConfigurer configurer : + ServiceLoader.load(SdkTracerProviderConfigurer.class)) { + configurer.configure(tracerProviderBuilder); + } + + String exporterName = config.getString(EnvOrJvmProperties.JVM_OTEL_TRACES_EXPORTER.getKey()); + if (exporterName == null) { + exporterName = "otlp"; + } + SpanExporter exporter = SpanExporterConfiguration.configureExporter(exporterName, config); + if (exporter != null) { + tracerProviderBuilder.addSpanProcessor( + configureSpanProcessor(config, exporter, exporterName)); + } + + SdkTracerProvider tracerProvider = tracerProviderBuilder.build(); + Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close)); + return tracerProvider; + } + + // VisibleForTesting + static SpanProcessor configureSpanProcessor( + ConfigProperties config, SpanExporter exporter, String exporterName) { + if (exporterName.equals("logging")) { + return SimpleSpanProcessor.create(exporter); + } + return configureSpanProcessor(config, exporter); + } + + // VisibleForTesting + static BatchSpanProcessor configureSpanProcessor(ConfigProperties config, SpanExporter exporter) { + BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(exporter); + + Duration scheduleDelay = config.getDuration("otel.bsp.schedule.delay"); + if (scheduleDelay != null) { + builder.setScheduleDelay(scheduleDelay); + } + + Integer maxQueue = config.getInt("otel.bsp.max.queue.size"); + if (maxQueue != null) { + builder.setMaxQueueSize(maxQueue); + } + + Integer maxExportBatch = config.getInt("otel.bsp.max.export.batch.size"); + if (maxExportBatch != null) { + builder.setMaxExportBatchSize(maxExportBatch); + } + + Duration timeout = config.getDuration("otel.bsp.export.timeout"); + if (timeout != null) { + builder.setExporterTimeout(timeout); + } + + return builder.build(); + } + + // Visible for testing + static SpanLimits configureSpanLimits(ConfigProperties config) { + SpanLimitsBuilder builder = SpanLimits.builder(); + + Integer maxAttrs = config.getInt("otel.span.attribute.count.limit"); + if (maxAttrs != null) { + builder.setMaxNumberOfAttributes(maxAttrs); + } + + Integer maxEvents = config.getInt("otel.span.event.count.limit"); + if (maxEvents != null) { + builder.setMaxNumberOfEvents(maxEvents); + } + + Integer maxLinks = config.getInt("otel.span.link.count.limit"); + if (maxLinks != null) { + builder.setMaxNumberOfLinks(maxLinks); + } + + return builder.build(); + } + + // Visible for testing + static Sampler configureSampler(String sampler, ConfigProperties config) { + Map spiSamplers = + StreamSupport.stream( + ServiceLoader.load(ConfigurableSamplerProvider.class).spliterator(), false) + .collect( + Collectors.toMap( + ConfigurableSamplerProvider::getName, + provider -> provider.createSampler(config))); + + switch (sampler) { + case "always_on": + return Sampler.alwaysOn(); + case "always_off": + return Sampler.alwaysOff(); + case "traceidratio": + { + Double ratio = config.getDouble("otel.traces.sampler.arg"); + if (ratio == null) { + ratio = 1.0d; + } + return Sampler.traceIdRatioBased(ratio); + } + case "parentbased_always_on": + return Sampler.parentBased(Sampler.alwaysOn()); + case "parentbased_always_off": + return Sampler.parentBased(Sampler.alwaysOff()); + case "parentbased_traceidratio": + { + Double ratio = config.getDouble("otel.traces.sampler.arg"); + if (ratio == null) { + ratio = 1.0d; + } + return Sampler.parentBased(Sampler.traceIdRatioBased(ratio)); + } + default: + Sampler spiSampler = spiSamplers.get(sampler); + if (spiSampler == null) { + throw new ConfigurationException( + "Unrecognized value for otel.traces.sampler: " + sampler); + } + return spiSampler; + } + } + + private TracerProviderConfiguration() {} +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/package-info.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/package-info.java new file mode 100644 index 000000000..c416b60cc --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.autoconfigure; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurablePropagatorProvider.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurablePropagatorProvider.java new file mode 100644 index 000000000..07478f9ae --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurablePropagatorProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.spi; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.TextMapPropagator; + +/** + * A service provider interface (SPI) for providing additional propagators that can be used with the + * autoconfigured SDK. If the {@code otel.propagators} property contains a value equal to what is + * returned by {@link #getName()}, the propagator returned by {@link #getPropagator()} will be + * enabled and available as part of {@link OpenTelemetry#getPropagators()}. + */ +public interface ConfigurablePropagatorProvider { + /** + * Returns a {@link TextMapPropagator} that can be registered to OpenTelemetry by providing the + * property value specified by {@link #getName()}. + */ + TextMapPropagator getPropagator(); + + /** + * Returns the name of this propagator, which can be specified with the {@code otel.propagators} + * property to enable it. If the name is the same as any other defined propagator name, it is + * undefined which will be used. + */ + String getName(); +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurableSamplerProvider.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurableSamplerProvider.java new file mode 100644 index 000000000..d2bf4d965 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurableSamplerProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.spi; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.trace.samplers.Sampler; + +/** + * A service provider interface (SPI) for providing additional samplers that can be used with the + * autoconfigured SDK. If the {@code otel.traces.sampler} property contains a value equal to what is + * returned by {@link #getName()}, the sampler returned by {@link #createSampler(ConfigProperties)} + * will be enabled and added to the SDK. + */ +public interface ConfigurableSamplerProvider { + + /** + * Returns a {@link Sampler} that can be registered to OpenTelemetry by providing the property + * value specified by {@link #getName()}. + */ + Sampler createSampler(ConfigProperties config); + + /** + * Returns the name of this sampler, which can be specified with the {@code otel.traces.sampler} + * property to enable it. The name returned should NOT be the same as any other exporter name. If + * the name does conflict with another exporter name, the resulting behavior is undefined and it + * is explicitly unspecified which exporter will actually be used. + */ + String getName(); +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurableSpanExporterProvider.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurableSpanExporterProvider.java new file mode 100644 index 000000000..6b2da7617 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ConfigurableSpanExporterProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.spi; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +/** + * A service provider interface (SPI) for providing additional exporters that can be used with the + * autoconfigured SDK. If the {@code otel.traces.exporter} property contains a value equal to what + * is returned by {@link #getName()}, the exporter returned by {@link + * #createExporter(ConfigProperties)} will be enabled and added to the SDK. + */ +public interface ConfigurableSpanExporterProvider { + + /** + * Returns a {@link SpanExporter} that can be registered to OpenTelemetry by providing the + * property value specified by {@link #getName()}. + */ + SpanExporter createExporter(ConfigProperties config); + + /** + * Returns the name of this exporter, which can be specified with the {@code otel.traces.exporter} + * property to enable it. The name returned should NOT be the same as any other exporter name. If + * the name does conflict with another exporter name, the resulting behavior is undefined and it + * is explicitly unspecified which exporter will actually be used. + */ + String getName(); +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ResourceProvider.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ResourceProvider.java new file mode 100644 index 000000000..6901ca1fe --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/ResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.spi; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; + +/** + * A service provider interface (SPI) for providing a {@link Resource} that is merged into the + * {@linkplain Resource#getDefault() default resource}. + */ +public interface ResourceProvider { + + Resource createResource(ConfigProperties config); +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/SdkMeterProviderConfigurer.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/SdkMeterProviderConfigurer.java new file mode 100644 index 000000000..124ce73ac --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/SdkMeterProviderConfigurer.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.spi; + +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; + +/** + * A service provider interface (SPI) for performing additional programmatic configuration of a + * {@link SdkMeterProviderBuilder} during initialization. When using auto-configuration, you should + * prefer to use system properties or environment variables for configuration, but this may be + * useful to register components that are not part of the SDK such as registering views. + * + * @since 1.1.0 + */ +public interface SdkMeterProviderConfigurer { + /** Configures the {@link SdkMeterProviderBuilder}. */ + void configure(SdkMeterProviderBuilder meterProviderBuilder); +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/SdkTracerProviderConfigurer.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/SdkTracerProviderConfigurer.java new file mode 100644 index 000000000..bdee0af97 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/SdkTracerProviderConfigurer.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.spi; + +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; + +/** + * A service provider interface (SPI) for performing additional programmatic configuration of a + * {@link SdkTracerProviderBuilder} during initialization. When using auto-configuration, you should + * prefer to use system properties or environment variables for configuration, but this may be + * useful to register components that are not part of the SDK such as custom exporters. + */ +public interface SdkTracerProviderConfigurer { + /** Configures the {@link SdkTracerProviderBuilder}. */ + void configure(SdkTracerProviderBuilder tracerProviderBuilder); +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ConfigPropertiesTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ConfigPropertiesTest.java new file mode 100644 index 000000000..2e89a2fab --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ConfigPropertiesTest.java @@ -0,0 +1,216 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ConfigPropertiesTest { + + @Test + void allValid() { + Map properties = new HashMap<>(); + properties.put("string", "str"); + properties.put("int", "10"); + properties.put("long", "20"); + properties.put("double", "5.4"); + properties.put("list", "cat,dog,bear"); + properties.put("map", "cat=meow,dog=bark,bear=growl"); + properties.put("duration", "1s"); + + ConfigProperties config = ConfigProperties.createForTest(properties); + assertThat(config.getString("string")).isEqualTo("str"); + assertThat(config.getInt("int")).isEqualTo(10); + assertThat(config.getLong("long")).isEqualTo(20); + assertThat(config.getDouble("double")).isEqualTo(5.4); + assertThat(config.getCommaSeparatedValues("list")).containsExactly("cat", "dog", "bear"); + assertThat(config.getCommaSeparatedMap("map")) + .containsExactly(entry("cat", "meow"), entry("dog", "bark"), entry("bear", "growl")); + assertThat(config.getDuration("duration")).isEqualTo(Duration.ofSeconds(1)); + } + + @Test + void allMissing() { + ConfigProperties config = ConfigProperties.createForTest(Collections.emptyMap()); + assertThat(config.getString("string")).isNull(); + assertThat(config.getInt("int")).isNull(); + assertThat(config.getLong("long")).isNull(); + assertThat(config.getDouble("double")).isNull(); + assertThat(config.getCommaSeparatedValues("list")).isEmpty(); + assertThat(config.getCommaSeparatedMap("map")).isEmpty(); + assertThat(config.getDuration("duration")).isNull(); + } + + @Test + void allEmpty() { + Map properties = new HashMap<>(); + properties.put("string", ""); + properties.put("int", ""); + properties.put("long", ""); + properties.put("double", ""); + properties.put("list", ""); + properties.put("map", ""); + properties.put("duration", ""); + + ConfigProperties config = ConfigProperties.createForTest(properties); + assertThat(config.getString("string")).isEmpty(); + assertThat(config.getInt("int")).isNull(); + assertThat(config.getLong("long")).isNull(); + assertThat(config.getDouble("double")).isNull(); + assertThat(config.getCommaSeparatedValues("list")).isEmpty(); + assertThat(config.getCommaSeparatedMap("map")).isEmpty(); + assertThat(config.getDuration("duration")).isNull(); + } + + @Test + void invalidInt() { + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("int", "bar")) + .getInt("int")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid value for property int=bar. Must be a integer."); + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("int", "999999999999999")) + .getInt("int")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid value for property int=999999999999999. Must be a integer."); + } + + @Test + void invalidLong() { + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("long", "bar")) + .getLong("long")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid value for property long=bar. Must be a long."); + assertThatThrownBy( + () -> + ConfigProperties.createForTest( + Collections.singletonMap("long", "99223372036854775807")) + .getLong("long")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid value for property long=99223372036854775807. Must be a long."); + } + + @Test + void invalidDouble() { + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("double", "bar")) + .getDouble("double")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid value for property double=bar. Must be a double."); + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("double", "1.0.1")) + .getDouble("double")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid value for property double=1.0.1. Must be a double."); + } + + @Test + void uncleanList() { + assertThat( + ConfigProperties.createForTest( + Collections.singletonMap("list", " a ,b,c , d,, ,")) + .getCommaSeparatedValues("list")) + .containsExactly("a", "b", "c", "d"); + } + + @Test + void uncleanMap() { + assertThat( + ConfigProperties.createForTest( + Collections.singletonMap("map", " a=1 ,b=2,c = 3 , d= 4,, ,")) + .getCommaSeparatedMap("map")) + .containsExactly(entry("a", "1"), entry("b", "2"), entry("c", "3"), entry("d", "4")); + } + + @Test + void invalidMap() { + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("map", "a=1,b=")) + .getCommaSeparatedMap("map")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid map property: map=a=1,b="); + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("map", "a=1,b")) + .getCommaSeparatedMap("map")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid map property: map=a=1,b"); + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("map", "a=1,=b")) + .getCommaSeparatedMap("map")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid map property: map=a=1,=b"); + } + + @Test + void invalidDuration() { + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("duration", "1a1ms")) + .getDuration("duration")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid duration property duration=1a1ms. Expected number, found: 1a1"); + assertThatThrownBy( + () -> + ConfigProperties.createForTest(Collections.singletonMap("duration", "9mm")) + .getDuration("duration")) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid duration property duration=9mm. Invalid duration string, found: mm"); + } + + @Test + void durationUnitParsing() { + assertThat( + ConfigProperties.createForTest(Collections.singletonMap("duration", "1")) + .getDuration("duration")) + .isEqualTo(Duration.ofMillis(1)); + assertThat( + ConfigProperties.createForTest(Collections.singletonMap("duration", "2ms")) + .getDuration("duration")) + .isEqualTo(Duration.ofMillis(2)); + assertThat( + ConfigProperties.createForTest(Collections.singletonMap("duration", "3s")) + .getDuration("duration")) + .isEqualTo(Duration.ofSeconds(3)); + assertThat( + ConfigProperties.createForTest(Collections.singletonMap("duration", "4m")) + .getDuration("duration")) + .isEqualTo(Duration.ofMinutes(4)); + assertThat( + ConfigProperties.createForTest(Collections.singletonMap("duration", "5h")) + .getDuration("duration")) + .isEqualTo(Duration.ofHours(5)); + assertThat( + ConfigProperties.createForTest(Collections.singletonMap("duration", "6d")) + .getDuration("duration")) + .isEqualTo(Duration.ofDays(6)); + // Check Space handling + assertThat( + ConfigProperties.createForTest(Collections.singletonMap("duration", "7 ms")) + .getDuration("duration")) + .isEqualTo(Duration.ofMillis(7)); + assertThat( + ConfigProperties.createForTest(Collections.singletonMap("duration", "8 ms")) + .getDuration("duration")) + .isEqualTo(Duration.ofMillis(8)); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResourceTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResourceTest.java new file mode 100644 index 000000000..920f6186d --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/EnvironmentResourceTest.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Test; + +class EnvironmentResourceTest { + + @Test + void get() { + assertThat(EnvironmentResource.get()).isNotNull(); + } + + @Test + void resourceFromConfig_empty() { + Attributes attributes = + EnvironmentResource.getAttributes(ConfigProperties.createForTest(emptyMap())); + + assertThat(attributes).isEmpty(); + } + + @Test + void resourceFromConfig() { + Attributes attributes = + EnvironmentResource.getAttributes( + ConfigProperties.createForTest( + singletonMap( + EnvironmentResource.ATTRIBUTE_PROPERTY, + "service.name=myService,appName=MyApp"))); + + assertThat(attributes) + .hasSize(2) + .containsEntry(ResourceAttributes.SERVICE_NAME, "myService") + .containsEntry("appName", "MyApp"); + } + + @Test + void serviceName() { + Attributes attributes = + EnvironmentResource.getAttributes( + ConfigProperties.createForTest( + singletonMap(EnvironmentResource.SERVICE_NAME_PROPERTY, "myService"))); + + assertThat(attributes).hasSize(1).containsEntry(ResourceAttributes.SERVICE_NAME, "myService"); + } + + @Test + void resourceFromConfig_overrideServiceName() { + Attributes attributes = + EnvironmentResource.getAttributes( + ConfigProperties.createForTest( + ImmutableMap.of( + EnvironmentResource.ATTRIBUTE_PROPERTY, + "service.name=myService,appName=MyApp", + EnvironmentResource.SERVICE_NAME_PROPERTY, + "ReallyMyService"))); + + assertThat(attributes) + .hasSize(2) + .containsEntry(ResourceAttributes.SERVICE_NAME, "ReallyMyService") + .containsEntry("appName", "MyApp"); + } + + @Test + void resourceFromConfig_emptyEnvVar() { + Attributes attributes = + EnvironmentResource.getAttributes( + ConfigProperties.createForTest( + singletonMap(EnvironmentResource.ATTRIBUTE_PROPERTY, ""))); + + assertThat(attributes).isEmpty(); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/NotOnClasspathTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/NotOnClasspathTest.java new file mode 100644 index 000000000..f047c6714 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/NotOnClasspathTest.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class NotOnClasspathTest { + + private static final ConfigProperties EMPTY = + ConfigProperties.createForTest(Collections.emptyMap()); + + @Test + void otlpSpans() { + assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("otlp", EMPTY)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining( + "OTLP Trace Exporter enabled but opentelemetry-exporter-otlp not found on " + + "classpath"); + } + + @Test + void jaeger() { + assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("jaeger", EMPTY)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining( + "Jaeger gRPC Exporter enabled but opentelemetry-exporter-jaeger not found on " + + "classpath"); + } + + @Test + void zipkin() { + assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("zipkin", EMPTY)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining( + "Zipkin Exporter enabled but opentelemetry-exporter-zipkin not found on classpath"); + } + + @Test + void logging() { + assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("logging", EMPTY)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining( + "Logging Trace Exporter enabled but opentelemetry-exporter-logging not found on " + + "classpath"); + } + + @Test + void logging_metrics() { + assertThatThrownBy( + () -> + MetricExporterConfiguration.configureExporter( + "logging", EMPTY, SdkMeterProvider.builder().build())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining( + "Logging Metrics Exporter enabled but opentelemetry-exporter-logging not found on " + + "classpath"); + } + + @Test + void otlpMetrics() { + assertThatCode( + () -> + MetricExporterConfiguration.configureExporter( + "otlp", EMPTY, SdkMeterProvider.builder().build())) + .doesNotThrowAnyException(); + } + + @Test + void prometheus() { + assertThatThrownBy( + () -> + MetricExporterConfiguration.configureExporter( + "prometheus", EMPTY, SdkMeterProvider.builder().build())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining( + "Prometheus Metrics Server enabled but opentelemetry-exporter-prometheus not found on " + + "classpath"); + } + + @Test + void b3propagator() { + assertThatThrownBy( + () -> + PropagatorConfiguration.configurePropagators( + ConfigProperties.createForTest( + Collections.singletonMap("otel.propagators", "b3")))) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("Unrecognized value for otel.propagators: b3"); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/PropagatorConfigurationTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/PropagatorConfigurationTest.java new file mode 100644 index 000000000..069861c71 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/PropagatorConfigurationTest.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.propagation.ContextPropagators; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class PropagatorConfigurationTest { + + @Test + void defaultPropagators() { + ContextPropagators contextPropagators = + PropagatorConfiguration.configurePropagators( + ConfigProperties.createForTest(Collections.emptyMap())); + + assertThat(contextPropagators.getTextMapPropagator().fields()) + .containsExactlyInAnyOrder("traceparent", "tracestate", "baggage"); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceTest.java new file mode 100644 index 000000000..68c017ce2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.Test; + +class ResourceTest { + + @Test + void noResourceProviders() { + assertThat(OpenTelemetrySdkAutoConfiguration.getResource()).isEqualTo(Resource.getDefault()); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/TracerProviderConfigurationTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/TracerProviderConfigurationTest.java new file mode 100644 index 000000000..04dfb55dc --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/TracerProviderConfigurationTest.java @@ -0,0 +1,197 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.sdk.trace.internal.JcTools; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +// NB: We use AssertJ extracting to reflectively access implementation details to test configuration +// because the use of BatchSpanProcessor makes it difficult to verify values through public means. +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class TracerProviderConfigurationTest { + + private static final ConfigProperties EMPTY = + ConfigProperties.createForTest(Collections.emptyMap()); + + @Mock private SpanExporter mockSpanExporter; + + @BeforeEach + void setUp() { + when(mockSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + } + + @Test + void configureTracerProvider() { + Map properties = new HashMap<>(); + properties.put("otel.bsp.schedule.delay", "100000"); + properties.put("otel.traces.sampler", "always_off"); + properties.put("otel.traces.exporter", "none"); + + Resource resource = Resource.create(Attributes.builder().put("cat", "meow").build()); + // We don't have any exporters on classpath for this test so check no-op case. Exporter cases + // are verified in other test sets like testFullConfig. + SdkTracerProvider tracerProvider = + TracerProviderConfiguration.configureTracerProvider( + resource, ConfigProperties.createForTest(properties)); + try { + assertThat(tracerProvider.getSampler()).isEqualTo(Sampler.alwaysOff()); + + assertThat(tracerProvider) + .extracting("sharedState") + .satisfies( + sharedState -> { + assertThat(sharedState).extracting("resource").isEqualTo(resource); + assertThat(sharedState) + .extracting("activeSpanProcessor") + .isEqualTo(SpanProcessor.composite()); + }); + } finally { + tracerProvider.shutdown(); + } + } + + @Test + void configureSpanProcessor_empty() { + BatchSpanProcessor processor = + TracerProviderConfiguration.configureSpanProcessor(EMPTY, mockSpanExporter); + + try { + assertThat(processor) + .extracting("worker") + .satisfies( + worker -> { + assertThat(worker) + .extracting("scheduleDelayNanos") + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(5000)); + assertThat(worker) + .extracting("exporterTimeoutNanos") + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(30000)); + assertThat(worker).extracting("maxExportBatchSize").isEqualTo(512); + assertThat(worker) + .extracting("queue") + .isInstanceOfSatisfying( + Queue.class, queue -> assertThat(JcTools.capacity(queue)).isEqualTo(2048)); + assertThat(worker).extracting("spanExporter").isEqualTo(mockSpanExporter); + }); + } finally { + processor.shutdown(); + } + } + + @Test + void configureSpanProcessor_configured() { + Map properties = new HashMap<>(); + properties.put("otel.bsp.schedule.delay", "100000"); + properties.put("otel.bsp.max.queue.size", "2"); + properties.put("otel.bsp.max.export.batch.size", "3"); + properties.put("otel.bsp.export.timeout", "4"); + + BatchSpanProcessor processor = + TracerProviderConfiguration.configureSpanProcessor( + ConfigProperties.createForTest(properties), mockSpanExporter); + + try { + assertThat(processor) + .extracting("worker") + .satisfies( + worker -> { + assertThat(worker) + .extracting("scheduleDelayNanos") + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(100000)); + assertThat(worker) + .extracting("exporterTimeoutNanos") + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(4)); + assertThat(worker).extracting("maxExportBatchSize").isEqualTo(3); + assertThat(worker) + .extracting("queue") + .isInstanceOfSatisfying( + Queue.class, queue -> assertThat(JcTools.capacity(queue)).isEqualTo(2)); + assertThat(worker).extracting("spanExporter").isEqualTo(mockSpanExporter); + }); + } finally { + processor.shutdown(); + } + } + + @Test + void configureTraceConfig_empty() { + assertThat(TracerProviderConfiguration.configureSpanLimits(EMPTY)) + .isEqualTo(SpanLimits.getDefault()); + } + + @Test + void configureTraceConfig_full() { + + Map properties = new HashMap<>(); + properties.put("otel.traces.sampler", "always_off"); + properties.put("otel.span.attribute.count.limit", "5"); + properties.put("otel.span.event.count.limit", "4"); + properties.put("otel.span.link.count.limit", "3"); + + SpanLimits config = + TracerProviderConfiguration.configureSpanLimits(ConfigProperties.createForTest(properties)); + assertThat(config.getMaxNumberOfAttributes()).isEqualTo(5); + assertThat(config.getMaxNumberOfEvents()).isEqualTo(4); + assertThat(config.getMaxNumberOfLinks()).isEqualTo(3); + } + + @Test + void configureSampler() { + assertThat(TracerProviderConfiguration.configureSampler("always_on", EMPTY)) + .isEqualTo(Sampler.alwaysOn()); + assertThat(TracerProviderConfiguration.configureSampler("always_off", EMPTY)) + .isEqualTo(Sampler.alwaysOff()); + assertThat( + TracerProviderConfiguration.configureSampler( + "traceidratio", + ConfigProperties.createForTest( + Collections.singletonMap("otel.traces.sampler.arg", "0.5")))) + .isEqualTo(Sampler.traceIdRatioBased(0.5)); + assertThat(TracerProviderConfiguration.configureSampler("traceidratio", EMPTY)) + .isEqualTo(Sampler.traceIdRatioBased(1.0d)); + assertThat(TracerProviderConfiguration.configureSampler("parentbased_always_on", EMPTY)) + .isEqualTo(Sampler.parentBased(Sampler.alwaysOn())); + assertThat(TracerProviderConfiguration.configureSampler("parentbased_always_off", EMPTY)) + .isEqualTo(Sampler.parentBased(Sampler.alwaysOff())); + assertThat( + TracerProviderConfiguration.configureSampler( + "parentbased_traceidratio", + ConfigProperties.createForTest( + Collections.singletonMap("otel.traces.sampler.arg", "0.4")))) + .isEqualTo(Sampler.parentBased(Sampler.traceIdRatioBased(0.4))); + assertThat(TracerProviderConfiguration.configureSampler("parentbased_traceidratio", EMPTY)) + .isEqualTo(Sampler.parentBased(Sampler.traceIdRatioBased(1.0d))); + + assertThatThrownBy(() -> TracerProviderConfiguration.configureSampler("catsampler", EMPTY)) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Unrecognized value for otel.traces.sampler: catsampler"); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testConfigError/java/io/opentelemetry/sdk/autoconfigure/ConfigErrorTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testConfigError/java/io/opentelemetry/sdk/autoconfigure/ConfigErrorTest.java new file mode 100644 index 000000000..7de0a76b5 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testConfigError/java/io/opentelemetry/sdk/autoconfigure/ConfigErrorTest.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.ContextPropagators; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junitpioneer.jupiter.SetSystemProperty; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; + +// All tests fail due to config errors so never register a global. We can test everything here +// without separating test sets. +class ConfigErrorTest { + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(GlobalOpenTelemetry.class); + + @Test + @SetSystemProperty(key = "otel.propagators", value = "cat") + void invalidPropagator() { + assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize) + .isInstanceOf(ConfigurationException.class) + .hasMessage( + "Unrecognized value for otel.propagators: cat. Make sure the artifact " + + "including the propagator is on the classpath."); + } + + @Test + @SetSystemProperty(key = "otel.traces.sampler", value = "traceidratio") + @SetSystemProperty(key = "otel.traces.sampler.arg", value = "bar") + void invalidTraceIdRatio() { + assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid value for property otel.traces.sampler.arg=bar. Must be a double."); + } + + @Test + @SetSystemProperty(key = "otel.traces.sampler", value = "parentbased_traceidratio") + @SetSystemProperty(key = "otel.traces.sampler.arg", value = "bar") + void invalidTraceIdRatioWithParent() { + assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Invalid value for property otel.traces.sampler.arg=bar. Must be a double."); + } + + @Test + @SetSystemProperty(key = "otel.traces.sampler", value = "cat") + void invalidSampler() { + assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Unrecognized value for otel.traces.sampler: cat"); + } + + @Test + @SetSystemProperty(key = "otel.traces.sampler", value = "traceidratio") + @SetSystemProperty(key = "otel.traces.sampler.arg", value = "bar") + void globalOpenTelemetryWhenError() { + assertThat(GlobalOpenTelemetry.get()) + .isInstanceOf(OpenTelemetry.class) + .extracting("propagators") + // Failed to initialize so is no-op + .isEqualTo(ContextPropagators.noop()); + + LoggingEvent log = + logs.assertContains( + "Error automatically configuring OpenTelemetry SDK. " + + "OpenTelemetry will not be enabled."); + assertThat(log.getLevel()).isEqualTo(Level.ERROR); + assertThat(log.getThrowable()).isInstanceOf(ConfigurationException.class); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSamplerTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSamplerTest.java new file mode 100644 index 000000000..1b1e0474d --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSamplerTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +public class ConfigurableSamplerTest { + + @Test + void configuration() { + ConfigProperties config = + ConfigProperties.createForTest(ImmutableMap.of("test.option", "true")); + Sampler sampler = TracerProviderConfiguration.configureSampler("testSampler", config); + + assertThat(sampler) + .isInstanceOfSatisfying( + TestConfigurableSamplerProvider.TestSampler.class, + s -> assertThat(s.getConfig()).isSameAs(config)); + } + + @Test + void samplerNotFound() { + assertThatThrownBy( + () -> + TracerProviderConfiguration.configureSampler( + "catSampler", ConfigProperties.createForTest(Collections.emptyMap()))) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("catSampler"); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSpanExporterTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSpanExporterTest.java new file mode 100644 index 000000000..251671d2b --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSpanExporterTest.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class ConfigurableSpanExporterTest { + + @Test + void configuration() { + ConfigProperties config = + ConfigProperties.createForTest(ImmutableMap.of("test.option", "true")); + SpanExporter spanExporter = SpanExporterConfiguration.configureExporter("testExporter", config); + + assertThat(spanExporter) + .isInstanceOf(TestConfigurableSpanExporterProvider.TestSpanExporter.class) + .extracting("config") + .isSameAs(config); + } + + @Test + void exporterNotFound() { + assertThatThrownBy( + () -> + SpanExporterConfiguration.configureExporter( + "catExporter", ConfigProperties.createForTest(Collections.emptyMap()))) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("catExporter"); + } + + @Test + void configureSpanProcessor_simpleSpanProcessor() { + String exporterName = "logging"; + Map propMap = Collections.singletonMap("otel.traces.exporter", exporterName); + SpanExporter exporter = new LoggingSpanExporter(); + ConfigProperties properties = ConfigProperties.createForTest(propMap); + + assertThat( + TracerProviderConfiguration.configureSpanProcessor(properties, exporter, exporterName)) + .isInstanceOf(SimpleSpanProcessor.class); + } + + @Test + void configureSpanProcessor_batchSpanProcessor() { + String exporterName = "zipkin"; + Map propMap = Collections.singletonMap("otel.traces.exporter", exporterName); + SpanExporter exporter = ZipkinSpanExporter.builder().build(); + ConfigProperties properties = ConfigProperties.createForTest(propMap); + + assertThat( + TracerProviderConfiguration.configureSpanProcessor(properties, exporter, exporterName)) + .isInstanceOf(BatchSpanProcessor.class); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/EndpointConfigurationTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/EndpointConfigurationTest.java new file mode 100644 index 000000000..3cc42233c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/EndpointConfigurationTest.java @@ -0,0 +1,190 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collections; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("InterruptedExceptionSwallowed") +class EndpointConfigurationTest { + + private static final BlockingQueue otlpTraceRequests = + new LinkedBlockingDeque<>(); + private static final BlockingQueue otlpMetricsRequests = + new LinkedBlockingDeque<>(); + + @RegisterExtension + public static final ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + // OTLP spans + .addService( + new TraceServiceGrpc.TraceServiceImplBase() { + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + otlpTraceRequests.add(request); + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + // OTLP metrics + .addService( + new MetricsServiceGrpc.MetricsServiceImplBase() { + @Override + public void export( + ExportMetricsServiceRequest request, + StreamObserver responseObserver) { + if (request.getResourceMetricsCount() > 0) { + otlpMetricsRequests.add(request); + } + responseObserver.onNext( + ExportMetricsServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .useBlockingTaskExecutor(true) + .build()); + } + }; + + @BeforeEach + void setUp() { + otlpTraceRequests.clear(); + otlpMetricsRequests.clear(); + GlobalOpenTelemetry.resetForTest(); + } + + @AfterEach + public void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + public void configure() { + SpanExporter spanExporter = + SpanExporterConfiguration.configureExporter( + "otlp", + ConfigProperties.createForTest( + ImmutableMap.of( + "otel.exporter.otlp.traces.endpoint", + "http://localhost:" + server.httpPort()))); + + OtlpGrpcMetricExporter metricExporter = + MetricExporterConfiguration.configureOtlpMetrics( + ConfigProperties.createForTest( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "http://localhost:" + server.httpPort())), + SdkMeterProvider.builder().build()); + + spanExporter.export( + Lists.newArrayList( + TestSpanData.builder() + .setHasEnded(true) + .setName("name") + .setStartEpochNanos(MILLISECONDS.toNanos(System.currentTimeMillis())) + .setEndEpochNanos(MILLISECONDS.toNanos(System.currentTimeMillis())) + .setKind(SpanKind.SERVER) + .setStatus(StatusData.error()) + .setTotalRecordedEvents(0) + .setTotalRecordedLinks(0) + .build())); + + metricExporter.export( + Lists.newArrayList( + MetricData.createLongSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + LongSumData.create( + false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + MILLISECONDS.toNanos(System.currentTimeMillis()), + MILLISECONDS.toNanos(System.currentTimeMillis()), + Labels.of("key", "value"), + 10)))))); + + await() + .untilAsserted( + () -> { + assertThat(otlpTraceRequests).hasSize(1); + assertThat(otlpMetricsRequests).hasSize(1); + }); + } + + @Test + void configuresGlobal() { + // Point "otel.exporter.otlp.endpoint" to wrong endpoint + System.setProperty("otel.exporter.otlp.endpoint", "http://localhost/wrong"); + + // Point "otel.exporter.otlp.traces.endpoint" to correct endpoint + System.setProperty( + "otel.exporter.otlp.traces.endpoint", "http://localhost:" + server.httpPort()); + + // Point "otel.exporter.otlp.metrics.endpoint" to correct endpoint + System.setProperty( + "otel.exporter.otlp.metrics.endpoint", "http://localhost:" + server.httpPort()); + + System.setProperty("otel.exporter.otlp.timeout", "10000"); + + GlobalOpenTelemetry.get().getTracer("test").spanBuilder("test").startSpan().end(); + + await() + .untilAsserted( + () -> { + assertThat(otlpTraceRequests).hasSize(1); + + // Not well defined how many metric exports would have happened by now, check that + // any + // did. The metrics will be BatchSpanProcessor metrics. + assertThat(otlpMetricsRequests).isNotEmpty(); + }); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FullConfigTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FullConfigTest.java new file mode 100644 index 000000000..2e5f3d72c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/FullConfigTest.java @@ -0,0 +1,222 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.linecorp.armeria.common.RequestHeaders; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.ServiceRequestContext; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.server.logging.LoggingService; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.extension.trace.propagation.JaegerPropagator; +import io.opentelemetry.extension.trace.propagation.OtTracePropagator; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("InterruptedExceptionSwallowed") +class FullConfigTest { + + private static final BlockingQueue otlpTraceRequests = + new LinkedBlockingDeque<>(); + private static final BlockingQueue otlpMetricsRequests = + new LinkedBlockingDeque<>(); + + @RegisterExtension + public static final ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + // OTLP spans + .addService( + new TraceServiceGrpc.TraceServiceImplBase() { + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + try { + RequestHeaders headers = + ServiceRequestContext.current().request().headers(); + assertThat(headers.get("cat")).isEqualTo("meow"); + assertThat(headers.get("dog")).isEqualTo("bark"); + } catch (Throwable t) { + responseObserver.onError(t); + return; + } + otlpTraceRequests.add(request); + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + // OTLP metrics + .addService( + new MetricsServiceGrpc.MetricsServiceImplBase() { + @Override + public void export( + ExportMetricsServiceRequest request, + StreamObserver responseObserver) { + try { + RequestHeaders headers = + ServiceRequestContext.current().request().headers(); + assertThat(headers.get("cat")).isEqualTo("meow"); + assertThat(headers.get("dog")).isEqualTo("bark"); + } catch (Throwable t) { + responseObserver.onError(t); + return; + } + if (request.getResourceMetricsCount() > 0) { + otlpMetricsRequests.add(request); + } + responseObserver.onNext( + ExportMetricsServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .useBlockingTaskExecutor(true) + .build()); + sb.decorator(LoggingService.newDecorator()); + } + }; + + @BeforeEach + void setUp() { + otlpTraceRequests.clear(); + otlpMetricsRequests.clear(); + } + + @Test + void configures() throws Exception { + String endpoint = "http://localhost:" + server.httpPort(); + System.setProperty("otel.exporter.otlp.endpoint", endpoint); + System.setProperty("otel.exporter.otlp.timeout", "10000"); + + Collection fields = + GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator().fields(); + List keys = new ArrayList<>(); + keys.addAll(W3CTraceContextPropagator.getInstance().fields()); + keys.addAll(W3CBaggagePropagator.getInstance().fields()); + keys.addAll(B3Propagator.injectingSingleHeader().fields()); + keys.addAll(B3Propagator.injectingMultiHeaders().fields()); + keys.addAll(JaegerPropagator.getInstance().fields()); + keys.addAll(OtTracePropagator.getInstance().fields()); + keys.addAll(AwsXrayPropagator.getInstance().fields()); + // Added by TestPropagatorProvider + keys.add("test"); + assertThat(fields).containsExactlyInAnyOrderElementsOf(keys); + + GlobalOpenTelemetry.get() + .getTracer("test") + .spanBuilder("test") + .startSpan() + .setAttribute("cat", "meow") + .setAttribute("dog", "bark") + .end(); + + await() + .untilAsserted( + () -> { + assertThat(otlpTraceRequests).hasSize(1); + + // Not well defined how many metric exports would have happened by now, check that + // any + // did. The metrics will be BatchSpanProcessor metrics. + assertThat(otlpMetricsRequests).isNotEmpty(); + }); + + ExportTraceServiceRequest traceRequest = otlpTraceRequests.take(); + assertThat(traceRequest.getResourceSpans(0).getResource().getAttributesList()) + .contains( + KeyValue.newBuilder() + .setKey("service.name") + .setValue(AnyValue.newBuilder().setStringValue("test").build()) + .build(), + KeyValue.newBuilder() + .setKey("cat") + .setValue(AnyValue.newBuilder().setStringValue("meow").build()) + .build()); + io.opentelemetry.proto.trace.v1.Span span = + traceRequest.getResourceSpans(0).getInstrumentationLibrarySpans(0).getSpans(0); + // Dog dropped by attribute limit. + assertThat(span.getAttributesList()) + .containsExactlyInAnyOrder( + KeyValue.newBuilder() + .setKey("configured") + .setValue(AnyValue.newBuilder().setBoolValue(true).build()) + .build(), + KeyValue.newBuilder() + .setKey("cat") + .setValue(AnyValue.newBuilder().setStringValue("meow").build()) + .build()); + + ExportMetricsServiceRequest metricRequest = otlpMetricsRequests.take(); + assertThat(metricRequest.getResourceMetrics(0).getResource().getAttributesList()) + .contains( + KeyValue.newBuilder() + .setKey("service.name") + .setValue(AnyValue.newBuilder().setStringValue("test").build()) + .build(), + KeyValue.newBuilder() + .setKey("cat") + .setValue(AnyValue.newBuilder().setStringValue("meow").build()) + .build()); + for (ResourceMetrics resourceMetrics : metricRequest.getResourceMetricsList()) { + for (InstrumentationLibraryMetrics instrumentationLibraryMetrics : + resourceMetrics.getInstrumentationLibraryMetricsList()) { + for (Metric metric : instrumentationLibraryMetrics.getMetricsList()) { + assertThat(getFirstDataPointLabels(metric)) + .contains( + KeyValue.newBuilder() + .setKey("configured") + .setValue(AnyValue.newBuilder().setStringValue("true").build()) + .build()); + } + } + } + } + + private static List getFirstDataPointLabels(Metric metric) { + switch (metric.getDataCase()) { + case GAUGE: + return metric.getGauge().getDataPoints(0).getAttributesList(); + case SUM: + return metric.getSum().getDataPoints(0).getAttributesList(); + case HISTOGRAM: + return metric.getHistogram().getDataPoints(0).getAttributesList(); + case SUMMARY: + return metric.getSummary().getDataPoints(0).getAttributesList(); + default: + throw new IllegalArgumentException( + "Unrecognized metric data case: " + metric.getDataCase().name()); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfigurationTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfigurationTest.java new file mode 100644 index 000000000..3e1ede240 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/MetricExporterConfigurationTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class MetricExporterConfigurationTest { + // Timeout difficult to test using real exports so just check implementation detail here. + @Test + void configureOtlpTimeout() { + OtlpGrpcMetricExporter exporter = + MetricExporterConfiguration.configureOtlpMetrics( + ConfigProperties.createForTest( + ImmutableMap.of( + "otel.exporter.otlp.timeout", "10ms", + "otel.imr.export.interval", "5s")), + SdkMeterProvider.builder().build()); + try { + assertThat(exporter) + .isInstanceOfSatisfying( + OtlpGrpcMetricExporter.class, + otlp -> + assertThat(otlp) + .extracting("timeoutNanos") + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(10L))); + } finally { + exporter.shutdown(); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/NotEnabledConfigurablePropagatorProvider.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/NotEnabledConfigurablePropagatorProvider.java new file mode 100644 index 000000000..c19f51550 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/NotEnabledConfigurablePropagatorProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; +import java.util.Collection; +import java.util.Collections; +import javax.annotation.Nullable; + +public class NotEnabledConfigurablePropagatorProvider implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return new TextMapPropagator() { + @Override + public Collection fields() { + return Collections.singleton("disabled"); + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) { + throw new UnsupportedOperationException(); + } + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public String getName() { + return "disabled"; + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ResourceTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ResourceTest.java new file mode 100644 index 000000000..450dd89ff --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ResourceTest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Test; + +class ResourceTest { + + @Test + void resource() { + Attributes attributes = OpenTelemetrySdkAutoConfiguration.getResource().getAttributes(); + + assertThat(attributes.get(ResourceAttributes.OS_TYPE)).isNotNull(); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotNull(); + + assertThat(attributes.get(ResourceAttributes.PROCESS_PID)).isNotNull(); + assertThat(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)).isNotNull(); + assertThat(attributes.get(ResourceAttributes.PROCESS_COMMAND_LINE)).isNotNull(); + + assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_NAME)).isNotNull(); + assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_VERSION)).isNotNull(); + assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_DESCRIPTION)).isNotNull(); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfigurationTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfigurationTest.java new file mode 100644 index 000000000..3c05675ed --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfigurationTest.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class SpanExporterConfigurationTest { + + // Timeout difficult to test using real exports so just check implementation detail here. + @Test + void configureOtlpTimeout() { + SpanExporter exporter = + SpanExporterConfiguration.configureExporter( + "otlp", + ConfigProperties.createForTest( + Collections.singletonMap("otel.exporter.otlp.timeout", "10"))); + try { + assertThat(exporter) + .isInstanceOfSatisfying( + OtlpGrpcSpanExporter.class, + otlp -> + assertThat(otlp) + .extracting("timeoutNanos") + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(10L))); + } finally { + exporter.shutdown(); + } + } + + // Timeout difficult to test using real exports so just check implementation detail here. + @Test + void configureJaegerTimeout() { + SpanExporter exporter = + SpanExporterConfiguration.configureExporter( + "jaeger", + ConfigProperties.createForTest( + Collections.singletonMap("otel.exporter.jaeger.timeout", "10"))); + try { + assertThat(exporter) + .isInstanceOfSatisfying( + JaegerGrpcSpanExporter.class, + jaeger -> + assertThat(jaeger) + .extracting("timeoutNanos") + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(10L))); + } finally { + exporter.shutdown(); + } + } + + // Timeout difficult to test using real exports so just check that things don't blow up. + @Test + void configureZipkinTimeout() { + SpanExporter exporter = + SpanExporterConfiguration.configureExporter( + "zipkin", + ConfigProperties.createForTest( + Collections.singletonMap("otel.exporter.zipkin.timeout", "5s"))); + try { + assertThat(exporter).isNotNull(); + } finally { + exporter.shutdown(); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurablePropagatorProvider.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurablePropagatorProvider.java new file mode 100644 index 000000000..e06410580 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurablePropagatorProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; +import java.util.Collection; +import java.util.Collections; +import javax.annotation.Nullable; + +public class TestConfigurablePropagatorProvider implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator() { + return new TextMapPropagator() { + @Override + public Collection fields() { + return Collections.singleton("test"); + } + + @Override + public void inject(Context context, @Nullable C carrier, TextMapSetter setter) {} + + @Override + public Context extract(Context context, @Nullable C carrier, TextMapGetter getter) { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public String getName() { + return "test"; + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurableSamplerProvider.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurableSamplerProvider.java new file mode 100644 index 000000000..b38d12630 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurableSamplerProvider.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSamplerProvider; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; + +public class TestConfigurableSamplerProvider implements ConfigurableSamplerProvider { + @Override + public Sampler createSampler(ConfigProperties config) { + return new TestSampler(config); + } + + @Override + public String getName() { + return "testSampler"; + } + + public static class TestSampler implements Sampler { + + private final ConfigProperties config; + + public TestSampler(ConfigProperties config) { + this.config = config; + } + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE); + } + + @Override + public String getDescription() { + return "test"; + } + + public ConfigProperties getConfig() { + return config; + } + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurableSpanExporterProvider.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurableSpanExporterProvider.java new file mode 100644 index 000000000..27bed4f3e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestConfigurableSpanExporterProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; + +public class TestConfigurableSpanExporterProvider implements ConfigurableSpanExporterProvider { + @Override + public SpanExporter createExporter(ConfigProperties config) { + return new TestSpanExporter(config); + } + + @Override + public String getName() { + return "testExporter"; + } + + public static class TestSpanExporter implements SpanExporter { + + private final ConfigProperties config; + + public TestSpanExporter(ConfigProperties config) { + this.config = config; + } + + @Override + public CompletableResultCode export(Collection spans) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + public ConfigProperties getConfig() { + return config; + } + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestMeterProviderConfigurer.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestMeterProviderConfigurer.java new file mode 100644 index 000000000..780a0fe57 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestMeterProviderConfigurer.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.sdk.autoconfigure.spi.SdkMeterProviderConfigurer; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessorFactory; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; + +public class TestMeterProviderConfigurer implements SdkMeterProviderConfigurer { + + @Override + public void configure(SdkMeterProviderBuilder meterProviderBuilder) { + LabelsProcessorFactory labelsProcessorFactory = + (resource, instrumentationLibraryInfo, descriptor) -> + (ctx, labels) -> labels.toBuilder().put("configured", "true").build(); + + for (InstrumentType instrumentType : InstrumentType.values()) { + meterProviderBuilder.registerView( + InstrumentSelector.builder().setInstrumentType(instrumentType).build(), + View.builder() + .setAggregatorFactory(AggregatorFactory.count(AggregationTemporality.DELTA)) + .setLabelsProcessorFactory(labelsProcessorFactory) + .build()); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestTracerProviderConfigurer.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestTracerProviderConfigurer.java new file mode 100644 index 000000000..14e5249dd --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/TestTracerProviderConfigurer.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.SpanProcessor; + +public class TestTracerProviderConfigurer implements SdkTracerProviderConfigurer { + @Override + public void configure(SdkTracerProviderBuilder tracerProvider) { + tracerProvider.addSpanProcessor( + new SpanProcessor() { + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + span.setAttribute("configured", true); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) {} + + @Override + public boolean isEndRequired() { + return false; + } + }); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider new file mode 100644 index 000000000..ca121a39d --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.TestConfigurablePropagatorProvider diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSamplerProvider b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSamplerProvider new file mode 100644 index 000000000..376eef3b0 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSamplerProvider @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.TestConfigurableSamplerProvider diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSpanExporterProvider b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSpanExporterProvider new file mode 100644 index 000000000..3cb293ec3 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSpanExporterProvider @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.TestConfigurableSpanExporterProvider diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkMeterProviderConfigurer b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkMeterProviderConfigurer new file mode 100644 index 000000000..e5df1a9ee --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkMeterProviderConfigurer @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.TestMeterProviderConfigurer diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer new file mode 100644 index 000000000..c4233f932 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testFullConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.SdkTracerProviderConfigurer @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.TestTracerProviderConfigurer diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testInitializeRegistersGlobal/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfigurationTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testInitializeRegistersGlobal/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfigurationTest.java new file mode 100644 index 000000000..76b1aff99 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testInitializeRegistersGlobal/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkAutoConfigurationTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import org.junit.jupiter.api.Test; + +class OpenTelemetrySdkAutoConfigurationTest { + + @Test + void initializeAndGet() { + OpenTelemetrySdk sdk = OpenTelemetrySdkAutoConfiguration.initialize(); + assertThat(GlobalOpenTelemetry.get()).isSameAs(sdk); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testJaeger/java/io/opentelemetry/sdk/autoconfigure/JaegerConfigTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testJaeger/java/io/opentelemetry/sdk/autoconfigure/JaegerConfigTest.java new file mode 100644 index 000000000..7c1b45805 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testJaeger/java/io/opentelemetry/sdk/autoconfigure/JaegerConfigTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.exporter.jaeger.proto.api_v2.Collector; +import io.opentelemetry.exporter.jaeger.proto.api_v2.CollectorServiceGrpc; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JaegerConfigTest { + + private static final BlockingQueue jaegerRequests = + new LinkedBlockingDeque<>(); + + @RegisterExtension + public static final ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + // Jaeger + .addService( + new CollectorServiceGrpc.CollectorServiceImplBase() { + @Override + public void postSpans( + Collector.PostSpansRequest request, + StreamObserver responseObserver) { + jaegerRequests.add(request); + responseObserver.onNext(Collector.PostSpansResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .useBlockingTaskExecutor(true) + .build()); + } + }; + + @BeforeEach + void setUp() { + jaegerRequests.clear(); + } + + @Test + void configures() { + String endpoint = "http://localhost:" + server.httpPort(); + + System.setProperty("otel.exporter.jaeger.endpoint", endpoint); + + OpenTelemetrySdkAutoConfiguration.initialize(); + + GlobalOpenTelemetry.get().getTracer("test").spanBuilder("test").startSpan().end(); + + await().untilAsserted(() -> assertThat(jaegerRequests).hasSize(1)); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testOtlpTls/java/io/opentelemetry/sdk/autoconfigure/OtlpTlsTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testOtlpTls/java/io/opentelemetry/sdk/autoconfigure/OtlpTlsTest.java new file mode 100644 index 000000000..ff55384ef --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testOtlpTls/java/io/opentelemetry/sdk/autoconfigure/OtlpTlsTest.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import java.nio.file.Paths; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OtlpTlsTest { + + private static final BlockingQueue otlpTraceRequests = + new LinkedBlockingDeque<>(); + + @RegisterExtension + @Order(1) + public static final SelfSignedCertificateExtension certificate = + new SelfSignedCertificateExtension(); + + @RegisterExtension + @Order(2) + public static final ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + // OTLP spans + .addService( + new TraceServiceGrpc.TraceServiceImplBase() { + @Override + public void export( + ExportTraceServiceRequest request, + StreamObserver responseObserver) { + otlpTraceRequests.add(request); + responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .useBlockingTaskExecutor(true) + .build()); + + sb.tls(certificate.certificateFile(), certificate.privateKeyFile()); + } + }; + + @BeforeEach + void setUp() { + otlpTraceRequests.clear(); + } + + @Test + void configures() { + String endpoint = "https://localhost:" + server.httpsPort(); + System.setProperty("otel.exporter.otlp.endpoint", endpoint); + System.setProperty("otel.exporter.otlp.timeout", "10000"); + System.setProperty( + "otel.exporter.otlp.certificate", certificate.certificateFile().getAbsolutePath()); + + GlobalOpenTelemetry.get().getTracer("test").spanBuilder("test").startSpan().end(); + + await().untilAsserted(() -> assertThat(otlpTraceRequests).hasSize(1)); + } + + @Test + void invalidCertificatePath() { + String endpoint = "https://localhost:" + server.httpsPort(); + System.setProperty("otel.exporter.otlp.endpoint", endpoint); + System.setProperty("otel.exporter.otlp.timeout", "10000"); + System.setProperty("otel.exporter.otlp.certificate", Paths.get("foo", "bar", "baz").toString()); + + assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize) + .isInstanceOf(ConfigurationException.class); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testPrometheus/java/io/opentelemetry/sdk/autoconfigure/PrometheusTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testPrometheus/java/io/opentelemetry/sdk/autoconfigure/PrometheusTest.java new file mode 100644 index 000000000..50c018f44 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testPrometheus/java/io/opentelemetry/sdk/autoconfigure/PrometheusTest.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.common.Labels; +import java.io.IOException; +import java.net.ServerSocket; +import org.junit.jupiter.api.Test; + +class PrometheusTest { + + @Test + void prometheusExporter() throws Exception { + int port = 9464; + // Just use prometheus standard port if it's available + try (ServerSocket unused = new ServerSocket(port)) { + // Port available + } catch (IOException e) { + // Otherwise use a random port. There's a small race if another process takes it before we + // initialize. Consider adding retries to this test if it flakes, presumably it never will on + // CI since there's no prometheus there blocking the well-known port. + try (ServerSocket socket2 = new ServerSocket(0)) { + port = socket2.getLocalPort(); + } + } + System.setProperty("otel.exporter.prometheus.host", "127.0.0.1"); + System.setProperty("otel.exporter.prometheus.port", String.valueOf(port)); + OpenTelemetrySdkAutoConfiguration.initialize(); + + GlobalMeterProvider.get() + .get("test") + .longValueObserverBuilder("test") + .setUpdater(result -> result.observe(2, Labels.empty())) + .build(); + + WebClient client = WebClient.of("http://127.0.0.1:" + port); + AggregatedHttpResponse response = client.get("/metrics").aggregate().join(); + assertThat(response.contentUtf8()).contains("test 2.0"); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testResourceDisabledByEnv/java/io/opentelemetry/sdk/resources/ResourceDisabledByEnvTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testResourceDisabledByEnv/java/io/opentelemetry/sdk/resources/ResourceDisabledByEnvTest.java new file mode 100644 index 000000000..24b5a4905 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testResourceDisabledByEnv/java/io/opentelemetry/sdk/resources/ResourceDisabledByEnvTest.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Test; + +class ResourceDisabledByEnvTest { + + @Test + void osAndProcessDisabled() { + Resource resource = OpenTelemetrySdkAutoConfiguration.getResource(); + + assertThat(resource.getAttributes().get(ResourceAttributes.OS_TYPE)).isNull(); + assertThat(resource.getAttributes().get(ResourceAttributes.PROCESS_PID)).isNull(); + assertThat(resource.getAttributes().get(ResourceAttributes.PROCESS_RUNTIME_NAME)).isNotNull(); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testResourceDisabledByProperty/java/io/opentelemetry/sdk/resources/ResourceDisabledByPropertyTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testResourceDisabledByProperty/java/io/opentelemetry/sdk/resources/ResourceDisabledByPropertyTest.java new file mode 100644 index 000000000..690fe3549 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testResourceDisabledByProperty/java/io/opentelemetry/sdk/resources/ResourceDisabledByPropertyTest.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Test; + +class ResourceDisabledByPropertyTest { + + @Test + void osAndProcessDisabled() { + Resource resource = OpenTelemetrySdkAutoConfiguration.getResource(); + + assertThat(resource.getAttributes().get(ResourceAttributes.OS_TYPE)).isNull(); + assertThat(resource.getAttributes().get(ResourceAttributes.PROCESS_PID)).isNull(); + assertThat(resource.getAttributes().get(ResourceAttributes.PROCESS_RUNTIME_NAME)).isNotNull(); + } +} diff --git a/opentelemetry-java/sdk-extensions/autoconfigure/src/testZipkin/java/io/opentelemetry/sdk/autoconfigure/ZipkinConfigTest.java b/opentelemetry-java/sdk-extensions/autoconfigure/src/testZipkin/java/io/opentelemetry/sdk/autoconfigure/ZipkinConfigTest.java new file mode 100644 index 000000000..24f6ba8a0 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/autoconfigure/src/testZipkin/java/io/opentelemetry/sdk/autoconfigure/ZipkinConfigTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.opentelemetry.api.GlobalOpenTelemetry; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ZipkinConfigTest { + + private static final BlockingQueue zipkinJsonRequests = new LinkedBlockingDeque<>(); + + @RegisterExtension + public static final ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + // Zipkin + sb.service( + "/api/v2/spans", + (ctx, req) -> + HttpResponse.from( + req.aggregate() + .thenApply( + aggRes -> { + zipkinJsonRequests.add(aggRes.contentUtf8()); + return HttpResponse.of(HttpStatus.OK); + }))); + } + }; + + @BeforeEach + void setUp() { + zipkinJsonRequests.clear(); + } + + @Test + void configures() { + String endpoint = "localhost:" + server.httpPort(); + + System.setProperty("otel.exporter.zipkin.endpoint", "http://" + endpoint + "/api/v2/spans"); + System.setProperty("otel.exporter.zipkin.timeout", "5s"); + + OpenTelemetrySdkAutoConfiguration.initialize(); + + GlobalOpenTelemetry.get().getTracer("test").spanBuilder("test").startSpan().end(); + + await().untilAsserted(() -> assertThat(zipkinJsonRequests).hasSize(1)); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/README.md b/opentelemetry-java/sdk-extensions/aws/README.md new file mode 100644 index 000000000..2698bb3a7 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/README.md @@ -0,0 +1,12 @@ +# OpenTelemetry AWS Utils + +[![Javadocs][javadoc-image]][javadoc-url] + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-sdk-aws.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-aws + +--- +#### Running micro-benchmarks +From the root of the repo run `./gradlew clean :opentelemetry-sdk-extension-aws:jmh` to run all the benchmarks +or run `./gradlew clean :opentelemetry-sdk-extension-aws:jmh -PjmhIncludeSingleClass=` +to run a specific benchmark class. \ No newline at end of file diff --git a/opentelemetry-java/sdk-extensions/aws/build.gradle.kts b/opentelemetry-java/sdk-extensions/aws/build.gradle.kts new file mode 100644 index 000000000..e921ed116 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `java-library` + `maven-publish` + + id("me.champeau.jmh") +} + +description = "OpenTelemetry SDK AWS Instrumentation Support" +extra["moduleName"] = "io.opentelemetry.sdk.extension.trace.aws" + +dependencies { + api(project(":api:all")) + api(project(":sdk:all")) + + compileOnly(project(":sdk-extensions:autoconfigure")) + + implementation(project(":semconv")) + + implementation("com.fasterxml.jackson.core:jackson-core") + implementation("com.fasterxml.jackson.core:jackson-databind") + + testImplementation(project(":sdk-extensions:autoconfigure")) + + testImplementation("com.linecorp.armeria:armeria-junit5") + testImplementation("com.google.guava:guava") +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/jmh/java/io/opentelemetry/sdk/extension/aws/AwsXrayIdGeneratorBenchmark.java b/opentelemetry-java/sdk-extensions/aws/src/jmh/java/io/opentelemetry/sdk/extension/aws/AwsXrayIdGeneratorBenchmark.java new file mode 100644 index 000000000..93b1147e7 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/jmh/java/io/opentelemetry/sdk/extension/aws/AwsXrayIdGeneratorBenchmark.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws; + +import io.opentelemetry.sdk.extension.aws.trace.AwsXrayIdGenerator; +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class AwsXrayIdGeneratorBenchmark { + private final AwsXrayIdGenerator idGenerator = AwsXrayIdGenerator.getInstance(); + + @Benchmark + @Measurement(iterations = 15, time = 1) + @Warmup(iterations = 5, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @BenchmarkMode(Mode.AverageTime) + @Fork(1) + public String generateTraceId() { + return idGenerator.generateTraceId(); + } + + @Benchmark + @Measurement(iterations = 15, time = 1) + @Warmup(iterations = 5, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @BenchmarkMode(Mode.AverageTime) + @Fork(1) + public String generateSpanId() { + return idGenerator.generateSpanId(); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/AwsResourceConstants.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/AwsResourceConstants.java new file mode 100644 index 000000000..6c0f5c4a7 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/AwsResourceConstants.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +final class AwsResourceConstants { + + static String cloudProvider() { + return "aws"; + } + + private AwsResourceConstants() {} +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResource.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResource.java new file mode 100644 index 000000000..99fbee415 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResource.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A factory for a {@link Resource} which provides information about the current EC2 instance if + * running on AWS Elastic Beanstalk. + */ +public final class BeanstalkResource { + + private static final Logger logger = Logger.getLogger(BeanstalkResource.class.getName()); + + private static final String DEVELOPMENT_ID = "deployment_id"; + private static final String VERSION_LABEL = "version_label"; + private static final String ENVIRONMENT_NAME = "environment_name"; + private static final String BEANSTALK_CONF_PATH = "/var/elasticbeanstalk/xray/environment.conf"; + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a factory for a {@link Resource} which provides information about the current EC2 + * instance if running on AWS Elastic Beanstalk. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + return buildResource(BEANSTALK_CONF_PATH); + } + + // Visible for testing + static Resource buildResource(String configPath) { + File configFile = new File(configPath); + if (!configFile.exists()) { + return Resource.empty(); + } + + AttributesBuilder attrBuilders = Attributes.builder(); + try (JsonParser parser = JSON_FACTORY.createParser(configFile)) { + parser.nextToken(); + + if (!parser.isExpectedStartObjectToken()) { + logger.log(Level.WARNING, "Invalid Beanstalk config: ", configPath); + return Resource.create(attrBuilders.build()); + } + + while (parser.nextToken() != JsonToken.END_OBJECT) { + parser.nextValue(); + String value = parser.getText(); + switch (parser.getCurrentName()) { + case DEVELOPMENT_ID: + attrBuilders.put(ResourceAttributes.SERVICE_INSTANCE_ID, value); + break; + case VERSION_LABEL: + attrBuilders.put(ResourceAttributes.SERVICE_VERSION, value); + break; + case ENVIRONMENT_NAME: + attrBuilders.put(ResourceAttributes.SERVICE_NAMESPACE, value); + break; + default: + parser.skipChildren(); + } + } + } catch (IOException e) { + logger.log(Level.WARNING, "Could not parse Beanstalk config.", e); + return Resource.empty(); + } + + attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, AwsResourceConstants.cloudProvider()); + + return Resource.create(attrBuilders.build()); + } + + private BeanstalkResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResourceProvider.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResourceProvider.java new file mode 100644 index 000000000..60df392e8 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link BeanstalkResource}. */ +public final class BeanstalkResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return BeanstalkResource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/DockerHelper.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/DockerHelper.java new file mode 100644 index 000000000..72206ec01 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/DockerHelper.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +class DockerHelper { + + private static final Logger logger = Logger.getLogger(DockerHelper.class.getName()); + private static final int CONTAINER_ID_LENGTH = 64; + private static final String DEFAULT_CGROUP_PATH = "/proc/self/cgroup"; + + private final String cgroupPath; + + DockerHelper() { + this(DEFAULT_CGROUP_PATH); + } + + // Visible for testing + DockerHelper(String cgroupPath) { + this.cgroupPath = cgroupPath; + } + + /** + * Get docker container id from local cgroup file. + * + * @return docker container ID. Empty string if it can`t be found. + */ + @SuppressWarnings("DefaultCharset") + public String getContainerId() { + try (BufferedReader br = new BufferedReader(new FileReader(cgroupPath))) { + String line; + while ((line = br.readLine()) != null) { + if (line.length() > CONTAINER_ID_LENGTH) { + return line.substring(line.length() - CONTAINER_ID_LENGTH); + } + } + } catch (FileNotFoundException e) { + logger.log(Level.WARNING, "Failed to read container id, cgroup file does not exist."); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to read container id: " + e.getMessage()); + } + + return ""; + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/Ec2Resource.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/Ec2Resource.java new file mode 100644 index 000000000..f5d8ce502 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/Ec2Resource.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A factory for a {@link Resource} which provides information about the current EC2 instance if + * running on AWS EC2. + */ +public final class Ec2Resource { + + private static final Logger logger = Logger.getLogger(Ec2Resource.class.getName()); + + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + + private static final String DEFAULT_IMDS_ENDPOINT = "169.254.169.254"; + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a @link Resource} which provides information about the current EC2 instance if running + * on AWS EC2. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + // This property is only for testing e.g., with a mock IMDS server and never in production so we + // just + // read from a system property. This is similar to the AWS SDK. + return buildResource( + System.getProperty("otel.aws.imds.endpointOverride", DEFAULT_IMDS_ENDPOINT)); + } + + // Visible for testing + static Resource buildResource(String endpoint) { + String urlBase = "http://" + endpoint; + final URL identityDocumentUrl; + final URL hostnameUrl; + final URL tokenUrl; + try { + identityDocumentUrl = new URL(urlBase + "/latest/dynamic/instance-identity/document"); + hostnameUrl = new URL(urlBase + "/latest/meta-data/hostname"); + tokenUrl = new URL(urlBase + "/latest/api/token"); + } catch (MalformedURLException e) { + // Can only happen when overriding the endpoint in testing so just throw. + throw new IllegalArgumentException("Illegal endpoint: " + endpoint, e); + } + + String token = fetchToken(tokenUrl); + + // If token is empty, either IMDSv2 isn't enabled or an unexpected failure happened. We can + // still get data if IMDSv1 is enabled. + String identity = fetchIdentity(identityDocumentUrl, token); + if (identity.isEmpty()) { + // If no identity document, assume we are not actually running on EC2. + return Resource.empty(); + } + + String hostname = fetchHostname(hostnameUrl, token); + + AttributesBuilder attrBuilders = Attributes.builder(); + attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, AwsResourceConstants.cloudProvider()); + + try (JsonParser parser = JSON_FACTORY.createParser(identity)) { + parser.nextToken(); + + if (!parser.isExpectedStartObjectToken()) { + throw new IOException("Invalid JSON:" + identity); + } + + while (parser.nextToken() != JsonToken.END_OBJECT) { + String value = parser.nextTextValue(); + switch (parser.getCurrentName()) { + case "instanceId": + attrBuilders.put(ResourceAttributes.HOST_ID, value); + break; + case "availabilityZone": + attrBuilders.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, value); + break; + case "instanceType": + attrBuilders.put(ResourceAttributes.HOST_TYPE, value); + break; + case "imageId": + attrBuilders.put(ResourceAttributes.HOST_IMAGE_ID, value); + break; + case "accountId": + attrBuilders.put(ResourceAttributes.CLOUD_ACCOUNT_ID, value); + break; + case "region": + attrBuilders.put(ResourceAttributes.CLOUD_REGION, value); + break; + default: + parser.skipChildren(); + } + } + } catch (IOException e) { + logger.log(Level.WARNING, "Could not parse identity document, resource not filled.", e); + return Resource.empty(); + } + + attrBuilders.put(ResourceAttributes.HOST_NAME, hostname); + + return Resource.create(attrBuilders.build()); + } + + private static String fetchToken(URL tokenUrl) { + return fetchString("PUT", tokenUrl, "", /* includeTtl= */ true); + } + + private static String fetchIdentity(URL identityDocumentUrl, String token) { + return fetchString("GET", identityDocumentUrl, token, /* includeTtl= */ false); + } + + private static String fetchHostname(URL hostnameUrl, String token) { + return fetchString("GET", hostnameUrl, token, /* includeTtl= */ false); + } + + // Generic HTTP fetch function for IMDS. + private static String fetchString(String httpMethod, URL url, String token, boolean includeTtl) { + JdkHttpClient client = new JdkHttpClient(); + Map headers = new HashMap<>(); + + if (includeTtl) { + headers.put("X-aws-ec2-metadata-token-ttl-seconds", "60"); + } + if (!token.isEmpty()) { + headers.put("X-aws-ec2-metadata-token", token); + } + + return client.fetchString(httpMethod, url.toString(), headers, null); + } + + private Ec2Resource() {} +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/Ec2ResourceProvider.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/Ec2ResourceProvider.java new file mode 100644 index 000000000..a3b02429a --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/Ec2ResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link Ec2Resource}. */ +public final class Ec2ResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return Ec2Resource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResource.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResource.java new file mode 100644 index 000000000..d97d9544f --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResource.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A factory for a {@link Resource} which provides information about the current ECS container if + * running on AWS ECS. + */ +public final class EcsResource { + private static final Logger logger = Logger.getLogger(EcsResource.class.getName()); + + private static final String ECS_METADATA_KEY_V4 = "ECS_CONTAINER_METADATA_URI_V4"; + private static final String ECS_METADATA_KEY_V3 = "ECS_CONTAINER_METADATA_URI"; + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a factory for a {@link Resource} which provides information about the current ECS + * container if running on AWS ECS. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + return buildResource(System.getenv(), new DockerHelper()); + } + + // Visible for testing + static Resource buildResource(Map sysEnv, DockerHelper dockerHelper) { + if (!isOnEcs(sysEnv)) { + return Resource.empty(); + } + + AttributesBuilder attrBuilders = Attributes.builder(); + attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, AwsResourceConstants.cloudProvider()); + try { + String hostName = InetAddress.getLocalHost().getHostName(); + attrBuilders.put(ResourceAttributes.CONTAINER_NAME, hostName); + } catch (UnknownHostException e) { + logger.log(Level.WARNING, "Could not get docker container name from hostname.", e); + } + + String containerId = dockerHelper.getContainerId(); + if (containerId != null && !containerId.isEmpty()) { + attrBuilders.put(ResourceAttributes.CONTAINER_ID, containerId); + } + + return Resource.create(attrBuilders.build()); + } + + private static boolean isOnEcs(Map sysEnv) { + return !sysEnv.getOrDefault(ECS_METADATA_KEY_V3, "").isEmpty() + || !sysEnv.getOrDefault(ECS_METADATA_KEY_V4, "").isEmpty(); + } + + private EcsResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceProvider.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceProvider.java new file mode 100644 index 000000000..7fc69954c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link EcsResource}. */ +public final class EcsResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return EcsResource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EksResource.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EksResource.java new file mode 100644 index 000000000..b0bd7a56f --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EksResource.java @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A factory for a {@link Resource} which provides information about the current ECS container if + * running on AWS EKS. + */ +public final class EksResource { + private static final Logger logger = Logger.getLogger(EksResource.class.getName()); + + static final String K8S_SVC_URL = "https://kubernetes.default.svc"; + static final String AUTH_CONFIGMAP_PATH = "/api/v1/namespaces/kube-system/configmaps/aws-auth"; + static final String CW_CONFIGMAP_PATH = + "/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info"; + private static final String K8S_TOKEN_PATH = + "/var/run/secrets/kubernetes.io/serviceaccount/token"; + private static final String K8S_CERT_PATH = + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a factory for a {@link Resource} which provides information about the current ECS + * container if running on AWS EKS. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + return buildResource(new JdkHttpClient(), new DockerHelper(), K8S_TOKEN_PATH, K8S_CERT_PATH); + } + + // Visible for testing + static Resource buildResource( + JdkHttpClient jdkHttpClient, + DockerHelper dockerHelper, + String k8sTokenPath, + String k8sKeystorePath) { + if (!isEks(k8sTokenPath, k8sKeystorePath, jdkHttpClient)) { + return Resource.empty(); + } + + AttributesBuilder attrBuilders = Attributes.builder(); + + String clusterName = getClusterName(jdkHttpClient); + if (clusterName != null && !clusterName.isEmpty()) { + attrBuilders.put(ResourceAttributes.K8S_CLUSTER_NAME, clusterName); + } + + String containerId = dockerHelper.getContainerId(); + if (containerId != null && !containerId.isEmpty()) { + attrBuilders.put(ResourceAttributes.CONTAINER_ID, containerId); + } + + return Resource.create(attrBuilders.build()); + } + + private static boolean isEks( + String k8sTokenPath, String k8sKeystorePath, JdkHttpClient jdkHttpClient) { + if (!isK8s(k8sTokenPath, k8sKeystorePath)) { + logger.log(Level.FINE, "Not running on k8s."); + return false; + } + + Map requestProperties = new HashMap<>(); + requestProperties.put("Authorization", getK8sCredHeader()); + String awsAuth = + jdkHttpClient.fetchString( + "GET", K8S_SVC_URL + AUTH_CONFIGMAP_PATH, requestProperties, K8S_CERT_PATH); + + return awsAuth != null && !awsAuth.isEmpty(); + } + + private static boolean isK8s(String k8sTokenPath, String k8sKeystorePath) { + File k8sTokeyFile = new File(k8sTokenPath); + File k8sKeystoreFile = new File(k8sKeystorePath); + return k8sTokeyFile.exists() && k8sKeystoreFile.exists(); + } + + private static String getClusterName(JdkHttpClient jdkHttpClient) { + Map requestProperties = new HashMap<>(); + requestProperties.put("Authorization", getK8sCredHeader()); + String json = + jdkHttpClient.fetchString( + "GET", K8S_SVC_URL + CW_CONFIGMAP_PATH, requestProperties, K8S_CERT_PATH); + + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readTree(json).at("/data/cluster.name").asText(); + } catch (JsonProcessingException e) { + logger.log(Level.WARNING, "Can't get cluster name on EKS.", e); + } + return ""; + } + + private static String getK8sCredHeader() { + try { + String content = + new String(Files.readAllBytes(Paths.get(K8S_TOKEN_PATH)), StandardCharsets.UTF_8); + return "Bearer " + content; + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to load K8s client token.", e); + } + return ""; + } + + private EksResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EksResourceProvider.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EksResourceProvider.java new file mode 100644 index 000000000..a65335a77 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EksResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link EksResource}. */ +public final class EksResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return EksResource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/JdkHttpClient.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/JdkHttpClient.java new file mode 100644 index 000000000..771d9068d --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/JdkHttpClient.java @@ -0,0 +1,152 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Collection; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +class JdkHttpClient { + + private static final Logger logger = Logger.getLogger(JdkHttpClient.class.getName()); + + private static final int TIMEOUT_MILLIS = 2000; + + String fetchString( + String httpMethod, + String urlStr, + Map requestPropertyMap, + @Nullable String certPath) { + final HttpURLConnection connection; + + try { + if (urlStr.startsWith("https")) { + connection = (HttpURLConnection) new URL(urlStr).openConnection(); + KeyStore keyStore = getKeystoreForTrustedCert(certPath); + if (keyStore != null) { + ((HttpsURLConnection) connection).setSSLSocketFactory(buildSslSocketFactory(keyStore)); + } + } else { + connection = (HttpURLConnection) new URL(urlStr).openConnection(); + } + + connection.setRequestMethod(httpMethod); + connection.setConnectTimeout(TIMEOUT_MILLIS); + connection.setReadTimeout(TIMEOUT_MILLIS); + + for (Map.Entry requestProperty : requestPropertyMap.entrySet()) { + connection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); + } + + int responseCode = connection.getResponseCode(); + if (responseCode != 200) { + logger.log( + Level.FINE, + "Error reponse from " + + urlStr + + " code (" + + responseCode + + ") text " + + readResponseString(connection)); + return ""; + } + + return readResponseString(connection).trim(); + + } catch (IOException e) { + logger.log(Level.FINE, "JdkHttpClient fetch string failed.", e); + } + + return ""; + } + + private static String readResponseString(HttpURLConnection connection) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (InputStream is = connection.getInputStream()) { + readTo(is, os); + } catch (IOException e) { + // Only best effort read if we can. + } + try (InputStream is = connection.getErrorStream()) { + if (is != null) { + readTo(is, os); + } + } catch (IOException e) { + // Only best effort read if we can. + } + try { + return os.toString(StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + logger.log(Level.WARNING, "UTF-8 not supported can't happen.", e); + } + return ""; + } + + private static SSLSocketFactory buildSslSocketFactory(KeyStore keyStore) { + try { + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(keyStore); + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, tmf.getTrustManagers(), null); + return context.getSocketFactory(); + + } catch (Exception e) { + logger.log(Level.WARNING, "Build SslSocketFactory for K8s restful client exception.", e); + } + return null; + } + + private static KeyStore getKeystoreForTrustedCert(String certPath) { + try (FileInputStream fis = new FileInputStream(certPath)) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + Collection certificates = certificateFactory.generateCertificates(fis); + + int i = 0; + for (Certificate certificate : certificates) { + trustStore.setCertificateEntry("cert_" + i, certificate); + i++; + } + return trustStore; + } catch (Exception e) { + logger.log(Level.WARNING, "Cannot load KeyStore from " + certPath); + return null; + } + } + + private static void readTo(@Nullable InputStream is, ByteArrayOutputStream os) + throws IOException { + if (is == null) { + return; + } + byte[] buf = new byte[8192]; + int read; + while ((read = is.read(buf)) > 0) { + os.write(buf, 0, read); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResource.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResource.java new file mode 100644 index 000000000..b53c85414 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResource.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.Map; +import java.util.stream.Stream; + +/** A factory for a {@link Resource} which provides information about the AWS Lambda function. */ +public final class LambdaResource { + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a factory for a {@link Resource} which provides information about the AWS Lambda + * function. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + return buildResource(System.getenv()); + } + + // Visible for testing + static Resource buildResource(Map environmentVariables) { + String region = environmentVariables.getOrDefault("AWS_REGION", ""); + String functionName = environmentVariables.getOrDefault("AWS_LAMBDA_FUNCTION_NAME", ""); + String functionVersion = environmentVariables.getOrDefault("AWS_LAMBDA_FUNCTION_VERSION", ""); + + if (!isLambda(functionName, functionVersion)) { + return Resource.empty(); + } + + AttributesBuilder builder = + Attributes.builder() + .put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS); + + if (!region.isEmpty()) { + builder.put(ResourceAttributes.CLOUD_REGION, region); + } + if (!functionName.isEmpty()) { + builder.put(ResourceAttributes.FAAS_NAME, functionName); + } + if (!functionVersion.isEmpty()) { + builder.put(ResourceAttributes.FAAS_VERSION, functionVersion); + } + + return Resource.create(builder.build()); + } + + private static boolean isLambda(String... envVariables) { + return Stream.of(envVariables).anyMatch(v -> !v.isEmpty()); + } + + private LambdaResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResourceProvider.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResourceProvider.java new file mode 100644 index 000000000..25f0a39ea --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link LambdaResource}. */ +public final class LambdaResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return LambdaResource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/package-info.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/package-info.java new file mode 100644 index 000000000..00f61ac22 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider} implementations for inferring + * resource information for AWS services. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.aws.resource; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/trace/AwsXrayIdGenerator.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/trace/AwsXrayIdGenerator.java new file mode 100644 index 000000000..1805a5676 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/trace/AwsXrayIdGenerator.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.trace; + +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.sdk.trace.IdGenerator; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +/** + * Generates tracing ids compatible with the AWS X-Ray tracing service. In the X-Ray system the + * first 32 bits of the trace id are the Unix epoch time in secords. Spans (AWS calls them segments) + * submit with trace id timestamps outside of the last 30 days are rejected. + * + * @see Generating + * Trace IDs + */ +public final class AwsXrayIdGenerator implements IdGenerator { + + private static final AwsXrayIdGenerator INSTANCE = new AwsXrayIdGenerator(); + + private static final IdGenerator RANDOM_ID_GENERATOR = IdGenerator.random(); + + /** Returns a singleton instance of {@link AwsXrayIdGenerator}. */ + public static AwsXrayIdGenerator getInstance() { + return INSTANCE; + } + + @Override + public String generateSpanId() { + return RANDOM_ID_GENERATOR.generateSpanId(); + } + + @Override + public String generateTraceId() { + // hi - 4 bytes timestamp, 4 bytes random + // low - 8 bytes random. + // Since we include timestamp, impossible to be invalid. + + Random random = ThreadLocalRandom.current(); + long timestampSecs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); + long hiRandom = random.nextInt() & 0xFFFFFFFFL; + + long lowRandom = random.nextLong(); + + return TraceId.fromLongs(timestampSecs << 32 | hiRandom, lowRandom); + } + + private AwsXrayIdGenerator() {} +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/trace/package-info.java b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/trace/package-info.java new file mode 100644 index 000000000..74a856bfc --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/trace/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Provides implementations of SDK interfaces which integrate natively with AWS infrastructure. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.aws.trace; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/aws/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/opentelemetry-java/sdk-extensions/aws/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider new file mode 100644 index 000000000..37eddafc8 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -0,0 +1,5 @@ +io.opentelemetry.sdk.extension.aws.resource.BeanstalkResourceProvider +io.opentelemetry.sdk.extension.aws.resource.Ec2ResourceProvider +io.opentelemetry.sdk.extension.aws.resource.EcsResourceProvider +io.opentelemetry.sdk.extension.aws.resource.EksResourceProvider +io.opentelemetry.sdk.extension.aws.resource.LambdaResourceProvider diff --git a/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResourceTest.java b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResourceTest.java new file mode 100644 index 000000000..ebe35fce4 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/BeanstalkResourceTest.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.io.IOException; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class BeanstalkResourceTest { + + @Test + void testCreateAttributes(@TempDir File tempFolder) throws IOException { + File file = new File(tempFolder, "beanstalk.config"); + String content = + "{\"noise\": \"noise\", \"deployment_id\":4,\"" + + "version_label\":\"2\",\"environment_name\":\"HttpSubscriber-env\"}"; + Files.write(content.getBytes(Charsets.UTF_8), file); + Attributes attributes = BeanstalkResource.buildResource(file.getPath()).getAttributes(); + assertThat(attributes) + .isEqualTo( + Attributes.of( + ResourceAttributes.CLOUD_PROVIDER, "aws", + ResourceAttributes.SERVICE_INSTANCE_ID, "4", + ResourceAttributes.SERVICE_VERSION, "2", + ResourceAttributes.SERVICE_NAMESPACE, "HttpSubscriber-env")); + } + + @Test + void testConfigFileMissing() { + Attributes attributes = + BeanstalkResource.buildResource("a_file_never_existing").getAttributes(); + assertThat(attributes.isEmpty()).isTrue(); + } + + @Test + void testBadConfigFile(@TempDir File tempFolder) throws IOException { + File file = new File(tempFolder, "beanstalk.config"); + String content = + "\"deployment_id\":4,\"version_label\":\"2\",\"" + + "environment_name\":\"HttpSubscriber-env\"}"; + Files.write(content.getBytes(Charsets.UTF_8), file); + Attributes attributes = BeanstalkResource.buildResource(file.getPath()).getAttributes(); + assertThat(attributes.isEmpty()).isTrue(); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(BeanstalkResourceProvider.class::isInstance); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/DockerHelperTest.java b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/DockerHelperTest.java new file mode 100644 index 000000000..5e32641be --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/DockerHelperTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class DockerHelperTest { + + @Test + void testCgroupFileMissing() { + DockerHelper dockerHelper = new DockerHelper("a_file_never_existing"); + assertThat(dockerHelper.getContainerId()).isEmpty(); + } + + @Test + void testContainerIdMissing(@TempDir File tempFolder) throws IOException { + File file = new File(tempFolder, "no_container_id"); + String content = "13:pids:/\n" + "12:hugetlb:/\n" + "11:net_prio:/"; + Files.write(content.getBytes(Charsets.UTF_8), file); + + DockerHelper dockerHelper = new DockerHelper(file.getPath()); + assertThat(dockerHelper.getContainerId()).isEmpty(); + } + + @Test + void testGetContainerId(@TempDir File tempFolder) throws IOException { + File file = new File(tempFolder, "cgroup"); + String expected = "386a1920640799b5bf5a39bd94e489e5159a88677d96ca822ce7c433ff350163"; + String content = "dummy\n11:devices:/ecs/bbc36dd0-5ee0-4007-ba96-c590e0b278d2/" + expected; + Files.write(content.getBytes(Charsets.UTF_8), file); + + DockerHelper dockerHelper = new DockerHelper(file.getPath()); + assertThat(dockerHelper.getContainerId()).isEqualTo(expected); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/Ec2ResourceTest.java b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/Ec2ResourceTest.java new file mode 100644 index 000000000..91c166770 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/Ec2ResourceTest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.linecorp.armeria.common.AggregatedHttpRequest; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.MediaType; +import com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Ec2ResourceTest { + + // From https://docs.amazonaws.cn/en_us/AWSEC2/latest/UserGuide/instance-identity-documents.html + private static final String 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" + + "}"; + + @RegisterExtension public static MockWebServerExtension server = new MockWebServerExtension(); + + @Test + void imdsv2() { + server.enqueue(HttpResponse.of("token")); + server.enqueue(HttpResponse.of(MediaType.JSON_UTF_8, IDENTITY_DOCUMENT)); + server.enqueue(HttpResponse.of("ec2-1-2-3-4")); + + Attributes attributes = + Ec2Resource.buildResource("localhost:" + server.httpPort()).getAttributes(); + AttributesBuilder expectedAttrBuilders = Attributes.builder(); + + expectedAttrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, "aws"); + expectedAttrBuilders.put(ResourceAttributes.HOST_ID, "i-1234567890abcdef0"); + expectedAttrBuilders.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "us-west-2b"); + expectedAttrBuilders.put(ResourceAttributes.HOST_TYPE, "t2.micro"); + expectedAttrBuilders.put(ResourceAttributes.HOST_IMAGE_ID, "ami-5fb8c835"); + expectedAttrBuilders.put(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789012"); + expectedAttrBuilders.put(ResourceAttributes.CLOUD_REGION, "us-west-2"); + expectedAttrBuilders.put(ResourceAttributes.HOST_NAME, "ec2-1-2-3-4"); + assertThat(attributes).isEqualTo(expectedAttrBuilders.build()); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/latest/api/token"); + assertThat(request1.headers().get("X-aws-ec2-metadata-token-ttl-seconds")).isEqualTo("60"); + + AggregatedHttpRequest request2 = server.takeRequest().request(); + assertThat(request2.path()).isEqualTo("/latest/dynamic/instance-identity/document"); + assertThat(request2.headers().get("X-aws-ec2-metadata-token")).isEqualTo("token"); + + AggregatedHttpRequest request3 = server.takeRequest().request(); + assertThat(request3.path()).isEqualTo("/latest/meta-data/hostname"); + assertThat(request3.headers().get("X-aws-ec2-metadata-token")).isEqualTo("token"); + } + + @Test + void imdsv1() { + server.enqueue(HttpResponse.of(HttpStatus.NOT_FOUND)); + server.enqueue(HttpResponse.of(MediaType.JSON_UTF_8, IDENTITY_DOCUMENT)); + server.enqueue(HttpResponse.of("ec2-1-2-3-4")); + + Attributes attributes = + Ec2Resource.buildResource("localhost:" + server.httpPort()).getAttributes(); + + AttributesBuilder expectedAttrBuilders = + Attributes.builder() + .put(ResourceAttributes.CLOUD_PROVIDER, "aws") + .put(ResourceAttributes.HOST_ID, "i-1234567890abcdef0") + .put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "us-west-2b") + .put(ResourceAttributes.HOST_TYPE, "t2.micro") + .put(ResourceAttributes.HOST_IMAGE_ID, "ami-5fb8c835") + .put(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789012") + .put(ResourceAttributes.CLOUD_REGION, "us-west-2") + .put(ResourceAttributes.HOST_NAME, "ec2-1-2-3-4"); + assertThat(attributes).isEqualTo(expectedAttrBuilders.build()); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/latest/api/token"); + assertThat(request1.headers().get("X-aws-ec2-metadata-token-ttl-seconds")).isEqualTo("60"); + + AggregatedHttpRequest request2 = server.takeRequest().request(); + assertThat(request2.path()).isEqualTo("/latest/dynamic/instance-identity/document"); + assertThat(request2.headers().get("X-aws-ec2-metadata-token")).isNull(); + } + + @Test + void badJson() { + server.enqueue(HttpResponse.of(HttpStatus.NOT_FOUND)); + server.enqueue(HttpResponse.of(MediaType.JSON_UTF_8, "I'm not JSON")); + + Attributes attributes = + Ec2Resource.buildResource("localhost:" + server.httpPort()).getAttributes(); + assertThat(attributes.isEmpty()).isTrue(); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/latest/api/token"); + assertThat(request1.headers().get("X-aws-ec2-metadata-token-ttl-seconds")).isEqualTo("60"); + + AggregatedHttpRequest request2 = server.takeRequest().request(); + assertThat(request2.path()).isEqualTo("/latest/dynamic/instance-identity/document"); + assertThat(request2.headers().get("X-aws-ec2-metadata-token")).isNull(); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(Ec2ResourceProvider.class::isInstance); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceTest.java b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceTest.java new file mode 100644 index 000000000..88f4104d1 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceTest.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class EcsResourceTest { + private static final String ECS_METADATA_KEY_V4 = "ECS_CONTAINER_METADATA_URI_V4"; + private static final String ECS_METADATA_KEY_V3 = "ECS_CONTAINER_METADATA_URI"; + + @Mock private DockerHelper mockDockerHelper; + + @Test + void testCreateAttributes() throws UnknownHostException { + when(mockDockerHelper.getContainerId()).thenReturn("0123456789A"); + Map mockSysEnv = new HashMap<>(); + mockSysEnv.put(ECS_METADATA_KEY_V3, "ecs_metadata_v3_uri"); + Attributes attributes = EcsResource.buildResource(mockSysEnv, mockDockerHelper).getAttributes(); + assertThat(attributes) + .isEqualTo( + Attributes.of( + ResourceAttributes.CLOUD_PROVIDER, + "aws", + ResourceAttributes.CONTAINER_NAME, + InetAddress.getLocalHost().getHostName(), + ResourceAttributes.CONTAINER_ID, + "0123456789A")); + } + + @Test + void testNotOnEcs() { + Map mockSysEnv = new HashMap<>(); + mockSysEnv.put(ECS_METADATA_KEY_V3, ""); + mockSysEnv.put(ECS_METADATA_KEY_V4, ""); + Attributes attributes = EcsResource.buildResource(mockSysEnv, mockDockerHelper).getAttributes(); + assertThat(attributes.isEmpty()).isTrue(); + } + + @Test + void testContainerIdMissing() throws UnknownHostException { + when(mockDockerHelper.getContainerId()).thenReturn(""); + Map mockSysEnv = new HashMap<>(); + mockSysEnv.put(ECS_METADATA_KEY_V4, "ecs_metadata_v4_uri"); + Attributes attributes = EcsResource.buildResource(mockSysEnv, mockDockerHelper).getAttributes(); + assertThat(attributes) + .isEqualTo( + Attributes.of( + ResourceAttributes.CLOUD_PROVIDER, + "aws", + ResourceAttributes.CONTAINER_NAME, + InetAddress.getLocalHost().getHostName())); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(EcsResourceProvider.class::isInstance); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EksResourceTest.java b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EksResourceTest.java new file mode 100644 index 000000000..56dba00c8 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EksResourceTest.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import static io.opentelemetry.sdk.extension.aws.resource.EksResource.AUTH_CONFIGMAP_PATH; +import static io.opentelemetry.sdk.extension.aws.resource.EksResource.CW_CONFIGMAP_PATH; +import static io.opentelemetry.sdk.extension.aws.resource.EksResource.K8S_SVC_URL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.io.IOException; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class EksResourceTest { + + @Mock private DockerHelper mockDockerHelper; + + @Mock private JdkHttpClient jdkHttpClient; + + @Test + void testEks(@TempDir File tempFolder) throws IOException { + File mockK8sTokenFile = new File(tempFolder, "k8sToken"); + String token = "token123"; + Files.write(token.getBytes(Charsets.UTF_8), mockK8sTokenFile); + File mockK8sKeystoreFile = new File(tempFolder, "k8sCert"); + String truststore = "truststore123"; + Files.write(truststore.getBytes(Charsets.UTF_8), mockK8sKeystoreFile); + + when(jdkHttpClient.fetchString( + any(), Mockito.eq(K8S_SVC_URL + AUTH_CONFIGMAP_PATH), any(), any())) + .thenReturn("not empty"); + when(jdkHttpClient.fetchString( + any(), Mockito.eq(K8S_SVC_URL + CW_CONFIGMAP_PATH), any(), any())) + .thenReturn("{\"data\":{\"cluster.name\":\"my-cluster\"}}"); + when(mockDockerHelper.getContainerId()).thenReturn("0123456789A"); + + Resource eksResource = + EksResource.buildResource( + jdkHttpClient, + mockDockerHelper, + mockK8sTokenFile.getPath(), + mockK8sKeystoreFile.getPath()); + Attributes attributes = eksResource.getAttributes(); + + assertThat(attributes) + .isEqualTo( + Attributes.of( + ResourceAttributes.K8S_CLUSTER_NAME, "my-cluster", + ResourceAttributes.CONTAINER_ID, "0123456789A")); + } + + @Test + void testNotEks() { + Resource eksResource = EksResource.buildResource(jdkHttpClient, mockDockerHelper, "", ""); + Attributes attributes = eksResource.getAttributes(); + assertThat(attributes.isEmpty()).isTrue(); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(EksResourceProvider.class::isInstance); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/JdkHttpClientTest.java b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/JdkHttpClientTest.java new file mode 100644 index 000000000..d9da0a0b1 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/JdkHttpClientTest.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.linecorp.armeria.common.AggregatedHttpRequest; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; + +class JdkHttpClientTest { + + @RegisterExtension public static MockWebServerExtension server = new MockWebServerExtension(); + + @Test + void testFetchString() { + server.enqueue(HttpResponse.of("expected result")); + + Map requestPropertyMap = ImmutableMap.of("key1", "value1", "key2", "value2"); + String urlStr = String.format("http://localhost:%s%s", server.httpPort(), "/path"); + JdkHttpClient jdkHttpClient = new JdkHttpClient(); + String result = jdkHttpClient.fetchString("GET", urlStr, requestPropertyMap, null); + + assertThat(result).isEqualTo("expected result"); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/path"); + assertThat(request1.headers().get("key1")).isEqualTo("value1"); + assertThat(request1.headers().get("key2")).isEqualTo("value2"); + } + + @Test + void testFailedFetchString() { + Map requestPropertyMap = ImmutableMap.of("key1", "value1", "key2", "value2"); + String urlStr = String.format("http://localhost:%s%s", server.httpPort(), "/path"); + JdkHttpClient jdkHttpClient = new JdkHttpClient(); + String result = jdkHttpClient.fetchString("GET", urlStr, requestPropertyMap, null); + assertThat(result).isEmpty(); + } + + static class HttpsServerTest { + @RegisterExtension + @Order(1) + public static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension(); + + @RegisterExtension + @Order(2) + public static ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.tls(certificate.certificateFile(), certificate.privateKeyFile()); + + sb.service("/", (ctx, req) -> HttpResponse.of("Thanks for trusting me")); + } + }; + + @Test + void goodCert() { + JdkHttpClient jdkHttpClient = new JdkHttpClient(); + String result = + jdkHttpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + certificate.certificateFile().getAbsolutePath()); + assertThat(result).isEqualTo("Thanks for trusting me"); + } + + @Test + void missingCert() { + JdkHttpClient jdkHttpClient = new JdkHttpClient(); + String result = + jdkHttpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + "/foo/bar/bad"); + assertThat(result).isEmpty(); + } + + @Test + void badCert(@TempDir Path tempDir) throws Exception { + Path certFile = tempDir.resolve("test.crt"); + Files.write(certFile, "bad cert".getBytes(StandardCharsets.UTF_8)); + JdkHttpClient jdkHttpClient = new JdkHttpClient(); + String result = + jdkHttpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + certFile.toString()); + assertThat(result).isEmpty(); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResourceTest.java b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResourceTest.java new file mode 100644 index 000000000..8af918c13 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/LambdaResourceTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.resource; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; + +class LambdaResourceTest { + @Test + void shouldNotCreateResourceForNotLambda() { + Attributes attributes = LambdaResource.buildResource(emptyMap()).getAttributes(); + assertThat(attributes.isEmpty()).isTrue(); + } + + @Test + void shouldAddNonEmptyAttributes() { + Attributes attributes = + LambdaResource.buildResource(singletonMap("AWS_LAMBDA_FUNCTION_NAME", "my-function")) + .getAttributes(); + assertThat(attributes) + .isEqualTo( + Attributes.of( + ResourceAttributes.CLOUD_PROVIDER, + "aws", + ResourceAttributes.FAAS_NAME, + "my-function")); + } + + @Test + void shouldAddAllAttributes() { + Map envVars = new HashMap<>(); + envVars.put("AWS_REGION", "us-east-1"); + envVars.put("AWS_LAMBDA_FUNCTION_NAME", "my-function"); + envVars.put("AWS_LAMBDA_FUNCTION_VERSION", "1.2.3"); + + Attributes attributes = LambdaResource.buildResource(envVars).getAttributes(); + + assertThat(attributes) + .isEqualTo( + Attributes.of( + ResourceAttributes.CLOUD_PROVIDER, + "aws", + ResourceAttributes.CLOUD_REGION, + "us-east-1", + ResourceAttributes.FAAS_NAME, + "my-function", + ResourceAttributes.FAAS_VERSION, + "1.2.3")); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(LambdaResourceProvider.class::isInstance); + } +} diff --git a/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/trace/AwsXRayIdGeneratorTest.java b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/trace/AwsXRayIdGeneratorTest.java new file mode 100644 index 000000000..51a4586b2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/trace/AwsXRayIdGeneratorTest.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.aws.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.sdk.trace.IdGenerator; +import java.util.Set; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AwsXrayIdGenerator}. */ +class AwsXRayIdGeneratorTest { + + @Test + void shouldGenerateValidIds() { + AwsXrayIdGenerator generator = AwsXrayIdGenerator.getInstance(); + for (int i = 0; i < 1000; i++) { + String traceId = generator.generateTraceId(); + assertThat(TraceId.isValid(traceId)).isTrue(); + String spanId = generator.generateSpanId(); + assertThat(SpanId.isValid(spanId)).isTrue(); + } + } + + @Test + void shouldGenerateTraceIdsWithTimestampsWithAllowedXrayTimeRange() { + AwsXrayIdGenerator generator = AwsXrayIdGenerator.getInstance(); + for (int i = 0; i < 1000; i++) { + String traceId = generator.generateTraceId(); + long unixSeconds = Long.valueOf(traceId.subSequence(0, 8).toString(), 16); + long ts = unixSeconds * 1000L; + long currentTs = System.currentTimeMillis(); + assertThat(ts).isLessThanOrEqualTo(currentTs); + long month = 86400000L * 30L; + assertThat(ts).isGreaterThan(currentTs - month); + } + } + + @Test + void shouldGenerateUniqueIdsInMultithreadedEnvironment() + throws BrokenBarrierException, InterruptedException { + AwsXrayIdGenerator generator = AwsXrayIdGenerator.getInstance(); + Set traceIds = new CopyOnWriteArraySet<>(); + Set spanIds = new CopyOnWriteArraySet<>(); + int threads = 8; + int generations = 128; + CyclicBarrier barrier = new CyclicBarrier(threads + 1); + Executor executor = Executors.newFixedThreadPool(threads); + for (int i = 0; i < threads; i++) { + executor.execute(new GenerateRunner(generations, generator, barrier, traceIds, spanIds)); + } + barrier.await(); + barrier.await(); + assertThat(traceIds).hasSize(threads * generations); + assertThat(spanIds).hasSize(threads * generations); + } + + static class GenerateRunner implements Runnable { + + private final int generations; + private final IdGenerator idsGenerator; + private final CyclicBarrier barrier; + private final Set traceIds; + private final Set spanIds; + + GenerateRunner( + int generations, + IdGenerator idsGenerator, + CyclicBarrier barrier, + Set traceIds, + Set spanIds) { + this.generations = generations; + this.idsGenerator = idsGenerator; + this.barrier = barrier; + this.traceIds = traceIds; + this.spanIds = spanIds; + } + + @Override + public void run() { + try { + barrier.await(); + for (int i = 0; i < generations; i++) { + traceIds.add(idsGenerator.generateTraceId()); + spanIds.add(idsGenerator.generateSpanId()); + } + barrier.await(); + } catch (InterruptedException | BrokenBarrierException cause) { + throw new IllegalStateException(cause); + } + } + } +} diff --git a/opentelemetry-java/sdk-extensions/build.gradle.kts b/opentelemetry-java/sdk-extensions/build.gradle.kts new file mode 100644 index 000000000..aa8eb5865 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/build.gradle.kts @@ -0,0 +1,8 @@ +subprojects { + val proj = this + plugins.withId("java") { + configure { + archivesBaseName = "opentelemetry-sdk-extension-${proj.name}" + } + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/README.md b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/README.md new file mode 100644 index 000000000..dc4a296df --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/README.md @@ -0,0 +1,18 @@ +# Jaeger Remote Sampler + +This module implements [Jaeger remote sampler](https://www.jaegertracing.io/docs/latest/sampling/#collector-sampling-configuration). +The sampler configuration is received from collector's gRPC endpoint. + +### Example + +The following example shows initialization and installation of the sampler: + +```java +Builder remoteSamplerBuilder = JaegerRemoteSampler.builder() + .setChannel(grpcChannel) + .setServiceName("my-service"); +TraceConfig traceConfig = provider.getActiveTraceConfig() + .toBuilder().setSampler(remoteSamplerBuilder.build()) + .build(); +provider.updateActiveTraceConfig(traceConfig); +``` diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/build.gradle.kts b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/build.gradle.kts new file mode 100644 index 000000000..759084053 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + `java-library` + `maven-publish` + + id("com.google.protobuf") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry - Jaeger Remote sampler" +extra["moduleName"] = "io.opentelemetry.sdk.extension.trace.jaeger" + +dependencies { + api(project(":sdk:all")) + + implementation(project(":sdk:all")) + implementation("io.grpc:grpc-api") + implementation("io.grpc:grpc-protobuf") + implementation("io.grpc:grpc-stub") + implementation("com.google.protobuf:protobuf-java") + + testImplementation("io.grpc:grpc-testing") + testImplementation("org.testcontainers:junit-jupiter") + + testRuntimeOnly("io.grpc:grpc-netty-shaded") +} + +// IntelliJ complains that the generated classes are not found, ask IntelliJ to include the +// generated Java directories as source folders. +idea { + module { + sourceDirs.add(file("build/generated/source/proto/main/java")) + // If you have additional sourceSets and/or codegen plugins, add all of them + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSampler.java b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSampler.java new file mode 100644 index 000000000..0ed9cec97 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSampler.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.jaeger.sampler; + +import io.grpc.ManagedChannel; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling.PerOperationSamplingStrategies; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling.SamplingStrategyParameters; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling.SamplingStrategyResponse; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.SamplingManagerGrpc; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.SamplingManagerGrpc.SamplingManagerBlockingStub; +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** Remote sampler that gets sampling configuration from remote Jaeger server. */ +public final class JaegerRemoteSampler implements Sampler { + private static final Logger logger = Logger.getLogger(JaegerRemoteSampler.class.getName()); + + private static final String WORKER_THREAD_NAME = + JaegerRemoteSampler.class.getSimpleName() + "_WorkerThread"; + + private final String serviceName; + private final SamplingManagerBlockingStub stub; + + private volatile Sampler sampler; + + @SuppressWarnings("FutureReturnValueIgnored") + JaegerRemoteSampler( + String serviceName, ManagedChannel channel, int pollingIntervalMs, Sampler initialSampler) { + this.serviceName = serviceName; + this.stub = SamplingManagerGrpc.newBlockingStub(channel); + this.sampler = initialSampler; + ScheduledExecutorService scheduledExecutorService = + Executors.newScheduledThreadPool(1, new DaemonThreadFactory(WORKER_THREAD_NAME)); + scheduledExecutorService.scheduleAtFixedRate( + this::getAndUpdateSampler, 0, pollingIntervalMs, TimeUnit.MILLISECONDS); + } + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + } + + private void getAndUpdateSampler() { + try { + SamplingStrategyParameters params = + SamplingStrategyParameters.newBuilder().setServiceName(this.serviceName).build(); + SamplingStrategyResponse response = stub.getSamplingStrategy(params); + this.sampler = updateSampler(response); + } catch (RuntimeException e) { // keep the timer thread alive + logger.log(Level.WARNING, "Failed to update sampler", e); + } + } + + private static Sampler updateSampler(SamplingStrategyResponse response) { + PerOperationSamplingStrategies operationSampling = response.getOperationSampling(); + if (operationSampling.getPerOperationStrategiesList().size() > 0) { + Sampler defaultSampler = + Sampler.traceIdRatioBased(operationSampling.getDefaultSamplingProbability()); + return Sampler.parentBased( + new PerOperationSampler( + defaultSampler, operationSampling.getPerOperationStrategiesList())); + } + switch (response.getStrategyType()) { + case PROBABILISTIC: + return Sampler.parentBased( + Sampler.traceIdRatioBased(response.getProbabilisticSampling().getSamplingRate())); + case RATE_LIMITING: + return Sampler.parentBased( + new RateLimitingSampler(response.getRateLimitingSampling().getMaxTracesPerSecond())); + case UNRECOGNIZED: + throw new AssertionError("unrecognized sampler type"); + } + throw new AssertionError("unrecognized sampler type"); + } + + @Override + public String getDescription() { + return String.format("JaegerRemoteSampler{%s}", this.sampler); + } + + @Override + public String toString() { + return getDescription(); + } + + // Visible for testing + Sampler getSampler() { + return this.sampler; + } + + public static JaegerRemoteSamplerBuilder builder() { + return new JaegerRemoteSamplerBuilder(); + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerBuilder.java b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerBuilder.java new file mode 100644 index 000000000..b9881acd2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerBuilder.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.jaeger.sampler; + +import static java.util.Objects.requireNonNull; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.opentelemetry.api.internal.Utils; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** A builder for {@link JaegerRemoteSampler}. */ +public final class JaegerRemoteSamplerBuilder { + private static final String DEFAULT_ENDPOINT = "localhost:14250"; + private static final int DEFAULT_POLLING_INTERVAL_MILLIS = 60000; + private static final Sampler INITIAL_SAMPLER = + Sampler.parentBased(Sampler.traceIdRatioBased(0.001)); + + private String endpoint = DEFAULT_ENDPOINT; + private ManagedChannel channel; + private String serviceName; + private Sampler initialSampler = INITIAL_SAMPLER; + private int pollingIntervalMillis = DEFAULT_POLLING_INTERVAL_MILLIS; + + /** + * Sets the service name to be used by this exporter. Required. + * + * @param serviceName the service name. + * @return this. + */ + public JaegerRemoteSamplerBuilder setServiceName(String serviceName) { + requireNonNull(serviceName, "serviceName"); + this.serviceName = serviceName; + return this; + } + + /** Sets the Jaeger endpoint to connect to. If unset, defaults to {@value DEFAULT_ENDPOINT}. */ + public JaegerRemoteSamplerBuilder setEndpoint(String endpoint) { + requireNonNull(endpoint, "endpoint"); + this.endpoint = endpoint; + return this; + } + + /** + * Sets the managed channel to use when communicating with the backend. Takes precedence over + * {@link #setEndpoint(String)} if both are called. + */ + public JaegerRemoteSamplerBuilder setChannel(ManagedChannel channel) { + requireNonNull(channel, "channel"); + this.channel = channel; + return this; + } + + /** + * Sets the polling interval for configuration updates. If unset, defaults to {@value + * DEFAULT_POLLING_INTERVAL_MILLIS}ms. Must be positive. + */ + public JaegerRemoteSamplerBuilder setPollingInterval(int interval, TimeUnit unit) { + requireNonNull(unit, "unit"); + Utils.checkArgument(interval > 0, "polling interval must be positive"); + pollingIntervalMillis = (int) unit.toMillis(interval); + return this; + } + + /** + * Sets the polling interval for configuration updates. If unset, defaults to {@value + * DEFAULT_POLLING_INTERVAL_MILLIS}ms. + */ + public JaegerRemoteSamplerBuilder setPollingInterval(Duration interval) { + requireNonNull(interval, "interval"); + return setPollingInterval((int) interval.toMillis(), TimeUnit.MILLISECONDS); + } + + /** + * Sets the initial sampler that is used before sampling configuration is obtained. If unset, + * defaults to a parent-based ratio-based sampler with a ratio of 0.001. + */ + public JaegerRemoteSamplerBuilder setInitialSampler(Sampler initialSampler) { + requireNonNull(initialSampler, "initialSampler"); + this.initialSampler = initialSampler; + return this; + } + + /** + * Builds the {@link JaegerRemoteSampler}. + * + * @return the remote sampler instance. + */ + public JaegerRemoteSampler build() { + if (channel == null) { + channel = ManagedChannelBuilder.forTarget(endpoint).usePlaintext().build(); + } + return new JaegerRemoteSampler(serviceName, channel, pollingIntervalMillis, initialSampler); + } + + JaegerRemoteSamplerBuilder() {} +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/PerOperationSampler.java b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/PerOperationSampler.java new file mode 100644 index 000000000..e78115d55 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/PerOperationSampler.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.jaeger.sampler; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling.OperationSamplingStrategy; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** {@link PerOperationSampler} samples spans per operation. */ +class PerOperationSampler implements Sampler { + + private final Sampler defaultSampler; + private final Map perOperationSampler; + + PerOperationSampler( + Sampler defaultSampler, List perOperationSampling) { + this.defaultSampler = defaultSampler; + this.perOperationSampler = new LinkedHashMap<>(perOperationSampling.size()); + for (OperationSamplingStrategy opSamplingStrategy : perOperationSampling) { + this.perOperationSampler.put( + opSamplingStrategy.getOperation(), + Sampler.traceIdRatioBased( + opSamplingStrategy.getProbabilisticSampling().getSamplingRate())); + } + } + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + Sampler sampler = this.perOperationSampler.get(name); + if (sampler == null) { + sampler = this.defaultSampler; + } + return sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + } + + @Override + public String getDescription() { + return String.format( + "PerOperationSampler{default=%s, perOperation=%s}", + this.defaultSampler, this.perOperationSampler); + } + + @Override + public String toString() { + return getDescription(); + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/RateLimitingSampler.java b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/RateLimitingSampler.java new file mode 100644 index 000000000..6d55c3106 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/RateLimitingSampler.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.jaeger.sampler; + +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.internal.RateLimiter; +import io.opentelemetry.sdk.internal.SystemClock; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; + +/** + * {@link RateLimitingSampler} sampler uses a leaky bucket rate limiter to ensure that traces are + * sampled with a certain constant rate. + */ +class RateLimitingSampler implements Sampler { + static final String TYPE = "ratelimiting"; + static final AttributeKey SAMPLER_TYPE = stringKey("sampler.type"); + static final AttributeKey SAMPLER_PARAM = doubleKey("sampler.param"); + + private final double maxTracesPerSecond; + private final RateLimiter rateLimiter; + private final SamplingResult onSamplingResult; + private final SamplingResult offSamplingResult; + + /** + * Creates rate limiting sampler. + * + * @param maxTracesPerSecond the maximum number of sampled traces per second. + */ + RateLimitingSampler(int maxTracesPerSecond) { + this.maxTracesPerSecond = maxTracesPerSecond; + double maxBalance = maxTracesPerSecond < 1.0 ? 1.0 : maxTracesPerSecond; + this.rateLimiter = new RateLimiter(maxTracesPerSecond, maxBalance, SystemClock.getInstance()); + Attributes attributes = + Attributes.of(SAMPLER_TYPE, TYPE, SAMPLER_PARAM, (double) maxTracesPerSecond); + this.onSamplingResult = SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE, attributes); + this.offSamplingResult = SamplingResult.create(SamplingDecision.DROP, attributes); + } + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return this.rateLimiter.trySpend(1.0) ? onSamplingResult : offSamplingResult; + } + + @Override + public String getDescription() { + return String.format("RateLimitingSampler{%.2f}", maxTracesPerSecond); + } + + @Override + public String toString() { + return getDescription(); + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/package-info.java b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/package-info.java new file mode 100644 index 000000000..1e8fddf2b --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Support for the Jaeger remote sampler. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.trace.jaeger.sampler; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/proto/jaeger/api_v2/sampling.proto b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/proto/jaeger/api_v2/sampling.proto new file mode 100644 index 000000000..a4a31d113 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/main/proto/jaeger/api_v2/sampling.proto @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +syntax = "proto3"; + +package jaeger.api_v2; + +import "google/api/annotations.proto"; + +option java_package = "io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2"; + +enum SamplingStrategyType { + PROBABILISTIC = 0; + RATE_LIMITING = 1; +}; + +message ProbabilisticSamplingStrategy { + double samplingRate = 1; +} + +message RateLimitingSamplingStrategy { + int32 maxTracesPerSecond = 1; +} + +message OperationSamplingStrategy { + string operation = 1; + ProbabilisticSamplingStrategy probabilisticSampling = 2; +} + +message PerOperationSamplingStrategies { + double defaultSamplingProbability = 1; + double defaultLowerBoundTracesPerSecond = 2; + repeated OperationSamplingStrategy perOperationStrategies = 3; + double defaultUpperBoundTracesPerSecond = 4; +} + +message SamplingStrategyResponse { + SamplingStrategyType strategyType = 1; + ProbabilisticSamplingStrategy probabilisticSampling = 2; + RateLimitingSamplingStrategy rateLimitingSampling = 3; + PerOperationSamplingStrategies operationSampling = 4; +} + +message SamplingStrategyParameters { + string serviceName = 1; +} + +service SamplingManager { + rpc GetSamplingStrategy(SamplingStrategyParameters) returns (SamplingStrategyResponse) { + option (google.api.http) = { + post: "/api/v2/samplingStrategy" + body: "*" + }; + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerIntegrationTest.java b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerIntegrationTest.java new file mode 100644 index 000000000..12b351509 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerIntegrationTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.jaeger.sampler; + +import static io.opentelemetry.sdk.extension.trace.jaeger.sampler.JaegerRemoteSamplerTest.samplerIsType; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class JaegerRemoteSamplerIntegrationTest { + + private static final int QUERY_PORT = 16686; + private static final int COLLECTOR_PORT = 14250; + private static final int HEALTH_PORT = 14269; + private static final String SERVICE_NAME = "E2E-test"; + private static final String SERVICE_NAME_RATE_LIMITING = "bar"; + + @Container + public static GenericContainer jaegerContainer = + new GenericContainer<>("ghcr.io/open-telemetry/java-test-containers:jaeger") + .withCommand("--sampling.strategies-file=/sampling.json") + .withExposedPorts(COLLECTOR_PORT, QUERY_PORT, HEALTH_PORT) + .waitingFor(Wait.forHttp("/").forPort(HEALTH_PORT)) + .withClasspathResourceMapping("sampling.json", "/sampling.json", BindMode.READ_ONLY); + + @Test + void remoteSampling_perOperation() { + final JaegerRemoteSampler remoteSampler = + JaegerRemoteSampler.builder() + .setEndpoint("127.0.0.1:" + jaegerContainer.getMappedPort(COLLECTOR_PORT)) + .setServiceName(SERVICE_NAME) + .build(); + + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(samplerIsType(remoteSampler, PerOperationSampler.class)); + assertThat(remoteSampler.getDescription()).contains("0.33").doesNotContain("150"); + } + + @Test + void remoteSampling_rateLimiting() { + final JaegerRemoteSampler remoteSampler = + JaegerRemoteSampler.builder() + .setEndpoint("127.0.0.1:" + jaegerContainer.getMappedPort(COLLECTOR_PORT)) + .setServiceName(SERVICE_NAME_RATE_LIMITING) + .build(); + + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(samplerIsType(remoteSampler, RateLimitingSampler.class)); + assertThat(remoteSampler.getDescription()).contains("RateLimitingSampler{150.00}"); + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerTest.java b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerTest.java new file mode 100644 index 000000000..7b898a896 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/JaegerRemoteSamplerTest.java @@ -0,0 +1,223 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.jaeger.sampler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.google.common.io.Closer; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling.RateLimitingSamplingStrategy; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.Sampling.SamplingStrategyType; +import io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2.SamplingManagerGrpc; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.io.IOException; +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.awaitility.core.ThrowingRunnable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +class JaegerRemoteSamplerTest { + + private static final String SERVICE_NAME = "my-service"; + private static final int RATE = 999; + + private static final AtomicInteger numPolls = new AtomicInteger(); + + private final String serverName = InProcessServerBuilder.generateName(); + private final ManagedChannel inProcessChannel = + InProcessChannelBuilder.forName(serverName).directExecutor().build(); + + private final SamplingManagerGrpc.SamplingManagerImplBase service = + mock( + SamplingManagerGrpc.SamplingManagerImplBase.class, + delegatesTo(new MockSamplingManagerService())); + + static class MockSamplingManagerService extends SamplingManagerGrpc.SamplingManagerImplBase { + + @Override + public void getSamplingStrategy( + Sampling.SamplingStrategyParameters request, + StreamObserver responseObserver) { + numPolls.incrementAndGet(); + Sampling.SamplingStrategyResponse response = + Sampling.SamplingStrategyResponse.newBuilder() + .setStrategyType(SamplingStrategyType.RATE_LIMITING) + .setRateLimitingSampling( + RateLimitingSamplingStrategy.newBuilder().setMaxTracesPerSecond(RATE).build()) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + } + + private final Closer closer = Closer.create(); + + @BeforeEach + public void before() throws IOException { + numPolls.set(0); + Server server = + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(service) + .build() + .start(); + closer.register(server::shutdownNow); + closer.register(inProcessChannel::shutdownNow); + } + + @AfterEach + void tearDown() throws Exception { + closer.close(); + } + + @Test + void connectionWorks() throws Exception { + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(Sampling.SamplingStrategyParameters.class); + + JaegerRemoteSampler sampler = + JaegerRemoteSampler.builder() + .setChannel(inProcessChannel) + .setServiceName(SERVICE_NAME) + .build(); + + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(samplerIsType(sampler, RateLimitingSampler.class)); + + // verify + verify(service).getSamplingStrategy(requestCaptor.capture(), ArgumentMatchers.any()); + assertThat(requestCaptor.getValue().getServiceName()).isEqualTo(SERVICE_NAME); + assertThat(sampler.getDescription()).contains("RateLimitingSampler{999.00}"); + + // Default poll interval is 60s, inconceivable to have polled multiple times by now. + assertThat(numPolls).hasValue(1); + } + + @Test + void description() { + JaegerRemoteSampler sampler = + JaegerRemoteSampler.builder() + .setChannel(inProcessChannel) + .setServiceName(SERVICE_NAME) + .build(); + assertThat(sampler.getDescription()) + .startsWith("JaegerRemoteSampler{ParentBased{root:TraceIdRatioBased{0.001000}"); + + // wait until the sampling strategy is retrieved before exiting test method + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(samplerIsType(sampler, RateLimitingSampler.class)); + + // Default poll interval is 60s, inconceivable to have polled multiple times by now. + assertThat(numPolls).hasValue(1); + } + + @Test + void initialSampler() { + JaegerRemoteSampler sampler = + JaegerRemoteSampler.builder() + .setEndpoint("example.com") + .setServiceName(SERVICE_NAME) + .setInitialSampler(Sampler.alwaysOn()) + .build(); + assertThat(sampler.getDescription()).startsWith("JaegerRemoteSampler{AlwaysOnSampler}"); + } + + @Test + void pollingInterval() throws Exception { + JaegerRemoteSampler sampler = + JaegerRemoteSampler.builder() + .setChannel(inProcessChannel) + .setServiceName(SERVICE_NAME) + .setPollingInterval(1, TimeUnit.MILLISECONDS) + .build(); + + // wait until the sampling strategy is retrieved before exiting test method + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(samplerIsType(sampler, RateLimitingSampler.class)); + + Thread.sleep(500); + + assertThat(numPolls).hasValueGreaterThanOrEqualTo(2); + } + + @Test + void pollingInterval_duration() throws Exception { + JaegerRemoteSampler sampler = + JaegerRemoteSampler.builder() + .setChannel(inProcessChannel) + .setServiceName(SERVICE_NAME) + .setPollingInterval(Duration.ofMillis(1)) + .build(); + + // wait until the sampling strategy is retrieved before exiting test method + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(samplerIsType(sampler, RateLimitingSampler.class)); + + Thread.sleep(500); + + assertThat(numPolls).hasValueGreaterThanOrEqualTo(2); + } + + @Test + void invalidArguments() { + assertThatThrownBy(() -> JaegerRemoteSampler.builder().setServiceName(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("serviceName"); + assertThatThrownBy(() -> JaegerRemoteSampler.builder().setEndpoint(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("endpoint"); + assertThatThrownBy(() -> JaegerRemoteSampler.builder().setChannel(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("channel"); + assertThatThrownBy( + () -> JaegerRemoteSampler.builder().setPollingInterval(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("polling interval must be positive"); + assertThatThrownBy(() -> JaegerRemoteSampler.builder().setPollingInterval(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy( + () -> JaegerRemoteSampler.builder().setPollingInterval(Duration.ofMillis(-1))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("polling interval must be positive"); + assertThatThrownBy(() -> JaegerRemoteSampler.builder().setPollingInterval(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("interval"); + } + + static ThrowingRunnable samplerIsType( + final JaegerRemoteSampler sampler, final Class expected) { + return () -> { + assertThat(sampler.getSampler().getClass().getName()) + .isEqualTo("io.opentelemetry.sdk.trace.samplers.ParentBasedSampler"); + + Field field = sampler.getSampler().getClass().getDeclaredField("root"); + field.setAccessible(true); + assertThat(field.get(sampler.getSampler()).getClass()).isEqualTo(expected); + }; + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/RateLimitingSamplerTest.java b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/RateLimitingSamplerTest.java new file mode 100644 index 000000000..873e4c5c0 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/java/io/opentelemetry/sdk/extension/trace/jaeger/sampler/RateLimitingSamplerTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.trace.jaeger.sampler; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class RateLimitingSamplerTest { + + private static final String SPAN_NAME = "MySpanName"; + private static final SpanKind SPAN_KIND = SpanKind.INTERNAL; + private static final String TRACE_ID = "12345678876543211234567887654321"; + private static final String PARENT_SPAN_ID = "8765432112345678"; + private static final Context spanContext = + Context.root() + .with( + Span.wrap( + SpanContext.create( + TRACE_ID, PARENT_SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault()))); + + @Test + void sampleOneTrace() { + RateLimitingSampler sampler = new RateLimitingSampler(1); + SamplingResult samplingResult = + sampler.shouldSample( + spanContext, + TRACE_ID, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()); + assertThat(samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + assertThat( + sampler + .shouldSample( + spanContext, + TRACE_ID, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + assertThat(samplingResult.getAttributes().size()).isEqualTo(2); + assertThat(samplingResult.getAttributes().get(RateLimitingSampler.SAMPLER_PARAM)).isEqualTo(1d); + assertThat(samplingResult.getAttributes().get(RateLimitingSampler.SAMPLER_TYPE)) + .isEqualTo(RateLimitingSampler.TYPE); + } + + @Test + void description() { + RateLimitingSampler sampler = new RateLimitingSampler(15); + assertThat(sampler.getDescription()).isEqualTo("RateLimitingSampler{15.00}"); + } +} diff --git a/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/resources/sampling.json b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/resources/sampling.json new file mode 100644 index 000000000..bf0f52f82 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jaeger-remote-sampler/src/test/resources/sampling.json @@ -0,0 +1,30 @@ +{ + "service_strategies": [ + { + "service": "E2E-test", + "type": "probabilistic", + "param": 0.33, + "operation_strategies": [ + { + "operation": "op1", + "type": "probabilistic", + "param": 0.2 + }, + { + "operation": "op2", + "type": "probabilistic", + "param": 0.4 + } + ] + }, + { + "service": "bar", + "type": "ratelimiting", + "param": 150 + } + ], + "default_strategy": { + "type": "probabilistic", + "param": 0.8 + } +} diff --git a/opentelemetry-java/sdk-extensions/jfr-events/README.md b/opentelemetry-java/sdk-extensions/jfr-events/README.md new file mode 100644 index 000000000..cd20cce83 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/README.md @@ -0,0 +1,19 @@ +OpenTelemetry SDK Extension Java Flight Recorder (JFR) Events +====================================================== + +Create JFR events that can be recorded and viewed in Java Mission Control (JMC). +* Creates Open Telemetry Tracing/Span events for spans + * The thread and stracktrace will be of the thead ending the span which might be different than the thread creating the span. + * Has the fields + * Operation Name + * Trace ID + * Parent Span ID + * Span ID +* Creates Open Telemetry Tracing/Scope events for scopes + * Thread will match the thread the scope was active in and the stacktrace will be when scope was closed + * Multiple scopes might be collected for a single span + * Has the fields + * Trace ID + * Span ID +* Supports the Open Source version of JFR in Java 11. + * Might support back port to OpenJDK 8, but not tested and classes are built with JDK 11 bytecode. diff --git a/opentelemetry-java/sdk-extensions/jfr-events/build.gradle.kts b/opentelemetry-java/sdk-extensions/jfr-events/build.gradle.kts new file mode 100644 index 000000000..5d9ce8ef9 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + `java-library` + `maven-publish` +} + +description = "OpenTelemetry SDK Extension JFR" +extra["moduleName"] = "io.opentelemetry.sdk.extension.jfr" + +dependencies { + implementation(project(":api:all")) + implementation(project(":sdk:all")) +} + +tasks { + withType(JavaCompile::class) { + options.release.set(11) + } + + named("testJava8") { + enabled = false + } + + named("test") { + // Disabled due to https://bugs.openjdk.java.net/browse/JDK-8245283 + configure { + enabled = false + } + } +} diff --git a/opentelemetry-java/sdk-extensions/jfr-events/gradle.properties b/opentelemetry-java/sdk-extensions/jfr-events/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageWrapper.java b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageWrapper.java new file mode 100644 index 000000000..25928b583 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageWrapper.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.Scope; + +public final class JfrContextStorageWrapper implements ContextStorage { + + private final ContextStorage wrapped; + + public JfrContextStorageWrapper(ContextStorage wrapped) { + this.wrapped = wrapped; + } + + @Override + public Scope attach(Context toAttach) { + Scope scope = wrapped.attach(toAttach); + ScopeEvent event = new ScopeEvent(Span.fromContext(toAttach).getSpanContext()); + event.begin(); + return () -> { + if (event.shouldCommit()) { + event.commit(); + } + scope.close(); + }; + } + + @Override + public Context current() { + return wrapped.current(); + } +} diff --git a/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java new file mode 100644 index 000000000..e3e9e914c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +/** + * Span processor to create new JFR events for the Span as they are started, and commit on end. + * + *

    NOTE: The JfrSpanProcessor measures the timing of spans, avoid if possible to wrap it with any + * other SpanProcessor which may affect timings. When possible, register it first before any other + * processors to allow the most accurate measurements. + */ +public final class JfrSpanProcessor implements SpanProcessor { + + private final WeakConcurrentMap spanEvents = + new WeakConcurrentMap.WithInlinedExpunction<>(); + + private volatile boolean closed; + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + if (closed) { + return; + } + if (span.getSpanContext().isValid()) { + SpanEvent event = new SpanEvent(span.toSpanData()); + event.begin(); + spanEvents.put(span.getSpanContext(), event); + } + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan rs) { + SpanEvent event = spanEvents.remove(rs.getSpanContext()); + if (!closed && event != null && event.shouldCommit()) { + event.commit(); + } + } + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public CompletableResultCode shutdown() { + closed = true; + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java new file mode 100644 index 000000000..cf40bb4e1 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import io.opentelemetry.api.trace.SpanContext; +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; +import jdk.jfr.Name; + +@Name("io.opentelemetry.context.Scope") +@Label("Scope") +@Category("Open Telemetry Tracing") +@Description( + "Open Telemetry trace event corresponding to the span currently " + + "in scope/active on this thread.") +class ScopeEvent extends Event { + + private final String traceId; + private final String spanId; + + ScopeEvent(SpanContext spanContext) { + this.traceId = spanContext.getTraceId(); + this.spanId = spanContext.getSpanId(); + } + + @Label("Trace Id") + public String getTraceId() { + return traceId; + } + + @Label("Span Id") + public String getSpanId() { + return spanId; + } +} diff --git a/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java new file mode 100644 index 000000000..1e239b929 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import io.opentelemetry.sdk.trace.data.SpanData; +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; +import jdk.jfr.Name; + +@Label("Span") +@Name("io.opentelemetry.trace.Span") +@Category("Open Telemetry Tracing") +@Description("Open Telemetry trace event corresponding to a span.") +class SpanEvent extends Event { + + private final String operationName; + private final String traceId; + private final String spanId; + private final String parentId; + + SpanEvent(SpanData spanData) { + this.operationName = spanData.getName(); + this.traceId = spanData.getTraceId(); + this.spanId = spanData.getSpanId(); + this.parentId = spanData.getParentSpanId(); + } + + @Label("Operation Name") + public String getOperationName() { + return operationName; + } + + @Label("Trace Id") + public String getTraceId() { + return traceId; + } + + @Label("Span Id") + public String getSpanId() { + return spanId; + } + + @Label("Parent Id") + public String getParentId() { + return parentId; + } +} diff --git a/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java new file mode 100644 index 000000000..ac7889ff0 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Capture Spans and Scopes as events in JFR recordings. + * + * @see io.opentelemetry.sdk.extension.jfr.JfrSpanProcessor + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.jfr; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/jfr-events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java b/opentelemetry-java/sdk-extensions/jfr-events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java new file mode 100644 index 000000000..530d29772 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/jfr-events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JfrSpanProcessorTest { + + private static final String OPERATION_NAME = "Test Span"; + + private SdkTracerProvider sdkTracerProvider; + private Tracer tracer; + + @BeforeEach + void setUp() { + sdkTracerProvider = + SdkTracerProvider.builder().addSpanProcessor(new JfrSpanProcessor()).build(); + tracer = sdkTracerProvider.get("JfrSpanProcessorTest"); + } + + @AfterEach + void tearDown() { + sdkTracerProvider.shutdown(); + } + + static { + ContextStorage.addWrapper(JfrContextStorageWrapper::new); + } + + /** + * Test basic single span. + * + * @throws java.io.IOException on io error + */ + @Test + public void basicSpan() throws IOException { + Path output = Files.createTempFile("test-basic-span", ".jfr"); + + try { + Recording recording = new Recording(); + recording.start(); + Span span; + + try (recording) { + + span = tracer.spanBuilder(OPERATION_NAME).setNoParent().startSpan(); + span.end(); + + recording.dump(output); + } + + List events = RecordingFile.readAllEvents(output); + assertEquals(1, events.size()); + events.stream() + .forEach( + e -> { + assertEquals(span.getSpanContext().getTraceId(), e.getValue("traceId")); + assertEquals(span.getSpanContext().getSpanId(), e.getValue("spanId")); + assertEquals(OPERATION_NAME, e.getValue("operationName")); + }); + + } finally { + Files.delete(output); + } + } + + /** + * Test basic single span with a scope. + * + * @throws java.io.IOException on io error + * @throws java.lang.InterruptedException interrupted sleep + */ + @Test + public void basicSpanWithScope() throws IOException, InterruptedException { + Path output = Files.createTempFile("test-basic-span-with-scope", ".jfr"); + + try { + Recording recording = new Recording(); + recording.start(); + Span span; + + try (recording) { + span = tracer.spanBuilder(OPERATION_NAME).setNoParent().startSpan(); + try (Scope s = span.makeCurrent()) { + Thread.sleep(10); + } + span.end(); + + recording.dump(output); + } + + List events = RecordingFile.readAllEvents(output); + assertEquals(2, events.size()); + events.stream() + .forEach( + e -> { + assertEquals(span.getSpanContext().getTraceId(), e.getValue("traceId")); + assertEquals(span.getSpanContext().getSpanId(), e.getValue("spanId")); + if ("Span".equals(e.getEventType().getLabel())) { + assertEquals(OPERATION_NAME, e.getValue("operationName")); + } + }); + + } finally { + Files.delete(output); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/README.md b/opentelemetry-java/sdk-extensions/logging/README.md new file mode 100644 index 000000000..70affb87d --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/README.md @@ -0,0 +1,6 @@ +# OpenTelemetry Experimental Logging Support + +This project contains experimental support for transport of logs via OpenTelemetry. The API +presented is intended for the use of logging library adapters to enable resource and request +correlation with other observability signals and transport of logs through the OpenTelemetry +collector. diff --git a/opentelemetry-java/sdk-extensions/logging/build.gradle.kts b/opentelemetry-java/sdk-extensions/logging/build.gradle.kts new file mode 100644 index 000000000..85db97db3 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Contrib Logging Support" +extra["moduleName"] = "io.opentelemetry.sdk.extension.logging" + +dependencies { + api(project(":sdk:all")) + + implementation(project(":api:metrics")) + + implementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("org.awaitility:awaitility") + + annotationProcessor("com.google.auto.value:auto-value") +} diff --git a/opentelemetry-java/sdk-extensions/logging/gradle.properties b/opentelemetry-java/sdk-extensions/logging/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogProcessor.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogProcessor.java new file mode 100644 index 000000000..b8c33bbce --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogProcessor.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logging.data.LogRecord; +import io.opentelemetry.sdk.trace.SdkTracerProvider; + +public interface LogProcessor { + + void addLogRecord(LogRecord record); + + /** + * Called when {@link SdkTracerProvider#shutdown()} is called. + * + * @return result + */ + CompletableResultCode shutdown(); + + /** + * Processes all span events that have not yet been processed. + * + * @return result + */ + CompletableResultCode forceFlush(); +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSink.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSink.java new file mode 100644 index 000000000..959d67b86 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSink.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging; + +import io.opentelemetry.sdk.logging.data.LogRecord; + +/** A LogSink accepts logging records for transmission to an aggregator or log processing system. */ +public interface LogSink { + /** + * Pass a record to the SDK for transmission to a logging exporter. + * + * @param record record to transmit + */ + void offer(LogRecord record); +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSinkSdkProvider.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSinkSdkProvider.java new file mode 100644 index 000000000..9a40ed370 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSinkSdkProvider.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logging.data.LogRecord; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public final class LogSinkSdkProvider { + private final LogSink logSink = new SdkLogSink(); + private final List processors = new ArrayList<>(); + + /** + * Returns a new {@link LogSinkSdkProviderBuilder} for this class. + * + * @return a new {@link LogSinkSdkProviderBuilder} for this class. + */ + static LogSinkSdkProviderBuilder builder() { + return new LogSinkSdkProviderBuilder(); + } + + LogSinkSdkProvider() {} + + public LogSink get(String instrumentationName, String instrumentationVersion) { + // Currently there is no differentiation by instrumentation library + return logSink; + } + + public void addLogProcessor(LogProcessor processor) { + processors.add(Objects.requireNonNull(processor, "Processor can not be null")); + } + + /** + * Flushes all attached processors. + * + * @return result + */ + public CompletableResultCode forceFlush() { + final List processorResults = new ArrayList<>(processors.size()); + for (LogProcessor processor : processors) { + processorResults.add(processor.forceFlush()); + } + return CompletableResultCode.ofAll(processorResults); + } + + /** + * Shut down of provider and associated processors. + * + * @return result + */ + public CompletableResultCode shutdown() { + Collection processorResults = new ArrayList<>(processors.size()); + for (LogProcessor processor : processors) { + processorResults.add(processor.shutdown()); + } + return CompletableResultCode.ofAll(processorResults); + } + + private class SdkLogSink implements LogSink { + @Override + public void offer(LogRecord record) { + for (LogProcessor processor : processors) { + processor.addLogRecord(record); + } + } + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSinkSdkProviderBuilder.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSinkSdkProviderBuilder.java new file mode 100644 index 000000000..fc115a6ac --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/LogSinkSdkProviderBuilder.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging; + +public final class LogSinkSdkProviderBuilder { + LogSinkSdkProviderBuilder() {} + + public LogSinkSdkProvider build() { + return new LogSinkSdkProvider(); + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/AnyValue.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/AnyValue.java new file mode 100644 index 000000000..55c8bc4e8 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/AnyValue.java @@ -0,0 +1,263 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.data; + +import com.google.auto.value.AutoValue; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * A class that represents all the possible values for a data body. An {@code AnyValue} can have 6 + * types of values: {@code String}, {@code boolean}, {@code int}, {@code double}, {@code array}, or + * {@code kvlist}. represented through {@code AnyValue.Type}. A {@code array} or a {@code kvlist} + * can in turn hold other {@code AnyValue} instances, allowing for mapping to JSON-like structures. + */ +@Immutable +public abstract class AnyValue { + + /** An enum that represents all the possible value types for an {@code AnyValue}. */ + public enum Type { + STRING, + BOOL, + INT64, + DOUBLE, + ARRAY, + KVLIST + } + + /** + * Returns an {@code AnyValue} with a string value. + * + * @param stringValue The new value. + * @return an {@code AnyValue} with a string value. + */ + public static AnyValue stringAnyValue(String stringValue) { + return AnyValueString.create(stringValue); + } + + /** + * Returns the string value of this {@code AnyValue}. An UnsupportedOperationException will be + * thrown if getType() is not {@link AnyValue.Type#STRING}. + * + * @return the string value of this {@code AttributeValue}. + */ + public String getStringValue() { + throw new UnsupportedOperationException( + String.format("This type can only return %s data", getType().name())); + } + + /** + * Returns an {@code AnyValue} with an int value. + * + * @param longValue The new value. + * @return an {@code AnyValue} with a int value. + */ + public static AnyValue longAnyValue(long longValue) { + return AnyValueLong.create(longValue); + } + + public long getLongValue() { + throw new UnsupportedOperationException( + String.format("This type can only return %s data", getType().name())); + } + + /** + * Returns an {@code AnyValue} with a bool value. + * + * @param boolValue The new value. + * @return an {@code AnyValue} with a bool value. + */ + public static AnyValue boolAnyValue(boolean boolValue) { + return AnyValueBool.create(boolValue); + } + + /** + * Returns the boolean value of this {@code AnyValue}. An UnsupportedOperationException will be + * thrown if getType() is not {@link AnyValue.Type#BOOL}. + * + * @return the boolean value of this {@code AttributeValue}. + */ + public boolean getBoolValue() { + throw new UnsupportedOperationException( + String.format("This type can only return %s data", getType().name())); + } + + /** + * Returns an {@code AnyValue} with a double value. + * + * @param doubleValue The new value. + * @return an {@code AnyValue} with a double value. + */ + public static AnyValue doubleAnyValue(double doubleValue) { + return AnyValueDouble.create(doubleValue); + } + + /** + * Returns the double value of this {@code AnyValue}. An UnsupportedOperationException will be + * thrown if getType() is not {@link AnyValue.Type#DOUBLE}. + * + * @return the double value of this {@code AttributeValue}. + */ + public double getDoubleValue() { + throw new UnsupportedOperationException( + String.format("This type can only return %s data", getType().name())); + } + + /** + * Returns an {@code AnyValue} with a array value. + * + * @param values The new value. + * @return an {@code AnyValue} with a array value. + */ + public static AnyValue arrayAnyValue(List values) { + return AnyValueArray.create(values); + } + + /** + * Returns the array value of this {@code AnyValue}. An UnsupportedOperationException will be + * thrown if getType() is not {@link AnyValue.Type#ARRAY}. + * + * @return the array value of this {@code AttributeValue}. + */ + public List getArrayValue() { + throw new UnsupportedOperationException( + String.format("This type can only return %s data", getType().name())); + } + + /** + * Returns an {@code AnyValue} with a kvlist value. + * + * @param values The new value. + * @return an {@code AnyValue} with a kvlist value. + */ + public static AnyValue kvlistAnyValue(Map values) { + return AnyValueKvlist.create(values); + } + + /** + * Returns the string value of this {@code AnyValue}. An UnsupportedOperationException will be + * thrown if getType() is not {@link AnyValue.Type#STRING}. + * + * @return the string value of this {@code AttributeValue}. + */ + public Map getKvlistValue() { + throw new UnsupportedOperationException( + String.format("This type can only return %s data", getType().name())); + } + + public abstract Type getType(); + + @Immutable + @AutoValue + abstract static class AnyValueString extends AnyValue { + AnyValueString() {} + + static AnyValue create(String stringValue) { + return new AutoValue_AnyValue_AnyValueString(stringValue); + } + + @Override + public final Type getType() { + return Type.STRING; + } + + @Override + @Nullable + public abstract String getStringValue(); + } + + @Immutable + @AutoValue + abstract static class AnyValueLong extends AnyValue { + AnyValueLong() {} + + static AnyValue create(long longValue) { + return new AutoValue_AnyValue_AnyValueLong(longValue); + } + + @Override + public final Type getType() { + return Type.INT64; + } + + @Override + public abstract long getLongValue(); + } + + @Immutable + @AutoValue + abstract static class AnyValueBool extends AnyValue { + AnyValueBool() {} + + static AnyValue create(boolean boolValue) { + return new AutoValue_AnyValue_AnyValueBool(boolValue); + } + + @Override + public final Type getType() { + return Type.BOOL; + } + + @Override + public abstract boolean getBoolValue(); + } + + @Immutable + @AutoValue + abstract static class AnyValueDouble extends AnyValue { + AnyValueDouble() {} + + static AnyValue create(double doubleValue) { + return new AutoValue_AnyValue_AnyValueDouble(doubleValue); + } + + @Override + public final Type getType() { + return Type.DOUBLE; + } + + @Override + public abstract double getDoubleValue(); + } + + @Immutable + @AutoValue + abstract static class AnyValueArray extends AnyValue { + AnyValueArray() {} + + static AnyValue create(List arrayValue) { + return new AutoValue_AnyValue_AnyValueArray(arrayValue); + } + + @Override + public final Type getType() { + return Type.ARRAY; + } + + @Override + public abstract List getArrayValue(); + } + + @Immutable + @AutoValue + abstract static class AnyValueKvlist extends AnyValue { + AnyValueKvlist() {} + + static AnyValue create(Map kvlistValue) { + return new AutoValue_AnyValue_AnyValueKvlist(kvlistValue); + } + + @Override + public final Type getType() { + return Type.KVLIST; + } + + @Override + public abstract Map getKvlistValue(); + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/LogRecord.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/LogRecord.java new file mode 100644 index 000000000..635d44331 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/LogRecord.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; + +/** + * A LogRecord is an implementation of the + * OpenTelemetry logging model. + */ +@AutoValue +public abstract class LogRecord { + + public static LogRecordBuilder builder() { + return new LogRecordBuilder(); + } + + static LogRecord create( + long timeUnixNano, + String traceId, + String spanId, + int flags, + Severity severity, + String severityText, + String name, + AnyValue body, + Attributes attributes) { + return new AutoValue_LogRecord( + timeUnixNano, traceId, spanId, flags, severity, severityText, name, body, attributes); + } + + public abstract long getTimeUnixNano(); + + public abstract String getTraceId(); + + public abstract String getSpanId(); + + public abstract int getFlags(); + + public abstract Severity getSeverity(); + + @Nullable + public abstract String getSeverityText(); + + @Nullable + public abstract String getName(); + + public abstract AnyValue getBody(); + + public abstract Attributes getAttributes(); + + public enum Severity { + UNDEFINED_SEVERITY_NUMBER(0), + TRACE(1), + TRACE2(2), + TRACE3(3), + TRACE4(4), + DEBUG(5), + DEBUG2(6), + DEBUG3(7), + DEBUG4(8), + INFO(9), + INFO2(10), + INFO3(11), + INFO4(12), + WARN(13), + WARN2(14), + WARN3(15), + WARN4(16), + ERROR(17), + ERROR2(18), + ERROR3(19), + ERROR4(20), + FATAL(21), + FATAL2(22), + FATAL3(23), + FATAL4(24), + ; + + private final int severityNumber; + + Severity(int severityNumber) { + this.severityNumber = severityNumber; + } + + public int getSeverityNumber() { + return severityNumber; + } + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/LogRecordBuilder.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/LogRecordBuilder.java new file mode 100644 index 000000000..476d08955 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/LogRecordBuilder.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.data; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import java.util.concurrent.TimeUnit; + +public final class LogRecordBuilder { + private long timeUnixNano; + private String traceId = ""; + private String spanId = ""; + private int flags; + private LogRecord.Severity severity = LogRecord.Severity.UNDEFINED_SEVERITY_NUMBER; + private String severityText; + private String name; + private AnyValue body = AnyValue.stringAnyValue(""); + private final AttributesBuilder attributeBuilder = Attributes.builder(); + + LogRecordBuilder() {} + + public LogRecordBuilder setUnixTimeNano(long timestamp) { + this.timeUnixNano = timestamp; + return this; + } + + public LogRecordBuilder setUnixTimeMillis(long timestamp) { + return setUnixTimeNano(TimeUnit.MILLISECONDS.toNanos(timestamp)); + } + + public LogRecordBuilder setTraceId(String traceId) { + this.traceId = traceId; + return this; + } + + public LogRecordBuilder setSpanId(String spanId) { + this.spanId = spanId; + return this; + } + + public LogRecordBuilder setFlags(int flags) { + this.flags = flags; + return this; + } + + public LogRecordBuilder setSeverity(LogRecord.Severity severity) { + this.severity = severity; + return this; + } + + public LogRecordBuilder setSeverityText(String severityText) { + this.severityText = severityText; + return this; + } + + public LogRecordBuilder setName(String name) { + this.name = name; + return this; + } + + public LogRecordBuilder setBody(AnyValue body) { + this.body = body; + return this; + } + + public LogRecordBuilder setBody(String body) { + return setBody(AnyValue.stringAnyValue(body)); + } + + public LogRecordBuilder setAttributes(Attributes attributes) { + this.attributeBuilder.putAll(attributes); + return this; + } + + /** + * Build a LogRecord instance. + * + * @return value object being built + */ + public LogRecord build() { + if (timeUnixNano == 0) { + timeUnixNano = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + } + return LogRecord.create( + timeUnixNano, + traceId, + spanId, + flags, + severity, + severityText, + name, + body, + attributeBuilder.build()); + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/package-info.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/package-info.java new file mode 100644 index 000000000..c08a1596b --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/data/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** The data format to model logs for export. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.logging.data; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/BatchLogProcessor.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/BatchLogProcessor.java new file mode 100644 index 000000000..83047bea5 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/BatchLogProcessor.java @@ -0,0 +1,217 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.export; + +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import io.opentelemetry.sdk.logging.LogProcessor; +import io.opentelemetry.sdk.logging.data.LogRecord; +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public final class BatchLogProcessor implements LogProcessor { + private static final String WORKER_THREAD_NAME = + BatchLogProcessor.class.getSimpleName() + "_WorkerThread"; + + private final Worker worker; + private final Thread workerThread; + + BatchLogProcessor( + int maxQueueSize, + long scheduleDelayMillis, + int maxExportBatchSize, + long exporterTimeoutMillis, + LogExporter logExporter) { + this.worker = + new Worker( + logExporter, + scheduleDelayMillis, + maxExportBatchSize, + exporterTimeoutMillis, + new ArrayBlockingQueue(maxQueueSize)); + this.workerThread = new DaemonThreadFactory(WORKER_THREAD_NAME).newThread(worker); + this.workerThread.start(); + } + + public static BatchLogProcessorBuilder builder(LogExporter logExporter) { + return new BatchLogProcessorBuilder(logExporter); + } + + @Override + public void addLogRecord(LogRecord record) { + worker.addLogRecord(record); + } + + @Override + public CompletableResultCode shutdown() { + workerThread.interrupt(); + return worker.shutdown(); + } + + @Override + public CompletableResultCode forceFlush() { + return worker.forceFlush(); + } + + private static class Worker implements Runnable { + static { + Meter meter = GlobalMeterProvider.getMeter("io.opentelemetry.sdk.logging"); + LongCounter logRecordsProcessed = + meter + .longCounterBuilder("logRecordsProcessed") + .setUnit("1") + .setDescription("Number of records processed") + .build(); + successCounter = logRecordsProcessed.bind(Labels.of("result", "success")); + exporterFailureCounter = + logRecordsProcessed.bind( + Labels.of("result", "dropped record", "cause", "exporter failure")); + queueFullRecordCounter = + logRecordsProcessed.bind(Labels.of("result", "dropped record", "cause", "queue full")); + } + + private static final BoundLongCounter exporterFailureCounter; + private static final BoundLongCounter queueFullRecordCounter; + private static final BoundLongCounter successCounter; + + private final long scheduleDelayNanos; + private final int maxExportBatchSize; + private final LogExporter logExporter; + private final long exporterTimeoutMillis; + private final ArrayList batch; + private final BlockingQueue queue; + + private final AtomicReference flushRequested = new AtomicReference<>(); + private volatile boolean continueWork = true; + private long nextExportTime; + + private Worker( + LogExporter logExporter, + long scheduleDelayMillis, + int maxExportBatchSize, + long exporterTimeoutMillis, + BlockingQueue queue) { + this.logExporter = logExporter; + this.maxExportBatchSize = maxExportBatchSize; + this.exporterTimeoutMillis = exporterTimeoutMillis; + this.scheduleDelayNanos = TimeUnit.MILLISECONDS.toNanos(scheduleDelayMillis); + this.queue = queue; + this.batch = new ArrayList<>(this.maxExportBatchSize); + } + + @Override + public void run() { + updateNextExportTime(); + + while (continueWork) { + if (flushRequested.get() != null) { + flush(); + } + + try { + LogRecord lastElement = queue.poll(100, TimeUnit.MILLISECONDS); + if (lastElement != null) { + batch.add(lastElement); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + if (batch.size() >= maxExportBatchSize || System.nanoTime() >= nextExportTime) { + exportCurrentBatch(); + updateNextExportTime(); + } + } + } + + private void flush() { + int recordsToFlush = queue.size(); + while (recordsToFlush > 0) { + LogRecord record = queue.poll(); + assert record != null; + batch.add(record); + recordsToFlush--; + if (batch.size() >= maxExportBatchSize) { + exportCurrentBatch(); + } + } + exportCurrentBatch(); + CompletableResultCode result = flushRequested.get(); + assert result != null; + flushRequested.set(null); + } + + private void updateNextExportTime() { + nextExportTime = System.nanoTime() + scheduleDelayNanos; + } + + private void exportCurrentBatch() { + if (batch.isEmpty()) { + return; + } + + try { + final CompletableResultCode result = logExporter.export(batch); + result.join(exporterTimeoutMillis, TimeUnit.MILLISECONDS); + if (result.isSuccess()) { + successCounter.add(batch.size()); + } else { + exporterFailureCounter.add(1); + } + } catch (RuntimeException t) { + exporterFailureCounter.add(batch.size()); + } finally { + batch.clear(); + } + } + + private CompletableResultCode shutdown() { + final CompletableResultCode result = new CompletableResultCode(); + final CompletableResultCode flushResult = forceFlush(); + flushResult.whenComplete( + new Runnable() { + @Override + public void run() { + continueWork = false; + final CompletableResultCode shutdownResult = logExporter.shutdown(); + shutdownResult.whenComplete( + new Runnable() { + @Override + public void run() { + if (flushResult.isSuccess() && shutdownResult.isSuccess()) { + result.succeed(); + } else { + result.fail(); + } + } + }); + } + }); + return result; + } + + private CompletableResultCode forceFlush() { + CompletableResultCode flushResult = new CompletableResultCode(); + this.flushRequested.compareAndSet(null, flushResult); + return this.flushRequested.get(); + } + + public void addLogRecord(LogRecord record) { + if (!queue.offer(record)) { + queueFullRecordCounter.add(1); + } + } + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/BatchLogProcessorBuilder.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/BatchLogProcessorBuilder.java new file mode 100644 index 000000000..5bfaa84e7 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/BatchLogProcessorBuilder.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.export; + +import io.opentelemetry.api.internal.Utils; +import java.util.Objects; + +public final class BatchLogProcessorBuilder { + private static final long DEFAULT_SCHEDULE_DELAY_MILLIS = 200; + private static final int DEFAULT_MAX_QUEUE_SIZE = 2048; + private static final int DEFAULT_MAX_EXPORT_BATCH_SIZE = 512; + private static final long DEFAULT_EXPORT_TIMEOUT_MILLIS = 30_000; + + private final LogExporter logExporter; + private long scheduleDelayMillis = DEFAULT_SCHEDULE_DELAY_MILLIS; + private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + private int maxExportBatchSize = DEFAULT_MAX_EXPORT_BATCH_SIZE; + private long exporterTimeoutMillis = DEFAULT_EXPORT_TIMEOUT_MILLIS; + + BatchLogProcessorBuilder(LogExporter logExporter) { + this.logExporter = Objects.requireNonNull(logExporter, "Exporter argument can not be null"); + } + + /** + * Build a BatchLogProcessor. + * + * @return configured processor + */ + public BatchLogProcessor build() { + return new BatchLogProcessor( + maxQueueSize, scheduleDelayMillis, maxExportBatchSize, exporterTimeoutMillis, logExporter); + } + + /** + * Sets the delay interval between two consecutive exports. The actual interval may be shorter if + * the batch size is getting larger than {@code maxQueuedSpans / 2}. + * + *

    Default value is {@code 250}ms. + * + * @param scheduleDelayMillis the delay interval between two consecutive exports. + * @return this. + * @see BatchLogProcessorBuilder#DEFAULT_SCHEDULE_DELAY_MILLIS + */ + public BatchLogProcessorBuilder setScheduleDelayMillis(long scheduleDelayMillis) { + this.scheduleDelayMillis = scheduleDelayMillis; + return this; + } + + public long getScheduleDelayMillis() { + return scheduleDelayMillis; + } + + /** + * Sets the maximum time an exporter will be allowed to run before being cancelled. + * + *

    Default value is {@code 30000}ms + * + * @param exporterTimeoutMillis the timeout for exports in milliseconds. + * @return this + * @see BatchLogProcessorBuilder#DEFAULT_EXPORT_TIMEOUT_MILLIS + */ + public BatchLogProcessorBuilder setExporterTimeoutMillis(int exporterTimeoutMillis) { + this.exporterTimeoutMillis = exporterTimeoutMillis; + return this; + } + + public long getExporterTimeoutMillis() { + return exporterTimeoutMillis; + } + + /** + * Sets the maximum number of Spans that are kept in the queue before start dropping. + * + *

    See the BatchSampledSpansProcessor class description for a high-level design description of + * this class. + * + *

    Default value is {@code 2048}. + * + * @param maxQueueSize the maximum number of Spans that are kept in the queue before start + * dropping. + * @return this. + * @see BatchLogProcessorBuilder#DEFAULT_MAX_QUEUE_SIZE + */ + public BatchLogProcessorBuilder setMaxQueueSize(int maxQueueSize) { + this.maxQueueSize = maxQueueSize; + return this; + } + + public int getMaxQueueSize() { + return maxQueueSize; + } + + /** + * Sets the maximum batch size for every export. This must be smaller or equal to {@code + * maxQueuedSpans}. + * + *

    Default value is {@code 512}. + * + * @param maxExportBatchSize the maximum batch size for every export. + * @return this. + * @see BatchLogProcessorBuilder#DEFAULT_MAX_EXPORT_BATCH_SIZE + */ + public BatchLogProcessorBuilder setMaxExportBatchSize(int maxExportBatchSize) { + Utils.checkArgument(maxExportBatchSize > 0, "maxExportBatchSize must be positive."); + this.maxExportBatchSize = maxExportBatchSize; + return this; + } + + public int getMaxExportBatchSize() { + return maxExportBatchSize; + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/LogExporter.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/LogExporter.java new file mode 100644 index 000000000..bf1de27ea --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/LogExporter.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.export; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logging.data.LogRecord; +import java.util.Collection; + +/** + * An exporter is responsible for taking a list of {@link LogRecord}s and transmitting them to their + * ultimate destination. + */ +public interface LogExporter { + CompletableResultCode export(Collection records); + + CompletableResultCode shutdown(); +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/package-info.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/package-info.java new file mode 100644 index 000000000..6785bd37c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/export/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Log exporters. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.logging.export; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/package-info.java b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/package-info.java new file mode 100644 index 000000000..0d62278b4 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/main/java/io/opentelemetry/sdk/logging/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The OpenTelemetry SDK implementation of logging. + * + * @see io.opentelemetry.sdk.logging.LogSinkSdkProvider + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.logging; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/LogSinkSdkProviderTest.java b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/LogSinkSdkProviderTest.java new file mode 100644 index 000000000..1f1e6a3d0 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/LogSinkSdkProviderTest.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logging.data.LogRecord; +import io.opentelemetry.sdk.logging.export.BatchLogProcessor; +import io.opentelemetry.sdk.logging.util.TestLogExporter; +import io.opentelemetry.sdk.logging.util.TestLogProcessor; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class LogSinkSdkProviderTest { + + private static LogRecord createLog(LogRecord.Severity severity, String message) { + return LogRecord.builder() + .setUnixTimeMillis(System.currentTimeMillis()) + .setTraceId(TraceId.getInvalid()) + .setSpanId(SpanId.getInvalid()) + .setFlags(TraceFlags.getDefault().asByte()) + .setSeverity(severity) + .setSeverityText("really severe") + .setName("log1") + .setBody(message) + .setAttributes(Attributes.builder().put("animal", "cat").build()) + .build(); + } + + @Test + void testLogSinkSdkProvider() { + TestLogExporter exporter = new TestLogExporter(); + LogProcessor processor = BatchLogProcessor.builder(exporter).build(); + LogSinkSdkProvider provider = LogSinkSdkProvider.builder().build(); + provider.addLogProcessor(processor); + LogSink sink = provider.get("test", "0.1a"); + LogRecord log = createLog(LogRecord.Severity.ERROR, "test"); + sink.offer(log); + provider.forceFlush().join(500, TimeUnit.MILLISECONDS); + List records = exporter.getRecords(); + assertThat(records).singleElement().isEqualTo(log); + assertThat(log.getSeverity().getSeverityNumber()) + .isEqualTo(LogRecord.Severity.ERROR.getSeverityNumber()); + } + + @Test + void testBatchSize() { + TestLogExporter exporter = new TestLogExporter(); + LogProcessor processor = + BatchLogProcessor.builder(exporter) + .setScheduleDelayMillis(10000) // Long enough to not be in play + .setMaxExportBatchSize(5) + .setMaxQueueSize(10) + .build(); + LogSinkSdkProvider provider = LogSinkSdkProvider.builder().build(); + provider.addLogProcessor(processor); + LogSink sink = provider.get("test", "0.1a"); + + for (int i = 0; i < 7; i++) { + sink.offer(createLog(LogRecord.Severity.WARN, "test #" + i)); + } + // Ensure that more than batch size kicks off a flush + await().atMost(Duration.ofSeconds(5)).until(() -> exporter.getRecords().size() > 0); + // Ensure that everything gets through + CompletableResultCode result = provider.forceFlush(); + result.join(1, TimeUnit.SECONDS); + assertThat(exporter.getCallCount()).isGreaterThanOrEqualTo(2); + } + + @Test + void testNoBlocking() { + TestLogExporter exporter = new TestLogExporter(); + exporter.setOnCall( + () -> { + try { + Thread.sleep(250); + } catch (InterruptedException ex) { + fail("Exporter wait interrupted", ex); + } + }); + LogProcessor processor = + BatchLogProcessor.builder(exporter) + .setScheduleDelayMillis(3000) // Long enough to not be in play + .setMaxExportBatchSize(5) + .setMaxQueueSize(10) + .build(); + LogSinkSdkProvider provider = LogSinkSdkProvider.builder().build(); + provider.addLogProcessor(processor); + LogSink sink = provider.get("test", "0.1a"); + + long start = System.currentTimeMillis(); + int testRecordCount = 700; + for (int i = 0; i < testRecordCount; i++) { + sink.offer(createLog(LogRecord.Severity.WARN, "test #" + i)); + } + long end = System.currentTimeMillis(); + assertThat(end - start).isLessThan(250L); + provider.forceFlush().join(1, TimeUnit.SECONDS); + assertThat(exporter.getRecords().size()).isLessThan(testRecordCount); // We dropped records + } + + @Test + void testMultipleProcessors() { + TestLogProcessor processorOne = new TestLogProcessor(); + TestLogProcessor processorTwo = new TestLogProcessor(); + LogSinkSdkProvider provider = LogSinkSdkProvider.builder().build(); + provider.addLogProcessor(processorOne); + provider.addLogProcessor(processorTwo); + LogSink sink = provider.get("test", "0.1"); + LogRecord record = LogRecord.builder().setBody("test").build(); + sink.offer(record); + assertThat(processorOne.getRecords().size()).isEqualTo(1); + assertThat(processorTwo.getRecords().size()).isEqualTo(1); + assertThat(processorOne.getRecords().get(0)).isEqualTo(record); + assertThat(processorTwo.getRecords().get(0)).isEqualTo(record); + + CompletableResultCode flushResult = provider.forceFlush(); + flushResult.join(1, TimeUnit.SECONDS); + assertThat(processorOne.getFlushes()).isEqualTo(1); + assertThat(processorTwo.getFlushes()).isEqualTo(1); + + CompletableResultCode shutdownResult = provider.shutdown(); + shutdownResult.join(1, TimeUnit.SECONDS); + assertThat(processorOne.shutdownHasBeenCalled()).isEqualTo(true); + assertThat(processorTwo.shutdownHasBeenCalled()).isEqualTo(true); + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/data/AnyValueTest.java b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/data/AnyValueTest.java new file mode 100644 index 000000000..52ce5a449 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/data/AnyValueTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class AnyValueTest { + + @Test + void stringValue() { + AnyValue value = AnyValue.stringAnyValue("foobar"); + assertThat(value.getStringValue()).isEqualTo("foobar"); + assertThat(value.getType()).isEqualTo(AnyValue.Type.STRING); + + assertThatThrownBy(value::getLongValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getBoolValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getDoubleValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getArrayValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getKvlistValue).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void longValue() { + AnyValue value = AnyValue.longAnyValue(10); + assertThat(value.getLongValue()).isEqualTo(10); + assertThat(value.getType()).isEqualTo(AnyValue.Type.INT64); + + assertThatThrownBy(value::getStringValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getBoolValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getDoubleValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getArrayValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getKvlistValue).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void boolValue() { + AnyValue value = AnyValue.boolAnyValue(true); + assertThat(value.getBoolValue()).isTrue(); + assertThat(value.getType()).isEqualTo(AnyValue.Type.BOOL); + + assertThatThrownBy(value::getStringValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getLongValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getDoubleValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getArrayValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getKvlistValue).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void doubleValue() { + AnyValue value = AnyValue.doubleAnyValue(1.0); + assertThat(value.getDoubleValue()).isEqualTo(1.0); + assertThat(value.getType()).isEqualTo(AnyValue.Type.DOUBLE); + + assertThatThrownBy(value::getStringValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getLongValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getBoolValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getArrayValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getKvlistValue).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void arrayValue() { + AnyValue value = + AnyValue.arrayAnyValue( + Arrays.asList(AnyValue.stringAnyValue("cat"), AnyValue.stringAnyValue("dog"))); + assertThat(value.getArrayValue()) + .containsExactly(AnyValue.stringAnyValue("cat"), AnyValue.stringAnyValue("dog")); + assertThat(value.getType()).isEqualTo(AnyValue.Type.ARRAY); + + assertThatThrownBy(value::getStringValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getLongValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getBoolValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getDoubleValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getKvlistValue).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void kvlistValue() { + Map map = new HashMap<>(); + map.put("animal", AnyValue.stringAnyValue("cat")); + map.put("temperature", AnyValue.doubleAnyValue(30.0)); + AnyValue value = AnyValue.kvlistAnyValue(map); + assertThat(value.getKvlistValue()).containsExactlyInAnyOrderEntriesOf(map); + assertThat(value.getType()).isEqualTo(AnyValue.Type.KVLIST); + + assertThatThrownBy(value::getStringValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getLongValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getBoolValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getDoubleValue).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(value::getArrayValue).isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/sdk/BatchLogProcessorTest.java b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/sdk/BatchLogProcessorTest.java new file mode 100644 index 000000000..11148636a --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/sdk/BatchLogProcessorTest.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.sdk; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.awaitility.Awaitility.await; + +import io.opentelemetry.sdk.logging.data.LogRecord; +import io.opentelemetry.sdk.logging.export.BatchLogProcessor; +import io.opentelemetry.sdk.logging.util.TestLogExporter; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class BatchLogProcessorTest { + + @Test + void testForceExport() { + int batchSize = 10; + int testRecordsToSend = 17; // greater than, but not a multiple of batch + TestLogExporter exporter = new TestLogExporter(); + BatchLogProcessor processor = + BatchLogProcessor.builder(exporter) + .setMaxExportBatchSize(batchSize) + .setMaxQueueSize(20) // more than we will send + .setScheduleDelayMillis(2000) // longer than test + .build(); + for (int i = 0; i < 17; i++) { + LogRecord record = LogRecord.builder().setBody(Integer.toString(i)).build(); + processor.addLogRecord(record); + } + await().until(() -> exporter.getCallCount() > 0); + assertThat(exporter.getRecords().size()).isEqualTo(batchSize); + processor.forceFlush().join(1, TimeUnit.SECONDS); + assertThat(exporter.getRecords().size()).isEqualTo(testRecordsToSend); + processor.shutdown().join(1, TimeUnit.SECONDS); + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/util/TestLogExporter.java b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/util/TestLogExporter.java new file mode 100644 index 000000000..1ae1439d4 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/util/TestLogExporter.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.util; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logging.data.LogRecord; +import io.opentelemetry.sdk.logging.export.LogExporter; +import java.util.ArrayList; +import java.util.Collection; +import javax.annotation.Nullable; + +public class TestLogExporter implements LogExporter { + + private final ArrayList records = new ArrayList<>(); + @Nullable private Runnable onCall = null; + private int callCount = 0; + + @Override + public synchronized CompletableResultCode export(Collection records) { + this.records.addAll(records); + callCount++; + if (onCall != null) { + onCall.run(); + } + return null; + } + + @Override + public CompletableResultCode shutdown() { + return new CompletableResultCode().succeed(); + } + + public synchronized ArrayList getRecords() { + return records; + } + + public synchronized void setOnCall(@Nullable Runnable onCall) { + this.onCall = onCall; + } + + public synchronized int getCallCount() { + return callCount; + } +} diff --git a/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/util/TestLogProcessor.java b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/util/TestLogProcessor.java new file mode 100644 index 000000000..d3210ccb0 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/logging/src/test/java/io/opentelemetry/sdk/logging/util/TestLogProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.logging.util; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logging.LogProcessor; +import io.opentelemetry.sdk.logging.data.LogRecord; +import java.util.ArrayList; +import java.util.List; + +public class TestLogProcessor implements LogProcessor { + private final List records = new ArrayList<>(); + private boolean shutdownCalled = false; + private int flushes = 0; + + @Override + public void addLogRecord(LogRecord record) { + records.add(record); + } + + @Override + public CompletableResultCode shutdown() { + shutdownCalled = true; + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode forceFlush() { + flushes++; + return CompletableResultCode.ofSuccess(); + } + + public List getRecords() { + return records; + } + + public int getFlushes() { + return flushes; + } + + public boolean shutdownHasBeenCalled() { + return shutdownCalled; + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/README.md b/opentelemetry-java/sdk-extensions/resources/README.md new file mode 100644 index 000000000..a2e7da01c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/README.md @@ -0,0 +1,43 @@ +# OpenTelemetry Resource Providers + +This package includes some standard `ResourceProvider`s for filling in attributes related to +common environments. Currently the resources provide the following semantic conventions + +## Populated attributes + +### Operating System + +Provider: `io.opentelemetry.sdk.extension.resources.OsResource` + +Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/semantic_conventions/os.md + +Implemented attributes: +- `os.name` +- `os.description` + +### Process + +Implementation: `io.opentelemetry.sdk.extension.resources.ProcessResource` + +Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/semantic_conventions/process.md#process + +Implemented attributes: +- `process.pid` +- `process.executable.path` (note, we assume the `java` binary is located in the `bin` subfolder of `JAVA_HOME`) +- `process.command_line` (note this includes all system properties and arguments when running) + +### Java Runtime + +Implementation: `io.opentelemetry.sdk.extension.resources.ProcessRuntimeResource` + +Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/resource/semantic_conventions/process.md#process-runtimes + +Implemented attributes: +- `process.runtime.name` +- `process.runtime.version` +- `process.runtime.description` + +## Platforms + +This package currently does not run on Android. It has been verified on OpenJDK and should work on +other server JVM distributions but if you find any issues please let us know. \ No newline at end of file diff --git a/opentelemetry-java/sdk-extensions/resources/build.gradle.kts b/opentelemetry-java/sdk-extensions/resources/build.gradle.kts new file mode 100644 index 000000000..2e46cd61a --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/build.gradle.kts @@ -0,0 +1,85 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry SDK Resource Providers" +extra["moduleName"] = "io.opentelemetry.sdk.extension.resources" + +val mrJarVersions = listOf(11) + +dependencies { + api(project(":sdk:common")) + + implementation(project(":semconv")) + + compileOnly(project(":sdk-extensions:autoconfigure")) + + compileOnly("org.codehaus.mojo:animal-sniffer-annotations") + + testImplementation("org.junit-pioneer:junit-pioneer") +} + +sourceSets { + main { + output.dir("build/generated/properties", "builtBy" to "generateVersionResource") + } +} + +tasks { + register("generateVersionResource") { + val propertiesDir = file("build/generated/properties/io/opentelemetry/sdk/extension/resources") + outputs.dir(propertiesDir) + + doLast { + File(propertiesDir, "version.properties").writeText("sdk.version=${project.version}") + } + } +} + +for (version in mrJarVersions) { + sourceSets { + create("java${version}") { + java { + setSrcDirs(listOf("src/main/java${version}")) + } + } + } + + tasks { + named("compileJava${version}Java") { + sourceCompatibility = "${version}" + targetCompatibility = "${version}" + options.release.set(version) + } + } + + configurations { + named("java${version}Implementation") { + extendsFrom(configurations["implementation"]) + } + named("java${version}CompileOnly") { + extendsFrom(configurations["compileOnly"]) + } + } + + dependencies { + // Common to reference classes in main sourceset from Java 9 one (e.g., to return a common interface) + add("java${version}Implementation", files(sourceSets.main.get().output.classesDirs)) + } +} + +tasks { + withType(Jar::class) { + for (version in mrJarVersions) { + into("META-INF/versions/${version}") { + from(sourceSets["java${version}"].output) + } + } + manifest.attributes( + "Multi-Release" to "true" + ) + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/HostResource.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/HostResource.java new file mode 100644 index 000000000..efe358198 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/HostResource.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** Factory for a {@link Resource} which provides information about the host info. */ +public final class HostResource { + + private static final Resource INSTANCE = buildResource(); + + private static final boolean simpleness = true; + + /** Returns a {@link Resource} which provides information about host. */ + public static Resource get() { + return INSTANCE; + } + + // Visible for testing + static Resource buildResource() { + + AttributesBuilder attributes = Attributes.builder(); + try { + attributes.put(ResourceAttributes.HOST_NAME, InetAddress.getLocalHost().getHostName()); + } catch (UnknownHostException e) { + // Ignore + } + if (simpleness) { + return Resource.create(attributes.build()); + } + String hostArch = null; + try { + hostArch = System.getProperty("os.arch"); + } catch (SecurityException t) { + // Ignore + } + if (hostArch != null) { + attributes.put(ResourceAttributes.HOST_ARCH, hostArch); + } + + return Resource.create(attributes.build()); + } + + private HostResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/HostResourceProvider.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/HostResourceProvider.java new file mode 100644 index 000000000..486ce4077 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/HostResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link HostResource}. */ +public final class HostResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return HostResource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/OsResource.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/OsResource.java new file mode 100644 index 000000000..ed22fa07a --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/OsResource.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import javax.annotation.Nullable; + +/** Factory of a {@link Resource} which provides information about the current operating system. */ +public final class OsResource { + + private static final Resource INSTANCE = buildResource(); + + private static final boolean simpleness = true; + + /** + * Returns a factory for a {@link Resource} which provides information about the current operating + * system. + */ + public static Resource get() { + return INSTANCE; + } + + // Visible for testing + static Resource buildResource() { + if(simpleness){ + return Resource.empty(); + } + final String os; + try { + os = System.getProperty("os.name"); + } catch (SecurityException t) { + // Security manager enabled, can't provide much os information. + return Resource.empty(); + } + + if (os == null) { + return Resource.empty(); + } + + AttributesBuilder attributes = Attributes.builder(); + + String osName = getOs(os); + if (osName != null) { + attributes.put(ResourceAttributes.OS_TYPE, osName); + } + + String version = null; + try { + version = System.getProperty("os.version"); + } catch (SecurityException e) { + // Ignore + } + String osDescription = version != null ? os + ' ' + version : os; + attributes.put(ResourceAttributes.OS_DESCRIPTION, osDescription); + + return Resource.create(attributes.build()); + } + + @Nullable + private static String getOs(String os) { + os = os.toLowerCase(); + if (os.startsWith("windows")) { + return ResourceAttributes.OsTypeValues.WINDOWS; + } else if (os.startsWith("linux")) { + return ResourceAttributes.OsTypeValues.LINUX; + } else if (os.startsWith("mac")) { + return ResourceAttributes.OsTypeValues.DARWIN; + } else if (os.startsWith("freebsd")) { + return ResourceAttributes.OsTypeValues.FREEBSD; + } else if (os.startsWith("netbsd")) { + return ResourceAttributes.OsTypeValues.NETBSD; + } else if (os.startsWith("openbsd")) { + return ResourceAttributes.OsTypeValues.OPENBSD; + } else if (os.startsWith("dragonflybsd")) { + return ResourceAttributes.OsTypeValues.DRAGONFLYBSD; + } else if (os.startsWith("hp-ux")) { + return ResourceAttributes.OsTypeValues.HPUX; + } else if (os.startsWith("aix")) { + return ResourceAttributes.OsTypeValues.AIX; + } else if (os.startsWith("solaris")) { + return ResourceAttributes.OsTypeValues.SOLARIS; + } else if (os.startsWith("z/os")) { + return ResourceAttributes.OsTypeValues.Z_OS; + } + return null; + } + + private OsResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/OsResourceProvider.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/OsResourceProvider.java new file mode 100644 index 000000000..fd27c89c2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/OsResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link OsResource}. */ +public final class OsResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return OsResource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessPid.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessPid.java new file mode 100644 index 000000000..43ad8e10f --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessPid.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import java.lang.management.ManagementFactory; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +final class ProcessPid { + + private ProcessPid() {} + + @IgnoreJRERequirement + static long getPid() { + // While this is not strictly defined, almost all commonly used JVMs format this as + // pid@hostname. + String runtimeName = ManagementFactory.getRuntimeMXBean().getName(); + int atIndex = runtimeName.indexOf('@'); + if (atIndex >= 0) { + String pidString = runtimeName.substring(0, atIndex); + try { + return Long.parseLong(pidString); + } catch (NumberFormatException ignored) { + // Ignore parse failure. + } + } + return -1; + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessResource.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessResource.java new file mode 100644 index 000000000..986e80590 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessResource.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +/** Factory of a {@link Resource} which provides information about the current running process. */ +public final class ProcessResource { + + private static final Resource INSTANCE = buildResource(); + + private static final boolean simpleness = true; + + /** + * Returns a factory for a {@link Resource} which provides information about the current running + * process. + */ + public static Resource get() { + return INSTANCE; + } + + // Visible for testing + static Resource buildResource() { + if(simpleness){ + return Resource.empty(); + } + try { + return doBuildResource(); + } catch (LinkageError t) { + // Will only happen on Android, where these attributes generally don't make much sense + // anyways. + return Resource.empty(); + } + } + + @IgnoreJRERequirement + private static Resource doBuildResource() { + AttributesBuilder attributes = Attributes.builder(); + + RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); + + long pid = ProcessPid.getPid(); + + if (pid >= 0) { + attributes.put(ResourceAttributes.PROCESS_PID, pid); + } + + String javaHome = null; + String osName = null; + try { + javaHome = System.getProperty("java.home"); + osName = System.getProperty("os.name"); + } catch (SecurityException e) { + // Ignore + } + if (javaHome != null) { + StringBuilder executablePath = new StringBuilder(javaHome); + executablePath + .append(File.pathSeparatorChar) + .append("bin") + .append(File.pathSeparatorChar) + .append("java"); + if (osName != null && osName.toLowerCase().startsWith("windows")) { + executablePath.append(".exe"); + } + + attributes.put(ResourceAttributes.PROCESS_EXECUTABLE_PATH, executablePath.toString()); + + StringBuilder commandLine = new StringBuilder(executablePath); + for (String arg : runtime.getInputArguments()) { + commandLine.append(' ').append(arg); + } + attributes.put(ResourceAttributes.PROCESS_COMMAND_LINE, commandLine.toString()); + } + + return Resource.create(attributes.build()); + } + + private ProcessResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessResourceProvider.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessResourceProvider.java new file mode 100644 index 000000000..87953ce14 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link ProcessResource}. */ +public final class ProcessResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return ProcessResource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResource.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResource.java new file mode 100644 index 000000000..5381edca5 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResource.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.PROCESS_RUNTIME_DESCRIPTION; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.PROCESS_RUNTIME_NAME; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.PROCESS_RUNTIME_VERSION; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; + +/** Factory of a {@link Resource} which provides information about the Java runtime. */ +public final class ProcessRuntimeResource { + + private static final Resource INSTANCE = buildResource(); + + /** Returns a factory for a {@link Resource} which provides information about the Java runtime. */ + public static Resource get() { + return INSTANCE; + } + + private static final boolean simpleness = true; + + // Visible for testing + static Resource buildResource() { + if (simpleness) { + return Resource.empty(); + } + + try { + String name = System.getProperty("java.runtime.name"); + String version = System.getProperty("java.runtime.version"); + String description = + System.getProperty("java.vm.vendor") + + " " + + System.getProperty("java.vm.name") + + " " + + System.getProperty("java.vm.version"); + + return Resource.create( + Attributes.of( + PROCESS_RUNTIME_NAME, + name, + PROCESS_RUNTIME_VERSION, + version, + PROCESS_RUNTIME_DESCRIPTION, + description)); + } catch (SecurityException ignored) { + return Resource.empty(); + } + } + + private ProcessRuntimeResource() {} +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResourceProvider.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResourceProvider.java new file mode 100644 index 000000000..0c72a0155 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import io.opentelemetry.sdk.autoconfigure.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link ProcessRuntimeResource}. */ +public final class ProcessRuntimeResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return ProcessRuntimeResource.get(); + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/package-info.java b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/package-info.java new file mode 100644 index 000000000..9d134a78c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java/io/opentelemetry/sdk/extension/resources/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider} implementations for common + * resource information. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.resources; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/java11/io/opentelemetry/sdk/extension/resources/ProcessPid.java b/opentelemetry-java/sdk-extensions/resources/src/main/java11/io/opentelemetry/sdk/extension/resources/ProcessPid.java new file mode 100644 index 000000000..e4da0fc94 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/java11/io/opentelemetry/sdk/extension/resources/ProcessPid.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import java.lang.management.ManagementFactory; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +final class ProcessPid { + + private ProcessPid() {} + + @IgnoreJRERequirement + static long getPid() { + return ManagementFactory.getRuntimeMXBean().getPid(); + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/opentelemetry-java/sdk-extensions/resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider new file mode 100644 index 000000000..b7120017c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -0,0 +1,4 @@ +io.opentelemetry.sdk.extension.resources.HostResourceProvider +io.opentelemetry.sdk.extension.resources.OsResourceProvider +io.opentelemetry.sdk.extension.resources.ProcessResourceProvider +io.opentelemetry.sdk.extension.resources.ProcessRuntimeResourceProvider diff --git a/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/HostResourceTest.java b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/HostResourceTest.java new file mode 100644 index 000000000..48740ff54 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/HostResourceTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; + +class HostResourceTest { + @Test + void shouldCreateRuntimeAttributes() { + // when + Attributes attributes = HostResource.buildResource().getAttributes(); + + // then + assertThat(attributes.get(ResourceAttributes.HOST_NAME)).isNotBlank(); + assertThat(attributes.get(ResourceAttributes.HOST_ARCH)).isNotBlank(); + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @ExtendWith(SecurityManagerExtension.class) + static class SecurityManagerEnabled { + @Test + void empty() { + Attributes attributes = HostResource.buildResource().getAttributes(); + assertThat(attributes.asMap()).containsOnlyKeys(ResourceAttributes.HOST_NAME); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/OsResourceTest.java b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/OsResourceTest.java new file mode 100644 index 000000000..48ab8501d --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/OsResourceTest.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.SetSystemProperty; + +class OsResourceTest { + + @Test + @SetSystemProperty(key = "os.name", value = "Linux 4.11") + void linux() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.LINUX); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "MacOS X 11") + void macos() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.DARWIN); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "Windows 10") + void windows() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.WINDOWS); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "FreeBSD 10") + void freebsd() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.FREEBSD); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "NetBSD 10") + void netbsd() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.NETBSD); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "OpenBSD 10") + void openbsd() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.OPENBSD); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "DragonFlyBSD 10") + void dragonflybsd() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.DRAGONFLYBSD); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "HP-UX 10") + void hpux() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.HPUX); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "AIX 10") + void aix() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.AIX); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "Solaris 10") + void solaris() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.SOLARIS); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "Z/OS 10") + void zos() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)) + .isEqualTo(ResourceAttributes.OsTypeValues.Z_OS); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Test + @SetSystemProperty(key = "os.name", value = "RagOS 10") + void unknown() { + Attributes attributes = OsResource.buildResource().getAttributes(); + assertThat(attributes.get(ResourceAttributes.OS_TYPE)).isNull(); + assertThat(attributes.get(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @ExtendWith(SecurityManagerExtension.class) + static class SecurityManagerEnabled { + @Test + void empty() { + assertThat(OsResource.buildResource()).isEqualTo(Resource.empty()); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ProcessResourceTest.java b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ProcessResourceTest.java new file mode 100644 index 000000000..9e882a206 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ProcessResourceTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.SetSystemProperty; + +class ProcessResourceTest { + + @Test + @SetSystemProperty(key = "os.name", value = "Linux 4.12") + void notWindows() { + Attributes attributes = ProcessResource.buildResource().getAttributes(); + + assertThat(attributes.get(ResourceAttributes.PROCESS_PID)).isGreaterThan(1); + assertThat(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)) + .contains("java") + .doesNotEndWith(".exe"); + assertThat(attributes.get(ResourceAttributes.PROCESS_COMMAND_LINE)) + .contains(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)); + } + + @Test + @SetSystemProperty(key = "os.name", value = "Windows 10") + void windows() { + Attributes attributes = ProcessResource.buildResource().getAttributes(); + + assertThat(attributes.get(ResourceAttributes.PROCESS_PID)).isGreaterThan(1); + assertThat(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)) + .contains("java") + .endsWith(".exe"); + assertThat(attributes.get(ResourceAttributes.PROCESS_COMMAND_LINE)) + .contains(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)); + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @ExtendWith(SecurityManagerExtension.class) + static class SecurityManagerEnabled { + @Test + void empty() { + Attributes attributes = ProcessResource.buildResource().getAttributes(); + assertThat(attributes.asMap()).containsOnlyKeys(ResourceAttributes.PROCESS_PID); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResourceTest.java b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResourceTest.java new file mode 100644 index 000000000..7f0e656a2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/ProcessRuntimeResourceTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; + +class ProcessRuntimeResourceTest { + @Test + void shouldCreateRuntimeAttributes() { + // when + Attributes attributes = ProcessRuntimeResource.buildResource().getAttributes(); + + // then + assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_NAME)).isNotBlank(); + assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_VERSION)).isNotBlank(); + assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_DESCRIPTION)).isNotBlank(); + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @ExtendWith(SecurityManagerExtension.class) + static class SecurityManagerEnabled { + @Test + void empty() { + assertThat(ProcessRuntimeResource.buildResource()).isEqualTo(Resource.empty()); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/SecurityManagerExtension.java b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/SecurityManagerExtension.java new file mode 100644 index 000000000..904814bff --- /dev/null +++ b/opentelemetry-java/sdk-extensions/resources/src/test/java/io/opentelemetry/sdk/extension/resources/SecurityManagerExtension.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.resources; + +import java.security.Permission; +import java.util.PropertyPermission; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +final class SecurityManagerExtension implements BeforeEachCallback, AfterEachCallback { + + private static final ExtensionContext.Namespace NAMESPACE = + ExtensionContext.Namespace.create(SecurityManagerExtension.class); + + @Override + public void beforeEach(ExtensionContext context) { + context.getStore(NAMESPACE).put(SecurityManager.class, System.getSecurityManager()); + System.setSecurityManager(BlockPropertiesAccess.INSTANCE); + } + + @Override + public void afterEach(ExtensionContext context) { + System.setSecurityManager( + (SecurityManager) context.getStore(NAMESPACE).get(SecurityManager.class)); + } + + private static class BlockPropertiesAccess extends SecurityManager { + + private static final BlockPropertiesAccess INSTANCE = new BlockPropertiesAccess(); + + @Override + public void checkPermission(Permission perm) { + if (perm instanceof PropertyPermission) { + throw new SecurityException("Property access not allowed."); + } + } + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/build.gradle.kts b/opentelemetry-java/sdk-extensions/tracing-incubator/build.gradle.kts new file mode 100644 index 000000000..62876432b --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + `java-library` + `maven-publish` + + id("me.champeau.jmh") + id("ru.vyarus.animalsniffer") +} + +// SDK modules that are still being developed. + +description = "OpenTelemetry SDK Tracing Incubator" +extra["moduleName"] = "io.opentelemetry.sdk.extension.trace.incubator" + +dependencies { + api(project(":api:all")) + api(project(":sdk:all")) + + implementation(project(":api:metrics")) + implementation(project(":semconv")) + implementation("org.jctools:jctools-core:3.3.0") + + annotationProcessor("com.google.auto.value:auto-value") + testImplementation(project(":sdk:testing")) + testImplementation("com.google.guava:guava-testlib") + + jmh(project(":sdk:metrics")) + jmh(project(":sdk:testing")) { + // JMH doesn"t handle dependencies that are duplicated between the main and jmh + // configurations properly, but luckily here it"s simple enough to just exclude transitive + // dependencies. + isTransitive = false + } + jmh(project(":exporters:otlp:trace")) { + // The opentelemetry-exporter-otlp-trace depends on this project itself. So don"t pull in + // the transitive dependencies. + isTransitive = false + } + // explicitly adding the opentelemetry-exporter-otlp dependencies + jmh(project(":exporters:otlp:common")) { + isTransitive = false + } + jmh(project(":proto")) + + jmh("com.google.guava:guava") + jmh("io.grpc:grpc-api") + jmh("io.grpc:grpc-netty-shaded") + jmh("org.testcontainers:testcontainers") // testContainer for OTLP collector +} \ No newline at end of file diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/gradle.properties b/opentelemetry-java/sdk-extensions/tracing-incubator/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/BatchSpanProcessorMetrics.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/BatchSpanProcessorMetrics.java new file mode 100644 index 000000000..337af27ec --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/BatchSpanProcessorMetrics.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.Collection; +import java.util.OptionalLong; + +public class BatchSpanProcessorMetrics { + + private final Collection allMetrics; + private final int numThreads; + + public BatchSpanProcessorMetrics(Collection allMetrics, int numThreads) { + this.allMetrics = allMetrics; + this.numThreads = numThreads; + } + + public double dropRatio() { + long exported = getMetric(false); + long dropped = getMetric(true); + long total = exported + dropped; + // Due to peculiarities of JMH reporting we have to divide this by the number of the + // concurrent threads running the actual benchmark. + return total == 0 ? 0 : (double) dropped / total / numThreads; + } + + public long exportedSpans() { + return getMetric(false) / numThreads; + } + + public long droppedSpans() { + return getMetric(true) / numThreads; + } + + private long getMetric(boolean dropped) { + String labelValue = String.valueOf(dropped); + OptionalLong value = + allMetrics.stream() + .filter(metricData -> metricData.getName().equals("processedSpans")) + .filter(metricData -> !metricData.isEmpty()) + .map(metricData -> metricData.getLongSumData().getPoints()) + .flatMap(Collection::stream) + .filter(point -> labelValue.equals(point.getLabels().get("dropped"))) + .mapToLong(LongPointData::getValue) + .findFirst(); + return value.isPresent() ? value.getAsLong() : 0; + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/DelayingSpanExporter.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/DelayingSpanExporter.java new file mode 100644 index 000000000..c982fa36b --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/DelayingSpanExporter.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class DelayingSpanExporter implements SpanExporter { + + private final ScheduledExecutorService executor; + + private final int delayMs; + + public DelayingSpanExporter(int delayMs) { + executor = Executors.newScheduledThreadPool(5); + this.delayMs = delayMs; + } + + @SuppressWarnings("FutureReturnValueIgnored") + @Override + public CompletableResultCode export(Collection spans) { + final CompletableResultCode result = new CompletableResultCode(); + executor.schedule((Runnable) result::succeed, delayMs, TimeUnit.MILLISECONDS); + return result; + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorBenchmark.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorBenchmark.java new file mode 100644 index 000000000..6a22aded5 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorBenchmark.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class ExecutorServiceSpanProcessorBenchmark { + + @Param({"0", "1", "5"}) + private int delayMs; + + @Param({"1000", "2000", "5000"}) + private int spanCount; + + private List spans; + + private ExecutorServiceSpanProcessor processor; + + @Setup(Level.Trial) + public final void setup() { + SpanExporter exporter = new DelayingSpanExporter(delayMs); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + processor = ExecutorServiceSpanProcessor.builder(exporter, executor, true).build(); + + ImmutableList.Builder spans = ImmutableList.builderWithExpectedSize(spanCount); + Tracer tracer = SdkTracerProvider.builder().build().get("benchmarkTracer"); + for (int i = 0; i < spanCount; i++) { + spans.add(tracer.spanBuilder("span").startSpan()); + } + this.spans = spans.build(); + } + + @TearDown(Level.Trial) + public final void tearDown() { + processor.shutdown().join(10, TimeUnit.SECONDS); + } + + /** Export spans through {@link ExecutorServiceSpanProcessor}. */ + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export() { + for (Span span : spans) { + processor.onEnd((ReadableSpan) span); + } + processor.forceFlush().join(10, TimeUnit.MINUTES); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorCpuBenchmark.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorCpuBenchmark.java new file mode 100644 index 000000000..dcacf1553 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorCpuBenchmark.java @@ -0,0 +1,169 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/* + * Run this along with a profiler to measure the CPU usage of BatchSpanProcessor's exporter thread. + */ +public class ExecutorServiceSpanProcessorCpuBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + private SdkMeterProvider sdkMeterProvider; + private ExecutorServiceSpanProcessor processor; + private Tracer tracer; + private int numThreads = 1; + + @Param({"1"}) + private int delayMs; + + private long exportedSpans; + private long droppedSpans; + + @Setup(Level.Iteration) + public final void setup() { + sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + SpanExporter exporter = new DelayingSpanExporter(delayMs); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + processor = ExecutorServiceSpanProcessor.builder(exporter, executor, true).build(); + tracer = + SdkTracerProvider.builder().addSpanProcessor(processor).build().get("benchmarkTracer"); + } + + @TearDown(Level.Iteration) + public final void recordMetrics() { + BatchSpanProcessorMetrics metrics = + new BatchSpanProcessorMetrics(sdkMeterProvider.collectAllMetrics(), numThreads); + exportedSpans = metrics.exportedSpans(); + droppedSpans = metrics.droppedSpans(); + } + + @TearDown(Level.Iteration) + public final void tearDown() { + processor.shutdown().join(10, TimeUnit.SECONDS); + } + } + + @State(Scope.Thread) + @AuxCounters(AuxCounters.Type.OPERATIONS) + public static class ThreadState { + BenchmarkState benchmarkState; + + @TearDown(Level.Iteration) + public final void recordMetrics(BenchmarkState benchmarkState) { + this.benchmarkState = benchmarkState; + } + + public long exportedSpans() { + return benchmarkState.exportedSpans; + } + + public long droppedSpans() { + return benchmarkState.droppedSpans; + } + } + + private static void doWork(BenchmarkState benchmarkState) { + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + // This sleep is essential to maintain a steady state of the benchmark run by generating 10k + // spans per second per thread. Without this JMH outer loop consumes as much CPU as possible + // making comparing different processor versions difficult. + // Note that time spent outside of the sleep is negligible allowing this sleep to control + // span generation rate. Here we get 1 / 100_000 = 10K spans generated per second. + LockSupport.parkNanos(100_000); + } + + @Benchmark + @Fork(1) + @Threads(1) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_01Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 1; + doWork(benchmarkState); + } + + @Benchmark + @Fork(1) + @Threads(2) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_02Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 2; + doWork(benchmarkState); + } + + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_05Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 5; + doWork(benchmarkState); + } + + @Benchmark + @Fork(1) + @Threads(10) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_10Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 10; + doWork(benchmarkState); + } + + @Benchmark + @Fork(1) + @Threads(20) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_20Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 20; + doWork(benchmarkState); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorDroppedSpansBenchmark.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorDroppedSpansBenchmark.java new file mode 100644 index 000000000..b80f97695 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorDroppedSpansBenchmark.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +public class ExecutorServiceSpanProcessorDroppedSpansBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + private SdkMeterProvider sdkMeterProvider; + private ExecutorServiceSpanProcessor processor; + private Tracer tracer; + private double dropRatio; + private long exportedSpans; + private long droppedSpans; + private int numThreads; + + @Setup(Level.Iteration) + public final void setup() { + sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + SpanExporter exporter = new DelayingSpanExporter(0); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + processor = ExecutorServiceSpanProcessor.builder(exporter, executor, true).build(); + + tracer = SdkTracerProvider.builder().build().get("benchmarkTracer"); + } + + @TearDown(Level.Iteration) + public final void recordMetrics() { + BatchSpanProcessorMetrics metrics = + new BatchSpanProcessorMetrics(sdkMeterProvider.collectAllMetrics(), numThreads); + dropRatio = metrics.dropRatio(); + exportedSpans = metrics.exportedSpans(); + droppedSpans = metrics.droppedSpans(); + } + + @TearDown(Level.Iteration) + public final void tearDown() { + processor.shutdown(); + } + } + + @State(Scope.Thread) + @AuxCounters(AuxCounters.Type.OPERATIONS) + public static class ThreadState { + BenchmarkState benchmarkState; + + @TearDown(Level.Iteration) + public final void recordMetrics(BenchmarkState benchmarkState) { + this.benchmarkState = benchmarkState; + } + + public double dropRatio() { + return benchmarkState.dropRatio; + } + + public long exportedSpans() { + return benchmarkState.exportedSpans; + } + + public long droppedSpans() { + return benchmarkState.droppedSpans; + } + } + + /** Export spans through {@link ExecutorServiceSpanProcessor}. */ + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 5, time = 20) + @BenchmarkMode(Mode.Throughput) + public void export( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 5; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorFlushBenchmark.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorFlushBenchmark.java new file mode 100644 index 000000000..4f4b09ef8 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorFlushBenchmark.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class ExecutorServiceSpanProcessorFlushBenchmark { + + private static class DelayingSpanExporter implements SpanExporter { + + private final ScheduledExecutorService executor; + + private final int delayMs; + + private DelayingSpanExporter(int delayMs) { + executor = Executors.newScheduledThreadPool(5); + this.delayMs = delayMs; + } + + @SuppressWarnings("FutureReturnValueIgnored") + @Override + public CompletableResultCode export(Collection spans) { + final CompletableResultCode result = new CompletableResultCode(); + executor.schedule((Runnable) result::succeed, delayMs, TimeUnit.MILLISECONDS); + return result; + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + } + + @Param({"0", "1", "5"}) + private int delayMs; + + @Param({"1000", "2000", "5000"}) + private int spanCount; + + private List spans; + + private ExecutorServiceSpanProcessor processor; + + @Setup(Level.Trial) + public final void setup() { + SpanExporter exporter = new DelayingSpanExporter(delayMs); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + processor = ExecutorServiceSpanProcessor.builder(exporter, executor, true).build(); + ImmutableList.Builder spans = ImmutableList.builderWithExpectedSize(spanCount); + Tracer tracer = SdkTracerProvider.builder().build().get("benchmarkTracer"); + for (int i = 0; i < spanCount; i++) { + spans.add(tracer.spanBuilder("span").startSpan()); + } + this.spans = spans.build(); + } + + @TearDown(Level.Trial) + public final void tearDown() { + processor.shutdown().join(10, TimeUnit.SECONDS); + } + + /** Export spans through {@link ExecutorServiceSpanProcessor}. */ + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export() { + for (Span span : spans) { + processor.onEnd((ReadableSpan) span); + } + processor.forceFlush().join(10, TimeUnit.MINUTES); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorMultiThreadBenchmark.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorMultiThreadBenchmark.java new file mode 100644 index 000000000..e5dfea5e5 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorMultiThreadBenchmark.java @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class ExecutorServiceSpanProcessorMultiThreadBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + private SdkMeterProvider sdkMeterProvider; + private ExecutorServiceSpanProcessor processor; + private Tracer tracer; + private int numThreads = 1; + + @Param({"0"}) + private int delayMs; + + private long exportedSpans; + private long droppedSpans; + + @Setup(Level.Iteration) + public final void setup() { + sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + SpanExporter exporter = new DelayingSpanExporter(delayMs); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + processor = ExecutorServiceSpanProcessor.builder(exporter, executor, true).build(); + tracer = + SdkTracerProvider.builder().addSpanProcessor(processor).build().get("benchmarkTracer"); + } + + @TearDown(Level.Iteration) + public final void recordMetrics() { + BatchSpanProcessorMetrics metrics = + new BatchSpanProcessorMetrics(sdkMeterProvider.collectAllMetrics(), numThreads); + exportedSpans = metrics.exportedSpans(); + droppedSpans = metrics.droppedSpans(); + } + + @TearDown(Level.Trial) + public final void tearDown() { + processor.shutdown().join(10, TimeUnit.SECONDS); + } + } + + @State(Scope.Thread) + @AuxCounters(AuxCounters.Type.OPERATIONS) + public static class ThreadState { + BenchmarkState benchmarkState; + + @TearDown(Level.Iteration) + public final void recordMetrics(BenchmarkState benchmarkState) { + this.benchmarkState = benchmarkState; + } + + public long exportedSpans() { + return benchmarkState.exportedSpans; + } + + public long droppedSpans() { + return benchmarkState.droppedSpans; + } + } + + @Benchmark + @Fork(1) + @Threads(1) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_01Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 1; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } + + @Benchmark + @Fork(1) + @Threads(2) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_02Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 2; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } + + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_05Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 5; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } + + @Benchmark + @Fork(1) + @Threads(10) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_10Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 10; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } + + @Benchmark + @Fork(1) + @Threads(20) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_20Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 20; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/SpanPipelineBenchmark.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/SpanPipelineBenchmark.java new file mode 100644 index 000000000..022fc06d2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/java/io/opentelemetry/sdk/extension/incubator/trace/SpanPipelineBenchmark.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.util.concurrent.Executors; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class SpanPipelineBenchmark { + private SpanPipelineBenchmark() {} + + @State(Scope.Benchmark) + public abstract static class AbstractProcessorBenchmark { + private static final DockerImageName OTLP_COLLECTOR_IMAGE = + DockerImageName.parse("otel/opentelemetry-collector-dev:latest"); + private static final int EXPOSED_PORT = 5678; + private static final int HEALTH_CHECK_PORT = 13133; + private SpanBuilder sdkSpanBuilder; + + protected abstract SpanProcessor getSpanProcessor(String collectorAddress); + + protected abstract void runThePipeline(); + + protected void doWork() { + Span span = sdkSpanBuilder.startSpan(); + for (int i = 0; i < 10; i++) { + span.setAttribute("benchmarkAttribute_" + i, "benchmarkAttrValue_" + i); + } + span.end(); + } + + @Setup(Level.Trial) + public void setup() { + // Configuring the collector test-container + GenericContainer collector = + new GenericContainer<>(OTLP_COLLECTOR_IMAGE) + .withExposedPorts(EXPOSED_PORT, HEALTH_CHECK_PORT) + .waitingFor(Wait.forHttp("/").forPort(HEALTH_CHECK_PORT)) + .withCopyFileToContainer( + MountableFile.forClasspathResource("/otel.yaml"), "/etc/otel.yaml") + .withCommand("--config /etc/otel.yaml"); + + collector.start(); + + SpanProcessor spanProcessor = makeSpanProcessor(collector); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOn()) + .addSpanProcessor(spanProcessor) + .build(); + + Tracer tracerSdk = tracerProvider.get("PipelineBenchmarkTracer"); + sdkSpanBuilder = tracerSdk.spanBuilder("PipelineBenchmarkSpan"); + } + + private SpanProcessor makeSpanProcessor(GenericContainer collector) { + try { + String host = collector.getHost(); + Integer port = collector.getMappedPort(EXPOSED_PORT); + String address = new URL("http", host, port, "").toString(); + return getSpanProcessor(address); + } catch (MalformedURLException e) { + throw new IllegalStateException("can't make a url", e); + } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.SECONDS) + @Fork(1) + @Threads(1) + public void measureSpanPipeline() { + runThePipeline(); + } + } + + public static class ExecutorServiceSpanProcessorBenchmark extends AbstractProcessorBenchmark { + + @Override + protected SpanProcessor getSpanProcessor(String collectorAddress) { + return ExecutorServiceSpanProcessor.builder( + OtlpGrpcSpanExporter.builder() + .setEndpoint(collectorAddress) + .setTimeout(Duration.ofSeconds(50)) + .build(), + Executors.newSingleThreadScheduledExecutor(), + true) + .build(); + } + + @Override + protected void runThePipeline() { + doWork(); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/resources/otel.yaml b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/resources/otel.yaml new file mode 100644 index 000000000..1a7e0172e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/jmh/resources/otel.yaml @@ -0,0 +1,24 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:5678 + +processors: + batch: + queued_retry: + +extensions: + health_check: + +exporters: + logging: + loglevel: debug + +service: + extensions: [health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch, queued_retry] + exporters: [logging] \ No newline at end of file diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessor.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessor.java new file mode 100644 index 000000000..a4dc5c680 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessor.java @@ -0,0 +1,281 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; +import org.jctools.queues.MpscArrayQueue; + +/** + * A Batch {@link SpanProcessor} that uses a user-provided {@link + * java.util.concurrent.ScheduledExecutorService} to run background tasks. + */ +@SuppressWarnings("FutureReturnValueIgnored") +public final class ExecutorServiceSpanProcessor implements SpanProcessor { + + private static final String SPAN_PROCESSOR_TYPE_LABEL = "spanProcessorType"; + private static final String SPAN_PROCESSOR_TYPE_VALUE = + ExecutorServiceSpanProcessor.class.getSimpleName(); + private static final Labels SPAN_PROCESSOR_LABELS = + Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE); + private static final Labels SPAN_PROCESSOR_DROPPED_LABELS = + Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE, "dropped", "true"); + private static final Labels SPAN_PROCESSOR_EXPORTED_LABELS = + Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE, "dropped", "false"); + + private final Worker worker; + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + private final boolean ownsExecutorService; + private final ScheduledExecutorService executorService; + + /** + * Create a new {@link ExecutorServiceSpanProcessorBuilder} with the required components. + * + * @param spanExporter The {@link SpanExporter} to be used for exports. + * @param executorService The {@link ScheduledExecutorService} for running background tasks. + * @param ownsExecutorService Whether this component can be considered the "owner" of the provided + * {@link ScheduledExecutorService}. If true, the {@link ScheduledExecutorService} will be + * shut down when this SpanProcessor is shut down. + */ + public static ExecutorServiceSpanProcessorBuilder builder( + SpanExporter spanExporter, + ScheduledExecutorService executorService, + boolean ownsExecutorService) { + return new ExecutorServiceSpanProcessorBuilder( + spanExporter, executorService, ownsExecutorService); + } + + ExecutorServiceSpanProcessor( + SpanExporter spanExporter, + long scheduleDelayNanos, + int maxQueueSize, + int maxExportBatchSize, + long exporterTimeoutNanos, + ScheduledExecutorService executorService, + boolean ownsExecutorService, + long workerScheduleIntervalNanos) { + this.worker = + new Worker( + spanExporter, + scheduleDelayNanos, + maxExportBatchSize, + exporterTimeoutNanos, + new MpscArrayQueue<>(maxQueueSize), + executorService, + isShutdown, + workerScheduleIntervalNanos); + this.ownsExecutorService = ownsExecutorService; + this.executorService = executorService; + executorService.schedule(worker, workerScheduleIntervalNanos, TimeUnit.NANOSECONDS); + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) {} + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan span) { + if (!span.getSpanContext().isSampled()) { + return; + } + worker.addSpan(span); + } + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public CompletableResultCode shutdown() { + if (isShutdown.getAndSet(true)) { + return CompletableResultCode.ofSuccess(); + } + CompletableResultCode result = worker.shutdown(); + // do the cleanup after worker finishes flush + result.whenComplete( + () -> { + if (ownsExecutorService) { + executorService.shutdown(); + } + }); + + return result; + } + + @Override + public CompletableResultCode forceFlush() { + return worker.forceFlush(); + } + + // Visible for testing + List getBatch() { + return new ArrayList<>(worker.batch); + } + + private static class Worker implements Runnable { + + private final AtomicLong nextExportTime = new AtomicLong(); + private final ArrayBlockingQueue batch; + private final AtomicBoolean isShutdown; + private final long workerScheduleIntervalNanos; + private final WorkerExporter workerExporter; + private final BoundLongCounter droppedSpans; + private final AtomicReference flushRequested = new AtomicReference<>(); + private final long scheduleDelayNanos; + private final MpscArrayQueue queue; + private final int maxExportBatchSize; + private final SpanExporter spanExporter; + private final ScheduledExecutorService executorService; + + private Worker( + SpanExporter spanExporter, + long scheduleDelayNanos, + int maxExportBatchSize, + long exporterTimeoutNanos, + MpscArrayQueue queue, + ScheduledExecutorService executorService, + AtomicBoolean isShutdown, + long workerScheduleIntervalNanos) { + Meter meter = GlobalMeterProvider.getMeter("io.opentelemetry.sdk.trace"); + meter + .longValueObserverBuilder("queueSize") + .setDescription("The number of spans queued") + .setUnit("1") + .setUpdater(result -> result.observe(queue.size(), SPAN_PROCESSOR_LABELS)) + .build(); + LongCounter processedSpansCounter = + meter + .longCounterBuilder("processedSpans") + .setUnit("1") + .setDescription( + "The number of spans processed by the BatchSpanProcessor. " + + "[dropped=true if they were dropped due to high throughput]") + .build(); + droppedSpans = processedSpansCounter.bind(SPAN_PROCESSOR_DROPPED_LABELS); + BoundLongCounter exportedSpans = processedSpansCounter.bind(SPAN_PROCESSOR_EXPORTED_LABELS); + this.isShutdown = isShutdown; + this.executorService = executorService; + this.spanExporter = spanExporter; + this.batch = new ArrayBlockingQueue<>(maxExportBatchSize); + this.workerScheduleIntervalNanos = workerScheduleIntervalNanos; + this.maxExportBatchSize = maxExportBatchSize; + this.workerExporter = + new WorkerExporter( + spanExporter, + executorService, + Logger.getLogger(getClass().getName()), + exporterTimeoutNanos, + exportedSpans, + flushRequested, + maxExportBatchSize); + this.scheduleDelayNanos = scheduleDelayNanos; + this.queue = queue; + updateNextExportTime(); + } + + private void updateNextExportTime() { + nextExportTime.set(System.nanoTime() + scheduleDelayNanos); + } + + @Override + public void run() { + // nextExportTime is set for the first time in the constructor + + boolean continueWork = true; + while (continueWork && !isShutdown.get()) { + if (flushRequested.get() != null) { + workerExporter.flush(batch, queue); + } + + ReadableSpan lastElement = queue.poll(); + if (lastElement != null) { + batch.add(lastElement.toSpanData()); + } else { + // nothing in the queue, so schedule next run and release the thread + continueWork = false; + scheduleNextRun(); + } + + if (batch.size() >= maxExportBatchSize || System.nanoTime() >= nextExportTime.get()) { + continueWork = false; + workerExporter.exportCurrentBatch(batch).whenComplete(this::scheduleNextRun); + updateNextExportTime(); + } + } + // flush may be requested when processor shuts down + if (flushRequested.get() != null) { + workerExporter.flush(batch, queue); + } + } + + private void scheduleNextRun() { + if (!isShutdown.get()) { + executorService.schedule(this, workerScheduleIntervalNanos, TimeUnit.NANOSECONDS); + } + } + + public CompletableResultCode shutdown() { + final CompletableResultCode result = new CompletableResultCode(); + + final CompletableResultCode flushResult = forceFlush(); + flushResult.whenComplete( + () -> { + final CompletableResultCode shutdownResult = spanExporter.shutdown(); + shutdownResult.whenComplete( + () -> { + if (!flushResult.isSuccess() || !shutdownResult.isSuccess()) { + result.fail(); + } else { + result.succeed(); + } + }); + }); + + return result; + } + + public void addSpan(ReadableSpan span) { + if (!queue.offer(span)) { + droppedSpans.add(1); + } + } + + public CompletableResultCode forceFlush() { + CompletableResultCode flushResult = new CompletableResultCode(); + // we set the atomic here to trigger the worker loop to do a flush on its next iteration. + flushRequested.compareAndSet(null, flushResult); + CompletableResultCode possibleResult = flushRequested.get(); + // there's a race here where the flush happening in the worker loop could complete before we + // get what's in the atomic. In that case, just return success, since we know it succeeded in + // the interim. + return possibleResult == null ? CompletableResultCode.ofSuccess() : possibleResult; + } + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorBuilder.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorBuilder.java new file mode 100644 index 000000000..d6fa954f6 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorBuilder.java @@ -0,0 +1,190 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import static io.opentelemetry.api.internal.Utils.checkArgument; +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Builder class for the {@link ExecutorServiceSpanProcessor}. + * + * @see ExecutorServiceSpanProcessor#builder(SpanExporter, ScheduledExecutorService, boolean) + */ +public class ExecutorServiceSpanProcessorBuilder { + + // Visible for testing + static final long DEFAULT_SCHEDULE_DELAY_MILLIS = 5000; + // Visible for testing + static final int DEFAULT_MAX_QUEUE_SIZE = 2048; + // Visible for testing + static final int DEFAULT_MAX_EXPORT_BATCH_SIZE = 512; + // Visible for testing + static final int DEFAULT_EXPORT_TIMEOUT_MILLIS = 30_000; + // Visible for testing + static final int WORKER_SCHEDULE_INTERVAL_NANOS = 100_000; + + private final SpanExporter spanExporter; + private final boolean ownsExecutorService; + private final ScheduledExecutorService executorService; + private long scheduleDelayNanos = TimeUnit.MILLISECONDS.toNanos(DEFAULT_SCHEDULE_DELAY_MILLIS); + private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + private int maxExportBatchSize = DEFAULT_MAX_EXPORT_BATCH_SIZE; + private long exporterTimeoutNanos = TimeUnit.MILLISECONDS.toNanos(DEFAULT_EXPORT_TIMEOUT_MILLIS); + private long workerScheduleIntervalNanos = WORKER_SCHEDULE_INTERVAL_NANOS; + + ExecutorServiceSpanProcessorBuilder( + SpanExporter spanExporter, + ScheduledExecutorService executorService, + boolean ownsExecutorService) { + this.spanExporter = spanExporter; + this.ownsExecutorService = ownsExecutorService; + this.executorService = executorService; + } + + /** + * Sets the delay interval between two consecutive exports. If unset, defaults to {@value + * DEFAULT_SCHEDULE_DELAY_MILLIS}ms. + */ + public ExecutorServiceSpanProcessorBuilder setScheduleDelay(long delay, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(delay >= 0, "delay must be non-negative"); + scheduleDelayNanos = unit.toNanos(delay); + return this; + } + + /** + * Sets the delay interval between two consecutive exports. If unset, defaults to {@value + * DEFAULT_SCHEDULE_DELAY_MILLIS}ms. + */ + public ExecutorServiceSpanProcessorBuilder setScheduleDelay(Duration delay) { + requireNonNull(delay, "delay"); + return setScheduleDelay(delay.toNanos(), TimeUnit.NANOSECONDS); + } + + // Visible for testing + long getScheduleDelayNanos() { + return scheduleDelayNanos; + } + + /** + * Sets the maximum time an export will be allowed to run before being cancelled. If unset, + * defaults to {@value DEFAULT_EXPORT_TIMEOUT_MILLIS}ms. + */ + public ExecutorServiceSpanProcessorBuilder setExporterTimeout(long timeout, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(timeout >= 0, "timeout must be non-negative"); + exporterTimeoutNanos = unit.toNanos(timeout); + return this; + } + + /** + * Sets the maximum time an export will be allowed to run before being cancelled. If unset, + * defaults to {@value DEFAULT_EXPORT_TIMEOUT_MILLIS}ms. + */ + public ExecutorServiceSpanProcessorBuilder setExporterTimeout(Duration timeout) { + requireNonNull(timeout, "timeout"); + return setExporterTimeout(timeout.toNanos(), TimeUnit.NANOSECONDS); + } + + // Visible for testing + long getExporterTimeoutNanos() { + return exporterTimeoutNanos; + } + + /** + * Sets the maximum number of Spans that are kept in the queue before start dropping. + * + *

    See the BatchSampledSpansProcessor class description for a high-level design description of + * this class. + * + *

    Default value is {@code 2048}. + * + * @param maxQueueSize the maximum number of Spans that are kept in the queue before start + * dropping. + * @return this. + * @see ExecutorServiceSpanProcessorBuilder#DEFAULT_MAX_QUEUE_SIZE + */ + public ExecutorServiceSpanProcessorBuilder setMaxQueueSize(int maxQueueSize) { + this.maxQueueSize = maxQueueSize; + return this; + } + + // Visible for testing + int getMaxQueueSize() { + return maxQueueSize; + } + + /** + * Sets the maximum batch size for every export. This must be smaller or equal to {@code + * maxQueuedSpans}. + * + *

    Default value is {@code 512}. + * + * @param maxExportBatchSize the maximum batch size for every export. + * @return this. + * @see ExecutorServiceSpanProcessorBuilder#DEFAULT_MAX_EXPORT_BATCH_SIZE + */ + public ExecutorServiceSpanProcessorBuilder setMaxExportBatchSize(int maxExportBatchSize) { + checkArgument(maxExportBatchSize > 0, "maxExportBatchSize must be positive."); + this.maxExportBatchSize = maxExportBatchSize; + return this; + } + + /** + * Sets the delay interval between two consecutive runs of the worker job. If unset, defaults to + * {@value WORKER_SCHEDULE_INTERVAL_NANOS}ns. + */ + public ExecutorServiceSpanProcessorBuilder setWorkerScheduleInterval(Duration interval) { + requireNonNull(interval, "interval"); + return setWorkerScheduleInterval(interval.toNanos(), TimeUnit.NANOSECONDS); + } + + /** + * Sets the delay interval between two consecutive runs of the worker job. If unset, defaults to + * {@value WORKER_SCHEDULE_INTERVAL_NANOS}ms. + */ + public ExecutorServiceSpanProcessorBuilder setWorkerScheduleInterval( + long interval, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(interval >= 0, "interval must be non-negative"); + workerScheduleIntervalNanos = unit.toNanos(interval); + return this; + } + + // Visible for testing + long getWorkerScheduleInterval() { + return workerScheduleIntervalNanos; + } + + // Visible for testing + int getMaxExportBatchSize() { + return maxExportBatchSize; + } + + /** + * Returns a new {@link ExecutorServiceSpanProcessor} that batches, then converts spans to proto + * and forwards them to the given {@code spanExporter}. + * + * @return a new {@link ExecutorServiceSpanProcessor}. + * @throws NullPointerException if the {@code spanExporter} is {@code null}. + */ + public ExecutorServiceSpanProcessor build() { + return new ExecutorServiceSpanProcessor( + spanExporter, + scheduleDelayNanos, + maxQueueSize, + maxExportBatchSize, + exporterTimeoutNanos, + executorService, + ownsExecutorService, + workerScheduleIntervalNanos); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/LeakDetectingSpanProcessor.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/LeakDetectingSpanProcessor.java new file mode 100644 index 000000000..8fd7b68ce --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/LeakDetectingSpanProcessor.java @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import static java.lang.Thread.currentThread; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import java.lang.ref.Reference; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link SpanProcessor} which will detect spans that are never ended. It will detect spans that + * are garbage collected without ever having `end()` called on them. + * + *

    Note: using this SpanProcessor will definitely impact the performance of your application. It + * is not recommended for production use, as it uses additional memory for each span to track where + * a leaked span was created. + */ +public final class LeakDetectingSpanProcessor implements SpanProcessor { + private static final Logger logger = Logger.getLogger(LeakDetectingSpanProcessor.class.getName()); + + private final PendingSpans pendingSpans; + + /** + * Create a new {@link LeakDetectingSpanProcessor} that will report any un-ended spans that get + * garbage collected. + */ + public static LeakDetectingSpanProcessor create() { + return new LeakDetectingSpanProcessor( + (message, throwable) -> + logger.log(Level.WARNING, "Span garbage collected before being ended.", throwable)); + } + + // Visible for testing + LeakDetectingSpanProcessor(BiConsumer reporter) { + pendingSpans = PendingSpans.create(reporter); + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + CallerStackTrace caller = new CallerStackTrace(span); + StackTraceElement[] stackTrace = caller.getStackTrace(); + + // take off the first 3 stack frames, as they are from the SDK itself. + caller.setStackTrace( + Arrays.copyOfRange(stackTrace, Math.min(3, stackTrace.length), stackTrace.length)); + + pendingSpans.put(span, caller); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + pendingSpans.remove(span).ended = true; + } + + @Override + public boolean isEndRequired() { + return true; + } + + private static class PendingSpans extends WeakConcurrentMap { + + private final ConcurrentHashMap, CallerStackTrace> map; + private final BiConsumer reporter; + + @SuppressWarnings("ThreadPriorityCheck") + private static PendingSpans create(BiConsumer reporter) { + PendingSpans pendingSpans = new PendingSpans(new ConcurrentHashMap<>(), reporter); + // Start cleaner thread ourselves to make sure it runs after initializing our fields. + Thread thread = new Thread(pendingSpans); + thread.setName("weak-ref-cleaner-leakingspandetector"); + thread.setPriority(Thread.MIN_PRIORITY); + thread.setDaemon(true); + thread.start(); + return pendingSpans; + } + + private PendingSpans( + ConcurrentHashMap, CallerStackTrace> map, + BiConsumer reporter) { + super(/* cleanerThread= */ false, /* reuseKeys= */ false, map); + this.map = map; + this.reporter = reporter; + } + + // Called by cleaner thread. + @Override + public void run() { + try { + while (!Thread.interrupted()) { + // call blocks until something is GC'd. + Reference gcdReference = remove(); + CallerStackTrace caller = map.remove(gcdReference); + if (caller != null && !caller.ended) { + reporter.accept("Span garbage collected before being ended.", callerError(caller)); + } + } + } catch (InterruptedException ignored) { + // do nothing + } + } + } + + private static class CallerStackTrace extends Throwable { + + private static final long serialVersionUID = 1234567896L; + + final String threadName = currentThread().getName(); + final String spanInformation; + + volatile boolean ended; + + CallerStackTrace(ReadableSpan span) { + super("Thread [" + currentThread().getName() + "] started span : " + span + " here:"); + this.spanInformation = span.getName() + " [" + span.getSpanContext() + "]"; + } + } + + private static AssertionError callerError(CallerStackTrace caller) { + AssertionError toThrow = + new AssertionError( + "Span garbage collected before being ended. Thread: [" + + caller.threadName + + "] started span : " + + caller.spanInformation + + " here:"); + toThrow.setStackTrace(caller.getStackTrace()); + return toThrow; + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/WorkerExporter.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/WorkerExporter.java new file mode 100644 index 000000000..66ed37bcf --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/WorkerExporter.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.AbstractQueue; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +class WorkerExporter { + + private final SpanExporter spanExporter; + private final ScheduledExecutorService executorService; + private final Logger logger; + private final long exporterTimeoutNanos; + private final BoundLongCounter exportedSpanCounter; + private final AtomicReference flushSignal; + private final int maxExportBatchSize; + + WorkerExporter( + SpanExporter spanExporter, + ScheduledExecutorService executorService, + Logger logger, + long exporterTimeoutNanos, + BoundLongCounter exportedSpanCounter, + AtomicReference flushSignal, + int maxExportBatchSize) { + this.spanExporter = spanExporter; + this.executorService = executorService; + this.logger = logger; + this.exporterTimeoutNanos = exporterTimeoutNanos; + this.exportedSpanCounter = exportedSpanCounter; + this.flushSignal = flushSignal; + this.maxExportBatchSize = maxExportBatchSize; + } + + /** + * Exports spans in current batch. + * + * @param batch Collection containing {@link SpanData} to export + * @return a {@link CompletableResultCode} which completes when export completes or timeouts + */ + public CompletableResultCode exportCurrentBatch(Collection batch) { + final CompletableResultCode thisOpResult = new CompletableResultCode(); + if (batch.isEmpty()) { + thisOpResult.succeed(); + return thisOpResult; + } + + final CompletableResultCode result = spanExporter.export(new ArrayList<>(batch)); + final AtomicBoolean cleaner = new AtomicBoolean(true); + final ScheduledFuture timeoutHandler = + executorService.schedule( + () -> { + if (cleaner.compareAndSet(true, false)) { + logger.log(Level.FINE, "Timeout happened when waiting for export to complete"); + batch.clear(); + thisOpResult.fail(); + } + }, + exporterTimeoutNanos, + TimeUnit.NANOSECONDS); + + result.whenComplete( + () -> { + if (cleaner.compareAndSet(true, false)) { + timeoutHandler.cancel(true); + if (result.isSuccess()) { + exportedSpanCounter.add(batch.size()); + batch.clear(); + thisOpResult.succeed(); + } else { + logger.log(Level.FINE, "Exporter failed"); + batch.clear(); + thisOpResult.fail(); + } + } + }); + return thisOpResult; + } + + /** + * Flushes (exports) spans from both batch and queue. + * + * @param batch a collection of {@link SpanData} to export + * @param queue {@link ReadableSpan} queue to be drained and then exported + */ + public void flush(Collection batch, AbstractQueue queue) { + int spansToFlush = queue.size(); + while (spansToFlush > 0) { + ReadableSpan span = queue.poll(); + assert span != null; + batch.add(span.toSpanData()); + spansToFlush--; + if (batch.size() >= maxExportBatchSize) { + exportCurrentBatch(batch); + } + } + exportCurrentBatch(batch); + flushSignal.get().succeed(); + flushSignal.set(null); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/DelegatingSpanData.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/DelegatingSpanData.java new file mode 100644 index 000000000..b455cdcdd --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/DelegatingSpanData.java @@ -0,0 +1,253 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.data; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.List; + +/** + * A {@link SpanData} which delegates all methods to another {@link SpanData}. Extend this class to + * modify the {@link SpanData} that will be exported, for example by creating a delegating {@link + * io.opentelemetry.sdk.trace.export.SpanExporter} which wraps {@link SpanData} with a custom + * implementation. + * + *

    {@code
    + * // class SpanDataWithClientType extends DelegatingSpanData {
    + * //
    + * //   private final Attributes attributes;
    + * //
    + * //   SpanDataWithClientType(SpanData delegate) {
    + * //     super(delegate);
    + * //     String clientType = ClientConfig.parseUserAgent(
    + * //       delegate.getAttributes().get(SemanticAttributes.HTTP_USER_AGENT).getStringValue());
    + * //     Attributes.Builder newAttributes = Attributes.builder(delegate.getAttributes());
    + * //     newAttributes.setAttribute("client_type", clientType);
    + * //     attributes = newAttributes.build();
    + * //   }
    + * //
    + * //   @Override
    + * //   public Attributes getAttributes() {
    + * //     return attributes;
    + * //   }
    + * // }
    + * }
    + */ +public abstract class DelegatingSpanData implements SpanData { + + private final SpanData delegate; + + protected DelegatingSpanData(SpanData delegate) { + this.delegate = requireNonNull(delegate, "delegate"); + } + + @Override + public SpanContext getSpanContext() { + return delegate.getSpanContext(); + } + + @Override + public SpanContext getParentSpanContext() { + return delegate.getParentSpanContext(); + } + + @Override + public Resource getResource() { + return delegate.getResource(); + } + + @Override + public InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return delegate.getInstrumentationLibraryInfo(); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public SpanKind getKind() { + return delegate.getKind(); + } + + @Override + public long getStartEpochNanos() { + return delegate.getStartEpochNanos(); + } + + @Override + public Attributes getAttributes() { + return delegate.getAttributes(); + } + + @Override + public List getEvents() { + return delegate.getEvents(); + } + + @Override + public List getLinks() { + return delegate.getLinks(); + } + + @Override + public StatusData getStatus() { + return delegate.getStatus(); + } + + @Override + public long getEndEpochNanos() { + return delegate.getEndEpochNanos(); + } + + @Override + public boolean hasEnded() { + return delegate.hasEnded(); + } + + @Override + public int getTotalRecordedEvents() { + return delegate.getTotalRecordedEvents(); + } + + @Override + public int getTotalRecordedLinks() { + return delegate.getTotalRecordedLinks(); + } + + @Override + public int getTotalAttributeCount() { + return delegate.getTotalAttributeCount(); + } + + @Override + public final boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof SpanData) { + SpanData that = (SpanData) o; + return getSpanContext().equals(that.getSpanContext()) + && getParentSpanContext().equals(that.getParentSpanContext()) + && getResource().equals(that.getResource()) + && getInstrumentationLibraryInfo().equals(that.getInstrumentationLibraryInfo()) + && getName().equals(that.getName()) + && getKind().equals(that.getKind()) + && getStartEpochNanos() == that.getStartEpochNanos() + && getAttributes().equals(that.getAttributes()) + && getEvents().equals(that.getEvents()) + && getLinks().equals(that.getLinks()) + && getStatus().equals(that.getStatus()) + && getEndEpochNanos() == that.getEndEpochNanos() + && hasEnded() == that.hasEnded() + && getTotalRecordedEvents() == that.getTotalRecordedEvents() + && getTotalRecordedLinks() == that.getTotalRecordedLinks() + && getTotalAttributeCount() == that.getTotalAttributeCount(); + } + return false; + } + + @Override + public int hashCode() { + int code = 1; + code *= 1000003; + code ^= getSpanContext().hashCode(); + code *= 1000003; + code ^= getParentSpanContext().hashCode(); + code *= 1000003; + code ^= getResource().hashCode(); + code *= 1000003; + code ^= getInstrumentationLibraryInfo().hashCode(); + code *= 1000003; + code ^= getName().hashCode(); + code *= 1000003; + code ^= getKind().hashCode(); + code *= 1000003; + code ^= (int) ((getStartEpochNanos() >>> 32) ^ getStartEpochNanos()); + code *= 1000003; + code ^= getAttributes().hashCode(); + code *= 1000003; + code ^= getEvents().hashCode(); + code *= 1000003; + code ^= getLinks().hashCode(); + code *= 1000003; + code ^= getStatus().hashCode(); + code *= 1000003; + code ^= (int) ((getEndEpochNanos() >>> 32) ^ getEndEpochNanos()); + code *= 1000003; + code ^= hasEnded() ? 1231 : 1237; + code *= 1000003; + code ^= getTotalRecordedEvents(); + code *= 1000003; + code ^= getTotalRecordedLinks(); + code *= 1000003; + code ^= getTotalAttributeCount(); + return code; + } + + @Override + public String toString() { + return "SpanDataImpl{" + + "spanContext=" + + getSpanContext() + + ", " + + "parentSpanContext=" + + getParentSpanContext() + + ", " + + "resource=" + + getResource() + + ", " + + "instrumentationLibraryInfo=" + + getInstrumentationLibraryInfo() + + ", " + + "name=" + + getName() + + ", " + + "kind=" + + getKind() + + ", " + + "startEpochNanos=" + + getStartEpochNanos() + + ", " + + "attributes=" + + getAttributes() + + ", " + + "events=" + + getEvents() + + ", " + + "links=" + + getLinks() + + ", " + + "status=" + + getStatus() + + ", " + + "endEpochNanos=" + + getEndEpochNanos() + + ", " + + "hasEnded=" + + hasEnded() + + ", " + + "totalRecordedEvents=" + + getTotalRecordedEvents() + + ", " + + "totalRecordedLinks=" + + getTotalRecordedLinks() + + ", " + + "totalAttributeCount=" + + getTotalAttributeCount() + + "}"; + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/SpanDataBuilder.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/SpanDataBuilder.java new file mode 100644 index 000000000..14565b7d5 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/SpanDataBuilder.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * A {@link SpanData} implementation with a builder that can be used to modify parts of a {@link + * SpanData}. + * + *
    {@code
    + * String clientType = ClientConfig.parseUserAgent(
    + *   data.getAttributes().get(SemanticAttributes.HTTP_USER_AGENT).getStringValue());
    + * Attributes newAttributes = Attributes.builder(data.getAttributes())
    + *   .setAttribute("client_type", clientType)
    + *   .build();
    + * data = io.opentelemetry.sdk.extension.incubator.trace.data.SpanData.builder(data)
    + *   .setAttributes(newAttributes)
    + *   .build();
    + * exporter.export(data);
    + *
    + * }
    + */ +// AutoValue generated hashCode is fine but we need to define equals to accept the base SpanData +// type. +@Immutable +@AutoValue +public abstract class SpanDataBuilder implements SpanData { + + /** + * Returns a {@link SpanDataBuilder.Builder} populated with the information in the provided {@link + * SpanData}. + */ + public static SpanDataBuilder.Builder builder(SpanData spanData) { + return new AutoValue_SpanDataBuilder.Builder() + .setSpanContext(spanData.getSpanContext()) + .setParentSpanContext(spanData.getParentSpanContext()) + .setResource(spanData.getResource()) + .setInstrumentationLibraryInfo(spanData.getInstrumentationLibraryInfo()) + .setName(spanData.getName()) + .setKind(spanData.getKind()) + .setStartEpochNanos(spanData.getStartEpochNanos()) + .setAttributes(spanData.getAttributes()) + .setEvents(spanData.getEvents()) + .setLinks(spanData.getLinks()) + .setStatus(spanData.getStatus()) + .setEndEpochNanos(spanData.getEndEpochNanos()) + .setHasEnded(spanData.hasEnded()) + .setTotalRecordedEvents(spanData.getTotalRecordedEvents()) + .setTotalRecordedLinks(spanData.getTotalRecordedLinks()) + .setTotalAttributeCount(spanData.getTotalAttributeCount()); + } + + public final SpanDataBuilder.Builder toBuilder() { + return autoToBuilder(); + } + + abstract Builder autoToBuilder(); + + abstract boolean getInternalHasEnded(); + + @Override + public final boolean hasEnded() { + return getInternalHasEnded(); + } + + // AutoValue won't generate equals that compares with SpanData interface but generates hash code + // fine. + @SuppressWarnings("EqualsHashCode") + @Override + public final boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o instanceof SpanData) { + SpanData that = (SpanData) o; + return getSpanContext().equals(that.getSpanContext()) + && getParentSpanContext().equals(that.getParentSpanContext()) + && getResource().equals(that.getResource()) + && getInstrumentationLibraryInfo().equals(that.getInstrumentationLibraryInfo()) + && getName().equals(that.getName()) + && getKind().equals(that.getKind()) + && getStartEpochNanos() == that.getStartEpochNanos() + && getAttributes().equals(that.getAttributes()) + && getEvents().equals(that.getEvents()) + && getLinks().equals(that.getLinks()) + && getStatus().equals(that.getStatus()) + && getEndEpochNanos() == that.getEndEpochNanos() + && hasEnded() == that.hasEnded() + && getTotalRecordedEvents() == that.getTotalRecordedEvents() + && getTotalRecordedLinks() == that.getTotalRecordedLinks() + && getTotalAttributeCount() == that.getTotalAttributeCount(); + } + return false; + } + + /** A {@code Builder} class for {@link SpanDataBuilder}. */ + @AutoValue.Builder + abstract static class Builder { + + public final SpanData build() { + return autoBuild(); + } + + abstract SpanDataBuilder autoBuild(); + + public abstract Builder setSpanContext(SpanContext spanContext); + + public abstract Builder setParentSpanContext(SpanContext parentSpanContext); + + public abstract Builder setResource(Resource resource); + + public abstract Builder setInstrumentationLibraryInfo( + InstrumentationLibraryInfo instrumentationLibraryInfo); + + public abstract Builder setName(String name); + + public abstract Builder setStartEpochNanos(long epochNanos); + + public abstract Builder setEndEpochNanos(long epochNanos); + + public abstract Builder setAttributes(Attributes attributes); + + public abstract Builder setEvents(List events); + + public abstract Builder setStatus(StatusData status); + + public abstract Builder setKind(SpanKind kind); + + public abstract Builder setLinks(List links); + + abstract Builder setInternalHasEnded(boolean hasEnded); + + public final Builder setHasEnded(boolean hasEnded) { + return setInternalHasEnded(hasEnded); + } + + public abstract Builder setTotalRecordedEvents(int totalRecordedEvents); + + public abstract Builder setTotalRecordedLinks(int totalRecordedLinks); + + public abstract Builder setTotalAttributeCount(int totalAttributeCount); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/package-info.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/package-info.java new file mode 100644 index 000000000..b13026d8a --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/data/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Experimental utilities for working with exported trace data. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.incubator.trace.data; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/BlockingSpanExporter.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/BlockingSpanExporter.java new file mode 100644 index 000000000..561838baa --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/BlockingSpanExporter.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; + +public final class BlockingSpanExporter implements SpanExporter { + + final Object monitor = new Object(); + + private enum State { + WAIT_TO_BLOCK, + BLOCKED, + UNBLOCKED + } + + @GuardedBy("monitor") + State state = State.WAIT_TO_BLOCK; + + @Override + public CompletableResultCode export(Collection spanDataList) { + synchronized (monitor) { + while (state != State.UNBLOCKED) { + try { + state = State.BLOCKED; + // Some threads may wait for Blocked State. + monitor.notifyAll(); + monitor.wait(); + } catch (InterruptedException e) { + // Do nothing + } + } + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + public void waitUntilIsBlocked() { + synchronized (monitor) { + while (state != State.BLOCKED) { + try { + monitor.wait(); + } catch (InterruptedException e) { + // Do nothing + } + } + } + } + + @Override + public CompletableResultCode shutdown() { + // Do nothing; + return CompletableResultCode.ofSuccess(); + } + + public void unblock() { + synchronized (monitor) { + state = State.UNBLOCKED; + monitor.notifyAll(); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/CompletableSpanExporter.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/CompletableSpanExporter.java new file mode 100644 index 000000000..6acc5618f --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/CompletableSpanExporter.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class CompletableSpanExporter implements SpanExporter { + + private final List results = new ArrayList<>(); + + private final List exported = new ArrayList<>(); + + private volatile boolean succeeded; + + List getExported() { + return exported; + } + + void succeed() { + succeeded = true; + results.forEach(CompletableResultCode::succeed); + } + + @Override + public CompletableResultCode export(Collection spans) { + exported.addAll(spans); + if (succeeded) { + return CompletableResultCode.ofSuccess(); + } + CompletableResultCode result = new CompletableResultCode(); + results.add(result); + return result; + } + + @Override + public CompletableResultCode flush() { + if (succeeded) { + return CompletableResultCode.ofSuccess(); + } else { + return CompletableResultCode.ofFailure(); + } + } + + @Override + public CompletableResultCode shutdown() { + return flush(); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorTest.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorTest.java new file mode 100644 index 000000000..6bb61f91c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/ExecutorServiceSpanProcessorTest.java @@ -0,0 +1,538 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@SuppressWarnings("PreferJavaTimeOverload") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ExecutorServiceSpanProcessorTest { + + private static final String SPAN_NAME_1 = "MySpanName/1"; + private static final String SPAN_NAME_2 = "MySpanName/2"; + private static final long MAX_SCHEDULE_DELAY_MILLIS = 500; + private static final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + private SdkTracerProvider sdkTracerProvider; + private final BlockingSpanExporter blockingSpanExporter = new BlockingSpanExporter(); + + @Mock private Sampler mockSampler; + @Mock private SpanExporter mockSpanExporter; + + @BeforeEach + void setUp() { + when(mockSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + } + + @AfterEach + void cleanup() { + if (sdkTracerProvider != null) { + sdkTracerProvider.shutdown(); + } + } + + @AfterAll + static void stopScheduler() { + executor.shutdown(); + } + + private ReadableSpan createEndedSpan(String spanName) { + Tracer tracer = sdkTracerProvider.get(getClass().getName()); + Span span = tracer.spanBuilder(spanName).startSpan(); + span.end(); + return (ReadableSpan) span; + } + + @Test + void configTest_EmptyOptions() { + ExecutorServiceSpanProcessorBuilder config = + ExecutorServiceSpanProcessor.builder( + new WaitingSpanExporter(0, CompletableResultCode.ofSuccess()), + mock(ScheduledExecutorService.class), + false); + assertThat(config.getScheduleDelayNanos()) + .isEqualTo( + TimeUnit.MILLISECONDS.toNanos( + ExecutorServiceSpanProcessorBuilder.DEFAULT_SCHEDULE_DELAY_MILLIS)); + assertThat(config.getMaxQueueSize()) + .isEqualTo(ExecutorServiceSpanProcessorBuilder.DEFAULT_MAX_QUEUE_SIZE); + assertThat(config.getMaxExportBatchSize()) + .isEqualTo(ExecutorServiceSpanProcessorBuilder.DEFAULT_MAX_EXPORT_BATCH_SIZE); + assertThat(config.getExporterTimeoutNanos()) + .isEqualTo( + TimeUnit.MILLISECONDS.toNanos( + ExecutorServiceSpanProcessorBuilder.DEFAULT_EXPORT_TIMEOUT_MILLIS)); + assertThat(config.getWorkerScheduleInterval()) + .isEqualTo(ExecutorServiceSpanProcessorBuilder.WORKER_SCHEDULE_INTERVAL_NANOS); + } + + private static ExecutorServiceSpanProcessorBuilder dummyBuilder( + SpanExporter exporter, ScheduledExecutorService executor) { + return ExecutorServiceSpanProcessor.builder(exporter, executor, false); + } + + @Test + void invalidConfig() { + SpanExporter exporter = mock(SpanExporter.class); + ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + assertThatThrownBy( + () -> dummyBuilder(exporter, executor).setScheduleDelay(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("delay must be non-negative"); + assertThatThrownBy(() -> dummyBuilder(exporter, executor).setScheduleDelay(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy(() -> dummyBuilder(exporter, executor).setScheduleDelay(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("delay"); + assertThatThrownBy( + () -> dummyBuilder(exporter, executor).setExporterTimeout(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("timeout must be non-negative"); + assertThatThrownBy(() -> dummyBuilder(exporter, executor).setExporterTimeout(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy(() -> dummyBuilder(exporter, executor).setExporterTimeout(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("timeout"); + assertThatThrownBy( + () -> + dummyBuilder(exporter, executor) + .setWorkerScheduleInterval(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("interval must be non-negative"); + assertThatThrownBy(() -> dummyBuilder(exporter, executor).setWorkerScheduleInterval(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy(() -> dummyBuilder(exporter, executor).setWorkerScheduleInterval(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("interval"); + } + + @Test + void startEndRequirements() { + ExecutorServiceSpanProcessor spansProcessor = + ExecutorServiceSpanProcessor.builder( + new WaitingSpanExporter(0, CompletableResultCode.ofSuccess()), executor, false) + .build(); + assertThat(spansProcessor.isStartRequired()).isFalse(); + assertThat(spansProcessor.isEndRequired()).isTrue(); + } + + @Test + void exportDifferentSampledSpans() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(2, CompletableResultCode.ofSuccess()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + ExecutorServiceSpanProcessor.builder(waitingSpanExporter, executor, false) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .build(); + + ReadableSpan span1 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span2 = createEndedSpan(SPAN_NAME_2); + List exported = waitingSpanExporter.waitForExport(); + assertThat(exported).containsExactly(span1.toSpanData(), span2.toSpanData()); + } + + @Test + void exportMoreSpansThanTheBufferSize() { + CompletableSpanExporter spanExporter = new CompletableSpanExporter(); + + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + ExecutorServiceSpanProcessor.builder(spanExporter, executor, false) + .setMaxQueueSize(6) + .setMaxExportBatchSize(2) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .build(); + + ReadableSpan span1 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span2 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span3 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span4 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span5 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span6 = createEndedSpan(SPAN_NAME_1); + + spanExporter.succeed(); + + await() + .untilAsserted( + () -> + assertThat(spanExporter.getExported()) + .containsExactly( + span1.toSpanData(), + span2.toSpanData(), + span3.toSpanData(), + span4.toSpanData(), + span5.toSpanData(), + span6.toSpanData())); + } + + @Test + void forceExport() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(100, CompletableResultCode.ofSuccess(), 1); + ExecutorServiceSpanProcessor executorServiceSpanProcessor = + ExecutorServiceSpanProcessor.builder(waitingSpanExporter, executor, false) + .setMaxQueueSize(10_000) + // Force flush should send all spans, make sure the number of spans we check here is + // not divisible by the batch size. + .setMaxExportBatchSize(49) + .setScheduleDelay(1000, TimeUnit.SECONDS) + .build(); + + sdkTracerProvider = + SdkTracerProvider.builder().addSpanProcessor(executorServiceSpanProcessor).build(); + for (int i = 0; i < 100; i++) { + createEndedSpan("notExported"); + } + List exported = waitingSpanExporter.waitForExport(); + assertThat(exported).isNotNull(); + assertThat(exported.size()).isEqualTo(98); + + executorServiceSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS); + exported = waitingSpanExporter.getExported(); + assertThat(exported).isNotNull(); + assertThat(exported.size()).isEqualTo(2); + } + + @Test + void exportSpansToMultipleServices() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(2, CompletableResultCode.ofSuccess()); + WaitingSpanExporter waitingSpanExporter2 = + new WaitingSpanExporter(2, CompletableResultCode.ofSuccess()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + ExecutorServiceSpanProcessor.builder( + SpanExporter.composite( + Arrays.asList(waitingSpanExporter, waitingSpanExporter2)), + executor, + false) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .build(); + + ReadableSpan span1 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span2 = createEndedSpan(SPAN_NAME_2); + List exported1 = waitingSpanExporter.waitForExport(); + List exported2 = waitingSpanExporter2.waitForExport(); + assertThat(exported1).containsExactly(span1.toSpanData(), span2.toSpanData()); + assertThat(exported2).containsExactly(span1.toSpanData(), span2.toSpanData()); + } + + @Test + void exportMoreSpansThanTheMaximumLimit() { + final int maxQueuedSpans = 8; + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(maxQueuedSpans, CompletableResultCode.ofSuccess()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + ExecutorServiceSpanProcessor.builder( + SpanExporter.composite( + Arrays.asList(blockingSpanExporter, waitingSpanExporter)), + executor, + false) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .setMaxQueueSize(maxQueuedSpans) + .setMaxExportBatchSize(maxQueuedSpans / 2) + .build()) + .build(); + + List spansToExport = new ArrayList<>(maxQueuedSpans + 1); + // Wait to block the worker thread in the BatchSampledSpansProcessor. This ensures that no items + // can be removed from the queue. Need to add a span to trigger the export otherwise the + // pipeline is never called. + spansToExport.add(createEndedSpan("blocking_span").toSpanData()); + blockingSpanExporter.waitUntilIsBlocked(); + + for (int i = 0; i < maxQueuedSpans; i++) { + // First export maxQueuedSpans, the worker thread is blocked so all items should be queued. + spansToExport.add(createEndedSpan("span_1_" + i).toSpanData()); + } + + // TODO: assertThat(spanExporter.getReferencedSpans()).isEqualTo(maxQueuedSpans); + + // Now we should start dropping. + for (int i = 0; i < 7; i++) { + createEndedSpan("span_2_" + i); + // TODO: assertThat(getDroppedSpans()).isEqualTo(i + 1); + } + + // TODO: assertThat(getReferencedSpans()).isEqualTo(maxQueuedSpans); + + // Release the blocking exporter + blockingSpanExporter.unblock(); + + // While we wait for maxQueuedSpans we ensure that the queue is also empty after this. + List exported = waitingSpanExporter.waitForExport(); + assertThat(exported).isNotNull(); + assertThat(exported).containsExactlyElementsOf(spansToExport); + exported.clear(); + spansToExport.clear(); + + waitingSpanExporter.reset(); + // We cannot compare with maxReferencedSpans here because the worker thread may get + // unscheduled immediately after exporting, but before updating the pushed spans, if that is + // the case at most bufferSize spans will miss. + // TODO: assertThat(getPushedSpans()).isAtLeast((long) maxQueuedSpans - maxBatchSize); + + for (int i = 0; i < maxQueuedSpans; i++) { + spansToExport.add(createEndedSpan("span_3_" + i).toSpanData()); + // No more dropped spans. + // TODO: assertThat(getDroppedSpans()).isEqualTo(7); + } + + exported = waitingSpanExporter.waitForExport(); + assertThat(exported).isNotNull(); + assertThat(exported).containsExactlyElementsOf(spansToExport); + } + + @Test + void exporterThrowsException() { + SpanExporter mockSpanExporter = mock(SpanExporter.class); + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + doThrow(new IllegalArgumentException("No export for you.")) + .when(mockSpanExporter) + .export(ArgumentMatchers.anyList()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + ExecutorServiceSpanProcessor.builder( + SpanExporter.composite( + Arrays.asList(mockSpanExporter, waitingSpanExporter)), + executor, + false) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .build(); + ReadableSpan span1 = createEndedSpan(SPAN_NAME_1); + List exported = waitingSpanExporter.waitForExport(); + assertThat(exported).containsExactly(span1.toSpanData()); + waitingSpanExporter.reset(); + // Continue to export after the exception was received. + ReadableSpan span2 = createEndedSpan(SPAN_NAME_2); + exported = waitingSpanExporter.waitForExport(); + assertThat(exported).containsExactly(span2.toSpanData()); + } + + @Test + @Timeout(5) + public void continuesIfExporterTimesOut() throws InterruptedException { + int exporterTimeoutMillis = 10; + ExecutorServiceSpanProcessor essp = + ExecutorServiceSpanProcessor.builder(mockSpanExporter, executor, false) + .setExporterTimeout(exporterTimeoutMillis, TimeUnit.MILLISECONDS) + .setScheduleDelay(1, TimeUnit.MILLISECONDS) + .setMaxQueueSize(1) + .build(); + sdkTracerProvider = SdkTracerProvider.builder().addSpanProcessor(essp).build(); + + CountDownLatch exported = new CountDownLatch(1); + // We return a result we never complete, meaning it will timeout. + when(mockSpanExporter.export( + argThat( + spans -> { + assertThat(spans) + .anySatisfy(span -> assertThat(span.getName()).isEqualTo(SPAN_NAME_1)); + exported.countDown(); + return true; + }))) + .thenReturn(new CompletableResultCode()); + createEndedSpan(SPAN_NAME_1); + exported.await(); + // Timed out so the span was dropped. + await().untilAsserted(() -> assertThat(essp.getBatch()).isEmpty()); + + // Still processing new spans. + CountDownLatch exportedAgain = new CountDownLatch(1); + reset(mockSpanExporter); + when(mockSpanExporter.export( + argThat( + spans -> { + assertThat(spans) + .anySatisfy(span -> assertThat(span.getName()).isEqualTo(SPAN_NAME_2)); + exportedAgain.countDown(); + return true; + }))) + .thenReturn(CompletableResultCode.ofSuccess()); + createEndedSpan(SPAN_NAME_2); + exported.await(); + await().untilAsserted(() -> assertThat(essp.getBatch()).isEmpty()); + } + + @Test + void exportNotSampledSpans() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + ExecutorServiceSpanProcessor.builder(waitingSpanExporter, executor, false) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .setSampler(mockSampler) + .build(); + + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.DROP)); + sdkTracerProvider.get("test").spanBuilder(SPAN_NAME_1).startSpan().end(); + sdkTracerProvider.get("test").spanBuilder(SPAN_NAME_2).startSpan().end(); + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); + ReadableSpan span = createEndedSpan(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 exported = waitingSpanExporter.waitForExport(); + // Need to check this because otherwise the variable span1 is unused, other option is to not + // have a span1 variable. + assertThat(exported).containsExactly(span.toSpanData()); + } + + @Test + void exportNotSampledSpans_recordOnly() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.RECORD_ONLY)); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + ExecutorServiceSpanProcessor.builder(waitingSpanExporter, executor, false) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .setSampler(mockSampler) + .build(); + + createEndedSpan(SPAN_NAME_1); + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); + ReadableSpan span = createEndedSpan(SPAN_NAME_2); + + // Spans are recorded and exported in the same order as they are ended, we test that a non + // exported 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 exported = waitingSpanExporter.waitForExport(); + // Need to check this because otherwise the variable span1 is unused, other option is to not + // have a span1 variable. + assertThat(exported).containsExactly(span.toSpanData()); + } + + @Test + @Timeout(10) + void shutdownFlushes() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + // Set the export delay to large value, in order to confirm the #flush() below works + + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + ExecutorServiceSpanProcessor.builder(waitingSpanExporter, executor, false) + .setScheduleDelay(10, TimeUnit.SECONDS) + .build()) + .build(); + + ReadableSpan span2 = createEndedSpan(SPAN_NAME_2); + + // Force a shutdown, which forces processing of all remaining spans. + sdkTracerProvider.shutdown().join(10, TimeUnit.SECONDS); + + List exported = waitingSpanExporter.getExported(); + assertThat(exported).containsExactly(span2.toSpanData()); + assertThat(waitingSpanExporter.shutDownCalled.get()).isTrue(); + } + + @Test + void shutdownPropagatesSuccess() { + SpanExporter mockSpanExporter = mock(SpanExporter.class); + when(mockSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + ExecutorServiceSpanProcessor processor = + ExecutorServiceSpanProcessor.builder(mockSpanExporter, executor, false).build(); + CompletableResultCode result = processor.shutdown(); + result.join(1, TimeUnit.SECONDS); + assertThat(result.isSuccess()).isTrue(); + } + + @Test + void shutdownPropagatesFailure() { + SpanExporter mockSpanExporter = mock(SpanExporter.class); + when(mockSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofFailure()); + ExecutorServiceSpanProcessor processor = + ExecutorServiceSpanProcessor.builder(mockSpanExporter, executor, false).build(); + CompletableResultCode result = processor.shutdown(); + result.join(1, TimeUnit.SECONDS); + assertThat(result.isSuccess()).isFalse(); + } + + @Test + void shouldShutdownOwnedExecutor() { + // given + ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + SpanExporter spanExporter = mock(SpanExporter.class); + ExecutorServiceSpanProcessor processor = + ExecutorServiceSpanProcessor.builder(spanExporter, executorService, true).build(); + + // when + when(spanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + CompletableResultCode result = processor.shutdown(); + + // then + result.join(5, TimeUnit.SECONDS); + await().untilAsserted(() -> Assertions.assertThat(executorService.isShutdown()).isTrue()); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/LeakDetectingSpanProcessorTest.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/LeakDetectingSpanProcessorTest.java new file mode 100644 index 000000000..27ef612e2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/LeakDetectingSpanProcessorTest.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.google.common.testing.GcFinalization; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.lang.ref.WeakReference; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class LeakDetectingSpanProcessorTest { + + @Test + void basics() { + assertThat(LeakDetectingSpanProcessor.create().isEndRequired()).isTrue(); + assertThat(LeakDetectingSpanProcessor.create().isStartRequired()).isTrue(); + } + + @Test + void garbageCollectedUnendedSpan() { + List logs = new ArrayList<>(); + LeakDetectingSpanProcessor spanProcessor = + new LeakDetectingSpanProcessor((message, callerStackTrace) -> logs.add(callerStackTrace)); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); + + Tracer tracer = tracerProvider.get("test"); + + tracer.spanBuilder("testSpan").startSpan(); + + await() + .atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + System.gc(); + assertThat(logs) + .singleElement() + .satisfies( + callerStackTrace -> + assertThat(callerStackTrace.getMessage()) + .matches( + "Span garbage collected before being ended\\. " + + "Thread: \\[.*\\] started span : .*")); + }); + } + + @Test + void garbageCollectedEndedSpan() { + List logs = new ArrayList<>(); + LeakDetectingSpanProcessor spanProcessor = + new LeakDetectingSpanProcessor((message, callerStackTrace) -> logs.add(callerStackTrace)); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); + + Tracer tracer = tracerProvider.get("test"); + + Span testSpan = tracer.spanBuilder("testSpan").startSpan(); + WeakReference spanRef = new WeakReference<>(testSpan); + + testSpan.end(); + testSpan = null; + + GcFinalization.awaitClear(spanRef); + + assertThat(logs).isEmpty(); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/WaitingSpanExporter.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/WaitingSpanExporter.java new file mode 100644 index 000000000..4acb0ab98 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/WaitingSpanExporter.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; + +public class WaitingSpanExporter implements SpanExporter { + + private final ConcurrentLinkedQueue spanDataList = new ConcurrentLinkedQueue<>(); + private final int numberToWaitFor; + private final CompletableResultCode exportResultCode; + private CountDownLatch countDownLatch; + private int timeout = 10; + public final AtomicBoolean shutDownCalled = new AtomicBoolean(false); + + WaitingSpanExporter(int numberToWaitFor, CompletableResultCode exportResultCode) { + countDownLatch = new CountDownLatch(numberToWaitFor); + this.numberToWaitFor = numberToWaitFor; + this.exportResultCode = exportResultCode; + } + + WaitingSpanExporter(int numberToWaitFor, CompletableResultCode exportResultCode, int timeout) { + this(numberToWaitFor, exportResultCode); + this.timeout = timeout; + } + + List getExported() { + List result = new ArrayList<>(spanDataList); + spanDataList.clear(); + return result; + } + + /** + * 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. + * + * @return the list of exported {@link SpanData} objects, otherwise {@code null} if the current + * thread is interrupted. + */ + @Nullable + List waitForExport() { + try { + countDownLatch.await(timeout, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // Preserve the interruption status as per guidance. + Thread.currentThread().interrupt(); + return null; + } + return getExported(); + } + + @Override + public CompletableResultCode export(Collection spans) { + this.spanDataList.addAll(spans); + for (int i = 0; i < spans.size(); i++) { + countDownLatch.countDown(); + } + return exportResultCode; + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + shutDownCalled.set(true); + return CompletableResultCode.ofSuccess(); + } + + public void reset() { + this.countDownLatch = new CountDownLatch(numberToWaitFor); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/data/DelegatingSpanDataTest.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/data/DelegatingSpanDataTest.java new file mode 100644 index 000000000..d928adf96 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/data/DelegatingSpanDataTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.data; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.testing.EqualsTester; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; +import org.junit.jupiter.api.Test; + +class DelegatingSpanDataTest { + + private static final AttributeKey CLIENT_TYPE_KEY = stringKey("client_type"); + + private static final class NoOpDelegatingSpanData extends DelegatingSpanData { + private NoOpDelegatingSpanData(SpanData delegate) { + super(delegate); + } + } + + private static final class SpanDataWithClientType extends DelegatingSpanData { + + private final Attributes attributes; + + private SpanDataWithClientType(SpanData delegate) { + super(delegate); + final String clientType; + String userAgent = delegate.getAttributes().get(SemanticAttributes.HTTP_USER_AGENT); + if (userAgent != null) { + clientType = parseUserAgent(userAgent); + } else { + clientType = "unknown"; + } + AttributesBuilder newAttributes = Attributes.builder(); + newAttributes.putAll(delegate.getAttributes()); + newAttributes.put("client_type", clientType); + attributes = newAttributes.build(); + } + + @Override + public Attributes getAttributes() { + return attributes; + } + + private static String parseUserAgent(String userAgent) { + if (userAgent.startsWith("Mozilla/")) { + return "browser"; + } else if (userAgent.startsWith("Phone/")) { + return "phone"; + } + return "unknown"; + } + } + + @Test + void delegates() { + SpanData spanData = createBasicSpanBuilder().build(); + SpanData noopWrapper = new NoOpDelegatingSpanData(spanData); + // Test should always verify delegation is working even when methods are added since it calls + // each method individually. + assertThat(noopWrapper) + .usingRecursiveComparison( + RecursiveComparisonConfiguration.builder().withIgnoredFields("delegate").build()) + .isEqualTo(spanData); + } + + @Test + void overrideDelegate() { + SpanData spanData = createBasicSpanBuilder().build(); + SpanData spanDataWithClientType = new SpanDataWithClientType(spanData); + assertThat(spanDataWithClientType.getAttributes().get(CLIENT_TYPE_KEY)).isEqualTo("unknown"); + } + + @Test + void equals() { + SpanData spanData = createBasicSpanBuilder().build(); + SpanData noopWrapper = new NoOpDelegatingSpanData(spanData); + SpanData spanDataWithClientType = new SpanDataWithClientType(spanData); + + assertThat(noopWrapper).isEqualTo(spanData); + // TODO(anuraaga): Bug - spanData.equals(noopWrapper) should be equal but AutoValue does not + // implement equals for interfaces properly. We can't add it as a separate group either since + // noopWrapper.equals(spanData) does work properly. + assertThat(spanData).isNotEqualTo(noopWrapper); + + new EqualsTester() + .addEqualityGroup(noopWrapper) + .addEqualityGroup(spanDataWithClientType) + .testEquals(); + assertThat(spanDataWithClientType.getAttributes().get(CLIENT_TYPE_KEY)).isEqualTo("unknown"); + } + + private static TestSpanData.Builder createBasicSpanBuilder() { + return TestSpanData.builder() + .setHasEnded(true) + .setName("spanName") + .setStartEpochNanos(100) + .setEndEpochNanos(200) + .setKind(SpanKind.SERVER) + .setStatus(StatusData.ok()) + .setTotalRecordedEvents(0) + .setTotalRecordedLinks(0); + } +} diff --git a/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/data/SpanDataBuilderTest.java b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/data/SpanDataBuilderTest.java new file mode 100644 index 000000000..b212820ac --- /dev/null +++ b/opentelemetry-java/sdk-extensions/tracing-incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/trace/data/SpanDataBuilderTest.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.trace.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.testing.EqualsTester; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import org.junit.jupiter.api.Test; + +class SpanDataBuilderTest { + + private static final String TRACE_ID = "00000000000000000000000000abc123"; + private static final String SPAN_ID = "0000000000def456"; + + private static final TestSpanData TEST_SPAN_DATA = + TestSpanData.builder() + .setHasEnded(true) + .setSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.getSampled(), TraceState.getDefault())) + .setName("GET /api/endpoint") + .setStartEpochNanos(0) + .setEndEpochNanos(100) + .setKind(SpanKind.SERVER) + .setStatus(StatusData.error()) + .setAttributes(Attributes.builder().put("cat", "meow").put("dog", "bark").build()) + .setTotalRecordedEvents(1000) + .setTotalRecordedLinks(2300) + .build(); + + @Test + void noOp() { + assertThat(SpanDataBuilder.builder(TEST_SPAN_DATA).build()) + .usingRecursiveComparison() + .isEqualTo(TEST_SPAN_DATA); + } + + @Test + void modifySpanData() { + assertThat(TEST_SPAN_DATA.getStatus()).isEqualTo(StatusData.error()); + SpanData modified = + SpanDataBuilder.builder(TEST_SPAN_DATA) + .setStatus(StatusData.create(StatusCode.ERROR, "ABORTED")) + .build(); + assertThat(modified.getStatus()).isEqualTo(StatusData.create(StatusCode.ERROR, "ABORTED")); + } + + @Test + void equalsHashCode() { + assertThat(SpanDataBuilder.builder(TEST_SPAN_DATA).build()).isEqualTo(TEST_SPAN_DATA); + EqualsTester tester = new EqualsTester(); + tester + .addEqualityGroup( + SpanDataBuilder.builder(TEST_SPAN_DATA).build(), + SpanDataBuilder.builder(TEST_SPAN_DATA).build()) + .addEqualityGroup( + SpanDataBuilder.builder(TEST_SPAN_DATA) + .setStatus(StatusData.create(StatusCode.ERROR, "ABORTED")) + .build()); + tester.testEquals(); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/README.md b/opentelemetry-java/sdk-extensions/zpages/README.md new file mode 100644 index 000000000..492c3573c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/README.md @@ -0,0 +1,71 @@ +# OpenTelemetry SDK Extension zPages + +This module contains code for the OpenTelemetry Java zPages, which are a collection of dynamic HTML +web pages embedded in your app that display stats and trace data. Learn more in [this blog post](https://medium.com/opentelemetry/zpages-in-opentelemetry-2b080a81eb47). + +### Register the zPages + +**Note:** The package `com.sun.net.httpserver` is required to use the default zPages setup. Please make sure your +version of the JDK includes this package. + +To set-up the zPages, simply call `ZPageServer.startHttpServerAndRegisterAllPages(int port)` in your +main function: + +```java +public class MyMainClass { + public static void main(String[] args) throws Exception { + ZPageServer.startHttpServerAndRegisterAllPages(8080); + // ... do work + } +} +``` + +Alternatively, you can call `ZPageServer.registerAllPagesToHttpServer(HttpServer server)` to +register the zPages to a shared server: + +```java +public class MyMainClass { + public static void main(String[] args) throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress(8000), 10); + ZPageServer.registerAllPagesToHttpServer(server); + server.start(); + // ... do work + } +} +``` + +### Access the zPages + +#### View all available zPages on the `/` index page + +The index page `/` lists all available zPages with a link and description. + +#### View trace spans on the `/tracez` zPage + +The /tracez zPage displays information on running spans, sample span latencies, and sample error +spans. The data is aggregated into a summary-level table: + +![tracez-table](img/tracez-table.png) + +You can click on each of the counts in the table cells to access the corresponding span +details. For example, here are the details of the `ChildSpan` latency sample (row 1, col 4): + +![tracez-details](img/tracez-details.png) + +#### View and update the tracing configuration on the `/traceconfigz` zPage + +The /traceconfigz zPage displays information about the currently active tracing configuration and +provides an interface for users to modify relevant parameters. Here is what the web page looks like: + +![traceconfigz](img/traceconfigz.png) + +## Benchmark Testing + +This module contains two sets of benchmark tests: one for adding spans to an instance of +TracezSpanBuckets and another for retrieving counts and spans with TracezDataAggregator. You can run +the tests yourself with the following commands: + +``` +./gradlew -PjmhIncludeSingleClass=TracezSpanBucketsBenchmark clean :opentelemetry-sdk-extension-zpages:jmh +./gradlew -PjmhIncludeSingleClass=TracezDataAggregatorBenchmark clean :opentelemetry-sdk-extension-zpages:jmh +``` diff --git a/opentelemetry-java/sdk-extensions/zpages/build.gradle.kts b/opentelemetry-java/sdk-extensions/zpages/build.gradle.kts new file mode 100644 index 000000000..044fe6f79 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `java-library` + `maven-publish` + + id("me.champeau.jmh") +} + +description = "OpenTelemetry - zPages" +extra["moduleName"] = "io.opentelemetry.sdk.extension.zpages" + +dependencies { + implementation(project(":api:all")) + implementation(project(":sdk:all")) + + testImplementation("com.google.guava:guava") + + compileOnly("com.sun.net.httpserver:http") +} diff --git a/opentelemetry-java/sdk-extensions/zpages/gradle.properties b/opentelemetry-java/sdk-extensions/zpages/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/sdk-extensions/zpages/img/traceconfigz.png b/opentelemetry-java/sdk-extensions/zpages/img/traceconfigz.png new file mode 100644 index 000000000..0047e1ab8 Binary files /dev/null and b/opentelemetry-java/sdk-extensions/zpages/img/traceconfigz.png differ diff --git a/opentelemetry-java/sdk-extensions/zpages/img/tracez-details.png b/opentelemetry-java/sdk-extensions/zpages/img/tracez-details.png new file mode 100644 index 000000000..d45a01391 Binary files /dev/null and b/opentelemetry-java/sdk-extensions/zpages/img/tracez-details.png differ diff --git a/opentelemetry-java/sdk-extensions/zpages/img/tracez-table.png b/opentelemetry-java/sdk-extensions/zpages/img/tracez-table.png new file mode 100644 index 000000000..c9605fe38 Binary files /dev/null and b/opentelemetry-java/sdk-extensions/zpages/img/tracez-table.png differ diff --git a/opentelemetry-java/sdk-extensions/zpages/src/jmh/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregatorBenchmark.java b/opentelemetry-java/sdk-extensions/zpages/src/jmh/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregatorBenchmark.java new file mode 100644 index 000000000..893bc97e8 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/jmh/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregatorBenchmark.java @@ -0,0 +1,165 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** Benchmark class for {@link TracezDataAggregator}. */ +@State(Scope.Benchmark) +public class TracezDataAggregatorBenchmark { + + private static final String runningSpan = "RUNNING_SPAN"; + private static final String latencySpan = "LATENCY_SPAN"; + private static final String errorSpan = "ERROR_SPAN"; + + private SdkTracerProvider tracerProvider; + private TracezDataAggregator dataAggregator; + + @Param({"1", "10", "1000", "1000000"}) + private int numberOfSpans; + + @Setup(Level.Trial) + public final void setup() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.builder().build(); + tracerProvider = SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); + dataAggregator = new TracezDataAggregator(spanProcessor); + Tracer tracer = tracerProvider.get("TracezDataAggregatorBenchmark"); + + for (int i = 0; i < numberOfSpans; i++) { + tracer.spanBuilder(runningSpan).startSpan(); + tracer.spanBuilder(latencySpan).startSpan().end(); + Span error = tracer.spanBuilder(errorSpan).startSpan(); + error.setStatus(StatusCode.ERROR); + error.end(); + } + } + + @TearDown(Level.Trial) + public final void tearDown() { + tracerProvider.shutdown(); + } + + /** Get span counts with 1 thread. */ + @Benchmark + @Threads(value = 1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void getCounts_01Thread(Blackhole blackhole) { + blackhole.consume(dataAggregator.getRunningSpanCounts()); + blackhole.consume(dataAggregator.getSpanLatencyCounts()); + blackhole.consume(dataAggregator.getErrorSpanCounts()); + } + + /** Get span counts with 5 threads. */ + @Benchmark + @Threads(value = 5) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void getCounts_05Threads(Blackhole blackhole) { + blackhole.consume(dataAggregator.getRunningSpanCounts()); + blackhole.consume(dataAggregator.getSpanLatencyCounts()); + blackhole.consume(dataAggregator.getErrorSpanCounts()); + } + + /** Get span counts with 10 threads. */ + @Benchmark + @Threads(value = 10) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void getCounts_10Threads(Blackhole blackhole) { + blackhole.consume(dataAggregator.getRunningSpanCounts()); + blackhole.consume(dataAggregator.getSpanLatencyCounts()); + blackhole.consume(dataAggregator.getErrorSpanCounts()); + } + + /** Get span counts with 20 threads. */ + @Benchmark + @Threads(value = 20) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void getCounts_20Threads(Blackhole blackhole) { + blackhole.consume(dataAggregator.getRunningSpanCounts()); + blackhole.consume(dataAggregator.getSpanLatencyCounts()); + blackhole.consume(dataAggregator.getErrorSpanCounts()); + } + + /** Get spans with 1 thread. */ + @Benchmark + @Threads(value = 1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void getSpans_01Thread(Blackhole blackhole) { + blackhole.consume(dataAggregator.getRunningSpans(runningSpan)); + blackhole.consume(dataAggregator.getOkSpans(latencySpan, 0, Long.MAX_VALUE)); + blackhole.consume(dataAggregator.getErrorSpans(errorSpan)); + } + + /** Get spans with 5 threads. */ + @Benchmark + @Threads(value = 5) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void getSpans_05Threads(Blackhole blackhole) { + blackhole.consume(dataAggregator.getRunningSpans(runningSpan)); + blackhole.consume(dataAggregator.getOkSpans(latencySpan, 0, Long.MAX_VALUE)); + blackhole.consume(dataAggregator.getErrorSpans(errorSpan)); + } + + /** Get spans with 10 threads. */ + @Benchmark + @Threads(value = 10) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void getSpans_10Threads(Blackhole blackhole) { + blackhole.consume(dataAggregator.getRunningSpans(runningSpan)); + blackhole.consume(dataAggregator.getOkSpans(latencySpan, 0, Long.MAX_VALUE)); + blackhole.consume(dataAggregator.getErrorSpans(errorSpan)); + } + + /** Get spans with 20 threads. */ + @Benchmark + @Threads(value = 20) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void getSpans_20Threads(Blackhole blackhole) { + blackhole.consume(dataAggregator.getRunningSpans(runningSpan)); + blackhole.consume(dataAggregator.getOkSpans(latencySpan, 0, Long.MAX_VALUE)); + blackhole.consume(dataAggregator.getErrorSpans(errorSpan)); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/jmh/java/io/opentelemetry/sdk/extension/zpages/TracezSpanBucketsBenchmark.java b/opentelemetry-java/sdk-extensions/zpages/src/jmh/java/io/opentelemetry/sdk/extension/zpages/TracezSpanBucketsBenchmark.java new file mode 100644 index 000000000..4108c3a9e --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/jmh/java/io/opentelemetry/sdk/extension/zpages/TracezSpanBucketsBenchmark.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/** Benchmark class for {@link TracezSpanBuckets}. */ +@State(Scope.Benchmark) +public class TracezSpanBucketsBenchmark { + + private static final String spanName = "BENCHMARK_SPAN"; + private static ReadableSpan readableSpan; + private TracezSpanBuckets bucket; + + @Setup(Level.Trial) + public final void setup() { + bucket = new TracezSpanBuckets(); + Tracer tracer = SdkTracerProvider.builder().build().get("TracezZPageBenchmark"); + Span span = tracer.spanBuilder(spanName).startSpan(); + span.end(); + readableSpan = (ReadableSpan) span; + } + + @Benchmark + @Threads(value = 1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void addToBucket_01Thread() { + bucket.addToBucket(readableSpan); + } + + @Benchmark + @Threads(value = 5) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void addToBucket_05Threads() { + bucket.addToBucket(readableSpan); + } + + @Benchmark + @Threads(value = 10) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void addToBucket_10Threads() { + bucket.addToBucket(readableSpan); + } + + @Benchmark + @Threads(value = 20) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void addToBucket_20Threads() { + bucket.addToBucket(readableSpan); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/IndexZPageHandler.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/IndexZPageHandler.java new file mode 100644 index 000000000..0d29692ee --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/IndexZPageHandler.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +final class IndexZPageHandler extends ZPageHandler { + private static final String INDEX_URL = "/"; + private static final String INDEX_NAME = "Index"; + private static final String INDEX_DESCRITION = "Index page of zPages"; + private static final Logger logger = Logger.getLogger(IndexZPageHandler.class.getName()); + private final List availableHandlers; + + IndexZPageHandler(List availableHandlers) { + this.availableHandlers = availableHandlers; + } + + @Override + public String getUrlPath() { + return INDEX_URL; + } + + @Override + public String getPageName() { + return INDEX_NAME; + } + + @Override + public String getPageDescription() { + return INDEX_DESCRITION; + } + + private static void emitPageLinkAndInfo(PrintStream out, ZPageHandler handler) { + out.print(""); + out.print("

    " + handler.getPageName() + "

    "); + out.print("
    "); + out.print("

    " + handler.getPageDescription() + "

    "); + } + + @Override + public void emitHtml(Map queryMap, OutputStream outputStream) { + // PrintStream for emiting HTML contents + try (PrintStream out = new PrintStream(outputStream, /* autoFlush= */ false, "UTF-8")) { + out.print(""); + out.print(""); + out.print(""); + out.print(""); + out.print( + ""); + out.print( + ""); + out.print( + ""); + out.print("zPages"); + out.print(""); + out.print(""); + out.print(""); + out.print( + ""); + out.print("

    zPages

    "); + out.print( + "

    OpenTelemetry provides in-process web pages that display collected data from" + + " the process that they are attached to. These are called \"zPages\"." + + " They are useful for in-process diagnostics without having to depend on" + + " any backend to examine traces or metrics.

    "); + + out.print( + "

    zPages can be useful during the development time or " + + "when the process to be inspected is known in production.

    "); + for (ZPageHandler handler : this.availableHandlers) { + emitPageLinkAndInfo(out, handler); + } + out.print(""); + out.print(""); + } catch (Throwable t) { + logger.log(Level.WARNING, "error while generating HTML", t); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/LatencyBoundary.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/LatencyBoundary.java new file mode 100644 index 000000000..5f8a7b3f0 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/LatencyBoundary.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import java.util.concurrent.TimeUnit; + +/** + * A class of boundaries for the latency buckets. The completed spans with a status of {@link + * io.opentelemetry.api.trace.Status#OK} are categorized into one of these buckets om the traceZ + * zPage. + */ +enum LatencyBoundary { + /** Stores finished successful requests of duration within the interval [0, 10us). */ + ZERO_MICROSx10(0, TimeUnit.MICROSECONDS.toNanos(10)), + + /** Stores finished successful requests of duration within the interval [10us, 100us). */ + MICROSx10_MICROSx100(TimeUnit.MICROSECONDS.toNanos(10), TimeUnit.MICROSECONDS.toNanos(100)), + + /** Stores finished successful requests of duration within the interval [100us, 1ms). */ + MICROSx100_MILLIx1(TimeUnit.MICROSECONDS.toNanos(100), TimeUnit.MILLISECONDS.toNanos(1)), + + /** Stores finished successful requests of duration within the interval [1ms, 10ms). */ + MILLIx1_MILLIx10(TimeUnit.MILLISECONDS.toNanos(1), TimeUnit.MILLISECONDS.toNanos(10)), + + /** Stores finished successful requests of duration within the interval [10ms, 100ms). */ + MILLIx10_MILLIx100(TimeUnit.MILLISECONDS.toNanos(10), TimeUnit.MILLISECONDS.toNanos(100)), + + /** Stores finished successful requests of duration within the interval [100ms, 1sec). */ + MILLIx100_SECONDx1(TimeUnit.MILLISECONDS.toNanos(100), TimeUnit.SECONDS.toNanos(1)), + + /** Stores finished successful requests of duration within the interval [1sec, 10sec). */ + SECONDx1_SECONDx10(TimeUnit.SECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10)), + + /** Stores finished successful requests of duration within the interval [10sec, 100sec). */ + SECONDx10_SECONDx100(TimeUnit.SECONDS.toNanos(10), TimeUnit.SECONDS.toNanos(100)), + + /** Stores finished successful requests of duration greater than or equal to 100sec. */ + SECONDx100_MAX(TimeUnit.SECONDS.toNanos(100), Long.MAX_VALUE); + + private final long latencyLowerBound; + private final long latencyUpperBound; + + /** + * Constructs a {@code LatencyBoundaries} with the given boundaries and label. + * + * @param latencyLowerBound the latency lower bound of the bucket. + * @param latencyUpperBound the latency upper bound of the bucket. + */ + LatencyBoundary(long latencyLowerBound, long latencyUpperBound) { + this.latencyLowerBound = latencyLowerBound; + this.latencyUpperBound = latencyUpperBound; + } + + /** + * Returns the latency lower bound of the bucket. + * + * @return the latency lower bound of the bucket. + */ + long getLatencyLowerBound() { + return latencyLowerBound; + } + + /** + * Returns the latency upper bound of the bucket. + * + * @return the latency upper bound of the bucket. + */ + long getLatencyUpperBound() { + return latencyUpperBound; + } + + /** + * Returns the LatencyBoundary that the argument falls into. + * + * @param latencyNanos latency in nanoseconds. + * @return the LatencyBoundary that latencyNanos falls into. + */ + static LatencyBoundary getBoundary(long latencyNanos) { + for (LatencyBoundary bucket : LatencyBoundary.values()) { + if (latencyNanos >= bucket.getLatencyLowerBound() + && latencyNanos < bucket.getLatencyUpperBound()) { + return bucket; + } + } + return ZERO_MICROSx10; + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/SpanBucket.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/SpanBucket.java new file mode 100644 index 000000000..47a4286c1 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/SpanBucket.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.sdk.trace.ReadableSpan; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +final class SpanBucket { + // A power of 2 means Integer.MAX_VALUE % bucketSize = bucketSize - 1, so the index will always + // loop back to 0. + private static final int LATENCY_BUCKET_SIZE = 16; + private static final int ERROR_BUCKET_SIZE = 8; + + private final AtomicReferenceArray spans; + private final AtomicInteger index; + private final int bucketSize; + + SpanBucket(boolean isLatencyBucket) { + bucketSize = isLatencyBucket ? LATENCY_BUCKET_SIZE : ERROR_BUCKET_SIZE; + spans = new AtomicReferenceArray<>(bucketSize); + index = new AtomicInteger(); + } + + void add(ReadableSpan span) { + spans.set(remainder(index.getAndIncrement(), bucketSize), span); + } + + int size() { + for (int i = bucketSize - 1; i >= 0; i--) { + if (spans.get(i) != null) { + return i + 1; + } + } + return 0; + } + + void addTo(List result) { + for (int i = 0; i < bucketSize; i++) { + ReadableSpan span = spans.get(i); + if (span != null) { + result.add(span); + } else { + break; + } + } + } + + private static int remainder(int dividend, int divisor) { + return (int) (Integer.toUnsignedLong(dividend) % Integer.toUnsignedLong(divisor)); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TraceConfigzZPageHandler.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TraceConfigzZPageHandler.java new file mode 100644 index 000000000..631e0d61f --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TraceConfigzZPageHandler.java @@ -0,0 +1,428 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.SpanLimitsBuilder; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +final class TraceConfigzZPageHandler extends ZPageHandler { + private static final String TRACE_CONFIGZ_URL = "/traceconfigz"; + private static final String TRACE_CONFIGZ_NAME = "TraceConfigZ"; + private static final String TRACE_CONFIGZ_DESCRIPTION = + "TraceConfigZ displays information about the current active tracing configuration" + + " and allows users to change it"; + private static final String QUERY_STRING_ACTION = "action"; + private static final String QUERY_STRING_ACTION_CHANGE = "change"; + private static final String QUERY_STRING_ACTION_DEFAULT = "default"; + private static final String QUERY_STRING_SAMPLING_PROBABILITY = "samplingprobability"; + private static final String QUERY_STRING_MAX_NUM_OF_ATTRIBUTES = "maxnumofattributes"; + private static final String QUERY_STRING_MAX_NUM_OF_EVENTS = "maxnumofevents"; + private static final String QUERY_STRING_MAX_NUM_OF_LINKS = "maxnumoflinks"; + private static final String QUERY_STRING_MAX_NUM_OF_ATTRIBUTES_PER_EVENT = + "maxnumofattributesperevent"; + private static final String QUERY_STRING_MAX_NUM_OF_ATTRIBUTES_PER_LINK = + "maxnumofattributesperlink"; + // Background color used for zebra striping rows in table + private static final String ZEBRA_STRIPE_COLOR = "#e6e6e6"; + private static final Logger logger = Logger.getLogger(TraceConfigzZPageHandler.class.getName()); + private final TracezTraceConfigSupplier configSupplier; + + TraceConfigzZPageHandler(TracezTraceConfigSupplier configSupplier) { + this.configSupplier = configSupplier; + } + + @Override + public String getUrlPath() { + return TRACE_CONFIGZ_URL; + } + + @Override + public String getPageName() { + return TRACE_CONFIGZ_NAME; + } + + @Override + public String getPageDescription() { + return TRACE_CONFIGZ_DESCRIPTION; + } + + /** + * Emits CSS styles to the {@link PrintStream} {@code out}. Content emited by this function should + * be enclosed by tag. + * + * @param out the {@link PrintStream} {@code out}. + */ + private static void emitHtmlStyle(PrintStream out) { + out.print(""); + } + + /** + * Emits a row of the change tracing parameter table to the {@link PrintStream} {@code out}. Each + * row corresponds to one tracing parameter. + * + * @param out the {@link PrintStream} {@code out}. + * @param rowName the display name of the corresponding tracing parameter. + * @param paramName the name of the corresponding tracing parameter (this will be used to + * construct the query parameter in URL). + * @param inputPlaceHolder placeholder for the HTML element. + * @param paramDefaultValue the default value of the corresponding tracing parameter. + * @param zebraStripeColor hex code of the color used for zebra striping rows. + * @param zebraStripe boolean indicating if the row is zebra striped. + */ + private static void emitChangeTableRow( + PrintStream out, + String rowName, + String paramName, + String inputPlaceHolder, + String paramDefaultValue, + String zebraStripeColor, + boolean zebraStripe) { + if (zebraStripe) { + out.print("

  • "); + } else { + out.print(""); + } + out.print(""); + out.print( + ""); + out.print(""); + out.print(""); + } + + /** + * Emits the change tracing parameter table to the {@link PrintStream} {@code out}. + * + * @param out the {@link PrintStream} {@code out}. + */ + private static void emitChangeTable(PrintStream out) { + out.print("
    Update " + rowName + "(" + paramDefaultValue + ")
    "); + out.print(""); + out.print( + ""); + out.print(""); + emitChangeTableRow( + /* out= */ out, + /* rowName= */ "SamplingProbability to", + /* paramName= */ QUERY_STRING_SAMPLING_PROBABILITY, + /* inputPlaceHolder= */ "[0.0, 1.0]", + /* paramDefaultValue= */ "1.0", + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ false); + emitChangeTableRow( + /* out= */ out, + /* rowName= */ "MaxNumberOfAttributes to", + /* paramName= */ QUERY_STRING_MAX_NUM_OF_ATTRIBUTES, + /* inputPlaceHolder= */ "", + /* paramDefaultValue= */ Integer.toString( + SpanLimits.getDefault().getMaxNumberOfAttributes()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ true); + emitChangeTableRow( + /* out= */ out, + /* rowName= */ "MaxNumberOfEvents to", + /* paramName= */ QUERY_STRING_MAX_NUM_OF_EVENTS, + /* inputPlaceHolder= */ "", + /* paramDefaultValue= */ Integer.toString(SpanLimits.getDefault().getMaxNumberOfEvents()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ false); + emitChangeTableRow( + /* out= */ out, + /* rowName= */ "MaxNumberOfLinks to", + /* paramName= */ QUERY_STRING_MAX_NUM_OF_LINKS, + /* inputPlaceHolder= */ "", + /* paramDefaultValue= */ Integer.toString(SpanLimits.getDefault().getMaxNumberOfLinks()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ true); + emitChangeTableRow( + /* out= */ out, + /* rowName= */ "MaxNumberOfAttributesPerEvent to", + /* paramName= */ QUERY_STRING_MAX_NUM_OF_ATTRIBUTES_PER_EVENT, + /* inputPlaceHolder= */ "", + /* paramDefaultValue= */ Integer.toString( + SpanLimits.getDefault().getMaxNumberOfAttributesPerEvent()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ false); + emitChangeTableRow( + /* out= */ out, + /* rowName= */ "MaxNumberOfAttributesPerLink to", + /* paramName= */ QUERY_STRING_MAX_NUM_OF_ATTRIBUTES_PER_LINK, + /* inputPlaceHolder= */ "", + /* paramDefaultValue= */ Integer.toString( + SpanLimits.getDefault().getMaxNumberOfAttributesPerLink()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ true); + out.print("
    " + + "Update active TraceConfigDefault
    "); + } + + /** + * Emits a row of the active tracing parameter table to the {@link PrintStream} {@code out}. Each + * row corresponds to one tracing parameter. + * + * @param out the {@link PrintStream} {@code out}. + * @param paramName the name of the corresponding tracing parameter. + * @param paramValue the value of the corresponding tracing parameter. + * @param zebraStripeColor hex code of the color used for zebra striping rows. + * @param zebraStripe boolean indicating if the row is zebra striped. + */ + private static void emitActiveTableRow( + PrintStream out, + String paramName, + String paramValue, + String zebraStripeColor, + boolean zebraStripe) { + if (zebraStripe) { + out.print(""); + } else { + out.print(""); + } + out.print("" + paramName + ""); + out.print("" + paramValue + ""); + out.print(""); + } + + /** + * Emits the active tracing parameters table to the {@link PrintStream} {@code out}. + * + * @param out the {@link PrintStream} {@code out}. + */ + private void emitActiveTable(PrintStream out) { + out.print(""); + out.print(""); + out.print(""); + out.print(""); + out.print(""); + emitActiveTableRow( + /* out= */ out, + /* paramName= */ "Sampler", + /* paramValue=*/ configSupplier.getSampler().getDescription(), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ false); + emitActiveTableRow( + /* out= */ out, + /* paramName= */ "MaxNumOfAttributes", + /* paramValue=*/ Integer.toString(configSupplier.get().getMaxNumberOfAttributes()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ true); + emitActiveTableRow( + /* out= */ out, + /* paramName= */ "MaxNumOfEvents", + /* paramValue=*/ Integer.toString(configSupplier.get().getMaxNumberOfEvents()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ false); + emitActiveTableRow( + /* out= */ out, + /* paramName= */ "MaxNumOfLinks", + /* paramValue=*/ Integer.toString(configSupplier.get().getMaxNumberOfLinks()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ true); + emitActiveTableRow( + /* out= */ out, + /* paramName= */ "MaxNumOfAttributesPerEvent", + /* paramValue=*/ Integer.toString(configSupplier.get().getMaxNumberOfAttributesPerEvent()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe= */ false); + emitActiveTableRow( + /* out= */ out, + /* paramName= */ "MaxNumOfAttributesPerLink", + /* paramValue=*/ Integer.toString(configSupplier.get().getMaxNumberOfAttributesPerLink()), + /* zebraStripeColor= */ ZEBRA_STRIPE_COLOR, + /* zebraStripe=*/ true); + out.print("
    NameValue
    "); + } + + /** + * Emits HTML body content to the {@link PrintStream} {@code out}. Content emitted by this + * function should be enclosed by tag. + * + * @param out the {@link PrintStream} {@code out}. + */ + private void emitHtmlBody(PrintStream out) { + out.print( + ""); + out.print("

    Trace Configuration

    "); + out.print("
    "); + out.print( + ""); + emitChangeTable(out); + // Button for submit + out.print(""); + out.print("
    "); + // Button for restore default + out.print("
    "); + out.print( + ""); + out.print(""); + out.print("
    "); + out.print("

    Active Tracing Parameters

    "); + emitActiveTable(out); + } + + @Override + public void emitHtml(Map queryMap, OutputStream outputStream) { + // PrintStream for emiting HTML contents + try (PrintStream out = new PrintStream(outputStream, /* autoFlush= */ false, "UTF-8")) { + out.print(""); + out.print(""); + out.print(""); + out.print(""); + out.print( + ""); + out.print( + ""); + out.print( + ""); + out.print("" + TRACE_CONFIGZ_NAME + ""); + emitHtmlStyle(out); + out.print(""); + out.print(""); + try { + emitHtmlBody(out); + } catch (Throwable t) { + out.print("Error while generating HTML: " + t.toString()); + logger.log(Level.WARNING, "error while generating HTML", t); + } + out.print(""); + out.print(""); + } catch (Throwable t) { + logger.log(Level.WARNING, "error while generating HTML", t); + } + } + + @Override + public boolean processRequest( + String requestMethod, Map queryMap, OutputStream outputStream) { + if (requestMethod.equalsIgnoreCase("POST")) { + try { + applyTraceConfig(queryMap); + } catch (Throwable t) { + try (PrintStream out = new PrintStream(outputStream, /* autoFlush= */ false, "UTF-8")) { + out.print(""); + out.print(""); + out.print(""); + out.print(""); + out.print( + ""); + out.print( + ""); + out.print( + ""); + out.print("" + TRACE_CONFIGZ_NAME + ""); + out.print(""); + out.print(""); + out.print("Error while applying trace config changes: " + t.toString()); + out.print(""); + out.print(""); + logger.log(Level.WARNING, "error while applying trace config changes", t); + } catch (Throwable e) { + logger.log(Level.WARNING, "error while applying trace config changes", e); + return true; + } + return true; + } + } + return false; + } + + /** + * Apply updated trace configuration through the tracerProvider based on query parameters. + * + * @param queryMap the map containing URL query parameters. + * @throws NumberFormatException if one of the {@code double}/{@code integer} valued query string + * does not contain a parsable {@code double}/{@code integer}. + */ + private void applyTraceConfig(Map queryMap) { + String action = queryMap.get(QUERY_STRING_ACTION); + if (action == null) { + return; + } + if (action.equals(QUERY_STRING_ACTION_CHANGE)) { + SpanLimitsBuilder newConfigBuilder = configSupplier.get().toBuilder(); + String samplingProbabilityStr = queryMap.get(QUERY_STRING_SAMPLING_PROBABILITY); + if (samplingProbabilityStr != null) { + try { + double samplingProbability = Double.parseDouble(samplingProbabilityStr); + configSupplier.setSampler(Sampler.traceIdRatioBased(samplingProbability)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("SamplingProbability must be of the type double", e); + } + } + String maxNumOfAttributesStr = queryMap.get(QUERY_STRING_MAX_NUM_OF_ATTRIBUTES); + if (maxNumOfAttributesStr != null) { + try { + int maxNumOfAttributes = Integer.parseInt(maxNumOfAttributesStr); + newConfigBuilder.setMaxNumberOfAttributes(maxNumOfAttributes); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("MaxNumOfAttributes must be of the type integer", e); + } + } + String maxNumOfEventsStr = queryMap.get(QUERY_STRING_MAX_NUM_OF_EVENTS); + if (maxNumOfEventsStr != null) { + try { + int maxNumOfEvents = Integer.parseInt(maxNumOfEventsStr); + newConfigBuilder.setMaxNumberOfEvents(maxNumOfEvents); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("MaxNumOfEvents must be of the type integer", e); + } + } + String maxNumOfLinksStr = queryMap.get(QUERY_STRING_MAX_NUM_OF_LINKS); + if (maxNumOfLinksStr != null) { + try { + int maxNumOfLinks = Integer.parseInt(maxNumOfLinksStr); + newConfigBuilder.setMaxNumberOfLinks(maxNumOfLinks); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("MaxNumOfLinks must be of the type integer", e); + } + } + String maxNumOfAttributesPerEventStr = + queryMap.get(QUERY_STRING_MAX_NUM_OF_ATTRIBUTES_PER_EVENT); + if (maxNumOfAttributesPerEventStr != null) { + try { + int maxNumOfAttributesPerEvent = Integer.parseInt(maxNumOfAttributesPerEventStr); + newConfigBuilder.setMaxNumberOfAttributesPerEvent(maxNumOfAttributesPerEvent); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "MaxNumOfAttributesPerEvent must be of the type integer", e); + } + } + String maxNumOfAttributesPerLinkStr = + queryMap.get(QUERY_STRING_MAX_NUM_OF_ATTRIBUTES_PER_LINK); + if (maxNumOfAttributesPerLinkStr != null) { + try { + int maxNumOfAttributesPerLink = Integer.parseInt(maxNumOfAttributesPerLinkStr); + newConfigBuilder.setMaxNumberOfAttributesPerLink(maxNumOfAttributesPerLink); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "MaxNumOfAttributesPerLink must be of the type integer", e); + } + } + configSupplier.setActiveTraceConfig(newConfigBuilder.build()); + } else if (action.equals(QUERY_STRING_ACTION_DEFAULT)) { + SpanLimits defaultConfig = SpanLimits.builder().build(); + configSupplier.setActiveTraceConfig(defaultConfig); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregator.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregator.java new file mode 100644 index 000000000..4412ff037 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregator.java @@ -0,0 +1,162 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +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.Set; +import java.util.TreeSet; +import javax.annotation.concurrent.ThreadSafe; + +/** + * A data aggregator for the traceZ zPage. + * + *

    The traceZ data aggregator compiles information about the running spans, span latencies, and + * error spans for the frontend of the zPage. + */ +@ThreadSafe +final class TracezDataAggregator { + private final TracezSpanProcessor spanProcessor; + + /** + * Constructor for {@link TracezDataAggregator}. + * + * @param spanProcessor collects span data. + */ + TracezDataAggregator(TracezSpanProcessor spanProcessor) { + this.spanProcessor = spanProcessor; + } + + /** + * Returns a Set of running and completed span names for {@link TracezDataAggregator}. + * + * @return a Set of {@link String}. + */ + Set getSpanNames() { + Set spanNames = new TreeSet<>(); + Collection allRunningSpans = spanProcessor.getRunningSpans(); + for (ReadableSpan span : allRunningSpans) { + spanNames.add(span.getName()); + } + spanNames.addAll(spanProcessor.getCompletedSpanCache().keySet()); + return spanNames; + } + + /** + * Returns a Map of the running span counts for {@link TracezDataAggregator}. + * + * @return a Map of span counts for each span name. + */ + Map getRunningSpanCounts() { + Collection allRunningSpans = spanProcessor.getRunningSpans(); + Map numSpansPerName = new HashMap<>(); + for (ReadableSpan span : allRunningSpans) { + Integer prevValue = numSpansPerName.get(span.getName()); + numSpansPerName.put(span.getName(), prevValue != null ? prevValue + 1 : 1); + } + return numSpansPerName; + } + + /** + * Returns a List of all running spans with a given span name for {@link TracezDataAggregator}. + * + * @param spanName name to filter returned spans. + * @return a List of {@link SpanData}. + */ + List getRunningSpans(String spanName) { + Collection allRunningSpans = spanProcessor.getRunningSpans(); + List filteredSpans = new ArrayList<>(); + for (ReadableSpan span : allRunningSpans) { + if (span.getName().equals(spanName)) { + filteredSpans.add(span.toSpanData()); + } + } + return filteredSpans; + } + + /** + * Returns a Map of span names to counts for all {@link StatusCode#OK} spans in {@link + * TracezDataAggregator}. + * + * @return a Map of span names to counts, where the counts are further indexed by the latency + * boundaries. + */ + Map> getSpanLatencyCounts() { + Map completedSpanCache = spanProcessor.getCompletedSpanCache(); + Map> numSpansPerName = new HashMap<>(); + for (Map.Entry cacheEntry : completedSpanCache.entrySet()) { + numSpansPerName.put( + cacheEntry.getKey(), cacheEntry.getValue().getLatencyBoundaryToCountMap()); + } + return numSpansPerName; + } + + /** + * Returns a List of all {@link StatusCode#OK} spans with a given span name between [lowerBound, + * upperBound) for {@link TracezDataAggregator}. + * + * @param spanName name to filter returned spans. + * @param lowerBound latency lower bound (inclusive) + * @param upperBound latency upper bound (exclusive) + * @return a List of {@link SpanData}. + */ + List getOkSpans(String spanName, long lowerBound, long upperBound) { + Map completedSpanCache = spanProcessor.getCompletedSpanCache(); + TracezSpanBuckets buckets = completedSpanCache.get(spanName); + if (buckets == null) { + return Collections.emptyList(); + } + Collection allOkSpans = buckets.getOkSpans(); + List filteredSpans = new ArrayList<>(); + for (ReadableSpan span : allOkSpans) { + if (span.getLatencyNanos() >= lowerBound && span.getLatencyNanos() < upperBound) { + filteredSpans.add(span.toSpanData()); + } + } + return Collections.unmodifiableList(filteredSpans); + } + + /** + * Returns a Map of error span counts for {@link TracezDataAggregator}. + * + * @return a Map of error span counts for each span name. + */ + Map getErrorSpanCounts() { + Map completedSpanCache = spanProcessor.getCompletedSpanCache(); + Map numErrorsPerName = new HashMap<>(); + for (Map.Entry cacheEntry : completedSpanCache.entrySet()) { + numErrorsPerName.put(cacheEntry.getKey(), cacheEntry.getValue().getErrorSpans().size()); + } + return numErrorsPerName; + } + + /** + * Returns a List of error spans with a given span name for {@link TracezDataAggregator}. + * + * @param spanName name to filter returned spans. + * @return a List of {@link SpanData}. + */ + List getErrorSpans(String spanName) { + Map completedSpanCache = spanProcessor.getCompletedSpanCache(); + TracezSpanBuckets buckets = completedSpanCache.get(spanName); + if (buckets == null) { + return Collections.emptyList(); + } + Collection allErrorSpans = buckets.getErrorSpans(); + List errorSpans = new ArrayList<>(); + for (ReadableSpan span : allErrorSpans) { + errorSpans.add(span.toSpanData()); + } + return Collections.unmodifiableList(errorSpans); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezSpanBuckets.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezSpanBuckets.java new file mode 100644 index 000000000..92c919dcd --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezSpanBuckets.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class TracezSpanBuckets { + private final Map latencyBuckets = new HashMap<>(); + private final Map errorBuckets = new HashMap<>(); + + TracezSpanBuckets() { + for (LatencyBoundary bucket : LatencyBoundary.values()) { + latencyBuckets.put(bucket, new SpanBucket(/* isLatencyBucket= */ true)); + } + for (StatusCode code : StatusCode.values()) { + if (code == StatusCode.ERROR) { + errorBuckets.put(code, new SpanBucket(/* isLatencyBucket= */ false)); + } + } + } + + void addToBucket(ReadableSpan span) { + StatusData status = span.toSpanData().getStatus(); + if (status.getStatusCode() != StatusCode.ERROR) { + latencyBuckets.get(LatencyBoundary.getBoundary(span.getLatencyNanos())).add(span); + return; + } + errorBuckets.get(status.getStatusCode()).add(span); + } + + Map getLatencyBoundaryToCountMap() { + Map latencyCounts = new EnumMap<>(LatencyBoundary.class); + for (LatencyBoundary bucket : LatencyBoundary.values()) { + latencyCounts.put(bucket, latencyBuckets.get(bucket).size()); + } + return latencyCounts; + } + + List getOkSpans() { + List okSpans = new ArrayList<>(); + for (SpanBucket latencyBucket : latencyBuckets.values()) { + latencyBucket.addTo(okSpans); + } + return okSpans; + } + + List getErrorSpans() { + List errorSpans = new ArrayList<>(); + for (SpanBucket errorBucket : errorBuckets.values()) { + errorBucket.addTo(errorSpans); + } + return errorSpans; + } + + List getSpans() { + List spans = new ArrayList<>(); + spans.addAll(getOkSpans()); + spans.addAll(getErrorSpans()); + return spans; + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezSpanProcessor.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezSpanProcessor.java new file mode 100644 index 000000000..7eabdeb87 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezSpanProcessor.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.annotation.concurrent.ThreadSafe; + +/** A {@link SpanProcessor} implementation for the traceZ zPage. */ +@ThreadSafe +final class TracezSpanProcessor implements SpanProcessor { + private final ConcurrentMap runningSpanCache; + private final ConcurrentMap completedSpanCache; + private final boolean sampled; + + /** + * Constructor for {@link TracezSpanProcessor}. + * + * @param sampled report only sampled spans. + */ + TracezSpanProcessor(boolean sampled) { + runningSpanCache = new ConcurrentHashMap<>(); + completedSpanCache = new ConcurrentHashMap<>(); + this.sampled = sampled; + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + runningSpanCache.put(span.getSpanContext().getSpanId(), span); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + runningSpanCache.remove(span.getSpanContext().getSpanId()); + if (!sampled || span.getSpanContext().isSampled()) { + completedSpanCache.putIfAbsent(span.getName(), new TracezSpanBuckets()); + completedSpanCache.get(span.getName()).addToBucket(span); + } + } + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public CompletableResultCode shutdown() { + // Do nothing. + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode forceFlush() { + // Do nothing. + return CompletableResultCode.ofSuccess(); + } + + /** + * Returns a Collection of all running spans for {@link TracezSpanProcessor}. + * + * @return a Collection of {@link ReadableSpan}. + */ + Collection getRunningSpans() { + return runningSpanCache.values(); + } + + /** + * Returns a Collection of all completed spans for {@link TracezSpanProcessor}. + * + * @return a Collection of {@link ReadableSpan}. + */ + Collection getCompletedSpans() { + Collection completedSpans = new ArrayList<>(); + for (TracezSpanBuckets buckets : completedSpanCache.values()) { + completedSpans.addAll(buckets.getSpans()); + } + return completedSpans; + } + + /** + * Returns the completed span cache for {@link TracezSpanProcessor}. + * + * @return a Map of String to {@link TracezSpanBuckets}. + */ + Map getCompletedSpanCache() { + return completedSpanCache; + } + + /** + * Returns a new Builder for {@link TracezSpanProcessor}. + * + * @return a new {@link TracezSpanProcessor}. + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder class for {@link TracezSpanProcessor}. */ + public static final class Builder { + + private static final boolean DEFAULT_EXPORT_ONLY_SAMPLED = true; + private boolean sampled = DEFAULT_EXPORT_ONLY_SAMPLED; + + private Builder() {} + + /** + * Sets whether only sampled spans should be exported. + * + *

    Default value is {@code true}. + * + * @see Builder#DEFAULT_EXPORT_ONLY_SAMPLED + * @param sampled report only sampled spans. + * @return this. + */ + public Builder setExportOnlySampled(boolean sampled) { + this.sampled = sampled; + return this; + } + + /** + * Returns a new {@link TracezSpanProcessor}. + * + * @return a new {@link TracezSpanProcessor}. + */ + public TracezSpanProcessor build() { + return new TracezSpanProcessor(sampled); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezTraceConfigSupplier.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezTraceConfigSupplier.java new file mode 100644 index 000000000..c83cb5207 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezTraceConfigSupplier.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; +import java.util.function.Supplier; + +final class TracezTraceConfigSupplier implements Supplier, Sampler { + + private volatile Sampler sampler; + private volatile SpanLimits activeSpanLimits; + + TracezTraceConfigSupplier() { + sampler = Sampler.traceIdRatioBased(1.0); + activeSpanLimits = SpanLimits.getDefault(); + } + + @Override + public SpanLimits get() { + return activeSpanLimits; + } + + Sampler getSampler() { + return sampler; + } + + void setSampler(Sampler sampler) { + this.sampler = sampler; + } + + void setActiveTraceConfig(SpanLimits spanLimits) { + activeSpanLimits = spanLimits; + } + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); + } + + @Override + public String getDescription() { + return sampler.getDescription(); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezZPageHandler.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezZPageHandler.java new file mode 100644 index 000000000..4e5703478 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/TracezZPageHandler.java @@ -0,0 +1,602 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; +import java.util.Collection; +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.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +final class TracezZPageHandler extends ZPageHandler { + private enum SampleType { + RUNNING(0), + LATENCY(1), + ERROR(2), + UNKNOWN(-1); + + private final int value; + + SampleType(int value) { + this.value = value; + } + + static SampleType fromString(String str) { + int value = Integer.parseInt(str); + switch (value) { + case 0: + return RUNNING; + case 1: + return LATENCY; + case 2: + return ERROR; + default: + return UNKNOWN; + } + } + + int getValue() { + return value; + } + } + + private static final String TRACEZ_URL = "/tracez"; + private static final String TRACEZ_NAME = "TraceZ"; + private static final String TRACEZ_DESCRIPTION = + "TraceZ displays information about all the running spans" + + " and all the sampled spans based on latency and errors"; + // Background color used for zebra striping rows of summary table + private static final String ZEBRA_STRIPE_COLOR = "#e6e6e6"; + // 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"; + // Query string parameter name for span name + private static final String PARAM_SPAN_NAME = "zspanname"; + // Query string parameter name for type to display + // * 0 = running, 1 = latency, 2 = error + private static final String PARAM_SAMPLE_TYPE = "ztype"; + // Query string parameter name for sub-type: + // * for latency based sampled spans [0, 8] corresponds to each latency boundaries + // where 0 corresponds to the first boundary + // * for error based sampled spans [0, 15], 0 means all, otherwise the error code + private static final String PARAM_SAMPLE_SUB_TYPE = "zsubtype"; + // Map from LatencyBoundary to human readable string on the UI + private static final Map LATENCY_BOUNDARIES_STRING_MAP = + buildLatencyBoundaryStringMap(); + private static final Logger logger = Logger.getLogger(TracezZPageHandler.class.getName()); + @Nullable private final TracezDataAggregator dataAggregator; + + /** Constructs a new {@code TracezZPageHandler}. */ + TracezZPageHandler(@Nullable TracezDataAggregator dataAggregator) { + this.dataAggregator = dataAggregator; + } + + @Override + public String getUrlPath() { + return TRACEZ_URL; + } + + @Override + public String getPageName() { + return TRACEZ_NAME; + } + + @Override + public String getPageDescription() { + return TRACEZ_DESCRIPTION; + } + + /** + * Emits CSS Styles to the {@link PrintStream} {@code out}. Content emitted by this function + * should be enclosed by tag. + * + * @param out the {@link PrintStream} {@code out}. + */ + private static void emitHtmlStyle(PrintStream out) { + out.print(""); + } + + /** + * Emits the header of the summary table to the {@link PrintStream} {@code out}. + * + * @param out the {@link PrintStream} {@code out}. + */ + private static void emitSummaryTableHeader(PrintStream out) { + // First row + out.print(""); + out.print("Span Name"); + out.print("Running"); + out.print("Latency Samples"); + out.print("Error Samples"); + out.print(""); + + // Second row + out.print(""); + out.print(""); + out.print(""); + for (LatencyBoundary latencyBoundary : LatencyBoundary.values()) { + out.print( + "[" + + LATENCY_BOUNDARIES_STRING_MAP.get(latencyBoundary) + + "]"); + } + out.print(""); + out.print(""); + } + + /** + * Emits a single cell of the summary table depends on the paramters passed in, to the {@link + * PrintStream} {@code out}. + * + * @param out the {@link PrintStream} {@code out}. + * @param spanName the name of the corresponding span. + * @param numOfSamples the number of samples of the corresponding span. + * @param type the type of the corresponding span (running, latency, error). + * @param subtype the sub-type of the corresponding span (latency [0, 8], error [0, 15]). + */ + private static void emitSummaryTableCell( + PrintStream out, String spanName, int numOfSamples, SampleType type, int subtype) { + // If numOfSamples is greater than 0, emit a link to see detailed span information + // If numOfSamples is smaller than 0, print the text "N/A", otherwise print the text "0" + if (numOfSamples > 0) { + out.print("" + numOfSamples + ""); + } else if (numOfSamples < 0) { + out.print("N/A"); + } else { + out.print("0"); + } + } + + /** + * Emits the summary table of running spans and sampled spans to the {@link PrintStream} {@code + * out}. + * + * @param out the {@link PrintStream} {@code out}. + */ + private void emitSummaryTable(PrintStream out) { + if (dataAggregator == null) { + return; + } + out.print(""); + emitSummaryTableHeader(out); + + Set spanNames = dataAggregator.getSpanNames(); + boolean zebraStripe = false; + + Map runningSpanCounts = dataAggregator.getRunningSpanCounts(); + Map> latencySpanCounts = + dataAggregator.getSpanLatencyCounts(); + Map errorSpanCounts = dataAggregator.getErrorSpanCounts(); + for (String spanName : spanNames) { + if (zebraStripe) { + out.print(""); + } else { + out.print(""); + } + zebraStripe = !zebraStripe; + out.print(""); + + // Running spans column + int numOfRunningSpans = + runningSpanCounts.containsKey(spanName) ? runningSpanCounts.get(spanName) : 0; + // subtype is ignored for running spans + emitSummaryTableCell(out, spanName, numOfRunningSpans, SampleType.RUNNING, 0); + + // Latency based sampled spans column + int subtype = 0; + for (LatencyBoundary latencyBoundary : LatencyBoundary.values()) { + int numOfLatencySamples = + latencySpanCounts.containsKey(spanName) + && latencySpanCounts.get(spanName).containsKey(latencyBoundary) + ? latencySpanCounts.get(spanName).get(latencyBoundary) + : 0; + emitSummaryTableCell(out, spanName, numOfLatencySamples, SampleType.LATENCY, subtype); + subtype += 1; + } + + // Error based sampled spans column + int numOfErrorSamples = + errorSpanCounts.containsKey(spanName) ? errorSpanCounts.get(spanName) : 0; + // subtype 0 means all errors + emitSummaryTableCell(out, spanName, numOfErrorSamples, SampleType.ERROR, 0); + } + out.print("
    " + escapeHtml(spanName) + "
    "); + } + + private static void emitSpanNameAndCount( + PrintStream out, String spanName, int count, SampleType type) { + out.print("

    Span Name: " + escapeHtml(spanName) + "

    "); + String typeString = + type == SampleType.RUNNING + ? "running" + : type == SampleType.LATENCY ? "latency samples" : "error samples"; + out.print("

    Number of " + typeString + ": " + count + "

    "); + } + + private static void emitSpanDetails( + PrintStream out, Formatter formatter, Collection spans) { + out.print(""); + out.print(""); + out.print( + ""); + out.print( + ""); + out.print(""); + out.print(""); + boolean zebraStripe = false; + for (SpanData span : spans) { + zebraStripe = emitSingleSpan(out, formatter, span, zebraStripe); + } + out.print("
    When
    " + + "
    Elapsed(s)
    "); + } + + private static boolean emitSingleSpan( + PrintStream out, Formatter formatter, SpanData span, boolean zebraStripe) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(TimeUnit.NANOSECONDS.toMillis(span.getStartEpochNanos())); + long microsField = TimeUnit.NANOSECONDS.toMicros(span.getStartEpochNanos()); + String elapsedSecondsStr = + span.hasEnded() + ? String.format("%.6f", (span.getEndEpochNanos() - span.getStartEpochNanos()) * 1.0e-9) + : ""; + formatter.format( + "", zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff"); + formatter.format( + "
    "
    +            + "%04d/%02d/%02d-%02d:%02d:%02d.%06d
    ", + 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); + formatter.format( + "
    %s
    ", + elapsedSecondsStr); + formatter.format( + "
    "
    +            + "TraceId: %s "
    +            + " | SpanId: %s | ParentSpanId: %s
    ", + span.getSpanContext().isSampled() ? SAMPLED_TRACE_ID_COLOR : NOT_SAMPLED_TRACE_ID_COLOR, + span.getTraceId(), + span.getSpanId(), + span.getParentSpanId()); + out.print(""); + zebraStripe = !zebraStripe; + + int lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR); + + long lastEpochNanos = span.getStartEpochNanos(); + List timedEvents = + span.getEvents().stream() + .sorted(Comparator.comparingLong(EventData::getEpochNanos)) + .collect(Collectors.toList()); + for (EventData event : timedEvents) { + calendar.setTimeInMillis(TimeUnit.NANOSECONDS.toMillis(event.getEpochNanos())); + formatter.format( + "", zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff"); + emitSingleEvent(out, formatter, event, calendar, lastEntryDayOfYear, lastEpochNanos); + out.print(""); + if (calendar.get(Calendar.DAY_OF_YEAR) != lastEntryDayOfYear) { + lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR); + } + lastEpochNanos = event.getEpochNanos(); + zebraStripe = !zebraStripe; + } + formatter.format( + "" + + "
    ",
    +        zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff");
    +    StatusData status = span.getStatus();
    +    if (status != null) {
    +      formatter.format("%s | ", escapeHtml(status.toString()));
    +    }
    +    formatter.format("%s
    ", escapeHtml(renderAttributes(span.getAttributes()))); + zebraStripe = !zebraStripe; + return zebraStripe; + } + + private static void emitSingleEvent( + PrintStream out, + Formatter formatter, + EventData event, + Calendar calendar, + int lastEntryDayOfYear, + long lastEpochNanos) { + if (calendar.get(Calendar.DAY_OF_YEAR) == lastEntryDayOfYear) { + out.print("
    ");
    +    } else {
    +      formatter.format(
    +          "
    %04d/%02d/%02d-",
    +          calendar.get(Calendar.YEAR),
    +          calendar.get(Calendar.MONTH) + 1,
    +          calendar.get(Calendar.DAY_OF_MONTH));
    +    }
    +
    +    // 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(event.getEpochNanos() - lastEpochNanos);
    +    String deltaString;
    +    if (deltaMicros >= 1000000) {
    +      deltaString = String.format("%.6f", (deltaMicros / 1000000.0));
    +    } else {
    +      deltaString = String.format("%1s.%6d", "", deltaMicros);
    +    }
    +
    +    long microsField = TimeUnit.NANOSECONDS.toMicros(event.getEpochNanos());
    +    formatter.format(
    +        "%02d:%02d:%02d.%06d
    " + + "
    %s
    " + + "
    %s
    ", + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + calendar.get(Calendar.SECOND), + microsField, + deltaString, + escapeHtml(renderEvent(event))); + } + + private static String renderAttributes(Attributes attributes) { + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Attributes:{"); + attributes.forEach( + new BiConsumer, Object>() { + private boolean first = true; + + @Override + public void accept(AttributeKey key, Object value) { + if (first) { + first = false; + } else { + stringBuilder.append(", "); + } + stringBuilder.append(key); + stringBuilder.append("="); + stringBuilder.append(value.toString()); + } + }); + stringBuilder.append("}"); + return stringBuilder.toString(); + } + + private static String renderEvent(EventData event) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(event.getName()); + if (!event.getAttributes().isEmpty()) { + stringBuilder.append(" | "); + stringBuilder.append(renderAttributes(event.getAttributes())); + } + return stringBuilder.toString(); + } + + /** + * Emits HTML body content to the {@link PrintStream} {@code out}. Content emitted by this + * function should be enclosed by tag. + * + * @param queryMap the map containing URL query parameters.s + * @param out the {@link PrintStream} {@code out}. + */ + private void emitHtmlBody(Map queryMap, PrintStream out) { + if (dataAggregator == null) { + out.print("OpenTelemetry implementation not available."); + return; + } + out.print( + ""); + out.print("

    TraceZ Summary

    "); + emitSummaryTable(out); + // spanName will be null if the query parameter doesn't exist in the URL + String spanName = queryMap.get(PARAM_SPAN_NAME); + if (spanName != null) { + // Show detailed information for the corresponding span + String typeStr = queryMap.get(PARAM_SAMPLE_TYPE); + if (typeStr != null) { + List spans = null; + SampleType type = SampleType.fromString(typeStr); + switch (type) { + case UNKNOWN: + // Type of unknown is garbage value + return; + case RUNNING: + // Display running span + spans = dataAggregator.getRunningSpans(spanName); + break; + default: + String subtypeStr = queryMap.get(PARAM_SAMPLE_SUB_TYPE); + if (subtypeStr != null) { + int subtype = Integer.parseInt(subtypeStr); + if (type == SampleType.LATENCY) { + if (subtype < 0 || subtype >= LatencyBoundary.values().length) { + // N/A or out-of-bound check for latency based subtype, valid values: [0, 8] + return; + } + // Display latency based span + LatencyBoundary latencyBoundary = LatencyBoundary.values()[subtype]; + spans = + dataAggregator.getOkSpans( + spanName, + latencyBoundary.getLatencyLowerBound(), + latencyBoundary.getLatencyUpperBound()); + } else { + if (subtype < 0 || subtype >= StatusCode.values().length) { + // N/A or out-of-bound cueck for error based subtype, valid values: [0, 15] + return; + } + // Display error based span + spans = dataAggregator.getErrorSpans(spanName); + } + } + } + out.print("

    Span Details

    "); + emitSpanNameAndCount(out, spanName, spans == null ? 0 : spans.size(), type); + + if (spans != null) { + Formatter formatter = new Formatter(out, Locale.US); + spans = + spans.stream() + .sorted(Comparator.comparingLong(SpanData::getStartEpochNanos)) + .collect(Collectors.toList()); + emitSpanDetails(out, formatter, spans); + } + } + } + } + + @Override + public void emitHtml(Map queryMap, OutputStream outputStream) { + // PrintStream for emiting HTML contents + try (PrintStream out = new PrintStream(outputStream, /* autoFlush= */ false, "UTF-8")) { + out.print(""); + out.print(""); + out.print(""); + out.print(""); + out.print( + ""); + out.print( + ""); + out.print( + ""); + out.print("" + TRACEZ_NAME + ""); + emitHtmlStyle(out); + out.print(""); + out.print(""); + try { + emitHtmlBody(queryMap, out); + } catch (Throwable t) { + out.print("Error while generating HTML: " + t.toString()); + logger.log(Level.WARNING, "error while generating HTML", t); + } + out.print(""); + out.print(""); + } catch (Throwable t) { + logger.log(Level.WARNING, "error while generating HTML", t); + } + } + + private static String latencyBoundaryToString(LatencyBoundary latencyBoundary) { + switch (latencyBoundary) { + 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: " + latencyBoundary); + } + + private static Map buildLatencyBoundaryStringMap() { + Map latencyBoundaryMap = new HashMap<>(); + for (LatencyBoundary latencyBoundary : LatencyBoundary.values()) { + latencyBoundaryMap.put(latencyBoundary, latencyBoundaryToString(latencyBoundary)); + } + return latencyBoundaryMap; + } + + private static String escapeHtml(String html) { + StringBuilder escaped = null; + for (int i = 0; i < html.length(); i++) { + char c = html.charAt(i); + switch (c) { + case '"': + escaped = lazyStringBuilder(escaped, html, i); + escaped.append("""); + break; + case '\'': + escaped = lazyStringBuilder(escaped, html, i); + escaped.append("'"); + break; + case '&': + escaped = lazyStringBuilder(escaped, html, i); + escaped.append("&"); + break; + case '<': + escaped = lazyStringBuilder(escaped, html, i); + escaped.append("<"); + break; + case '>': + escaped = lazyStringBuilder(escaped, html, i); + escaped.append(">"); + break; + default: + if (escaped != null) { + escaped.append(c); + } + } + } + return escaped != null ? escaped.toString() : html; + } + + private static StringBuilder lazyStringBuilder( + @Nullable StringBuilder sb, String str, int currentCharIdx) { + if (sb != null) { + return sb; + } + sb = new StringBuilder(str.length()); + sb.append(str.substring(0, currentCharIdx - 1)); + return sb; + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageHandler.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageHandler.java new file mode 100644 index 000000000..c19fe82c8 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import java.io.OutputStream; +import java.util.Map; + +/** + * The main interface for all zPages. All zPages should implement this interface to allow the HTTP + * server implementation to support these pages. + */ +public abstract class ZPageHandler { + + /** + * Returns the URL path that should be used to register this zPage to the HTTP server. + * + * @return the URL path that should be used to register this zPage to the HTTP server. + */ + public abstract String getUrlPath(); + + /** + * Returns the name of the zPage. + * + * @return the name of the zPage. + */ + public abstract String getPageName(); + + /** + * Returns the description of the zPage. + * + * @return the description of the zPage. + */ + public abstract String getPageDescription(); + + /** + * Process requests that require changes (POST/PUT/DELETE). + * + * @param requestMethod the request method HttpHandler received. + * @param queryMap the map of the URL query parameters. + * @return true if theres an error while processing the request. + */ + public boolean processRequest( + String requestMethod, Map queryMap, OutputStream outputStream) { + // base no-op method + return false; + } + + /** + * Emits the generated HTML page to the {@code outputStream}. + * + * @param queryMap the map of the URL query parameters. + * @param outputStream the output for the generated HTML page. + */ + public abstract void emitHtml(Map queryMap, OutputStream outputStream); + + /** Package protected constructor to disallow users to extend this class. */ + ZPageHandler() {} +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageHttpHandler.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageHttpHandler.java new file mode 100644 index 000000000..603b7deef --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageHttpHandler.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** An {@link HttpHandler} that will be used to render HTML pages using any {@code ZPageHandler}. */ +final class ZPageHttpHandler implements HttpHandler { + // Query string parameter name for span name + private static final String PARAM_SPAN_NAME = "zspanname"; + // The corresponding ZPageHandler for the zPage (e.g. TracezZPageHandler) + private final ZPageHandler zpageHandler; + + /** Constructs a new {@code ZPageHttpHandler}. */ + ZPageHttpHandler(ZPageHandler zpageHandler) { + this.zpageHandler = zpageHandler; + } + + /** + * Build a query map from the query string. + * + * @param queryString the query string for buiding the query map. + * @return the query map built based on the query string. + */ + // Visible for testing + static Map parseQueryString(String queryString) { + if (queryString == null) { + return Collections.emptyMap(); + } + Map queryMap = new HashMap(); + Arrays.stream(queryString.split("&")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .forEach( + param -> { + List keyValuePair = + Arrays.stream(param.split("=")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + if (keyValuePair.size() > 1) { + if (keyValuePair.get(0).equals(PARAM_SPAN_NAME)) { + try { + queryMap.put( + keyValuePair.get(0), URLDecoder.decode(keyValuePair.get(1), "UTF-8")); + } catch (UnsupportedEncodingException e) { + // Ignore encoding exception. + } + } else { + queryMap.put(keyValuePair.get(0), keyValuePair.get(1)); + } + } + }); + return Collections.unmodifiableMap(queryMap); + } + + @Override + public final void handle(HttpExchange httpExchange) throws IOException { + try { + String requestMethod = httpExchange.getRequestMethod(); + httpExchange.sendResponseHeaders(200, 0); + if (requestMethod.equalsIgnoreCase("GET")) { + zpageHandler.emitHtml( + parseQueryString(httpExchange.getRequestURI().getRawQuery()), + httpExchange.getResponseBody()); + } else { + final String queryString; + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(httpExchange.getRequestBody(), "utf-8"))) { + // Query strings can only have one line + queryString = reader.readLine(); + } + boolean error = + zpageHandler.processRequest( + requestMethod, parseQueryString(queryString), httpExchange.getResponseBody()); + if (!error) { + zpageHandler.emitHtml(parseQueryString(queryString), httpExchange.getResponseBody()); + } + } + } finally { + httpExchange.close(); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageLogo.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageLogo.java new file mode 100644 index 000000000..a3c0b02c2 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageLogo.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; +import java.util.logging.Level; +import java.util.logging.Logger; + +final class ZPageLogo { + private static final Logger logger = Logger.getLogger(ZPageLogo.class.getName()); + + private ZPageLogo() {} + + /** + * Get OpenTelemetry logo in base64 encoding. + * + * @return OpenTelemetry logo in base64 encoding. + */ + public static String getLogoBase64() { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + try (InputStream is = ZPageLogo.class.getClassLoader().getResourceAsStream("logo.png")) { + readTo(is, os); + } + } catch (Throwable t) { + logger.log(Level.WARNING, "error while getting OpenTelemetry Logo", t); + return ""; + } + byte[] bytes = os.toByteArray(); + return Base64.getEncoder().encodeToString(bytes); + } + + /** + * Get OpenTelemetry favicon in base64 encoding. + * + * @return OpenTelemetry favicon in base64 encoding. + */ + public static String getFaviconBase64() { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (InputStream is = ZPageLogo.class.getClassLoader().getResourceAsStream("favicon.png")) { + readTo(is, os); + } catch (Throwable t) { + logger.log(Level.WARNING, "error while getting OpenTelemetry Logo", t); + return ""; + } + byte[] bytes = os.toByteArray(); + return Base64.getEncoder().encodeToString(bytes); + } + + private static void readTo(InputStream is, ByteArrayOutputStream os) throws IOException { + int b; + while ((b = is.read()) != -1) { + os.write(b); + } + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageServer.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageServer.java new file mode 100644 index 000000000..5d04a7fbe --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageServer.java @@ -0,0 +1,214 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import com.sun.net.httpserver.HttpServer; +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** + * A collection of HTML pages to display stats and trace data and allow library configuration + * control. To use, add {@linkplain ZPageServer#getSpanProcessor() the z-pages span processor}, + * {@linkplain ZPageServer#getTracezTraceConfigSupplier() the z-pages dynamic trace config} and + * {@linkplain ZPageServer#getTracezSampler() the z-pages dynamic sampler} to a {@link + * io.opentelemetry.sdk.trace.SdkTracerProviderBuilder}. Currently all tracers can only be made + * visible to a singleton {@link ZPageServer}. + * + *

    Example usage with private {@link HttpServer} + * + *

    {@code
    + * public class Main {
    + *   public static void main(String[] args) throws Exception {
    + *     OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
    + *         .setTracerProvider(SdkTracerProvider.builder()
    + *             .addSpanProcessor(ZPageServer.getSpanProcessor())
    + *             .setTraceConfigSupplier(ZPageServer.getTraceConfigSupplier())
    + *             .setSampler(ZPageServer.getSampler())
    + *             .build();
    + *         .build();
    + *
    + *     ZPageServer.startHttpServerAndRegisterAllPages(8000);
    + *     ... // do work
    + *   }
    + * }
    + * }
    + * + *

    Example usage with shared {@link HttpServer} + * + *

    {@code
    + * public class Main {
    + *   public static void main(String[] args) throws Exception {
    + *     OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
    + *         .setTracerProvider(SdkTracerProvider.builder()
    + *             .addSpanProcessor(ZPageServer.getSpanProcessor())
    + *             .setTraceConfigSupplier(ZPageServer.getTraceConfigSupplier())
    + *             .setSampler(ZPageServer.getSampler())
    + *             .build();
    + *         .build();
    + *
    + *     HttpServer server = HttpServer.create(new InetSocketAddress(8000), 10);
    + *     ZPageServer.registerAllPagesToHttpServer(server);
    + *     server.start();
    + *     ... // do work
    + *   }
    + * }
    + * }
    + */ +@ThreadSafe +public final class ZPageServer { + // The maximum number of queued incoming connections allowed on the HttpServer listening socket. + private static final int HTTPSERVER_BACKLOG = 5; + // Length of time to wait for the HttpServer to stop + private static final int HTTPSERVER_STOP_DELAY = 1; + // Tracez SpanProcessor and DataAggregator for constructing TracezZPageHandler + private static final TracezSpanProcessor tracezSpanProcessor = + TracezSpanProcessor.builder().build(); + private static final TracezTraceConfigSupplier tracezTraceConfigSupplier = + new TracezTraceConfigSupplier(); + private static final TracezDataAggregator tracezDataAggregator = + new TracezDataAggregator(tracezSpanProcessor); + // Handler for /tracez page + private static final ZPageHandler tracezZPageHandler = + new TracezZPageHandler(tracezDataAggregator); + // Handler for /traceconfigz page + private static final ZPageHandler traceConfigzZPageHandler = + new TraceConfigzZPageHandler(tracezTraceConfigSupplier); + // Handler for index page, **please include all available ZPageHandlers in the constructor** + private static final ZPageHandler indexZPageHandler = + new IndexZPageHandler(Arrays.asList(tracezZPageHandler, traceConfigzZPageHandler)); + + private static final Object mutex = new Object(); + + @GuardedBy("mutex") + @Nullable + private static HttpServer server; + + /** Returns a supplier of {@link SpanLimits} which can be reconfigured using zpages. */ + public static Supplier getTracezTraceConfigSupplier() { + return tracezTraceConfigSupplier; + } + + /** Returns a {@link Sampler} which can be reconfigured using zpages. */ + public static Sampler getTracezSampler() { + return tracezTraceConfigSupplier; + } + + /** + * Returns a {@link SpanProcessor} which will allow processing of spans by {@link ZPageServer}. + */ + public static SpanProcessor getSpanProcessor() { + return tracezSpanProcessor; + } + + /** + * Registers a {@link ZPageHandler} for the index page of zPages. The page displays information + * about all available zPages with links to those zPages. + * + * @param server the {@link HttpServer} for the page to register to. + */ + static void registerIndexZPageHandler(HttpServer server) { + server.createContext(indexZPageHandler.getUrlPath(), new ZPageHttpHandler(indexZPageHandler)); + } + + /** + * Registers a {@link ZPageHandler} for tracing debug to the server. The page displays information + * about all running spans and all sampled spans based on latency and error. + * + *

    It displays a summary table which contains one row for each span name and data about number + * of running and sampled spans. + * + *

    Clicking on a cell in the table with a number that is greater than zero will display + * detailed information about that span. + * + *

    This method will add the TracezSpanProcessor to the tracerProvider, it should only be called + * once. + * + * @param server the {@link HttpServer} for the page to register to. + */ + static void registerTracezZPageHandler(HttpServer server) { + server.createContext(tracezZPageHandler.getUrlPath(), new ZPageHttpHandler(tracezZPageHandler)); + } + + /** + * Registers a {@code ZPageHandler} for tracing config. The page displays information about all + * active configuration and allow changing the active configuration. + * + *

    It displays a change table which allows users to change active configuration. + * + *

    It displays an active value table which displays current active configuration. + * + *

    Refreshing the page will show the updated active configuration. + * + * @param server the {@link HttpServer} for the page to register to. + */ + static void registerTraceConfigzZPageHandler(HttpServer server) { + server.createContext( + traceConfigzZPageHandler.getUrlPath(), new ZPageHttpHandler(traceConfigzZPageHandler)); + } + + /** + * Registers all zPages to the given {@link HttpServer} {@code server}. + * + * @param server the {@link HttpServer} for the page to register to. + */ + public static void registerAllPagesToHttpServer(HttpServer server) { + // For future zPages, register them to the server in here + registerIndexZPageHandler(server); + registerTracezZPageHandler(server); + registerTraceConfigzZPageHandler(server); + } + + /** Method for stopping the {@link HttpServer} {@code server}. */ + private static void stop() { + synchronized (mutex) { + if (server == null) { + return; + } + server.stop(HTTPSERVER_STOP_DELAY); + server = null; + } + } + + /** + * Starts a private {@link HttpServer} and registers all zPages to it. When the JVM shuts down the + * server is stopped. + * + *

    Users can only call this function once per process. + * + * @param port the port used to bind the {@link HttpServer} {@code server} + * @throws IllegalStateException if the server is already started. + * @throws IOException if the server cannot bind to the specified port. + */ + public static void startHttpServerAndRegisterAllPages(int port) throws IOException { + synchronized (mutex) { + if (server != null) { + throw new IllegalStateException("The HttpServer is already started."); + } + server = HttpServer.create(new InetSocketAddress(port), HTTPSERVER_BACKLOG); + ZPageServer.registerAllPagesToHttpServer(server); + server.start(); + } + + Runtime.getRuntime() + .addShutdownHook( + new Thread() { + @Override + public void run() { + ZPageServer.stop(); + } + }); + } + + private ZPageServer() {} +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageStyle.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageStyle.java new file mode 100644 index 000000000..2407ad18f --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/ZPageStyle.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +/** This class contains the unified CSS styles for all zPages. */ +final class ZPageStyle { + private ZPageStyle() {} + + /** Style here will be applied to the generated HTML pages for all zPages. */ + static String style = + "body{font-family: \"Roboto\", sans-serif; font-size: 16px;" + + "background-color: #fff;}" + + "h1{padding: 0 20px; color: #363636; text-align: center; margin-bottom: 20px;}" + + "h2{padding: 0 20px; color: #363636; text-align: center; margin-bottom: 20px;}" + + "p{padding: 0 20px; color: #363636;}" + + "tr.bg-color{background-color: #4b5fab;}" + + "table{margin: 0 auto;}" + + "th{padding: 0 1em; line-height: 2.0}" + + "td{padding: 0 1em; line-height: 2.0}" + + ".border-right-white{border-right: 1px solid #fff;}" + + ".border-left-white{border-left: 1px solid #fff;}" + + ".border-left-dark{border-left: 1px solid #363636;}" + + "th.header-text{color: #fff; line-height: 3.0;}" + + ".align-center{text-align: center;}" + + ".align-right{text-align: right;}" + + "pre.no-margin{margin: 0;}" + + "pre.wrap-text{white-space:pre-wrap;}" + + "td.bg-white{background-color: #fff;}" + + "button.button{background-color: #fff; margin-top: 15px;}" + + "form.form-flex{display: flex; flex-direction: column; align-items: center;}"; +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/package-info.java b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/package-info.java new file mode 100644 index 000000000..b5de275da --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/main/java/io/opentelemetry/sdk/extension/zpages/package-info.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The ZPages endpoint for providing debug information about an app instrumented with OpenTelemetry. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.zpages; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/resources/favicon.png b/opentelemetry-java/sdk-extensions/zpages/src/main/resources/favicon.png new file mode 100644 index 000000000..91f2be7b2 Binary files /dev/null and b/opentelemetry-java/sdk-extensions/zpages/src/main/resources/favicon.png differ diff --git a/opentelemetry-java/sdk-extensions/zpages/src/main/resources/logo.png b/opentelemetry-java/sdk-extensions/zpages/src/main/resources/logo.png new file mode 100644 index 000000000..f225db4f3 Binary files /dev/null and b/opentelemetry-java/sdk-extensions/zpages/src/main/resources/logo.png differ diff --git a/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/IndexZPageHandlerTest.java b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/IndexZPageHandlerTest.java new file mode 100644 index 000000000..a42e56c1c --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/IndexZPageHandlerTest.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link IndexZPageHandler}. */ +public final class IndexZPageHandlerTest { + private static final ZPageHandler tracezZPageHandler = new TracezZPageHandler(null); + private final Map emptyQueryMap = ImmutableMap.of(); + + @Test + void emitHtmlCorrectly() { + OutputStream output = new ByteArrayOutputStream(); + IndexZPageHandler indexZPageHandler = + new IndexZPageHandler(ImmutableList.of(tracezZPageHandler)); + + indexZPageHandler.emitHtml(emptyQueryMap, output); + + assertThat(output.toString()).contains(""); + assertThat(output.toString()).contains(">" + tracezZPageHandler.getPageName() + ""); + assertThat(output.toString()) + .contains("

    " + tracezZPageHandler.getPageDescription() + "

    "); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/SpanBucketTest.java b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/SpanBucketTest.java new file mode 100644 index 000000000..b3e0f366d --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/SpanBucketTest.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +class SpanBucketTest { + private static final String SPAN_NAME = "span"; + private static final int LATENCY_BUCKET_SIZE = 16; + private static final int ERROR_BUCKET_SIZE = 8; + private final SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().build(); + private final Tracer tracer = sdkTracerProvider.get("SpanBucketTest"); + + @Test + void verifyLatencyBucketSizeLimit() { + SpanBucket latencyBucket = new SpanBucket(/* isLatencyBucket= */ true); + Span[] spans = new Span[LATENCY_BUCKET_SIZE + 1]; + for (int i = 0; i < LATENCY_BUCKET_SIZE + 1; i++) { + spans[i] = tracer.spanBuilder(SPAN_NAME).startSpan(); + latencyBucket.add((ReadableSpan) spans[i]); + spans[i].end(); + } + List bucketSpans = new ArrayList<>(); + latencyBucket.addTo(bucketSpans); + /* The latency SpanBucket should have the most recent LATENCY_BUCKET_SIZE spans */ + assertThat(latencyBucket.size()).isEqualTo(LATENCY_BUCKET_SIZE); + assertThat(bucketSpans.size()).isEqualTo(LATENCY_BUCKET_SIZE); + assertThat(bucketSpans).doesNotContain((ReadableSpan) spans[0]); + for (int i = 1; i < LATENCY_BUCKET_SIZE + 1; i++) { + assertThat(bucketSpans).contains((ReadableSpan) spans[i]); + } + } + + @Test + void verifyErrorBucketSizeLimit() { + SpanBucket errorBucket = new SpanBucket(/* isLatencyBucket= */ false); + Span[] spans = new Span[ERROR_BUCKET_SIZE + 1]; + for (int i = 0; i < ERROR_BUCKET_SIZE + 1; i++) { + spans[i] = tracer.spanBuilder(SPAN_NAME).startSpan(); + errorBucket.add((ReadableSpan) spans[i]); + spans[i].end(); + } + List bucketSpans = new ArrayList<>(); + errorBucket.addTo(bucketSpans); + /* The error SpanBucket should have the most recent ERROR_BUCKET_SIZE spans */ + assertThat(errorBucket.size()).isEqualTo(ERROR_BUCKET_SIZE); + assertThat(bucketSpans.size()).isEqualTo(ERROR_BUCKET_SIZE); + assertThat(bucketSpans).doesNotContain((ReadableSpan) spans[0]); + for (int i = 1; i < ERROR_BUCKET_SIZE + 1; i++) { + assertThat(bucketSpans).contains((ReadableSpan) spans[i]); + } + } + + @Timeout(value = 1) + public void verifyThreadSafety() throws InterruptedException { + int numberOfThreads = 4; + int numberOfSpans = 4; + SpanBucket spanBucket = new SpanBucket(/* isLatencyBucket= */ true); + final CountDownLatch startSignal = new CountDownLatch(1); + final CountDownLatch endSignal = new CountDownLatch(numberOfThreads); + for (int i = 0; i < numberOfThreads; i++) { + new Thread( + () -> { + try { + startSignal.await(); + for (int j = 0; j < numberOfSpans; j++) { + Span span = tracer.spanBuilder(SPAN_NAME).startSpan(); + spanBucket.add((ReadableSpan) span); + span.end(); + } + endSignal.countDown(); + } catch (InterruptedException e) { + return; + } + }) + .start(); + } + startSignal.countDown(); + endSignal.await(); + /* The SpanBucket should have exactly 16 spans */ + assertThat(spanBucket.size()).isEqualTo(numberOfThreads * numberOfSpans); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TraceConfigzZPageHandlerTest.java b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TraceConfigzZPageHandlerTest.java new file mode 100644 index 000000000..023c3bf42 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TraceConfigzZPageHandlerTest.java @@ -0,0 +1,352 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TraceConfigzZPageHandlerTest { + private static final Map emptyQueryMap = ImmutableMap.of(); + + private TracezTraceConfigSupplier configSupplier; + + @BeforeEach + void setup() { + configSupplier = new TracezTraceConfigSupplier(); + } + + @Test + void changeTable_emitRowsCorrectly() { + OutputStream output = new ByteArrayOutputStream(); + String querySamplingProbability = "samplingprobability"; + String queryMaxNumOfAttributes = "maxnumofattributes"; + String queryMaxNumOfEvents = "maxnumofevents"; + String queryMaxNumOfLinks = "maxnumoflinks"; + String queryMaxNumOfAttributesPerEvent = "maxnumofattributesperevent"; + String queryMaxNumOfAttributesPerLink = "maxnumofattributesperlink"; + + TraceConfigzZPageHandler traceConfigzZPageHandler = + new TraceConfigzZPageHandler(configSupplier); + traceConfigzZPageHandler.emitHtml(emptyQueryMap, output); + + assertThat(output.toString()).contains("SamplingProbability to"); + assertThat(output.toString()).contains("name=" + querySamplingProbability); + assertThat(output.toString()).contains("(1.0)"); + assertThat(output.toString()).contains("MaxNumberOfAttributes to"); + assertThat(output.toString()).contains("name=" + queryMaxNumOfAttributes); + assertThat(output.toString()) + .contains("(" + Integer.toString(SpanLimits.getDefault().getMaxNumberOfAttributes()) + ")"); + assertThat(output.toString()).contains("MaxNumberOfEvents to"); + assertThat(output.toString()).contains("name=" + queryMaxNumOfEvents); + assertThat(output.toString()) + .contains("(" + Integer.toString(SpanLimits.getDefault().getMaxNumberOfEvents()) + ")"); + assertThat(output.toString()).contains("MaxNumberOfLinks to"); + assertThat(output.toString()).contains("name=" + queryMaxNumOfLinks); + assertThat(output.toString()) + .contains("(" + Integer.toString(SpanLimits.getDefault().getMaxNumberOfLinks()) + ")"); + assertThat(output.toString()).contains("MaxNumberOfAttributesPerEvent to"); + assertThat(output.toString()).contains("name=" + queryMaxNumOfAttributesPerEvent); + assertThat(output.toString()) + .contains( + "(" + + Integer.toString(SpanLimits.getDefault().getMaxNumberOfAttributesPerEvent()) + + ")"); + assertThat(output.toString()).contains("MaxNumberOfAttributesPerLink to"); + assertThat(output.toString()).contains("name=" + queryMaxNumOfAttributesPerLink); + assertThat(output.toString()) + .contains( + "(" + + Integer.toString(SpanLimits.getDefault().getMaxNumberOfAttributesPerLink()) + + ")"); + } + + @Test + void activeTable_emitRowsCorrectly() { + OutputStream output = new ByteArrayOutputStream(); + + TraceConfigzZPageHandler traceConfigzZPageHandler = + new TraceConfigzZPageHandler(configSupplier); + traceConfigzZPageHandler.emitHtml(emptyQueryMap, output); + + assertThat(output.toString()).contains("Sampler"); + assertThat(output.toString()) + .contains(">" + configSupplier.getSampler().getDescription() + "<"); + assertThat(output.toString()).contains("MaxNumberOfAttributes"); + assertThat(output.toString()) + .contains(">" + Integer.toString(configSupplier.get().getMaxNumberOfAttributes()) + "<"); + assertThat(output.toString()).contains("MaxNumberOfEvents"); + assertThat(output.toString()) + .contains(">" + Integer.toString(configSupplier.get().getMaxNumberOfEvents()) + "<"); + assertThat(output.toString()).contains("MaxNumberOfLinks"); + assertThat(output.toString()) + .contains(">" + Integer.toString(configSupplier.get().getMaxNumberOfLinks()) + "<"); + assertThat(output.toString()).contains("MaxNumberOfAttributesPerEvent"); + assertThat(output.toString()) + .contains( + ">" + Integer.toString(configSupplier.get().getMaxNumberOfAttributesPerEvent()) + "<"); + assertThat(output.toString()).contains("MaxNumberOfAttributesPerLink"); + assertThat(output.toString()) + .contains( + ">" + Integer.toString(configSupplier.get().getMaxNumberOfAttributesPerLink()) + "<"); + } + + @Test + void appliesChangesCorrectly_formSubmit() { + OutputStream output = new ByteArrayOutputStream(); + String querySamplingProbability = "samplingprobability"; + String queryMaxNumOfAttributes = "maxnumofattributes"; + String queryMaxNumOfEvents = "maxnumofevents"; + String queryMaxNumOfLinks = "maxnumoflinks"; + String queryMaxNumOfAttributesPerEvent = "maxnumofattributesperevent"; + String queryMaxNumOfAttributesPerLink = "maxnumofattributesperlink"; + String newSamplingProbability = "0.001"; + String newMaxNumOfAttributes = "16"; + String newMaxNumOfEvents = "16"; + String newMaxNumOfLinks = "16"; + String newMaxNumOfAttributesPerEvent = "16"; + String newMaxNumOfAttributesPerLink = "16"; + + Map queryMap = + new ImmutableMap.Builder() + .put("action", "change") + .put(querySamplingProbability, newSamplingProbability) + .put(queryMaxNumOfAttributes, newMaxNumOfAttributes) + .put(queryMaxNumOfEvents, newMaxNumOfEvents) + .put(queryMaxNumOfLinks, newMaxNumOfLinks) + .put(queryMaxNumOfAttributesPerEvent, newMaxNumOfAttributesPerEvent) + .put(queryMaxNumOfAttributesPerLink, newMaxNumOfAttributesPerLink) + .build(); + + TraceConfigzZPageHandler traceConfigzZPageHandler = + new TraceConfigzZPageHandler(configSupplier); + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + traceConfigzZPageHandler.emitHtml(queryMap, output); + + assertThat(configSupplier.getSampler().getDescription()) + .isEqualTo( + Sampler.traceIdRatioBased(Double.parseDouble(newSamplingProbability)).getDescription()); + assertThat(configSupplier.get().getMaxNumberOfAttributes()) + .isEqualTo(Integer.parseInt(newMaxNumOfAttributes)); + assertThat(configSupplier.get().getMaxNumberOfEvents()) + .isEqualTo(Integer.parseInt(newMaxNumOfEvents)); + assertThat(configSupplier.get().getMaxNumberOfLinks()) + .isEqualTo(Integer.parseInt(newMaxNumOfLinks)); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerEvent()) + .isEqualTo(Integer.parseInt(newMaxNumOfAttributesPerEvent)); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerLink()) + .isEqualTo(Integer.parseInt(newMaxNumOfAttributesPerLink)); + } + + @Test + void appliesChangesCorrectly_restoreDefault() { + OutputStream output = new ByteArrayOutputStream(); + + Map queryMap = ImmutableMap.of("action", "default"); + + TraceConfigzZPageHandler traceConfigzZPageHandler = + new TraceConfigzZPageHandler(configSupplier); + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + traceConfigzZPageHandler.emitHtml(queryMap, output); + + assertThat(configSupplier.getSampler().getDescription()) + .isEqualTo(Sampler.traceIdRatioBased(1.0).getDescription()); + assertThat(configSupplier.get().getMaxNumberOfAttributes()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributes()); + assertThat(configSupplier.get().getMaxNumberOfEvents()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfEvents()); + assertThat(configSupplier.get().getMaxNumberOfLinks()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfLinks()); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerEvent()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributesPerEvent()); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerLink()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributesPerLink()); + } + + @Test + void appliesChangesCorrectly_doNotCrashOnNullParameters() { + OutputStream output = new ByteArrayOutputStream(); + + Map queryMap = ImmutableMap.of("action", "change"); + + TraceConfigzZPageHandler traceConfigzZPageHandler = + new TraceConfigzZPageHandler(configSupplier); + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + traceConfigzZPageHandler.emitHtml(queryMap, output); + + assertThat(configSupplier.getSampler().getDescription()) + .isEqualTo(Sampler.traceIdRatioBased(1.0).getDescription()); + assertThat(configSupplier.get().getMaxNumberOfAttributes()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributes()); + assertThat(configSupplier.get().getMaxNumberOfEvents()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfEvents()); + assertThat(configSupplier.get().getMaxNumberOfLinks()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfLinks()); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerEvent()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributesPerEvent()); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerLink()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributesPerLink()); + } + + @Test + void applyChanges_emitErrorOnInvalidInput() { + // Invalid samplingProbability (not type of double) + OutputStream output = new ByteArrayOutputStream(); + TraceConfigzZPageHandler traceConfigzZPageHandler = + new TraceConfigzZPageHandler(configSupplier); + Map queryMap = + ImmutableMap.of("action", "change", "samplingprobability", "invalid"); + + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + + assertThat(output.toString()).contains("Error while applying trace config changes: "); + assertThat(output.toString()).contains("SamplingProbability must be of the type double"); + + // Invalid samplingProbability (< 0) + output = new ByteArrayOutputStream(); + traceConfigzZPageHandler = new TraceConfigzZPageHandler(configSupplier); + queryMap = ImmutableMap.of("action", "change", "samplingprobability", "-1"); + + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + + assertThat(output.toString()).contains("Error while applying trace config changes: "); + assertThat(output.toString()).contains("ratio must be in range [0.0, 1.0]"); + + // Invalid samplingProbability (> 1) + output = new ByteArrayOutputStream(); + traceConfigzZPageHandler = new TraceConfigzZPageHandler(configSupplier); + queryMap = ImmutableMap.of("action", "change", "samplingprobability", "1.1"); + + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + + assertThat(output.toString()).contains("Error while applying trace config changes: "); + assertThat(output.toString()).contains("ratio must be in range [0.0, 1.0]"); + + // Invalid maxNumOfAttributes + output = new ByteArrayOutputStream(); + traceConfigzZPageHandler = new TraceConfigzZPageHandler(configSupplier); + queryMap = ImmutableMap.of("action", "change", "maxnumofattributes", "invalid"); + + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + + assertThat(output.toString()).contains("Error while applying trace config changes: "); + assertThat(output.toString()).contains("MaxNumOfAttributes must be of the type integer"); + + // Invalid maxNumOfEvents + output = new ByteArrayOutputStream(); + traceConfigzZPageHandler = new TraceConfigzZPageHandler(configSupplier); + queryMap = ImmutableMap.of("action", "change", "maxnumofevents", "invalid"); + + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + + assertThat(output.toString()).contains("Error while applying trace config changes: "); + assertThat(output.toString()).contains("MaxNumOfEvents must be of the type integer"); + + // Invalid maxNumLinks + output = new ByteArrayOutputStream(); + traceConfigzZPageHandler = new TraceConfigzZPageHandler(configSupplier); + queryMap = ImmutableMap.of("action", "change", "maxnumoflinks", "invalid"); + + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + + assertThat(output.toString()).contains("Error while applying trace config changes: "); + assertThat(output.toString()).contains("MaxNumOfLinks must be of the type integer"); + + // Invalid maxNumOfAttributesPerEvent + output = new ByteArrayOutputStream(); + traceConfigzZPageHandler = new TraceConfigzZPageHandler(configSupplier); + queryMap = ImmutableMap.of("action", "change", "maxnumofattributesperevent", "invalid"); + + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + + assertThat(output.toString()).contains("Error while applying trace config changes: "); + assertThat(output.toString()) + .contains("MaxNumOfAttributesPerEvent must be of the type integer"); + + // Invalid maxNumOfAttributesPerLink + output = new ByteArrayOutputStream(); + traceConfigzZPageHandler = new TraceConfigzZPageHandler(configSupplier); + queryMap = ImmutableMap.of("action", "change", "maxnumofattributesperlink", "invalid"); + + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + + assertThat(output.toString()).contains("Error while applying trace config changes: "); + assertThat(output.toString()).contains("MaxNumOfAttributesPerLink must be of the type integer"); + } + + @Test + void applyChanges_shouldNotUpdateOnGetRequest() { + OutputStream output = new ByteArrayOutputStream(); + String querySamplingProbability = "samplingprobability"; + String queryMaxNumOfAttributes = "maxnumofattributes"; + String queryMaxNumOfEvents = "maxnumofevents"; + String queryMaxNumOfLinks = "maxnumoflinks"; + String queryMaxNumOfAttributesPerEvent = "maxnumofattributesperevent"; + String queryMaxNumOfAttributesPerLink = "maxnumofattributesperlink"; + String newSamplingProbability = "0.001"; + String newMaxNumOfAttributes = "16"; + String newMaxNumOfEvents = "16"; + String newMaxNumOfLinks = "16"; + String newMaxNumOfAttributesPerEvent = "16"; + String newMaxNumOfAttributesPerLink = "16"; + + // Apply new config + Map queryMap = + new ImmutableMap.Builder() + .put("action", "change") + .put(querySamplingProbability, newSamplingProbability) + .put(queryMaxNumOfAttributes, newMaxNumOfAttributes) + .put(queryMaxNumOfEvents, newMaxNumOfEvents) + .put(queryMaxNumOfLinks, newMaxNumOfLinks) + .put(queryMaxNumOfAttributesPerEvent, newMaxNumOfAttributesPerEvent) + .put(queryMaxNumOfAttributesPerLink, newMaxNumOfAttributesPerLink) + .build(); + + TraceConfigzZPageHandler traceConfigzZPageHandler = + new TraceConfigzZPageHandler(configSupplier); + + // GET request, Should not apply changes + traceConfigzZPageHandler.emitHtml(queryMap, output); + + assertThat(configSupplier.getSampler().getDescription()) + .isEqualTo(Sampler.traceIdRatioBased(1.0).getDescription()); + assertThat(configSupplier.get().getMaxNumberOfAttributes()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributes()); + assertThat(configSupplier.get().getMaxNumberOfEvents()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfEvents()); + assertThat(configSupplier.get().getMaxNumberOfLinks()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfLinks()); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerEvent()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributesPerEvent()); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerLink()) + .isEqualTo(SpanLimits.getDefault().getMaxNumberOfAttributesPerLink()); + + // POST request, Should apply changes + traceConfigzZPageHandler.processRequest("POST", queryMap, output); + traceConfigzZPageHandler.emitHtml(queryMap, output); + + assertThat(configSupplier.getSampler().getDescription()) + .isEqualTo( + Sampler.traceIdRatioBased(Double.parseDouble(newSamplingProbability)).getDescription()); + assertThat(configSupplier.get().getMaxNumberOfAttributes()) + .isEqualTo(Integer.parseInt(newMaxNumOfAttributes)); + assertThat(configSupplier.get().getMaxNumberOfEvents()) + .isEqualTo(Integer.parseInt(newMaxNumOfEvents)); + assertThat(configSupplier.get().getMaxNumberOfLinks()) + .isEqualTo(Integer.parseInt(newMaxNumOfLinks)); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerEvent()) + .isEqualTo(Integer.parseInt(newMaxNumOfAttributesPerEvent)); + assertThat(configSupplier.get().getMaxNumberOfAttributesPerLink()) + .isEqualTo(Integer.parseInt(newMaxNumOfAttributesPerLink)); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregatorTest.java b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregatorTest.java new file mode 100644 index 000000000..36496db54 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezDataAggregatorTest.java @@ -0,0 +1,297 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link TracezDataAggregator}. */ +public final class TracezDataAggregatorTest { + private static final String SPAN_NAME_ONE = "one"; + private static final String SPAN_NAME_TWO = "two"; + private final TestClock testClock = TestClock.create(); + private final TracezSpanProcessor spanProcessor = TracezSpanProcessor.builder().build(); + private final SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder().setClock(testClock).addSpanProcessor(spanProcessor).build(); + private final Tracer tracer = sdkTracerProvider.get("TracezDataAggregatorTest"); + private final TracezDataAggregator dataAggregator = new TracezDataAggregator(spanProcessor); + + @Test + void getSpanNames_noSpans() { + assertThat(dataAggregator.getSpanNames()).isEmpty(); + } + + @Test + void getSpanNames_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + Span span3 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getSpanNames should return a set with 2 span names */ + Set names = dataAggregator.getSpanNames(); + assertThat(names).containsExactly(SPAN_NAME_ONE, SPAN_NAME_TWO); + span1.end(); + span2.end(); + span3.end(); + /* getSpanNames should still return a set with 2 span names */ + names = dataAggregator.getSpanNames(); + assertThat(names).containsExactly(SPAN_NAME_ONE, SPAN_NAME_TWO); + } + + @Test + void getRunningSpanCounts_noSpans() { + /* getRunningSpanCounts should return a an empty map */ + Map counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts).isEmpty(); + } + + @Test + void getRunningSpanCounts_oneSpanName() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span3 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + /* getRunningSpanCounts should return a map with 1 span name */ + Map counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts.get(SPAN_NAME_ONE)).isEqualTo(3); + span1.end(); + span2.end(); + span3.end(); + /* getRunningSpanCounts should return a map with no span names */ + counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts).isEmpty(); + } + + @Test + void getRunningSpanCounts_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getRunningSpanCounts should return a map with 2 different span names */ + Map counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts.get(SPAN_NAME_ONE)).isEqualTo(1); + assertThat(counts.get(SPAN_NAME_TWO)).isEqualTo(1); + + span1.end(); + /* getRunningSpanCounts should return a map with 1 unique span name */ + counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts.get(SPAN_NAME_ONE)).isNull(); + assertThat(counts.get(SPAN_NAME_TWO)).isEqualTo(1); + + span2.end(); + /* getRunningSpanCounts should return a map with no span names */ + counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts).isEmpty(); + } + + @Test + void getRunningSpans_noSpans() { + /* getRunningSpans should return an empty List */ + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_ONE)).isEmpty(); + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_TWO)).isEmpty(); + } + + @Test + void getRunningSpans_oneSpanName() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span3 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + /* getRunningSpans should return a List with all 3 spans */ + List spans = dataAggregator.getRunningSpans(SPAN_NAME_ONE); + assertThat(spans) + .containsExactlyInAnyOrder( + ((ReadableSpan) span1).toSpanData(), + ((ReadableSpan) span2).toSpanData(), + ((ReadableSpan) span3).toSpanData()); + span1.end(); + span2.end(); + span3.end(); + /* getRunningSpans should return an empty List */ + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_ONE)).isEmpty(); + } + + @Test + void getRunningSpans_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getRunningSpans should return a List with only the corresponding span */ + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_ONE)) + .containsExactly(((ReadableSpan) span1).toSpanData()); + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_TWO)) + .containsExactly(((ReadableSpan) span2).toSpanData()); + span1.end(); + span2.end(); + /* getRunningSpans should return an empty List for each span name */ + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_ONE)).isEmpty(); + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_TWO)).isEmpty(); + } + + @Test + void getSpanLatencyCounts_noSpans() { + /* getSpanLatencyCounts should return a an empty map */ + Map> counts = dataAggregator.getSpanLatencyCounts(); + assertThat(counts).isEmpty(); + } + + @Test + void getSpanLatencyCounts_noCompletedSpans() { + /* getSpanLatencyCounts should return a an empty map */ + Span span = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Map> counts = dataAggregator.getSpanLatencyCounts(); + span.end(); + assertThat(counts).isEmpty(); + } + + @Test + void getSpanLatencyCounts_oneSpanPerLatencyBucket() { + for (LatencyBoundary bucket : LatencyBoundary.values()) { + Span span = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + testClock.advanceNanos(bucket.getLatencyLowerBound()); + span.end(); + } + /* getSpanLatencyCounts should return 1 span per latency bucket */ + Map> counts = dataAggregator.getSpanLatencyCounts(); + for (LatencyBoundary bucket : LatencyBoundary.values()) { + assertThat(counts.get(SPAN_NAME_ONE).get(bucket)).isEqualTo(1); + } + } + + @Test + void getOkSpans_noSpans() { + /* getOkSpans should return an empty List */ + assertThat(dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE)).isEmpty(); + assertThat(dataAggregator.getOkSpans(SPAN_NAME_TWO, 0, Long.MAX_VALUE)).isEmpty(); + } + + @Test + void getOkSpans_oneSpanNameWithDifferentLatencies() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + /* getOkSpans should return an empty List */ + assertThat(dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE)).isEmpty(); + span1.end(); + testClock.advanceNanos(1000); + span2.end(); + /* getOkSpans should return a List with both spans */ + List spans = dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE); + assertThat(spans) + .containsExactly(((ReadableSpan) span1).toSpanData(), ((ReadableSpan) span2).toSpanData()); + /* getOkSpans should return a List with only the first span */ + spans = dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, 1000); + assertThat(spans).containsExactly(((ReadableSpan) span1).toSpanData()); + /* getOkSpans should return a List with only the second span */ + spans = dataAggregator.getOkSpans(SPAN_NAME_ONE, 1000, Long.MAX_VALUE); + assertThat(spans).containsExactly(((ReadableSpan) span2).toSpanData()); + } + + @Test + void getOkSpans_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getOkSpans should return an empty List for each span name */ + assertThat(dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE)).isEmpty(); + assertThat(dataAggregator.getOkSpans(SPAN_NAME_TWO, 0, Long.MAX_VALUE)).isEmpty(); + span1.end(); + span2.end(); + /* getOkSpans should return a List with only the corresponding span */ + assertThat(dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE)) + .containsExactly(((ReadableSpan) span1).toSpanData()); + assertThat(dataAggregator.getOkSpans(SPAN_NAME_TWO, 0, Long.MAX_VALUE)) + .containsExactly(((ReadableSpan) span2).toSpanData()); + } + + @Test + void getErrorSpanCounts_noSpans() { + Map counts = dataAggregator.getErrorSpanCounts(); + assertThat(counts).isEmpty(); + } + + @Test + void getErrorSpanCounts_noCompletedSpans() { + /* getErrorSpanCounts should return a an empty map */ + Span span = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Map counts = dataAggregator.getErrorSpanCounts(); + span.setStatus(StatusCode.ERROR); + span.end(); + assertThat(counts).isEmpty(); + } + + @Test + void getErrorSpanCounts_oneSpanPerErrorCode() { + for (StatusCode errorCode : StatusCode.values()) { + if (errorCode.equals(StatusCode.ERROR)) { + Span span = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + span.setStatus(errorCode); + span.end(); + } + } + Map errorCounts = dataAggregator.getErrorSpanCounts(); + assertThat(errorCounts.get(SPAN_NAME_ONE)).isEqualTo(1); + } + + @Test + void getErrorSpanCounts_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + span1.setStatus(StatusCode.ERROR); + span1.end(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + span2.setStatus(StatusCode.ERROR); + span2.end(); + /* getErrorSpanCounts should return a map with 2 different span names */ + Map errorCounts = dataAggregator.getErrorSpanCounts(); + assertThat(errorCounts.get(SPAN_NAME_ONE)).isEqualTo(1); + assertThat(errorCounts.get(SPAN_NAME_TWO)).isEqualTo(1); + } + + @Test + void getErrorSpans_noSpans() { + /* getErrorSpans should return an empty List */ + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_ONE)).isEmpty(); + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_TWO)).isEmpty(); + } + + @Test + void getErrorSpans_oneSpanNameWithDifferentErrors() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + /* getErrorSpans should return an empty List */ + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_ONE)).isEmpty(); + span1.setStatus(StatusCode.ERROR); + span1.end(); + span2.setStatus(StatusCode.ERROR, "ABORTED"); + span2.end(); + /* getErrorSpans should return a List with both spans */ + List errorSpans = dataAggregator.getErrorSpans(SPAN_NAME_ONE); + assertThat(errorSpans) + .containsExactly(((ReadableSpan) span1).toSpanData(), ((ReadableSpan) span2).toSpanData()); + } + + @Test + void getErrorSpans_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getErrorSpans should return an empty List for each span name */ + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_ONE)).isEmpty(); + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_TWO)).isEmpty(); + span1.setStatus(StatusCode.ERROR); + span1.end(); + span2.setStatus(StatusCode.ERROR); + span2.end(); + /* getErrorSpans should return a List with only the corresponding span */ + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_ONE)) + .containsExactly(((ReadableSpan) span1).toSpanData()); + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_TWO)) + .containsExactly(((ReadableSpan) span2).toSpanData()); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezSpanProcessorTest.java b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezSpanProcessorTest.java new file mode 100644 index 000000000..044ad5621 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezSpanProcessorTest.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.Collection; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link TracezSpanProcessor}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class TracezSpanProcessorTest { + private static final String SPAN_NAME = "span"; + private static final SpanContext SAMPLED_SPAN_CONTEXT = + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.getDefault()); + private static final SpanContext NOT_SAMPLED_SPAN_CONTEXT = SpanContext.getInvalid(); + private static final StatusData SPAN_STATUS = StatusData.error(); + + private static void assertSpanCacheSizes( + TracezSpanProcessor spanProcessor, int runningSpanCacheSize, int completedSpanCacheSize) { + Collection runningSpans = spanProcessor.getRunningSpans(); + Collection completedSpans = spanProcessor.getCompletedSpans(); + assertThat(runningSpans.size()).isEqualTo(runningSpanCacheSize); + assertThat(completedSpans.size()).isEqualTo(completedSpanCacheSize); + } + + @Mock private ReadableSpan readableSpan; + @Mock private ReadWriteSpan readWriteSpan; + @Mock private SpanData spanData; + + @Test + void onStart_sampledSpan_inCache() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.builder().build(); + /* Return a sampled span, which should be added to the running cache by default */ + when(readWriteSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + spanProcessor.onStart(Context.root(), readWriteSpan); + assertSpanCacheSizes(spanProcessor, 1, 0); + } + + @Test + void onEnd_sampledSpan_inCache() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.builder().build(); + /* Return a sampled span, which should be added to the completed cache upon ending */ + when(readWriteSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + when(readWriteSpan.getName()).thenReturn(SPAN_NAME); + spanProcessor.onStart(Context.root(), readWriteSpan); + + when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + when(readableSpan.getName()).thenReturn(SPAN_NAME); + when(readableSpan.toSpanData()).thenReturn(spanData); + when(spanData.getStatus()).thenReturn(SPAN_STATUS); + spanProcessor.onEnd(readableSpan); + assertSpanCacheSizes(spanProcessor, 0, 1); + } + + @Test + void onStart_notSampledSpan_inCache() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.builder().build(); + /* Return a non-sampled span, which should not be added to the running cache by default */ + when(readWriteSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + spanProcessor.onStart(Context.root(), readWriteSpan); + assertSpanCacheSizes(spanProcessor, 1, 0); + } + + @Test + void onEnd_notSampledSpan_notInCache() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.builder().build(); + /* Return a non-sampled span, which should not be added to the running cache by default */ + when(readWriteSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + spanProcessor.onStart(Context.root(), readWriteSpan); + spanProcessor.onEnd(readableSpan); + assertSpanCacheSizes(spanProcessor, 0, 0); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezZPageHandlerTest.java b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezZPageHandlerTest.java new file mode 100644 index 000000000..88ffbbe61 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/TracezZPageHandlerTest.java @@ -0,0 +1,428 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import static com.google.common.net.UrlEscapers.urlFormParameterEscaper; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link TracezZPageHandler}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class TracezZPageHandlerTest { + private static final String FINISHED_SPAN_ONE = "FinishedSpanOne"; + private static final String FINISHED_SPAN_TWO = "FinishedSpanTwo"; + private static final String RUNNING_SPAN = "RunningSpan"; + private static final String LATENCY_SPAN = "LatencySpan"; + private static final String ERROR_SPAN = "ErrorSpan"; + private final TestClock testClock = TestClock.create(); + private final TracezSpanProcessor spanProcessor = TracezSpanProcessor.builder().build(); + private final SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder().setClock(testClock).addSpanProcessor(spanProcessor).build(); + private final Tracer tracer = sdkTracerProvider.get("TracezZPageHandlerTest"); + private final TracezDataAggregator dataAggregator = new TracezDataAggregator(spanProcessor); + private final Map emptyQueryMap = ImmutableMap.of(); + + @Test + void summaryTable_emitRowForEachSpan() { + OutputStream output = new ByteArrayOutputStream(); + Span finishedSpan1 = tracer.spanBuilder(FINISHED_SPAN_ONE).startSpan(); + Span finishedSpan2 = tracer.spanBuilder(FINISHED_SPAN_TWO).startSpan(); + finishedSpan1.end(); + finishedSpan2.end(); + + Span runningSpan = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + + Span latencySpan = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpan.end(10002, TimeUnit.NANOSECONDS); + + Span errorSpan = tracer.spanBuilder(ERROR_SPAN).startSpan(); + errorSpan.setStatus(StatusCode.ERROR); + errorSpan.end(); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(emptyQueryMap, output); + + // Emit a row for all types of spans + assertThat(output.toString()).contains(FINISHED_SPAN_ONE); + assertThat(output.toString()).contains(FINISHED_SPAN_TWO); + assertThat(output.toString()).contains(RUNNING_SPAN); + assertThat(output.toString()).contains(LATENCY_SPAN); + assertThat(output.toString()).contains(ERROR_SPAN); + + runningSpan.end(); + } + + @Test + void summaryTable_linkForRunningSpans() { + OutputStream output = new ByteArrayOutputStream(); + Span runningSpan1 = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + Span runningSpan2 = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + Span runningSpan3 = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + Span finishedSpan = tracer.spanBuilder(FINISHED_SPAN_ONE).startSpan(); + finishedSpan.end(); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(emptyQueryMap, output); + + // Link for running span with 3 running + assertThat(output.toString()) + .contains("href=\"?zspanname=" + RUNNING_SPAN + "&ztype=0&zsubtype=0\">3"); + // No link for finished spans + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + FINISHED_SPAN_ONE + "&ztype=0&subtype=0\""); + + runningSpan1.end(); + runningSpan2.end(); + runningSpan3.end(); + } + + @Test + void summaryTable_linkForLatencyBasedSpans_NoneForEmptyBoundary() { + OutputStream output = new ByteArrayOutputStream(); + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(emptyQueryMap, output); + + // No link for boundary 0 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=0\""); + // No link for boundary 1 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=1\""); + // No link for boundary 2 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=2\""); + // No link for boundary 3 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=3\""); + // No link for boundary 4 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=4\""); + // No link for boundary 5 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=5\""); + // No link for boundary 6 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=6\""); + // No link for boundary 7 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=7\""); + // No link for boundary 8 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=8\""); + } + + @Test + void summaryTable_linkForLatencyBasedSpans_OnePerBoundary() { + OutputStream output = new ByteArrayOutputStream(); + // Boundary 0, >1us + Span latencySpanSubtype0 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype0.end(1002, TimeUnit.NANOSECONDS); + // Boundary 1, >10us + Span latencySpanSubtype1 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype1.end(10002, TimeUnit.NANOSECONDS); + // Boundary 2, >100us + Span latencySpanSubtype2 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype2.end(100002, TimeUnit.NANOSECONDS); + // Boundary 3, >1ms + Span latencySpanSubtype3 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype3.end(1000002, TimeUnit.NANOSECONDS); + // Boundary 4, >10ms + Span latencySpanSubtype4 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype4.end(10000002, TimeUnit.NANOSECONDS); + // Boundary 5, >100ms + Span latencySpanSubtype5 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype5.end(100000002, TimeUnit.NANOSECONDS); + // Boundary 6, >1s + Span latencySpanSubtype6 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype6.end(1000000002, TimeUnit.NANOSECONDS); + // Boundary 7, >10s + Span latencySpanSubtype7 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype7.end(10000000002L, TimeUnit.NANOSECONDS); + // Boundary 8, >100s + Span latencySpanSubtype8 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpanSubtype8.end(100000000002L, TimeUnit.NANOSECONDS); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(emptyQueryMap, output); + + // Link for boundary 0 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=0\">1"); + // Link for boundary 1 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=1\">1"); + // Link for boundary 2 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=2\">1"); + // Link for boundary 3 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=3\">1"); + // Link for boundary 4 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=4\">1"); + // Link for boundary 5 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=5\">1"); + // Link for boundary 6 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=6\">1"); + // Link for boundary 7 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=7\">1"); + // Link for boundary 8 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=8\">1"); + } + + @Test + void summaryTable_linkForLatencyBasedSpans_MultipleForOneBoundary() { + OutputStream output = new ByteArrayOutputStream(); + // 4 samples in boundary 5, >100ms + Span latencySpan100ms1 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpan100ms1.end(112931232L, TimeUnit.NANOSECONDS); + Span latencySpan100ms2 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpan100ms2.end(138694322L, TimeUnit.NANOSECONDS); + Span latencySpan100ms3 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpan100ms3.end(154486482L, TimeUnit.NANOSECONDS); + Span latencySpan100ms4 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpan100ms4.end(194892582L, TimeUnit.NANOSECONDS); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(emptyQueryMap, output); + + // Link for boundary 5 with 4 samples + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=5\">4"); + } + + @Test + void summaryTable_linkForErrorSpans() { + OutputStream output = new ByteArrayOutputStream(); + Span errorSpan1 = tracer.spanBuilder(ERROR_SPAN).startSpan(); + Span errorSpan2 = tracer.spanBuilder(ERROR_SPAN).startSpan(); + Span errorSpan3 = tracer.spanBuilder(ERROR_SPAN).startSpan(); + Span finishedSpan = tracer.spanBuilder(FINISHED_SPAN_ONE).startSpan(); + errorSpan1.setStatus(StatusCode.ERROR, "CANCELLED"); + errorSpan2.setStatus(StatusCode.ERROR, "ABORTED"); + errorSpan3.setStatus(StatusCode.ERROR, "DEADLINE_EXCEEDED"); + errorSpan1.end(); + errorSpan2.end(); + errorSpan3.end(); + finishedSpan.end(); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(emptyQueryMap, output); + + // Link for error based spans with 3 samples + assertThat(output.toString()) + .contains("href=\"?zspanname=" + ERROR_SPAN + "&ztype=2&zsubtype=0\">3"); + // No link for Status{#OK} spans + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + FINISHED_SPAN_ONE + "&ztype=2&subtype=0\""); + } + + @Test + void spanDetails_emitRunningSpanDetailsCorrectly() { + OutputStream output = new ByteArrayOutputStream(); + Span runningSpan = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + Map queryMap = + ImmutableMap.of("zspanname", RUNNING_SPAN, "ztype", "0", "zsubtype", "0"); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + assertThat(output.toString()).contains("

    Span Details

    "); + assertThat(output.toString()).contains(" Span Name: " + RUNNING_SPAN + ""); + assertThat(output.toString()).contains(" Number of running: 1"); + assertThat(output.toString()).contains(runningSpan.getSpanContext().getTraceId()); + assertThat(output.toString()).contains(runningSpan.getSpanContext().getSpanId()); + + runningSpan.end(); + } + + @Test + void spanDetails_emitLatencySpanDetailsCorrectly() { + OutputStream output = new ByteArrayOutputStream(); + Span latencySpan1 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpan1.end(10002, TimeUnit.NANOSECONDS); + Span latencySpan2 = + tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L, TimeUnit.NANOSECONDS).startSpan(); + latencySpan2.end(10002, TimeUnit.NANOSECONDS); + Map queryMap = + ImmutableMap.of("zspanname", LATENCY_SPAN, "ztype", "1", "zsubtype", "1"); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + assertThat(output.toString()).contains("

    Span Details

    "); + assertThat(output.toString()).contains(" Span Name: " + LATENCY_SPAN + ""); + assertThat(output.toString()).contains(" Number of latency samples: 2"); + assertThat(output.toString()).contains(latencySpan1.getSpanContext().getTraceId()); + assertThat(output.toString()).contains(latencySpan1.getSpanContext().getSpanId()); + assertThat(output.toString()).contains(latencySpan2.getSpanContext().getTraceId()); + assertThat(output.toString()).contains(latencySpan2.getSpanContext().getSpanId()); + } + + @Test + void spanDetails_emitErrorSpanDetailsCorrectly() { + OutputStream output = new ByteArrayOutputStream(); + Span errorSpan1 = tracer.spanBuilder(ERROR_SPAN).startSpan(); + Span errorSpan2 = tracer.spanBuilder(ERROR_SPAN).startSpan(); + errorSpan1.setStatus(StatusCode.ERROR, "CANCELLED"); + errorSpan2.setStatus(StatusCode.ERROR, "ABORTED"); + errorSpan1.end(); + errorSpan2.end(); + Map queryMap = + ImmutableMap.of("zspanname", ERROR_SPAN, "ztype", "2", "zsubtype", "0"); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + assertThat(output.toString()).contains("

    Span Details

    "); + assertThat(output.toString()).contains(" Span Name: " + ERROR_SPAN + ""); + assertThat(output.toString()).contains(" Number of error samples: 2"); + assertThat(output.toString()).contains(errorSpan1.getSpanContext().getTraceId()); + assertThat(output.toString()).contains(errorSpan1.getSpanContext().getSpanId()); + assertThat(output.toString()).contains(errorSpan2.getSpanContext().getTraceId()); + assertThat(output.toString()).contains(errorSpan2.getSpanContext().getSpanId()); + } + + @Test + void spanDetails_shouldNotBreakOnUnknownType() { + OutputStream output = new ByteArrayOutputStream(); + Map queryMap = + ImmutableMap.of("zspanname", "Span", "ztype", "-1", "zsubtype", "0"); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + assertThat(output.toString()).doesNotContain("

    Span Details

    "); + assertThat(output.toString()).doesNotContain(" Span Name: Span"); + } + + private static Map generateQueryMap(String spanName, String type, String subtype) + throws URISyntaxException { + return ZPageHttpHandler.parseQueryString( + new URI( + "tracez?zspanname=" + + urlFormParameterEscaper().escape(spanName) + + "&ztype=" + + type + + "&zsubtype=" + + subtype) + .getRawQuery()); + } + + @Test + void spanDetails_emitNameWithSpaceCorrectly() + throws UnsupportedEncodingException, URISyntaxException { + OutputStream output = new ByteArrayOutputStream(); + String nameWithSpace = "SPAN NAME"; + Span runningSpan = tracer.spanBuilder(nameWithSpace).startSpan(); + tracer.spanBuilder(nameWithSpace).startSpan().end(); + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + + tracezZPageHandler.emitHtml(generateQueryMap(nameWithSpace, "0", "0"), output); + assertThat(output.toString()).contains(" Span Name: " + nameWithSpace + ""); + assertThat(output.toString()).contains(" Number of running: 1"); + tracezZPageHandler.emitHtml(generateQueryMap(nameWithSpace, "1", "0"), output); + assertThat(output.toString()).contains(" Span Name: " + nameWithSpace + ""); + assertThat(output.toString()).contains(" Number of latency samples: 1"); + + runningSpan.end(); + } + + @Test + void spanDetails_emitNameWithPlusCorrectly() + throws UnsupportedEncodingException, URISyntaxException { + OutputStream output = new ByteArrayOutputStream(); + String nameWithPlus = "SPAN+NAME"; + Span runningSpan = tracer.spanBuilder(nameWithPlus).startSpan(); + tracer.spanBuilder(nameWithPlus).startSpan().end(); + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + + tracezZPageHandler.emitHtml(generateQueryMap(nameWithPlus, "0", "0"), output); + assertThat(output.toString()).contains(" Span Name: " + nameWithPlus + ""); + assertThat(output.toString()).contains(" Number of running: 1"); + tracezZPageHandler.emitHtml(generateQueryMap(nameWithPlus, "1", "0"), output); + assertThat(output.toString()).contains(" Span Name: " + nameWithPlus + ""); + assertThat(output.toString()).contains(" Number of latency samples: 1"); + + runningSpan.end(); + } + + @Test + void spanDetails_emitNamesWithSpaceAndPlusCorrectly() + throws UnsupportedEncodingException, URISyntaxException { + OutputStream output = new ByteArrayOutputStream(); + String nameWithSpaceAndPlus = "SPAN + NAME"; + Span runningSpan = tracer.spanBuilder(nameWithSpaceAndPlus).startSpan(); + tracer.spanBuilder(nameWithSpaceAndPlus).startSpan().end(); + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + + tracezZPageHandler.emitHtml(generateQueryMap(nameWithSpaceAndPlus, "0", "0"), output); + assertThat(output.toString()).contains(" Span Name: " + nameWithSpaceAndPlus + ""); + assertThat(output.toString()).contains(" Number of running: 1"); + tracezZPageHandler.emitHtml(generateQueryMap(nameWithSpaceAndPlus, "1", "0"), output); + assertThat(output.toString()).contains(" Span Name: " + nameWithSpaceAndPlus + ""); + assertThat(output.toString()).contains(" Number of latency samples: 1"); + + runningSpan.end(); + } + + @Test + void spanDetails_emitNamesWithSpecialUrlCharsCorrectly() + throws UnsupportedEncodingException, URISyntaxException { + OutputStream output = new ByteArrayOutputStream(); + String nameWithUrlChars = "{SPAN/NAME}"; + Span runningSpan = tracer.spanBuilder(nameWithUrlChars).startSpan(); + tracer.spanBuilder(nameWithUrlChars).startSpan().end(); + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + + tracezZPageHandler.emitHtml(generateQueryMap(nameWithUrlChars, "0", "0"), output); + assertThat(output.toString()).contains(" Span Name: " + nameWithUrlChars + ""); + assertThat(output.toString()).contains(" Number of running: 1"); + tracezZPageHandler.emitHtml(generateQueryMap(nameWithUrlChars, "1", "0"), output); + assertThat(output.toString()).contains(" Span Name: " + nameWithUrlChars + ""); + assertThat(output.toString()).contains(" Number of latency samples: 1"); + + runningSpan.end(); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/ZPageHttpHandlerTest.java b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/ZPageHttpHandlerTest.java new file mode 100644 index 000000000..2ac3f4195 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/ZPageHttpHandlerTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ZPageHttpHandler}. */ +public final class ZPageHttpHandlerTest { + @Test + void parseEmptyQuery() throws URISyntaxException, UnsupportedEncodingException { + URI uri = new URI("http://localhost:8000/tracez"); + String queryString = ""; + assertThat(ZPageHttpHandler.parseQueryString(uri.getRawQuery())).isEmpty(); + assertThat(ZPageHttpHandler.parseQueryString(queryString)).isEmpty(); + } + + @Test + void parseNormalQuery() throws URISyntaxException, UnsupportedEncodingException { + URI uri = + new URI("http://localhost:8000/tracez/tracez?zspanname=Test&ztype=1&zsubtype=5&noval"); + String queryString = "zspanname=Test&ztype=1&zsubtype=5&noval"; + assertThat(ZPageHttpHandler.parseQueryString(uri.getRawQuery())) + .containsOnly(entry("zspanname", "Test"), entry("ztype", "1"), entry("zsubtype", "5")); + assertThat(ZPageHttpHandler.parseQueryString(queryString)) + .containsOnly(entry("zspanname", "Test"), entry("ztype", "1"), entry("zsubtype", "5")); + } +} diff --git a/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/ZPageServerTest.java b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/ZPageServerTest.java new file mode 100644 index 000000000..c844a63f8 --- /dev/null +++ b/opentelemetry-java/sdk-extensions/zpages/src/test/java/io/opentelemetry/sdk/extension/zpages/ZPageServerTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.zpages; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class ZPageServerTest { + + @Test + void spanProcessor() { + assertThat(ZPageServer.getSpanProcessor()).isInstanceOf(TracezSpanProcessor.class); + } + + @Test + void traceConfigSupplier() { + assertThat(ZPageServer.getTracezTraceConfigSupplier()) + .isInstanceOf(TracezTraceConfigSupplier.class); + } + + @Test + void testSampler() { + assertThat(ZPageServer.getTracezSampler()).isInstanceOf(TracezTraceConfigSupplier.class); + } +} diff --git a/opentelemetry-java/sdk/all/README.md b/opentelemetry-java/sdk/all/README.md new file mode 100644 index 000000000..15b5c9492 --- /dev/null +++ b/opentelemetry-java/sdk/all/README.md @@ -0,0 +1,14 @@ +OpenTelemetry SDK +====================================================== + +[![Javadocs][javadoc-image]][javadoc-url] + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-sdk.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-sdk + +--- +#### Running micro-benchmarks +From the root of the repo run `./gradlew clean :opentelemetry-sdk:jmh` to run all the benchmarks +or run `./gradlew clean :opentelemetry-sdk:jmh -PjmhIncludeSingleClass=` +to run a specific benchmark class. + diff --git a/opentelemetry-java/sdk/all/build.gradle.kts b/opentelemetry-java/sdk/all/build.gradle.kts new file mode 100644 index 000000000..480e58492 --- /dev/null +++ b/opentelemetry-java/sdk/all/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + id("java-library") + id("maven-publish") + + id("me.champeau.jmh") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry SDK" +extra["moduleName"] = "io.opentelemetry.sdk" +base.archivesBaseName = "opentelemetry-sdk" + +dependencies { + api(project(":api:all")) + api(project(":sdk:common")) + api(project(":sdk:trace")) + + annotationProcessor("com.google.auto.value:auto-value") + + testAnnotationProcessor("com.google.auto.value:auto-value") + + testImplementation(project(":sdk:testing")) +} + +sourceSets { + main { + output.dir("build/generated/properties", "builtBy" to "generateVersionResource") + } +} + +tasks { + register("generateVersionResource") { + val propertiesDir = file("build/generated/properties/io/opentelemetry/sdk") + outputs.dir(propertiesDir) + + doLast { + File(propertiesDir, "version.properties").writeText("sdk.version=${project.version}") + } + } +} diff --git a/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/OpenTelemetrySdk.java b/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/OpenTelemetrySdk.java new file mode 100644 index 000000000..9bdc58be0 --- /dev/null +++ b/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/OpenTelemetrySdk.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import javax.annotation.concurrent.ThreadSafe; + +/** The SDK implementation of {@link OpenTelemetry}. */ +@ThreadSafe +public final class OpenTelemetrySdk implements OpenTelemetry { + private final ObfuscatedTracerProvider tracerProvider; + private final ContextPropagators propagators; + + OpenTelemetrySdk(ObfuscatedTracerProvider tracerProvider, ContextPropagators propagators) { + this.tracerProvider = tracerProvider; + this.propagators = propagators; + } + + /** + * Returns a new {@link OpenTelemetrySdkBuilder} for configuring an instance of {@linkplain + * OpenTelemetrySdk the OpenTelemetry SDK}. + */ + public static OpenTelemetrySdkBuilder builder() { + return new OpenTelemetrySdkBuilder(); + } + + @Override + public TracerProvider getTracerProvider() { + return tracerProvider; + } + + /** Returns the {@link SdkTracerProvider} for this {@link OpenTelemetrySdk}. */ + public SdkTracerProvider getSdkTracerProvider() { + return tracerProvider.unobfuscate(); + } + + @Override + public ContextPropagators getPropagators() { + return propagators; + } + + /** + * This class allows the SDK to unobfuscate an obfuscated static global provider. + * + *

    Static global providers are obfuscated when they are returned from the API to prevent users + * from casting them to their SDK specific implementation. For example, we do not want users to + * use patterns like {@code (TracerSdkProvider) OpenTelemetry.getGlobalTracerProvider()}. + */ + @ThreadSafe + // Visible for testing + static class ObfuscatedTracerProvider implements TracerProvider { + + private final SdkTracerProvider delegate; + + ObfuscatedTracerProvider(SdkTracerProvider delegate) { + this.delegate = delegate; + } + + @Override + public Tracer get(String instrumentationName) { + return delegate.get(instrumentationName); + } + + @Override + public Tracer get(String instrumentationName, String instrumentationVersion) { + return delegate.get(instrumentationName, instrumentationVersion); + } + + public SdkTracerProvider unobfuscate() { + return delegate; + } + } +} diff --git a/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/OpenTelemetrySdkBuilder.java b/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/OpenTelemetrySdkBuilder.java new file mode 100644 index 000000000..c666576f3 --- /dev/null +++ b/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/OpenTelemetrySdkBuilder.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk.ObfuscatedTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; + +/** A builder for configuring an {@link OpenTelemetrySdk}. */ +public final class OpenTelemetrySdkBuilder { + + private ContextPropagators propagators = ContextPropagators.noop(); + private SdkTracerProvider tracerProvider; + + /** + * Package protected to disallow direct initialization. + * + * @see OpenTelemetrySdk#builder() + */ + OpenTelemetrySdkBuilder() {} + + /** + * Sets the {@link SdkTracerProvider} to use. This can be used to configure tracing settings by + * returning the instance created by a {@link SdkTracerProviderBuilder}. + * + *

    If you use this method, it is assumed that you are providing a fully configured + * TracerSdkProvider, and other settings will be ignored. + * + *

    Note: the parameter passed in here must be a {@link SdkTracerProvider} instance. + * + * @param tracerProvider A {@link SdkTracerProvider} to use with this instance. + * @see SdkTracerProvider#builder() + */ + public OpenTelemetrySdkBuilder setTracerProvider(SdkTracerProvider tracerProvider) { + this.tracerProvider = tracerProvider; + return this; + } + + /** Sets the {@link ContextPropagators} to use. */ + public OpenTelemetrySdkBuilder setPropagators(ContextPropagators propagators) { + this.propagators = propagators; + return this; + } + + /** + * Returns a new {@link OpenTelemetrySdk} built with the configuration of this {@link + * OpenTelemetrySdkBuilder} and registers it as the global {@link + * io.opentelemetry.api.OpenTelemetry}. An exception will be thrown if this method is attempted to + * be called multiple times in the lifecycle of an application - ensure you have only one SDK for + * use as the global instance. If you need to configure multiple SDKs for tests, use {@link + * GlobalOpenTelemetry#resetForTest()} between them. + * + * @see GlobalOpenTelemetry + */ + public OpenTelemetrySdk buildAndRegisterGlobal() { + OpenTelemetrySdk sdk = build(); + GlobalOpenTelemetry.set(sdk); + return sdk; + } + + /** + * Returns a new {@link OpenTelemetrySdk} built with the configuration of this {@link + * OpenTelemetrySdkBuilder}. This SDK is not registered as the global {@link + * io.opentelemetry.api.OpenTelemetry}. It is recommended that you register one SDK using {@link + * OpenTelemetrySdkBuilder#buildAndRegisterGlobal()} for use by instrumentation that requires + * access to a global instance of {@link io.opentelemetry.api.OpenTelemetry}. + * + * @see GlobalOpenTelemetry + */ + public OpenTelemetrySdk build() { + if (tracerProvider == null) { + tracerProvider = SdkTracerProvider.builder().build(); + } + + return new OpenTelemetrySdk(new ObfuscatedTracerProvider(tracerProvider), propagators); + } +} diff --git a/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/package-info.java b/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/package-info.java new file mode 100644 index 000000000..f14dcf4b6 --- /dev/null +++ b/opentelemetry-java/sdk/all/src/main/java/io/opentelemetry/sdk/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The OpenTelemetry SDK. + * + * @see io.opentelemetry.sdk.OpenTelemetrySdk + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java b/opentelemetry-java/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java new file mode 100644 index 000000000..0627c7162 --- /dev/null +++ b/opentelemetry-java/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java @@ -0,0 +1,206 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; +import static org.mockito.Mockito.mock; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.OpenTelemetrySdk.ObfuscatedTracerProvider; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.IdGenerator; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class OpenTelemetrySdkTest { + + @Mock private SdkTracerProvider tracerProvider; + @Mock private ContextPropagators propagators; + @Mock private Clock clock; + + @AfterEach + void tearDown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + void testRegisterGlobal() { + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder().setPropagators(propagators).buildAndRegisterGlobal(); + assertThat(GlobalOpenTelemetry.get()).extracting("delegate").isSameAs(sdk); + assertThat(sdk.getTracerProvider().get("")) + .isSameAs(GlobalOpenTelemetry.getTracerProvider().get("")) + .isSameAs(GlobalOpenTelemetry.get().getTracer("")); + + assertThat(GlobalOpenTelemetry.getPropagators()) + .isSameAs(GlobalOpenTelemetry.get().getPropagators()) + .isSameAs(sdk.getPropagators()) + .isSameAs(propagators); + } + + @Test + void castingGlobalToSdkFails() { + OpenTelemetrySdk.builder().buildAndRegisterGlobal(); + + assertThatThrownBy( + () -> { + @SuppressWarnings("unused") + OpenTelemetrySdk shouldFail = (OpenTelemetrySdk) GlobalOpenTelemetry.get(); + }) + .isInstanceOf(ClassCastException.class); + } + + @Test + void testShortcutVersions() { + assertThat(GlobalOpenTelemetry.getTracer("testTracer1")) + .isEqualTo(GlobalOpenTelemetry.getTracerProvider().get("testTracer1")); + assertThat(GlobalOpenTelemetry.getTracer("testTracer2", "testVersion")) + .isEqualTo(GlobalOpenTelemetry.getTracerProvider().get("testTracer2", "testVersion")); + } + + @Test + void testBuilderDefaults() { + OpenTelemetrySdk openTelemetry = OpenTelemetrySdk.builder().build(); + assertThat(openTelemetry.getTracerProvider()) + .isInstanceOfSatisfying( + ObfuscatedTracerProvider.class, + obfuscatedTracerProvider -> + assertThat(obfuscatedTracerProvider.unobfuscate()) + .isInstanceOf(SdkTracerProvider.class)); + } + + @Test + void building() { + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .setPropagators(propagators) + .build(); + assertThat(((ObfuscatedTracerProvider) openTelemetry.getTracerProvider()).unobfuscate()) + .isEqualTo(tracerProvider); + assertThat(openTelemetry.getSdkTracerProvider()).isEqualTo(tracerProvider); + assertThat(openTelemetry.getPropagators()).isEqualTo(propagators); + } + + @Test + void testConfiguration_tracerSettings() { + Resource resource = Resource.create(Attributes.builder().put("cat", "meow").build()); + IdGenerator idGenerator = mock(IdGenerator.class); + SpanLimits spanLimits = SpanLimits.getDefault(); + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .setClock(clock) + .setResource(resource) + .setIdGenerator(idGenerator) + .setSpanLimits(spanLimits) + .build()) + .build(); + TracerProvider unobfuscatedTracerProvider = + ((ObfuscatedTracerProvider) openTelemetry.getTracerProvider()).unobfuscate(); + + assertThat(unobfuscatedTracerProvider) + .isInstanceOfSatisfying( + SdkTracerProvider.class, + sdkTracerProvider -> + assertThat(sdkTracerProvider.getSpanLimits()).isEqualTo(spanLimits)); + // Since TracerProvider is in a different package, the only alternative to this reflective + // approach would be to make the fields public for testing which is worse than this. + assertThat(unobfuscatedTracerProvider) + .extracting("sharedState") + .hasFieldOrPropertyWithValue("clock", clock) + .hasFieldOrPropertyWithValue("resource", resource) + .hasFieldOrPropertyWithValue("idGenerator", idGenerator); + } + + @Test + void testTracerProviderAccess() { + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + assertThat(openTelemetry.getTracerProvider()) + .asInstanceOf(type(ObfuscatedTracerProvider.class)) + .isNotNull() + .matches(obfuscated -> obfuscated.unobfuscate() == tracerProvider); + assertThat(openTelemetry.getSdkTracerProvider()).isNotNull(); + } + + // This is just a demonstration of maximum that one can do with OpenTelemetry configuration. + // Demonstrates how clear or confusing is SDK configuration + @Test + void fullOpenTelemetrySdkConfigurationDemo() { + SpanLimits newConfig = SpanLimits.builder().setMaxNumberOfAttributes(512).build(); + + OpenTelemetrySdkBuilder sdkBuilder = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .setSampler(mock(Sampler.class)) + .addSpanProcessor(SimpleSpanProcessor.create(mock(SpanExporter.class))) + .addSpanProcessor(SimpleSpanProcessor.create(mock(SpanExporter.class))) + .setClock(mock(Clock.class)) + .setIdGenerator(mock(IdGenerator.class)) + .setResource(Resource.empty()) + .setSpanLimits(newConfig) + .build()); + + sdkBuilder.build(); + } + + // This is just a demonstration of the bare minimal required configuration in order to get useful + // SDK. + // Demonstrates how clear or confusing is SDK configuration + @Test + void trivialOpenTelemetrySdkConfigurationDemo() { + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(mock(SpanExporter.class))) + .build()) + .setPropagators(ContextPropagators.create(mock(TextMapPropagator.class))) + .build(); + } + + // This is just a demonstration of two small but not trivial configurations. + // Demonstrates how clear or confusing is SDK configuration + @Test + void minimalOpenTelemetrySdkConfigurationDemo() { + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(mock(SpanExporter.class))) + .setSampler(mock(Sampler.class)) + .build()) + .setPropagators(ContextPropagators.create(mock(TextMapPropagator.class))) + .build(); + + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(mock(SpanExporter.class))) + .setSampler(mock(Sampler.class)) + .setIdGenerator(mock(IdGenerator.class)) + .build()) + .setPropagators(ContextPropagators.create(mock(TextMapPropagator.class))) + .build(); + } +} diff --git a/opentelemetry-java/sdk/all/src/test/java/io/opentelemetry/sdk/internal/SystemClockTest.java b/opentelemetry-java/sdk/all/src/test/java/io/opentelemetry/sdk/internal/SystemClockTest.java new file mode 100644 index 000000000..4dfe917a2 --- /dev/null +++ b/opentelemetry-java/sdk/all/src/test/java/io/opentelemetry/sdk/internal/SystemClockTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.JRE; + +// This test is placed in the all artifact instead of the common one so it uses the dependency jar +// instead of the classes directly, which allows verifying mrjar behavior. +class SystemClockTest { + + @EnabledOnJre(JRE.JAVA_8) + @Test + void millisPrecision() { + // If we test many times, we can be fairly sure we didn't just get lucky with having a rounded + // result on a higher than expected precision timestamp. + for (int i = 0; i < 100; i++) { + long now = SystemClock.getInstance().now(); + assertThat(now % 1000000).isZero(); + } + } + + @DisabledOnJre(JRE.JAVA_8) + @Test + void microsPrecision() { + // If we test many times, we can be fairly sure we get at least one timestamp that isn't + // coincidentally rounded to millis precision. + int numHasMicros = 0; + for (int i = 0; i < 100; i++) { + long now = SystemClock.getInstance().now(); + if (now % 1000000 != 0) { + numHasMicros++; + } + } + assertThat(numHasMicros).isNotZero(); + } +} diff --git a/opentelemetry-java/sdk/all/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/opentelemetry-java/sdk/all/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/opentelemetry-java/sdk/all/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/opentelemetry-java/sdk/build.gradle.kts b/opentelemetry-java/sdk/build.gradle.kts new file mode 100644 index 000000000..781be1893 --- /dev/null +++ b/opentelemetry-java/sdk/build.gradle.kts @@ -0,0 +1,10 @@ +subprojects { + // Workaround https://github.com/gradle/gradle/issues/847 + group = "io.opentelemetry.sdk" + val proj = this + plugins.withId("java") { + configure { + archivesBaseName = "opentelemetry-sdk-${proj.name}" + } + } +} diff --git a/opentelemetry-java/sdk/common/build.gradle.kts b/opentelemetry-java/sdk/common/build.gradle.kts new file mode 100644 index 000000000..d11974c4d --- /dev/null +++ b/opentelemetry-java/sdk/common/build.gradle.kts @@ -0,0 +1,90 @@ +plugins { + id("java-library") + id("maven-publish") + + id("ru.vyarus.animalsniffer") + id("org.unbroken-dome.test-sets") +} + +description = "OpenTelemetry SDK Common" +extra["moduleName"] = "io.opentelemetry.sdk.common" + +val mrJarVersions = listOf(9) + +testSets { + create("testResourceDisabledByProperty") + create("testResourceDisabledByEnv") +} + +dependencies { + api(project(":api:all")) + + implementation(project(":semconv")) + + annotationProcessor("com.google.auto.value:auto-value") + + testAnnotationProcessor("com.google.auto.value:auto-value") + + testImplementation(project(":sdk:testing")) + testImplementation(project(":sdk-extensions:resources")) + testImplementation("com.google.guava:guava-testlib") +} + +sourceSets { + main { + output.dir("build/generated/properties", "builtBy" to "generateVersionResource") + } +} + +tasks { + register("generateVersionResource") { + val propertiesDir = file("build/generated/properties/io/opentelemetry/sdk/common") + outputs.dir(propertiesDir) + + doLast { + File(propertiesDir, "version.properties").writeText("sdk.version=${project.version}") + } + } +} + +for (version in mrJarVersions) { + sourceSets { + create("java${version}") { + java { + setSrcDirs(listOf("src/main/java${version}")) + } + } + } + + tasks { + named("compileJava${version}Java") { + sourceCompatibility = "${version}" + targetCompatibility = "${version}" + options.release.set(version) + } + } + + configurations { + named("java${version}Implementation") { + extendsFrom(configurations["implementation"]) + } + } + + dependencies { + // Common to reference classes in main sourceset from Java 9 one (e.g., to return a common interface) + add("java${version}Implementation", files(sourceSets.main.get().output.classesDirs)) + } +} + +tasks { + withType(Jar::class) { + for (version in mrJarVersions) { + into("META-INF/versions/${version}") { + from(sourceSets["java${version}"].output) + } + } + manifest.attributes( + "Multi-Release" to "true" + ) + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/Clock.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/Clock.java new file mode 100644 index 000000000..d50b71104 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/Clock.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common; + +import javax.annotation.concurrent.ThreadSafe; + +/** Interface for getting the current time. */ +@ThreadSafe +public interface Clock { + /** + * Obtains the current epoch timestamp in nanos from this clock. + * + * @return the current epoch timestamp in nanos. + */ + long 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. + */ + long nanoTime(); +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/CompletableResultCode.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/CompletableResultCode.java new file mode 100644 index 000000000..5018b9f06 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/CompletableResultCode.java @@ -0,0 +1,163 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common; + +import io.opentelemetry.api.internal.GuardedBy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; + +/** + * This class models JDK 8's CompletableFuture to afford migration should Open Telemetry's SDK + * select JDK 8 or greater as a baseline, and also to offer familiarity to developers. + * + *

    The implementation of Export operations are often asynchronous in nature, hence the need to + * convey a result at a later time. CompletableResultCode facilitates this. + */ +public final class CompletableResultCode { + /** Returns a {@link CompletableResultCode} that has been completed successfully. */ + public static CompletableResultCode ofSuccess() { + return SUCCESS; + } + + /** Returns a {@link CompletableResultCode} that has been completed unsuccessfully. */ + public static CompletableResultCode ofFailure() { + return FAILURE; + } + + /** + * Returns a {@link CompletableResultCode} that completes after all the provided {@link + * CompletableResultCode}s complete. If any of the results fail, the result will be failed. + */ + public static CompletableResultCode ofAll(final Collection codes) { + if (codes.isEmpty()) { + return ofSuccess(); + } + final CompletableResultCode result = new CompletableResultCode(); + final AtomicInteger pending = new AtomicInteger(codes.size()); + final AtomicBoolean failed = new AtomicBoolean(); + for (final CompletableResultCode code : codes) { + code.whenComplete( + () -> { + if (!code.isSuccess()) { + failed.set(true); + } + if (pending.decrementAndGet() == 0) { + if (failed.get()) { + result.fail(); + } else { + result.succeed(); + } + } + }); + } + return result; + } + + private static final CompletableResultCode SUCCESS = new CompletableResultCode().succeed(); + private static final CompletableResultCode FAILURE = new CompletableResultCode().fail(); + + public CompletableResultCode() {} + + @Nullable + @GuardedBy("lock") + private Boolean succeeded = null; + + @GuardedBy("lock") + private final List completionActions = new ArrayList<>(); + + private final Object lock = new Object(); + + /** Complete this {@link CompletableResultCode} successfully if it is not already completed. */ + public CompletableResultCode succeed() { + synchronized (lock) { + if (succeeded == null) { + succeeded = true; + for (Runnable action : completionActions) { + action.run(); + } + } + } + return this; + } + + /** Complete this {@link CompletableResultCode} unsuccessfully if it is not already completed. */ + public CompletableResultCode fail() { + synchronized (lock) { + if (succeeded == null) { + succeeded = false; + for (Runnable action : completionActions) { + action.run(); + } + } + } + return this; + } + + /** + * Obtain the current state of completion. Generally call once completion is achieved via the + * thenRun method. + * + * @return the current state of completion + */ + public boolean isSuccess() { + synchronized (lock) { + return succeeded != null && succeeded; + } + } + + /** + * Perform an action on completion. Actions are guaranteed to be called only once. + * + * @param action the action to perform + * @return this completable result so that it may be further composed + */ + public CompletableResultCode whenComplete(Runnable action) { + synchronized (lock) { + if (succeeded != null) { + action.run(); + } else { + this.completionActions.add(action); + } + } + return this; + } + + /** Returns whether this {@link CompletableResultCode} has completed. */ + public boolean isDone() { + synchronized (lock) { + return succeeded != null; + } + } + + /** + * Waits up to the specified amount of time for this {@link CompletableResultCode} to complete. + * Even after this method returns, the result may not be complete yet - you should always check + * {@link #isSuccess()} or {@link #isDone()} after calling this method to determine the result. + * + * @return this {@link CompletableResultCode} + */ + public CompletableResultCode join(long timeout, TimeUnit unit) { + if (isDone()) { + return this; + } + final CountDownLatch latch = new CountDownLatch(1); + whenComplete(latch::countDown); + try { + if (!latch.await(timeout, unit)) { + return this; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return this; + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/EnvOrJvmProperties.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/EnvOrJvmProperties.java new file mode 100644 index 000000000..a6d6668e8 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/EnvOrJvmProperties.java @@ -0,0 +1,52 @@ +package io.opentelemetry.sdk.common; + +import java.util.ArrayList; +import java.util.List; + +public class EnvOrJvmProperties { + + private EnvOrJvmProperties(){} + + public static final HeraJavaagentConfig JVM_OTEL_RESOURCE_ATTRIBUTES = new HeraJavaagentConfig("otel.resource.attributes", HeraJavaagentConfigType.JVM); + public static final HeraJavaagentConfig JVM_OTEL_TRACES_EXPORTER = new HeraJavaagentConfig("otel.traces.exporter",HeraJavaagentConfigType.JVM,"log4j2"); + public static final HeraJavaagentConfig JVM_OTEL_METRICS_EXPORTER = new HeraJavaagentConfig("otel.metrics.exporter", HeraJavaagentConfigType.JVM,"prometheus"); + public static final HeraJavaagentConfig JVM_OTEL_NACOS_ADDRESS = new HeraJavaagentConfig("otel.exporter.prometheus.nacos.addr", HeraJavaagentConfigType.JVM, "nacos.hera-namespace:80"); + public static final HeraJavaagentConfig JVM_OTEL_EXCLUDE_CLASSES = new HeraJavaagentConfig("otel.javaagent.exclude-classes", HeraJavaagentConfigType.JVM,"com.dianping.cat.*"); + public static final HeraJavaagentConfig JVM_OTEL_EXPORTER_LOG_ISASYNC = new HeraJavaagentConfig("otel.exporter.log.isasync", HeraJavaagentConfigType.JVM,"true"); + public static final HeraJavaagentConfig JVM_OTEL_EXPORTER_LOG_PATH_PREFIX = new HeraJavaagentConfig("otel.exporter.log.pathprefix", HeraJavaagentConfigType.JVM,"/home/work/log/"); + public static final HeraJavaagentConfig JVM_OTEL_PROPAGATORS = new HeraJavaagentConfig("otel.propagators", HeraJavaagentConfigType.JVM,"tracecontext"); + public static final HeraJavaagentConfig JVM_OTEL_SERVICE_IP = new HeraJavaagentConfig("otel.service.ip", HeraJavaagentConfigType.JVM); + public static final HeraJavaagentConfig JVM_OTEL_METRICS_PROMETHEUS_PORT = new HeraJavaagentConfig("otel.metrics.prometheus.port", HeraJavaagentConfigType.JVM); + public static final HeraJavaagentConfig JVM_OTEL_EXPORTER_LOG_INTERVAL = new HeraJavaagentConfig("otel.exporter.log.interval", HeraJavaagentConfigType.JVM); + public static final HeraJavaagentConfig JVM_OTEL_EXPORTER_LOG_DELETE_AGE = new HeraJavaagentConfig("otel.exporter.log.delete.age", HeraJavaagentConfigType.JVM); + public static final HeraJavaagentConfig JVM_OTEL_MIONE_PROJECT_ENV_ID = new HeraJavaagentConfig("otel.mione.project.env.id", HeraJavaagentConfigType.JVM); + public static final HeraJavaagentConfig JVM_OTEL_MIONE_PROJECT_ENV_NAME = new HeraJavaagentConfig("otel.mione.project.env.name", HeraJavaagentConfigType.JVM); + public static final HeraJavaagentConfig ENV_HOST_IP = new HeraJavaagentConfig("host.ip", HeraJavaagentConfigType.ENV); + public static final HeraJavaagentConfig ENV_NODE_IP = new HeraJavaagentConfig("node.ip", HeraJavaagentConfigType.ENV); + public static final HeraJavaagentConfig ENV_MIONE_LOG_PATH = new HeraJavaagentConfig("MIONE_LOG_PATH", HeraJavaagentConfigType.ENV); + public static final HeraJavaagentConfig ENV_JAVAAGENT_PROMETHEUS_PORT = new HeraJavaagentConfig("JAVAAGENT_PROMETHEUS_PORT", HeraJavaagentConfigType.ENV, "55433"); + public static final HeraJavaagentConfig ENV_HERA_BUILD_K8S = new HeraJavaagentConfig("hera.buildin.k8s", HeraJavaagentConfigType.ENV,"1"); + public static final HeraJavaagentConfig MIONE_PROJECT_NAME = new HeraJavaagentConfig("MIONE_PROJECT_NAME", HeraJavaagentConfigType.ENV, "none"); + public static final HeraJavaagentConfig ENV_MIONE_PROJECT_ENV_NAME = new HeraJavaagentConfig("MIONE_PROJECT_ENV_NAME", HeraJavaagentConfigType.ENV, "default"); + public static final HeraJavaagentConfig ENV_MIONE_PROJECT_ENV_ID = new HeraJavaagentConfig("MIONE_PROJECT_ENV_ID", HeraJavaagentConfigType.ENV); + public static final List INIT_ENV_JVM_LIST = new ArrayList<>(); + + static { + INIT_ENV_JVM_LIST.add(JVM_OTEL_TRACES_EXPORTER); + INIT_ENV_JVM_LIST.add(JVM_OTEL_METRICS_EXPORTER); + INIT_ENV_JVM_LIST.add(JVM_OTEL_NACOS_ADDRESS); + INIT_ENV_JVM_LIST.add(JVM_OTEL_EXCLUDE_CLASSES); + INIT_ENV_JVM_LIST.add(JVM_OTEL_EXPORTER_LOG_ISASYNC); + INIT_ENV_JVM_LIST.add(JVM_OTEL_EXPORTER_LOG_PATH_PREFIX); + INIT_ENV_JVM_LIST.add(JVM_OTEL_PROPAGATORS); + + INIT_ENV_JVM_LIST.add(ENV_HOST_IP); + INIT_ENV_JVM_LIST.add(ENV_NODE_IP); + INIT_ENV_JVM_LIST.add(ENV_MIONE_LOG_PATH); + INIT_ENV_JVM_LIST.add(ENV_JAVAAGENT_PROMETHEUS_PORT); + INIT_ENV_JVM_LIST.add(ENV_HERA_BUILD_K8S); + INIT_ENV_JVM_LIST.add(ENV_MIONE_PROJECT_ENV_NAME); + INIT_ENV_JVM_LIST.add(ENV_MIONE_PROJECT_ENV_ID); + INIT_ENV_JVM_LIST.add(MIONE_PROJECT_NAME); + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/HeraJavaagentConfig.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/HeraJavaagentConfig.java new file mode 100644 index 000000000..a4b3233a9 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/HeraJavaagentConfig.java @@ -0,0 +1,45 @@ +package io.opentelemetry.sdk.common; + +public class HeraJavaagentConfig { + + private String key; + + private String type; + + private String defaultValue; + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public HeraJavaagentConfig(String key, String type){ + this.key = key; + this.type = type; + } + + public HeraJavaagentConfig(String key, String type, String defaultValue){ + this.key = key; + this.type = type; + this.defaultValue = defaultValue; + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/HeraJavaagentConfigType.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/HeraJavaagentConfigType.java new file mode 100644 index 000000000..45fb69e0a --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/HeraJavaagentConfigType.java @@ -0,0 +1,10 @@ +package io.opentelemetry.sdk.common; + +public class HeraJavaagentConfigType { + + private HeraJavaagentConfigType() {} + + public static final String JVM = "jvm"; + + public static final String ENV = "env"; +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/InstrumentationLibraryInfo.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/InstrumentationLibraryInfo.java new file mode 100644 index 000000000..364aa36f3 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/InstrumentationLibraryInfo.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common; + +import static java.util.Objects.requireNonNull; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.trace.Tracer; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Holds information about the instrumentation library specified when creating an instance of {@link + * Tracer} using the Tracer Provider. + */ +@AutoValue +@Immutable +public abstract class InstrumentationLibraryInfo { + private static final InstrumentationLibraryInfo EMPTY = create("", null); + + /** + * Creates a new instance of {@link InstrumentationLibraryInfo}. + * + * @param name name of the instrumentation library (e.g., "io.opentelemetry.contrib.mongodb"), + * must not be null + * @param version version of the instrumentation library (e.g., "1.0.0"), might be null + * @return the new instance + */ + public static InstrumentationLibraryInfo create(String name, @Nullable String version) { + requireNonNull(name, "name"); + return new AutoValue_InstrumentationLibraryInfo(name, version); + } + + /** + * Returns an "empty" {@code InstrumentationLibraryInfo}. + * + * @return an "empty" {@code InstrumentationLibraryInfo}. + */ + public static InstrumentationLibraryInfo empty() { + return EMPTY; + } + + /** + * Returns the name of the instrumentation library. + * + * @return the name of the instrumentation library. + */ + public abstract String getName(); + + /** + * Returns the version of the instrumentation library, or {@code null} if not available. + * + * @return the version of the instrumentation library, or {@code null} if not available. + */ + @Nullable + public abstract String getVersion(); + + // Package protected ctor to avoid others to extend this class. + InstrumentationLibraryInfo() {} +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/SystemCommon.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/SystemCommon.java new file mode 100644 index 000000000..10edd0879 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/SystemCommon.java @@ -0,0 +1,13 @@ +package io.opentelemetry.sdk.common; + +public final class SystemCommon { + public static String getEnvOrProperties(String key) { + String result = System.getenv(key); + if (result == null) { + result = System.getProperty(key); + } + return result; + } + + private SystemCommon(){} +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/package-info.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/package-info.java new file mode 100644 index 000000000..da5199deb --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Common utilities used by SDK exporters. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.common.export; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/package-info.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/package-info.java new file mode 100644 index 000000000..9182e0448 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/common/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Common utilities used by all SDK components. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.common; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ComponentRegistry.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ComponentRegistry.java new file mode 100644 index 000000000..4233068f4 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ComponentRegistry.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * Base class for all the provider classes (TracerProvider, MeterProvider, etc.). + * + * @param the type of the registered value. + */ +public final class ComponentRegistry { + + private final ConcurrentMap registry = new ConcurrentHashMap<>(); + private final Function factory; + + public ComponentRegistry(Function factory) { + this.factory = factory; + } + + /** + * Returns the registered value associated with this name and {@code null} version if any, + * otherwise creates a new instance and associates it with the given name and {@code null} + * version. + * + * @param instrumentationName the name of the instrumentation library. + * @return the registered value associated with this name and {@code null} version. + */ + public final V get(String instrumentationName) { + return get(instrumentationName, null); + } + + /** + * Returns the registered value associated with this name and version if any, otherwise creates a + * new instance and associates it with the given name and version. + * + * @param instrumentationName the name of the instrumentation library. + * @param instrumentationVersion the version of the instrumentation library. + * @return the registered value associated with this name and version. + */ + public final V get(String instrumentationName, @Nullable String instrumentationVersion) { + InstrumentationLibraryInfo instrumentationLibraryInfo = + InstrumentationLibraryInfo.create(instrumentationName, instrumentationVersion); + + // Optimistic lookup, before creating the new component. + V component = registry.get(instrumentationLibraryInfo); + if (component != null) { + return component; + } + + V newComponent = factory.apply(instrumentationLibraryInfo); + V oldComponent = registry.putIfAbsent(instrumentationLibraryInfo, newComponent); + return oldComponent != null ? oldComponent : newComponent; + } + + /** + * Returns a {@code Collection} view of the registered components. + * + * @return a {@code Collection} view of the registered components. + */ + public final Collection getComponents() { + return Collections.unmodifiableCollection(new ArrayList<>(registry.values())); + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/CurrentJavaVersionSpecific.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/CurrentJavaVersionSpecific.java new file mode 100644 index 000000000..f4f5e07c9 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/CurrentJavaVersionSpecific.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2019 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.sdk.internal; + +final class CurrentJavaVersionSpecific { + + static JavaVersionSpecific get() { + return new JavaVersionSpecific(); + } + + private CurrentJavaVersionSpecific() {} +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/DaemonThreadFactory.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/DaemonThreadFactory.java new file mode 100644 index 000000000..af66e8be2 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/DaemonThreadFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A {@link ThreadFactory} that delegates to {@code Executors.defaultThreadFactory()} and marks all + * threads as daemon. + */ +public final class DaemonThreadFactory implements ThreadFactory { + private final String namePrefix; + private final AtomicInteger counter = new AtomicInteger(); + + public DaemonThreadFactory(String namePrefix) { + this.namePrefix = namePrefix; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread t = Executors.defaultThreadFactory().newThread(runnable); + try { + t.setDaemon(true); + t.setName(namePrefix + "-" + counter.incrementAndGet()); + } catch (SecurityException e) { + // Well, we tried. + } + return t; + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/JavaVersionSpecific.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/JavaVersionSpecific.java new file mode 100644 index 000000000..f0307b728 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/JavaVersionSpecific.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2019 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.sdk.internal; + +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Contains APIs that are implemented differently based on the version of Java being run. This class + * implements the default, using Java 8 APIs, the minimum version supported by OpenTelemetry. All + * implementations in this class must be forwards-compatible on all Java versions because this class + * may be used outside the multi-release JAR, e.g., in testing or when a user shades without + * creating their own multi-release JAR. + */ +class JavaVersionSpecific { + + private static final Logger logger = Logger.getLogger(JavaVersionSpecific.class.getName()); + + private static final JavaVersionSpecific CURRENT = CurrentJavaVersionSpecific.get(); + + static { + if (CURRENT.getClass() != JavaVersionSpecific.class) { + logger.log(Level.FINE, "Using the APIs optimized for: {0}", CURRENT.name()); + } + } + + /** Returns the {@link JavaVersionSpecific} for the current version of Java. */ + static JavaVersionSpecific get() { + return CURRENT; + } + + String name() { + return "Java 8"; + } + + /** Returns the number of nanoseconds since the epoch (00:00:00, 01-Jan-1970, GMT). */ + long currentTimeNanos() { + return TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/MonotonicClock.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/MonotonicClock.java new file mode 100644 index 000000000..a6b3f7f68 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/MonotonicClock.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.sdk.common.Clock; +import javax.annotation.concurrent.Immutable; + +/** + * This class provides a mechanism for calculating the epoch time using {@link System#nanoTime()} + * and a reference epoch timestamp. + * + *

    This is needed because Java has millisecond granularity for epoch times and tracing events are + * recorded more often. + * + *

    This clock needs to be re-created periodically in order to re-sync with the kernel clock, and + * it is not recommended to use only one instance for a very long period of time. + */ +@Immutable +public final class MonotonicClock implements Clock { + private final Clock clock; + private final long epochNanos; + private final long nanoTime; + + private MonotonicClock(Clock clock, long epochNanos, long nanoTime) { + this.clock = clock; + this.epochNanos = epochNanos; + this.nanoTime = nanoTime; + } + + /** + * Returns a {@code MonotonicClock}. + * + * @param clock the {@code Clock} to be used to read the current epoch time and nanoTime. + * @return a {@code MonotonicClock}. + */ + public static MonotonicClock create(Clock clock) { + return new MonotonicClock(clock, clock.now(), clock.nanoTime()); + } + + /** + * Returns the current epoch timestamp in nanos calculated using {@link System#nanoTime()} since + * the reference time read in the constructor. + * + * @return the current epoch timestamp in nanos. + */ + @Override + public long now() { + long deltaNanos = clock.nanoTime() - this.nanoTime; + return epochNanos + deltaNanos; + } + + @Override + public long nanoTime() { + return clock.nanoTime(); + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/RateLimiter.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/RateLimiter.java new file mode 100644 index 000000000..94ce8accd --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/RateLimiter.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.sdk.common.Clock; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class was taken from Jaeger java client. + * https://github.com/jaegertracing/jaeger-client-java/blob/master/jaeger-core/src/main/java/io/jaegertracing/internal/samplers/RateLimitingSampler.java + * + *

    Variables have been renamed for clarity. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class RateLimiter { + private final Clock clock; + private final double creditsPerNanosecond; + private final long maxBalance; // max balance in nano ticks + private final AtomicLong currentBalance; // last op nano time less remaining balance + + /** + * Create a new RateLimiter with the provided parameters. + * + * @param creditsPerSecond How many credits to accrue per second. + * @param maxBalance The maximum balance that the limiter can hold, which corresponds to the rate + * that is being limited to. + * @param clock An implementation of the {@link Clock} interface. + */ + public RateLimiter(double creditsPerSecond, double maxBalance, Clock clock) { + this.clock = clock; + this.creditsPerNanosecond = creditsPerSecond / 1.0e9; + this.maxBalance = (long) (maxBalance / creditsPerNanosecond); + this.currentBalance = new AtomicLong(clock.nanoTime() - this.maxBalance); + } + + /** + * Check to see if the provided cost can be spent within the current limits. Will deduct the cost + * from the current balance if it can be spent. + */ + public boolean trySpend(double itemCost) { + long cost = (long) (itemCost / creditsPerNanosecond); + long currentNanos; + long currentBalanceNanos; + long availableBalanceAfterWithdrawal; + do { + currentBalanceNanos = this.currentBalance.get(); + currentNanos = clock.nanoTime(); + long currentAvailableBalance = currentNanos - currentBalanceNanos; + if (currentAvailableBalance > maxBalance) { + currentAvailableBalance = maxBalance; + } + availableBalanceAfterWithdrawal = currentAvailableBalance - cost; + if (availableBalanceAfterWithdrawal < 0) { + return false; + } + } while (!this.currentBalance.compareAndSet( + currentBalanceNanos, currentNanos - availableBalanceAfterWithdrawal)); + return true; + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/SystemClock.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/SystemClock.java new file mode 100644 index 000000000..aa0ba3661 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/SystemClock.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.sdk.common.Clock; +import javax.annotation.concurrent.ThreadSafe; + +/** A {@link Clock} that uses {@link System#currentTimeMillis()} and {@link System#nanoTime()}. */ +@ThreadSafe +public final class SystemClock implements Clock { + + private static final SystemClock INSTANCE = new SystemClock(); + + private SystemClock() {} + + /** + * Returns a {@code MillisClock}. + * + * @return a {@code MillisClock}. + */ + public static SystemClock getInstance() { + return INSTANCE; + } + + @Override + public long now() { + return JavaVersionSpecific.get().currentTimeNanos(); + } + + @Override + public long nanoTime() { + return System.nanoTime(); + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/TestClock.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/TestClock.java new file mode 100644 index 000000000..7265396c9 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/TestClock.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.sdk.common.Clock; +import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.ThreadSafe; + +/** A mutable {@link Clock} that allows the time to be set for testing. */ +@ThreadSafe +public final class TestClock implements Clock { + + @GuardedBy("this") + private long currentEpochNanos; + + private TestClock(long epochNanos) { + currentEpochNanos = epochNanos; + } + + /** + * Creates a clock initialized to a constant non-zero time. + * + * @return a clock initialized to a constant non-zero time. + */ + public static TestClock create() { + // Set Time to Tuesday, May 7, 2019 12:00:00 AM GMT-07:00 DST + return create(TimeUnit.MILLISECONDS.toNanos(1_557_212_400_000L)); + } + + /** + * Creates a clock with the given time. + * + * @param epochNanos the initial time in nanos since epoch. + * @return a new {@code TestClock} with the given time. + */ + public static TestClock create(long epochNanos) { + return new TestClock(epochNanos); + } + + /** + * Sets the time. + * + * @param epochNanos the new time. + */ + public synchronized void setTime(long epochNanos) { + currentEpochNanos = epochNanos; + } + + /** + * Advances the time by millis and mutates this instance. + * + * @param millis the increase in time. + */ + public synchronized void advanceMillis(long millis) { + long nanos = TimeUnit.MILLISECONDS.toNanos(millis); + currentEpochNanos += nanos; + } + + /** + * Advances the time by nanos and mutates this instance. + * + * @param nanos the increase in time. + */ + public synchronized void advanceNanos(long nanos) { + currentEpochNanos += nanos; + } + + @Override + public synchronized long now() { + return currentEpochNanos; + } + + @Override + public synchronized long nanoTime() { + return currentEpochNanos; + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ThrottlingLogger.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ThrottlingLogger.java new file mode 100644 index 000000000..ad2b4ed1a --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ThrottlingLogger.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static java.util.concurrent.TimeUnit.MINUTES; + +import io.opentelemetry.sdk.common.Clock; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** Will limit the number of log messages emitted, so as not to spam when problems are happening. */ +public class ThrottlingLogger { + private static final double RATE_LIMIT = 5; + private static final double THROTTLED_RATE_LIMIT = 1; + private static final TimeUnit rateTimeUnit = MINUTES; + + private final Logger delegate; + private final AtomicBoolean throttled = new AtomicBoolean(false); + private final RateLimiter fastRateLimiter; + private final RateLimiter throttledRateLimiter; + + /** Create a new logger which will enforce a max number of messages per minute. */ + public ThrottlingLogger(Logger delegate) { + this(delegate, SystemClock.getInstance()); + } + + // visible for testing + ThrottlingLogger(Logger delegate, Clock clock) { + this.delegate = delegate; + this.fastRateLimiter = + new RateLimiter(RATE_LIMIT / rateTimeUnit.toSeconds(1), RATE_LIMIT, clock); + this.throttledRateLimiter = + new RateLimiter(RATE_LIMIT / rateTimeUnit.toSeconds(1), THROTTLED_RATE_LIMIT, clock); + } + + /** Log a message at the given level. */ + public void log(Level level, String message) { + log(level, message, null); + } + + /** Log a message at the given level with a throwable. */ + public void log(Level level, String message, @Nullable Throwable throwable) { + if (!isLoggable(level)) { + return; + } + if (throttled.get()) { + if (throttledRateLimiter.trySpend(1.0)) { + doLog(level, message, throwable); + } + return; + } + + if (fastRateLimiter.trySpend(1.0)) { + doLog(level, message, throwable); + return; + } + + if (throttled.compareAndSet(false, true)) { + // spend the balance in the throttled one, so that it starts at zero. + throttledRateLimiter.trySpend(THROTTLED_RATE_LIMIT); + delegate.log( + level, "Too many log messages detected. Will only log once per minute from now on."); + doLog(level, message, throwable); + } + } + + private void doLog(Level level, String message, @Nullable Throwable throwable) { + if (throwable != null) { + delegate.log(level, message, throwable); + } else { + delegate.log(level, message); + } + } + + /** + * Returns whether the current wrapped logger is set to log at the given level. + * + * @return true if the logger set to log at the requested level. + */ + public boolean isLoggable(Level level) { + return delegate.isLoggable(level); + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/package-info.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/package-info.java new file mode 100644 index 000000000..ca31e897e --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/internal/package-info.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Interfaces and implementations that are internal to OpenTelemetry. + * + *

    All the content under this package and its subpackages are considered not part of the public + * API, and must not be used by users of the OpenTelemetry library. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.internal; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java new file mode 100644 index 000000000..6c92aefec --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java @@ -0,0 +1,190 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources; + +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.TELEMETRY_SDK_LANGUAGE; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.TELEMETRY_SDK_NAME; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.TELEMETRY_SDK_VERSION; + +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.internal.Utils; +import java.util.Objects; +import java.util.Properties; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * {@link Resource} represents a resource, which capture identifying information about the entities + * for which signals (stats or traces) are reported. + */ +@Immutable +@AutoValue +public abstract class Resource { + + private static final int MAX_LENGTH = 255; + private static final String ERROR_MESSAGE_INVALID_CHARS = + " should be a ASCII string with a length greater than 0 and not exceed " + + MAX_LENGTH + + " characters."; + private static final String ERROR_MESSAGE_INVALID_VALUE = + " should be a ASCII string with a length not exceed " + MAX_LENGTH + " characters."; + private static final Resource EMPTY = create(Attributes.empty()); + private static final Resource TELEMETRY_SDK; + + /** + * The MANDATORY Resource instance contains the mandatory attributes that must be used if they are + * not provided by the Resource that is given to an SDK signal provider. + */ + private static final Resource MANDATORY = + create(Attributes.of(SERVICE_NAME, "unknown_service:java")); + + static { + TELEMETRY_SDK = + create( + Attributes.builder() + .put(TELEMETRY_SDK_NAME, "opentelemetry") + .put(TELEMETRY_SDK_LANGUAGE, "java") + .put(TELEMETRY_SDK_VERSION, readVersion()) + .build()); + } + + private static final Resource DEFAULT = MANDATORY.merge(TELEMETRY_SDK); + + /** + * Returns the default {@link Resource}. This resource contains the default attributes provided by + * the SDK. + * + * @return a {@code Resource}. + */ + public static Resource getDefault() { + return DEFAULT; + } + + /** + * Returns an empty {@link Resource}. When creating a {@link Resource}, it is strongly recommended + * to start with {@link Resource#getDefault()} instead of this method to include SDK required + * attributes. + * + * @return an empty {@code Resource}. + */ + public static Resource empty() { + return EMPTY; + } + + /** + * Returns a {@link Resource}. + * + * @param attributes a map of attributes that describe the resource. + * @return a {@code Resource}. + * @throws NullPointerException if {@code attributes} is null. + * @throws IllegalArgumentException if attribute key or attribute value is not a valid printable + * ASCII string or exceed {@link #MAX_LENGTH} characters. + */ + public static Resource create(Attributes attributes) { + checkAttributes(Objects.requireNonNull(attributes, "attributes")); + return new AutoValue_Resource(attributes); + } + + @Nullable + private static String readVersion() { + Properties properties = new Properties(); + try { + properties.load( + Resource.class.getResourceAsStream("/io/opentelemetry/sdk/version.properties")); + } catch (Exception e) { + // we left the attribute empty + return "unknown"; + } + return properties.getProperty("sdk.version"); + } + + Resource() {} + + /** + * Returns a map of attributes that describe the resource. + * + * @return a map of attributes. + */ + public abstract Attributes getAttributes(); + + @Memoized + @Override + public abstract int hashCode(); + + /** + * Returns a new, merged {@link Resource} by merging the current {@code Resource} with the {@code + * other} {@code Resource}. In case of a collision, the "other" {@code Resource} takes precedence. + * + * @param other the {@code Resource} that will be merged with {@code this}. + * @return the newly merged {@code Resource}. + */ + public Resource merge(@Nullable Resource other) { + if (other == null) { + return this; + } + + AttributesBuilder attrBuilder = Attributes.builder(); + attrBuilder.putAll(this.getAttributes()); + attrBuilder.putAll(other.getAttributes()); + return new AutoValue_Resource(attrBuilder.build()); + } + + private static void checkAttributes(Attributes attributes) { + attributes.forEach( + (key, value) -> { + Utils.checkArgument( + isValidAndNotEmpty(key), "Attribute key" + ERROR_MESSAGE_INVALID_CHARS); + Objects.requireNonNull(value, "Attribute value" + ERROR_MESSAGE_INVALID_VALUE); + }); + } + + /** + * Determines whether the given {@code String} is a valid printable ASCII string with a length not + * exceed {@link #MAX_LENGTH} characters. + * + * @param name the name to be validated. + * @return whether the name is valid. + */ + private static boolean isValid(String name) { + return name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name); + } + + /** + * Determines whether the given {@code String} is a valid printable ASCII string with a length + * greater than 0 and not exceed {@link #MAX_LENGTH} characters. + * + * @param name the name to be validated. + * @return whether the name is valid. + */ + private static boolean isValidAndNotEmpty(AttributeKey name) { + return !name.getKey().isEmpty() && isValid(name.getKey()); + } + + /** + * Returns a new {@link ResourceBuilder} instance for creating arbitrary {@link Resource}. + * + * @since 1.1.0 + */ + public static ResourceBuilder builder() { + return new ResourceBuilder(); + } + + /** + * Returns a new {@link ResourceBuilder} instance populated with the data of this {@link + * Resource}. + * + * @since 1.1.0 + */ + public ResourceBuilder toBuilder() { + return builder().putAll(this); + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java new file mode 100644 index 000000000..ad2603c46 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java @@ -0,0 +1,178 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +/** + * A builder of {@link Resource} that allows to add key-value pairs and copy attributes from other + * {@link Attributes} or {@link Resource}. + * + * @since 1.1.0 + */ +public class ResourceBuilder { + + private final AttributesBuilder attributesBuilder = Attributes.builder(); + + /** + * Puts a String attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + public ResourceBuilder put(String key, String value) { + if (key != null && value != null) { + attributesBuilder.put(key, value); + } + return this; + } + + /** + * Puts a long attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + public ResourceBuilder put(String key, long value) { + if (key != null) { + attributesBuilder.put(key, value); + } + return this; + } + + /** + * Puts a double attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + public ResourceBuilder put(String key, double value) { + if (key != null) { + attributesBuilder.put(key, value); + } + return this; + } + + /** + * Puts a boolean attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + public ResourceBuilder put(String key, boolean value) { + if (key != null) { + attributesBuilder.put(key, value); + } + return this; + } + + /** + * Puts a String array attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + public ResourceBuilder put(String key, String... values) { + if (key != null && values != null) { + attributesBuilder.put(key, values); + } + return this; + } + + /** + * Puts a Long array attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + public ResourceBuilder put(String key, long... values) { + if (key != null && values != null) { + attributesBuilder.put(key, values); + } + return this; + } + + /** + * Puts a Double array attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + public ResourceBuilder put(String key, double... values) { + if (key != null && values != null) { + attributesBuilder.put(key, values); + } + return this; + } + + /** + * Puts a Boolean array attribute into this. + * + *

    Note: It is strongly recommended to use {@link #put(AttributeKey, Object)}, and pre-allocate + * your keys, if possible. + * + * @return this Builder + */ + public ResourceBuilder put(String key, boolean... values) { + if (key != null && values != null) { + attributesBuilder.put(key, values); + } + return this; + } + + /** Puts a {@link AttributeKey} with associated value into this. */ + public ResourceBuilder put(AttributeKey key, T value) { + if (key != null && key.getKey() != null && key.getKey().length() > 0 && value != null) { + attributesBuilder.put(key, value); + } + return this; + } + + /** Puts a {@link AttributeKey} with associated value into this. */ + public ResourceBuilder put(AttributeKey key, int value) { + if (key != null && key.getKey() != null) { + attributesBuilder.put(key, value); + } + return this; + } + + /** Puts all {@link Attributes} into this. */ + public ResourceBuilder putAll(Attributes attributes) { + if (attributes != null) { + attributesBuilder.putAll(attributes); + } + return this; + } + + /** Puts all attributes from {@link Resource} into this. */ + public ResourceBuilder putAll(Resource resource) { + if (resource != null) { + attributesBuilder.putAll(resource.getAttributes()); + } + return this; + } + + /** Create the {@link Resource} from this. */ + public Resource build() { + return Resource.create(attributesBuilder.build()); + } +} diff --git a/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/package-info.java b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/package-info.java new file mode 100644 index 000000000..f905c4c3f --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java/io/opentelemetry/sdk/resources/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * API for resource information population. + * + *

    The resources library primarily defines a type "Resource" that captures information about the + * entity for which stats or traces are recorded. For example, metrics exposed by a Kubernetes + * container can be linked to a resource that specifies the cluster, namespace, pod, and container + * name. + * + *

    Attribute keys, and attribute values MUST contain only printable ASCII (codes between 32 and + * 126, inclusive) and less than 256 characters. Type and attribute keys MUST have a length greater + * than zero. They SHOULD start with a domain name and separate hierarchies with / characters, e.g. + * k8s.io/namespace/name. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.resources; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/common/src/main/java9/io/opentelemetry/sdk/internal/CurrentJavaVersionSpecific.java b/opentelemetry-java/sdk/common/src/main/java9/io/opentelemetry/sdk/internal/CurrentJavaVersionSpecific.java new file mode 100644 index 000000000..08dfe8eab --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java9/io/opentelemetry/sdk/internal/CurrentJavaVersionSpecific.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2019 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.sdk.internal; + +final class CurrentJavaVersionSpecific { + + static JavaVersionSpecific get() { + return new Java9VersionSpecific(); + } + + private CurrentJavaVersionSpecific() {} +} diff --git a/opentelemetry-java/sdk/common/src/main/java9/io/opentelemetry/sdk/internal/Java9VersionSpecific.java b/opentelemetry-java/sdk/common/src/main/java9/io/opentelemetry/sdk/internal/Java9VersionSpecific.java new file mode 100644 index 000000000..3aaf01b60 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/main/java9/io/opentelemetry/sdk/internal/Java9VersionSpecific.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright 2019 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.opentelemetry.sdk.internal; + +import java.time.Clock; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +/** Implementation of {@link JavaVersionSpecific} using Java 9 APIs. */ +class Java9VersionSpecific extends JavaVersionSpecific { + + @Override + String name() { + return "Java 9+"; + } + + @Override + public long currentTimeNanos() { + final Instant now = Clock.systemUTC().instant(); + return TimeUnit.SECONDS.toNanos(now.getEpochSecond()) + now.getNano(); + } +} diff --git a/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/common/CompletableResultCodeTest.java b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/common/CompletableResultCodeTest.java new file mode 100644 index 000000000..544777430 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/common/CompletableResultCodeTest.java @@ -0,0 +1,190 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.google.common.util.concurrent.Uninterruptibles; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +class CompletableResultCodeTest { + + @Test + void ofSuccess() { + assertThat(CompletableResultCode.ofSuccess().isSuccess()).isTrue(); + } + + @Test + void ofFailure() { + assertThat(CompletableResultCode.ofFailure().isSuccess()).isFalse(); + } + + @Test + void succeed() throws InterruptedException { + CompletableResultCode resultCode = new CompletableResultCode(); + + CountDownLatch completions = new CountDownLatch(1); + + new Thread(resultCode::succeed).start(); + + resultCode.whenComplete(completions::countDown); + + completions.await(3, TimeUnit.SECONDS); + + assertThat(resultCode.isSuccess()).isTrue(); + } + + @Test + void fail() throws InterruptedException { + CompletableResultCode resultCode = new CompletableResultCode(); + + CountDownLatch completions = new CountDownLatch(1); + + new Thread(resultCode::fail).start(); + + resultCode.whenComplete(completions::countDown); + + completions.await(3, TimeUnit.SECONDS); + + assertThat(resultCode.isSuccess()).isFalse(); + } + + @Test + void whenDoublyCompleteSuccessfully() throws InterruptedException { + CompletableResultCode resultCode = new CompletableResultCode(); + + CountDownLatch completions = new CountDownLatch(2); + + new Thread(resultCode::succeed).start(); + + resultCode.whenComplete(completions::countDown).whenComplete(completions::countDown); + + completions.await(3, TimeUnit.SECONDS); + + assertThat(resultCode.isSuccess()).isTrue(); + } + + @Test + void whenDoublyNestedComplete() throws InterruptedException { + CompletableResultCode resultCode = new CompletableResultCode(); + + CountDownLatch completions = new CountDownLatch(2); + + new Thread(resultCode::succeed).start(); + + resultCode.whenComplete( + () -> { + completions.countDown(); + + resultCode.whenComplete(completions::countDown); + }); + + completions.await(3, TimeUnit.SECONDS); + + assertThat(resultCode.isSuccess()).isTrue(); + } + + @Test + void whenSuccessThenFailure() throws InterruptedException { + CompletableResultCode resultCode = new CompletableResultCode(); + + CountDownLatch completions = new CountDownLatch(1); + + new Thread(() -> resultCode.succeed().fail()).start(); + + resultCode.whenComplete(completions::countDown); + + completions.await(3, TimeUnit.SECONDS); + + assertThat(resultCode.isSuccess()).isTrue(); + } + + @Test + void isDone() { + CompletableResultCode result = new CompletableResultCode(); + assertThat(result.isDone()).isFalse(); + result.fail(); + assertThat(result.isDone()).isTrue(); + } + + @Test + void ofAll() { + CompletableResultCode result1 = new CompletableResultCode(); + CompletableResultCode result2 = new CompletableResultCode(); + CompletableResultCode result3 = new CompletableResultCode(); + + CompletableResultCode all = + CompletableResultCode.ofAll(Arrays.asList(result1, result2, result3)); + assertThat(all.isDone()).isFalse(); + result1.succeed(); + assertThat(all.isDone()).isFalse(); + result2.succeed(); + assertThat(all.isDone()).isFalse(); + result3.succeed(); + assertThat(all.isDone()).isTrue(); + assertThat(all.isSuccess()).isTrue(); + + assertThat(CompletableResultCode.ofAll(Collections.emptyList()).isSuccess()).isTrue(); + } + + @Test + void ofAllWithFailure() { + assertThat( + CompletableResultCode.ofAll( + Arrays.asList( + CompletableResultCode.ofSuccess(), + CompletableResultCode.ofFailure(), + CompletableResultCode.ofSuccess())) + .isSuccess()) + .isFalse(); + } + + @Test + void join() { + CompletableResultCode result = new CompletableResultCode(); + new Thread( + () -> { + Uninterruptibles.sleepUninterruptibly(Duration.ofMillis(50)); + result.succeed(); + }) + .start(); + assertThat(result.join(10, TimeUnit.SECONDS).isSuccess()).isTrue(); + // Already completed, synchronous call. + assertThat(result.join(0, TimeUnit.NANOSECONDS).isSuccess()).isTrue(); + } + + @Test + void joinTimesOut() { + CompletableResultCode result = new CompletableResultCode(); + assertThat(result.join(1, TimeUnit.MILLISECONDS).isSuccess()).isFalse(); + assertThat(result.isDone()).isFalse(); + } + + @Test + void joinInterrupted() { + CompletableResultCode result = new CompletableResultCode(); + AtomicReference interrupted = new AtomicReference<>(); + Thread thread = + new Thread( + () -> { + result.join(10, TimeUnit.SECONDS); + interrupted.set(Thread.currentThread().isInterrupted()); + }); + thread.start(); + thread.interrupt(); + // Different thread so wait a bit for result to be propagated. + await().untilAsserted(() -> assertThat(interrupted).hasValue(true)); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.isDone()).isFalse(); + } +} diff --git a/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/common/InstrumentationLibraryInfoTest.java b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/common/InstrumentationLibraryInfoTest.java new file mode 100644 index 000000000..b0aae742c --- /dev/null +++ b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/common/InstrumentationLibraryInfoTest.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +/** Tests for {@link InstrumentationLibraryInfo}. */ +class InstrumentationLibraryInfoTest { + + @Test + void emptyLibraryInfo() { + assertThat(InstrumentationLibraryInfo.empty().getName()).isEmpty(); + assertThat(InstrumentationLibraryInfo.empty().getVersion()).isNull(); + } + + @Test + void nullName() { + assertThatThrownBy(() -> InstrumentationLibraryInfo.create(null, "1.0.0")) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } +} diff --git a/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ComponentRegistryTest.java b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ComponentRegistryTest.java new file mode 100644 index 000000000..93c983104 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ComponentRegistryTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import org.junit.jupiter.api.Test; + +/** Tests for {@link InstrumentationLibraryInfo}. */ +class ComponentRegistryTest { + + private static final String INSTRUMENTATION_NAME = "test_name"; + private static final String INSTRUMENTATION_VERSION = "version"; + private final ComponentRegistry registry = + new ComponentRegistry<>(TestComponent::new); + + @Test + void libraryName_MustNotBeNull() { + assertThatThrownBy(() -> registry.get(null, "version")) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void libraryVersion_AllowsNull() { + TestComponent testComponent = registry.get(INSTRUMENTATION_NAME, null); + assertThat(testComponent).isNotNull(); + assertThat(testComponent.instrumentationLibraryInfo.getName()).isEqualTo(INSTRUMENTATION_NAME); + assertThat(testComponent.instrumentationLibraryInfo.getVersion()).isNull(); + } + + @Test + void getSameInstanceForSameName_WithoutVersion() { + assertThat(registry.get(INSTRUMENTATION_NAME)).isSameAs(registry.get(INSTRUMENTATION_NAME)); + assertThat(registry.get(INSTRUMENTATION_NAME)) + .isSameAs(registry.get(INSTRUMENTATION_NAME, null)); + } + + @Test + void getSameInstanceForSameName_WithVersion() { + assertThat(registry.get(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION)) + .isSameAs(registry.get(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION)); + } + + @Test + void getDifferentInstancesForDifferentNames() { + assertThat(registry.get(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION)) + .isNotSameAs(registry.get(INSTRUMENTATION_NAME + "_2", INSTRUMENTATION_VERSION)); + } + + @Test + void getDifferentInstancesForDifferentVersions() { + assertThat(registry.get(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION)) + .isNotSameAs(registry.get(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION + "_1")); + } + + private static final class TestComponent { + private final InstrumentationLibraryInfo instrumentationLibraryInfo; + + private TestComponent(InstrumentationLibraryInfo instrumentationLibraryInfo) { + this.instrumentationLibraryInfo = instrumentationLibraryInfo; + } + } +} diff --git a/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/MonotonicClockTest.java b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/MonotonicClockTest.java new file mode 100644 index 000000000..5f25bdb18 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/MonotonicClockTest.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link MonotonicClock}. */ +class MonotonicClockTest { + private static final long EPOCH_NANOS = 1234_000_005_678L; + private final TestClock testClock = TestClock.create(EPOCH_NANOS); + + @Test + void nanoTime() { + assertThat(testClock.now()).isEqualTo(EPOCH_NANOS); + MonotonicClock monotonicClock = MonotonicClock.create(testClock); + assertThat(monotonicClock.nanoTime()).isEqualTo(testClock.nanoTime()); + testClock.advanceNanos(12345); + assertThat(monotonicClock.nanoTime()).isEqualTo(testClock.nanoTime()); + } + + @Test + void now_PositiveIncrease() { + MonotonicClock monotonicClock = MonotonicClock.create(testClock); + assertThat(monotonicClock.now()).isEqualTo(testClock.now()); + testClock.advanceNanos(3210); + assertThat(monotonicClock.now()).isEqualTo(1234_000_008_888L); + // Initial + 1000 + testClock.advanceNanos(-2210); + assertThat(monotonicClock.now()).isEqualTo(1234_000_006_678L); + testClock.advanceNanos(15_999_993_322L); + assertThat(monotonicClock.now()).isEqualTo(1250_000_000_000L); + } + + @Test + void now_NegativeIncrease() { + MonotonicClock monotonicClock = MonotonicClock.create(testClock); + assertThat(monotonicClock.now()).isEqualTo(testClock.now()); + testClock.advanceNanos(-3456); + assertThat(monotonicClock.now()).isEqualTo(1234_000_002_222L); + // Initial - 1000 + testClock.advanceNanos(2456); + assertThat(monotonicClock.now()).isEqualTo(1234_000_004_678L); + testClock.advanceNanos(-14_000_004_678L); + assertThat(monotonicClock.now()).isEqualTo(1220_000_000_000L); + } +} diff --git a/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/RateLimiterTest.java b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/RateLimiterTest.java new file mode 100644 index 000000000..5ecca531a --- /dev/null +++ b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/RateLimiterTest.java @@ -0,0 +1,180 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +/** + * This class was taken from Jaeger java client. + * https://github.com/jaegertracing/jaeger-client-java/blob/master/jaeger-core/src/test/java/io/jaegertracing/internal/utils/RateLimiterTest.java + */ +class RateLimiterTest { + + @Test + void testRateLimiterWholeNumber() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(2.0, 2.0, clock); + + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + // move time 250ms forward, not enough credits to pay for 1.0 item + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(250)); + assertThat(limiter.trySpend(1.0)).isFalse(); + + // move time 500ms forward, now enough credits to pay for 1.0 item + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(500)); + + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + + // move time 5s forward, enough to accumulate credits for 10 messages, but it should still be + // capped at 2 + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(5000)); + + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + assertThat(limiter.trySpend(1.0)).isFalse(); + assertThat(limiter.trySpend(1.0)).isFalse(); + } + + @Test + void testRateLimiterSteadyRate() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(5.0 / 60.0, 5.0, clock); + for (int i = 0; i < 100; i++) { + assertThat(limiter.trySpend(1.0)).isTrue(); + clock.advanceNanos(TimeUnit.SECONDS.toNanos(20)); + } + } + + @Test + void cantWithdrawMoreThanMax() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(1, 1.0, clock); + assertThat(limiter.trySpend(2)).isFalse(); + } + + @Test + void testRateLimiterLessThanOne() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(0.5, 0.5, clock); + + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isFalse(); + // move time 250ms forward, not enough credits to pay for 1.0 item + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(250)); + assertThat(limiter.trySpend(0.25)).isFalse(); + + // move time 500ms forward, now enough credits to pay for 1.0 item + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(500)); + + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isFalse(); + + // move time 5s forward, enough to accumulate credits for 10 messages, but it should still be + // capped at 2 + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(5000)); + + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isFalse(); + assertThat(limiter.trySpend(0.25)).isFalse(); + assertThat(limiter.trySpend(0.25)).isFalse(); + } + + @Test + void testRateLimiterMaxBalance() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(0.1, 1.0, clock); + + clock.advanceNanos(TimeUnit.MICROSECONDS.toNanos(100)); + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + + // move time 20s forward, enough to accumulate credits for 2 messages, but it should still be + // capped at 1 + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(20000)); + + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + } + + /** + * Validates rate limiter behavior with {@link System#nanoTime()}-like (non-zero) initial nano + * ticks. + */ + @Test + void testRateLimiterInitial() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(1000, 100, clock); + + assertThat(limiter.trySpend(100)).isTrue(); // consume initial (max) balance + assertThat(limiter.trySpend(1)).isFalse(); + + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(49)); // add 49 credits + assertThat(limiter.trySpend(50)).isFalse(); + + clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(1)); // add one credit + assertThat(limiter.trySpend(50)).isTrue(); // consume accrued balance + assertThat(limiter.trySpend(1)).isFalse(); + + clock.advanceNanos( + TimeUnit.MILLISECONDS.toNanos(1_000_000)); // add a lot of credits (max out balance) + assertThat(limiter.trySpend(1)).isTrue(); // take one credit + + clock.advanceNanos( + TimeUnit.MILLISECONDS.toNanos(1_000_000)); // add a lot of credits (max out balance) + assertThat(limiter.trySpend(101)).isFalse(); // can't consume more than max balance + assertThat(limiter.trySpend(100)).isTrue(); // consume max balance + assertThat(limiter.trySpend(1)).isFalse(); + } + + /** Validates concurrent credit check correctness. */ + @Test + void testRateLimiterConcurrency() throws InterruptedException, ExecutionException { + int numWorkers = 8; + ExecutorService executorService = Executors.newFixedThreadPool(numWorkers); + final int creditsPerWorker = 1000; + TestClock clock = TestClock.create(); + final RateLimiter limiter = new RateLimiter(1, numWorkers * creditsPerWorker, clock); + final AtomicInteger count = new AtomicInteger(); + List> futures = new ArrayList<>(numWorkers); + for (int w = 0; w < numWorkers; ++w) { + Future future = + executorService.submit( + () -> { + for (int i = 0; i < creditsPerWorker * 2; ++i) { + if (limiter.trySpend(1)) { + count.getAndIncrement(); // count allowed operations + } + } + }); + futures.add(future); + } + for (Future future : futures) { + future.get(); + } + executorService.shutdown(); + executorService.awaitTermination(1, TimeUnit.SECONDS); + assertThat(count.get()) + .withFailMessage("Exactly the allocated number of credits must be consumed") + .isEqualTo(numWorkers * creditsPerWorker); + assertThat(limiter.trySpend(1)).isFalse(); + } +} diff --git a/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/TestClockTest.java b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/TestClockTest.java new file mode 100644 index 000000000..ad5a80264 --- /dev/null +++ b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/TestClockTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** Tests for {@link TestClock}. */ +public final class TestClockTest { + + @Test + void setAndGetTime() { + TestClock clock = TestClock.create(1234); + assertThat(clock.now()).isEqualTo(1234); + clock.setTime(9876543210L); + assertThat(clock.now()).isEqualTo(9876543210L); + } + + @Test + void advanceMillis() { + TestClock clock = TestClock.create(1_500_000_000L); + clock.advanceMillis(2600); + assertThat(clock.now()).isEqualTo(4_100_000_000L); + } + + @Test + void measureElapsedTime() { + TestClock clock = TestClock.create(10_000_000_001L); + long nanos1 = clock.nanoTime(); + clock.setTime(11_000_000_005L); + long nanos2 = clock.nanoTime(); + assertThat(nanos2 - nanos1).isEqualTo(1_000_000_004L); + } +} diff --git a/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ThrottlingLoggerTest.java b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ThrottlingLoggerTest.java new file mode 100644 index 000000000..821c1684b --- /dev/null +++ b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/internal/ThrottlingLoggerTest.java @@ -0,0 +1,144 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.slf4j.event.Level.ERROR; +import static org.slf4j.event.Level.INFO; +import static org.slf4j.event.Level.WARN; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.sdk.common.Clock; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ThrottlingLoggerTest { + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(ThrottlingLoggerTest.class); + + @Test + void delegation() { + ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(ThrottlingLoggerTest.class.getName())); + + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.INFO, "oh yes!"); + RuntimeException throwable = new RuntimeException(); + logger.log(Level.SEVERE, "secrets", throwable); + + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(WARN), "oh no!"); + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(INFO), "oh yes!"); + assertThat( + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(ERROR), "secrets") + .getThrowable()) + .isSameAs(throwable); + } + + @Test + void logsBelowLevelDontCount() { + ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(ThrottlingLoggerTest.class.getName())); + + for (int i = 0; i < 100; i++) { + // FINE is below the default level and thus shouldn't impact the rate. + logger.log(Level.FINE, "secrets", new RuntimeException()); + } + logger.log(Level.INFO, "oh yes!"); + + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(INFO), "oh yes!"); + } + + @Test + void fiveInAMinuteTriggersLimiting() { + Clock clock = TestClock.create(); + ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(ThrottlingLoggerTest.class.getName()), clock); + + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + + logger.log(Level.WARNING, "oh no I should trigger suppression!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + + assertThat(logs.getEvents()).hasSize(7); + logs.assertDoesNotContain("oh no I should be suppressed!"); + logs.assertContains( + "Too many log messages detected. Will only log once per minute from now on."); + logs.assertContains("oh no I should trigger suppression!"); + } + + @Test + void allowsTrickleOfMessages() { + TestClock clock = TestClock.create(); + ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(ThrottlingLoggerTest.class.getName()), clock); + logger.log(Level.WARNING, "oh no!"); + assertThat(logs.size()).isEqualTo(1); + logger.log(Level.WARNING, "oh no!"); + assertThat(logs.size()).isEqualTo(2); + clock.advanceMillis(30_001); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + assertThat(logs.size()).isEqualTo(4); + + clock.advanceMillis(30_001); + logger.log(Level.WARNING, "oh no 2nd minute!"); + logger.log(Level.WARNING, "oh no 2nd minute!"); + assertThat(logs.size()).isEqualTo(6); + clock.advanceMillis(30_001); + logger.log(Level.WARNING, "oh no 2nd minute!"); + logger.log(Level.WARNING, "oh no 2nd minute!"); + assertThat(logs.size()).isEqualTo(8); + + clock.advanceMillis(30_001); + logger.log(Level.WARNING, "oh no 3rd minute!"); + logger.log(Level.WARNING, "oh no 3rd minute!"); + assertThat(logs.size()).isEqualTo(10); + clock.advanceMillis(30_001); + logger.log(Level.WARNING, "oh no 3rd minute!"); + logger.log(Level.WARNING, "oh no 3rd minute!"); + assertThat(logs.size()).isEqualTo(12); + } + + @Test + void afterAMinuteLetOneThrough() { + TestClock clock = TestClock.create(); + ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(ThrottlingLoggerTest.class.getName()), clock); + + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + + logger.log(Level.WARNING, "oh no I should trigger suppression!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + + assertThat(logs.getEvents()).hasSize(7); + logs.assertDoesNotContain("oh no I should be suppressed!"); + logs.assertContains("oh no I should trigger suppression!"); + logs.assertContains( + "Too many log messages detected. Will only log once per minute from now on."); + + clock.advanceMillis(60_001); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + assertThat(logs.getEvents()).hasSize(8); + assertThat(logs.getEvents().get(7).getMessage()).isEqualTo("oh no!"); + + clock.advanceMillis(60_001); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + assertThat(logs.getEvents()).hasSize(9); + assertThat(logs.getEvents().get(8).getMessage()).isEqualTo("oh no!"); + } +} diff --git a/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java new file mode 100644 index 000000000..bfdbae2da --- /dev/null +++ b/opentelemetry-java/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java @@ -0,0 +1,268 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.testing.EqualsTester; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link Resource}. */ +class ResourceTest { + private Resource resource1; + private Resource resource2; + + @BeforeEach + void setUp() { + Attributes attributes1 = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); + Attributes attribute2 = + Attributes.of(stringKey("a"), "1", stringKey("b"), "3", stringKey("c"), "4"); + resource1 = Resource.create(attributes1); + resource2 = Resource.create(attribute2); + } + + @Test + void create() { + Attributes attributes = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); + Resource resource = Resource.create(attributes); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(2); + assertThat(resource.getAttributes()).isEqualTo(attributes); + + Resource resource1 = Resource.create(Attributes.empty()); + assertThat(resource1.getAttributes()).isNotNull(); + assertThat(resource1.getAttributes().isEmpty()).isTrue(); + } + + @Test + void create_ignoreNull() { + AttributesBuilder attributes = Attributes.builder(); + + attributes.put(stringKey("string"), null); + Resource resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isZero(); + attributes.put(stringArrayKey("stringArray"), Arrays.asList(null, "a")); + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(1); + + attributes.put(booleanKey("bool"), true); + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(2); + attributes.put(booleanArrayKey("boolArray"), Arrays.asList(null, true)); + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(3); + + attributes.put(longKey("long"), 0L); + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(4); + attributes.put(longArrayKey("longArray"), Arrays.asList(1L, null)); + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(5); + + attributes.put(doubleKey("double"), 1.1); + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(6); + attributes.put(doubleArrayKey("doubleArray"), Arrays.asList(1.1, null)); + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(7); + } + + @Test + void create_NullEmptyArray() { + AttributesBuilder attributes = Attributes.builder(); + + // Empty arrays should be maintained + attributes.put(stringArrayKey("stringArrayAttribute"), Collections.emptyList()); + attributes.put(booleanArrayKey("boolArrayAttribute"), Collections.emptyList()); + attributes.put(longArrayKey("longArrayAttribute"), Collections.emptyList()); + attributes.put(doubleArrayKey("doubleArrayAttribute"), Collections.emptyList()); + + Resource resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(4); + + // Arrays with null values should be maintained + attributes.put(stringArrayKey("ArrayWithNullStringKey"), singletonList(null)); + attributes.put(longArrayKey("ArrayWithNullLongKey"), singletonList(null)); + attributes.put(doubleArrayKey("ArrayWithNullDoubleKey"), singletonList(null)); + attributes.put(booleanArrayKey("ArrayWithNullBooleanKey"), singletonList(null)); + + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(8); + + // Null arrays should be dropped + attributes.put(stringArrayKey("NullArrayStringKey"), null); + attributes.put(longArrayKey("NullArrayLongKey"), null); + attributes.put(doubleArrayKey("NullArrayDoubleKey"), null); + attributes.put(booleanArrayKey("NullArrayBooleanKey"), null); + + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(8); + + attributes.put(stringKey("dropNullString"), null); + attributes.put(longKey("dropNullLong"), null); + attributes.put(doubleKey("dropNullDouble"), null); + attributes.put(booleanKey("dropNullBool"), null); + + resource = Resource.create(attributes.build()); + assertThat(resource.getAttributes()).isNotNull(); + assertThat(resource.getAttributes().size()).isEqualTo(8); + } + + @Test + void testResourceEquals() { + Attributes attribute1 = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); + Attributes attribute2 = + Attributes.of(stringKey("a"), "1", stringKey("b"), "3", stringKey("c"), "4"); + new EqualsTester() + .addEqualityGroup(Resource.create(attribute1), Resource.create(attribute1), resource1) + .addEqualityGroup(Resource.create(attribute2), resource2) + .testEquals(); + } + + @Test + void testMergeResources() { + Attributes expectedAttributes = + Attributes.of(stringKey("a"), "1", stringKey("b"), "3", stringKey("c"), "4"); + + Resource resource = Resource.empty().merge(resource1).merge(resource2); + assertThat(resource.getAttributes()).isEqualTo(expectedAttributes); + } + + @Test + void testMergeResources_Resource1() { + Attributes expectedAttributes = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); + + Resource resource = Resource.empty().merge(resource1); + assertThat(resource.getAttributes()).isEqualTo(expectedAttributes); + } + + @Test + void testMergeResources_Resource1_Null() { + Attributes expectedAttributes = + Attributes.of( + stringKey("a"), "1", + stringKey("b"), "3", + stringKey("c"), "4"); + + Resource resource = Resource.empty().merge(null).merge(resource2); + assertThat(resource.getAttributes()).isEqualTo(expectedAttributes); + } + + @Test + void testMergeResources_Resource2_Null() { + Attributes expectedAttributes = Attributes.of(stringKey("a"), "1", stringKey("b"), "2"); + Resource resource = Resource.empty().merge(resource1).merge(null); + assertThat(resource.getAttributes()).isEqualTo(expectedAttributes); + } + + @Test + void testDefaultResources() { + Resource resource = Resource.getDefault(); + Attributes attributes = resource.getAttributes(); + assertThat(attributes.get(ResourceAttributes.SERVICE_NAME)).isEqualTo("unknown_service:java"); + assertThat(attributes.get(ResourceAttributes.TELEMETRY_SDK_NAME)).isEqualTo("opentelemetry"); + assertThat(attributes.get(ResourceAttributes.TELEMETRY_SDK_LANGUAGE)).isEqualTo("java"); + assertThat(attributes.get(ResourceAttributes.TELEMETRY_SDK_VERSION)).isNotNull(); + } + + @Test + void shouldBuilderNotFailWithNullResource() { + // given + ResourceBuilder builder = Resource.getDefault().toBuilder(); + + // when + builder.putAll((Resource) null); + + // then no exception is thrown + // and + assertThat(builder.build().getAttributes().get(ResourceAttributes.SERVICE_NAME)) + .isEqualTo("unknown_service:java"); + } + + @Test + void shouldBuilderCopyResource() { + // given + ResourceBuilder builder = Resource.getDefault().toBuilder(); + + // when + builder.put("dog says what?", "woof"); + + // then + Resource resource = builder.build(); + assertThat(resource).isNotSameAs(Resource.getDefault()); + assertThat(resource.getAttributes().get(stringKey("dog says what?"))).isEqualTo("woof"); + } + + @Test + void shouldBuilderHelperMethodsBuildResource() { + // given + ResourceBuilder builder = Resource.getDefault().toBuilder(); + Attributes sourceAttributes = Attributes.of(stringKey("hello"), "world"); + Resource source = Resource.create(sourceAttributes); + Attributes sourceAttributes2 = Attributes.of(stringKey("OpenTelemetry"), "Java"); + + // when + Resource resource = + builder + .put("long", 42L) + .put("double", Math.E) + .put("boolean", true) + .put("string", "abc") + .put("long array", 1L, 2L, 3L) + .put("double array", Math.E, Math.PI) + .put("boolean array", true, false) + .put("string array", "first", "second") + .put(longKey("long key"), 4242L) + .put(longKey("int in disguise"), 21) + .putAll(source) + .putAll(sourceAttributes2) + .build(); + + // then + Attributes attributes = resource.getAttributes(); + assertThat(attributes.get(longKey("long"))).isEqualTo(42L); + assertThat(attributes.get(doubleKey("double"))).isEqualTo(Math.E); + assertThat(attributes.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(attributes.get(stringKey("string"))).isEqualTo("abc"); + assertThat(attributes.get(longArrayKey("long array"))).isEqualTo(Arrays.asList(1L, 2L, 3L)); + assertThat(attributes.get(doubleArrayKey("double array"))) + .isEqualTo(Arrays.asList(Math.E, Math.PI)); + assertThat(attributes.get(booleanArrayKey("boolean array"))) + .isEqualTo(Arrays.asList(true, false)); + assertThat(attributes.get(stringArrayKey("string array"))) + .isEqualTo(Arrays.asList("first", "second")); + assertThat(attributes.get(longKey("long key"))).isEqualTo(4242L); + assertThat(attributes.get(longKey("int in disguise"))).isEqualTo(21); + assertThat(attributes.get(stringKey("hello"))).isEqualTo("world"); + assertThat(attributes.get(stringKey("OpenTelemetry"))).isEqualTo("Java"); + } +} diff --git a/opentelemetry-java/sdk/metrics/build.gradle.kts b/opentelemetry-java/sdk/metrics/build.gradle.kts new file mode 100644 index 000000000..e6f051320 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("java-library") + id("maven-publish") + + id("me.champeau.jmh") + + // TODO(anuraaga): Enable animalsniffer by the time we are getting ready to release a stable + // version. Long/DoubleAdder are not part of Android API 21 which is our current target. + // id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry SDK Metrics" +extra["moduleName"] = "io.opentelemetry.sdk.metrics" + +dependencies { + api(project(":api:metrics")) + api(project(":sdk:common")) + + annotationProcessor("com.google.auto.value:auto-value") + + testAnnotationProcessor("com.google.auto.value:auto-value") + + testImplementation(project(":sdk:testing")) + testImplementation("com.google.guava:guava") +} + +sourceSets { + main { + output.dir("build/generated/properties", "builtBy" to "generateVersionResource") + } +} + +tasks { + register("generateVersionResource") { + val propertiesDir = file("build/generated/properties/io/opentelemetry/sdk/metrics") + outputs.dir(propertiesDir) + + doLast { + File(propertiesDir, "version.properties").writeText("sdk.version=${project.version}") + } + } +} diff --git a/opentelemetry-java/sdk/metrics/gradle.properties b/opentelemetry-java/sdk/metrics/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/sdk/metrics/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsBenchmarks.java b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsBenchmarks.java new file mode 100644 index 000000000..d5c18ac11 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsBenchmarks.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +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.Measurement; +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.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.ThreadParams; + +@BenchmarkMode({Mode.AverageTime}) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +@Fork(1) +public class MetricsBenchmarks { + + @State(Scope.Thread) + public static class ThreadState { + + @Param TestSdk sdk; + + @Param MetricsTestOperationBuilder opBuilder; + + MetricsTestOperationBuilder.Operation op; + final Labels sharedLabelSet = Labels.of("KEY", "VALUE"); + Labels threadUniqueLabelSet; + + @Setup + public void setup(ThreadParams threadParams) { + Meter meter = sdk.getMeter(); + op = opBuilder.build(meter); + threadUniqueLabelSet = Labels.of("KEY", String.valueOf(threadParams.getThreadIndex())); + } + } + + @Benchmark + @Threads(1) + public void oneThread(ThreadState threadState) { + threadState.op.perform(threadState.sharedLabelSet); + } + + @Benchmark + @Threads(1) + public void oneThreadBound(ThreadState threadState) { + threadState.op.performBound(); + } + + @Benchmark + @Threads(8) + public void eightThreadsCommonLabelSet(ThreadState threadState) { + threadState.op.perform(threadState.sharedLabelSet); + } + + @Benchmark + @Threads(8) + public void eightThreadsSeparateLabelSets(ThreadState threadState) { + threadState.op.perform(threadState.threadUniqueLabelSet); + } + + @Benchmark + @Threads(8) + public void eightThreadsBound(ThreadState threadState) { + threadState.op.performBound(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsTestOperationBuilder.java b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsTestOperationBuilder.java new file mode 100644 index 000000000..b08488733 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/MetricsTestOperationBuilder.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.BoundDoubleCounter; +import io.opentelemetry.api.metrics.BoundDoubleValueRecorder; +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.api.metrics.BoundLongValueRecorder; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleValueRecorder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongValueRecorder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; + +/** + * This enum allows for iteration over all of the operations that we want to benchmark. To ensure + * that the enum cannot change state, each enum holds a builder function- passing a meter in will + * return a wrapper for both bound and unbound versions of that operation which can then be used in + * a benchmark. + */ +@SuppressWarnings("ImmutableEnumChecker") +public enum MetricsTestOperationBuilder { + LongCounterAdd( + meter -> { + return new Operation() { + final LongCounter metric = meter.longCounterBuilder("long_counter").build(); + final BoundLongCounter boundMetric = + meter + .longCounterBuilder("bound_long_counter") + .build() + .bind(Labels.of("KEY", "VALUE")); + + @Override + public void perform(Labels labels) { + metric.add(5L, labels); + } + + @Override + public void performBound() { + boundMetric.add(5L); + } + }; + }), + DoubleCounterAdd( + meter -> { + return new Operation() { + final DoubleCounter metric = meter.doubleCounterBuilder("double_counter").build(); + final BoundDoubleCounter boundMetric = + meter + .doubleCounterBuilder("bound_double_counter") + .build() + .bind(Labels.of("KEY", "VALUE")); + + @Override + public void perform(Labels labels) { + metric.add(5.0d, labels); + } + + @Override + public void performBound() { + boundMetric.add(5.0d); + } + }; + }), + DoubleValueRecorderRecord( + meter -> { + return new Operation() { + final DoubleValueRecorder metric = + meter.doubleValueRecorderBuilder("double_value_recorder").build(); + final BoundDoubleValueRecorder boundMetric = + meter + .doubleValueRecorderBuilder("bound_double_value_recorder") + .build() + .bind(Labels.of("KEY", "VALUE")); + + @Override + public void perform(Labels labels) { + metric.record(5.0d, labels); + } + + @Override + public void performBound() { + boundMetric.record(5.0d); + } + }; + }), + LongValueRecorderRecord( + meter -> { + return new Operation() { + final LongValueRecorder metric = + meter.longValueRecorderBuilder("long_value_recorder").build(); + final BoundLongValueRecorder boundMetric = + meter + .longValueRecorderBuilder("bound_long_value_recorder") + .build() + .bind(Labels.of("KEY", "VALUE")); + + @Override + public void perform(Labels labels) { + metric.record(5L, labels); + } + + @Override + public void performBound() { + boundMetric.record(5L); + } + }; + }); + + private final OperationBuilder builder; + + MetricsTestOperationBuilder(final OperationBuilder builder) { + this.builder = builder; + } + + public Operation build(Meter meter) { + return this.builder.build(meter); + } + + private interface OperationBuilder { + Operation build(Meter meter); + } + + interface Operation { + void perform(Labels labels); + + void performBound(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java new file mode 100644 index 000000000..04e36aaa4 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/TestSdk.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.SystemClock; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.view.View; +import io.opentelemetry.sdk.resources.Resource; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.regex.Pattern; + +@SuppressWarnings("ImmutableEnumChecker") +public enum TestSdk { + API_ONLY( + new SdkBuilder() { + @Override + Meter build() { + return MeterProvider.noop().get("io.opentelemetry.sdk.metrics"); + } + }), + SDK( + new SdkBuilder() { + @Override + Meter build() { + MeterProviderSharedState meterProviderSharedState = + MeterProviderSharedState.create( + SystemClock.getInstance(), + Resource.empty(), + new ViewRegistry( + new EnumMap>( + InstrumentType.class))); + InstrumentationLibraryInfo instrumentationLibraryInfo = + InstrumentationLibraryInfo.create("io.opentelemetry.sdk.metrics", null); + + return new SdkMeter(meterProviderSharedState, instrumentationLibraryInfo); + } + }); + + private final SdkBuilder sdkBuilder; + + TestSdk(SdkBuilder sdkBuilder) { + this.sdkBuilder = sdkBuilder; + } + + public Meter getMeter() { + return sdkBuilder.build(); + } + + private abstract static class SdkBuilder { + abstract Meter build(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramBenchmark.java b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramBenchmark.java new file mode 100644 index 000000000..f1ae34ba6 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramBenchmark.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class DoubleHistogramBenchmark { + private static final Aggregator aggregator = + AggregatorFactory.histogram(Arrays.asList(10.0, 100.0, 1_000.0), AggregationTemporality.DELTA) + .create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "1", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.DOUBLE)); + private AggregatorHandle aggregatorHandle; + + @Setup(Level.Trial) + public final void setup() { + aggregatorHandle = aggregator.createHandle(); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 10) + public void aggregate_10Threads() { + aggregatorHandle.recordDouble(100.0056); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 5) + public void aggregate_5Threads() { + aggregatorHandle.recordDouble(100.0056); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 1) + public void aggregate_1Threads() { + aggregatorHandle.recordDouble(100.0056); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountBenchmark.java b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountBenchmark.java new file mode 100644 index 000000000..52365ce1e --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountBenchmark.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.resources.Resource; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class DoubleMinMaxSumCountBenchmark { + private static final Aggregator aggregator = + AggregatorFactory.minMaxSumCount() + .create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "1", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.DOUBLE)); + private AggregatorHandle aggregatorHandle; + + @Setup(Level.Trial) + public final void setup() { + aggregatorHandle = aggregator.createHandle(); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 10) + public void aggregate_10Threads() { + aggregatorHandle.recordDouble(100.0056); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 5) + public void aggregate_5Threads() { + aggregatorHandle.recordDouble(100.0056); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 1) + public void aggregate_1Threads() { + aggregatorHandle.recordDouble(100.0056); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountBenchmark.java b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountBenchmark.java new file mode 100644 index 000000000..b15d47042 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/jmh/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountBenchmark.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.resources.Resource; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class LongMinMaxSumCountBenchmark { + private static final Aggregator aggregator = + AggregatorFactory.minMaxSumCount() + .create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "1", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.LONG)); + private AggregatorHandle aggregatorHandle; + + @Setup(Level.Trial) + public final void setup() { + aggregatorHandle = aggregator.createHandle(); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 10) + public void aggregate_10Threads() { + aggregatorHandle.recordLong(100); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 5) + public void aggregate_5Threads() { + aggregatorHandle.recordLong(100); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(value = 1) + public void aggregate_1Threads() { + aggregatorHandle.recordLong(100); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractAccumulator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractAccumulator.java new file mode 100644 index 000000000..b3543c6b8 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractAccumulator.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.sdk.metrics.aggregator.Aggregator; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessor; +import java.util.List; + +abstract class AbstractAccumulator { + /** + * Returns the list of metrics collected. + * + * @return returns the list of metrics collected. + */ + abstract List collectAll(long epochNanos); + + static Aggregator getAggregator( + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState, + InstrumentDescriptor descriptor) { + return meterProviderSharedState + .getViewRegistry() + .findView(descriptor) + .getAggregatorFactory() + .create( + meterProviderSharedState.getResource(), + meterSharedState.getInstrumentationLibraryInfo(), + descriptor); + } + + static LabelsProcessor getLabelsProcessor( + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState, + InstrumentDescriptor descriptor) { + return meterProviderSharedState + .getViewRegistry() + .findView(descriptor) + .getLabelsProcessorFactory() + .create( + meterProviderSharedState.getResource(), + meterSharedState.getInstrumentationLibraryInfo(), + descriptor); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractAsynchronousInstrument.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractAsynchronousInstrument.java new file mode 100644 index 000000000..915f11cac --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractAsynchronousInstrument.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.List; + +abstract class AbstractAsynchronousInstrument extends AbstractInstrument { + private final AsynchronousInstrumentAccumulator accumulator; + + AbstractAsynchronousInstrument( + InstrumentDescriptor descriptor, AsynchronousInstrumentAccumulator accumulator) { + super(descriptor); + this.accumulator = accumulator; + } + + @Override + final List collectAll(long epochNanos) { + return accumulator.collectAll(epochNanos); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractDoubleAsynchronousInstrumentBuilder.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractDoubleAsynchronousInstrumentBuilder.java new file mode 100644 index 000000000..59526f761 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractDoubleAsynchronousInstrumentBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.AsynchronousInstrument; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +abstract class AbstractDoubleAsynchronousInstrumentBuilder> + extends AbstractInstrument.Builder { + private final MeterProviderSharedState meterProviderSharedState; + private final MeterSharedState meterSharedState; + @Nullable private Consumer updater; + + AbstractDoubleAsynchronousInstrumentBuilder( + String name, + InstrumentType instrumentType, + InstrumentValueType instrumentValueType, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super(name, instrumentType, instrumentValueType); + this.meterProviderSharedState = meterProviderSharedState; + this.meterSharedState = meterSharedState; + } + + public B setUpdater(Consumer updater) { + this.updater = updater; + return getThis(); + } + + final I buildInstrument( + BiFunction instrumentFactory) { + InstrumentDescriptor descriptor = buildDescriptor(); + return meterSharedState + .getInstrumentRegistry() + .register( + instrumentFactory.apply( + descriptor, + AsynchronousInstrumentAccumulator.doubleAsynchronousAccumulator( + meterProviderSharedState, meterSharedState, descriptor, updater))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractInstrument.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractInstrument.java new file mode 100644 index 000000000..c8330d1a7 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractInstrument.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.internal.Utils; +import io.opentelemetry.api.metrics.Instrument; +import io.opentelemetry.api.metrics.InstrumentBuilder; +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.List; +import java.util.Objects; + +abstract class AbstractInstrument implements Instrument { + + private final InstrumentDescriptor descriptor; + + // All arguments cannot be null because they are checked in the abstract builder classes. + AbstractInstrument(InstrumentDescriptor descriptor) { + this.descriptor = descriptor; + } + + final InstrumentDescriptor getDescriptor() { + return descriptor; + } + + /** + * Collects records from all the entries (labelSet, Bound) that changed since the previous call. + */ + abstract List collectAll(long epochNanos); + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AbstractInstrument)) { + return false; + } + + AbstractInstrument that = (AbstractInstrument) o; + + return descriptor.equals(that.descriptor); + } + + @Override + public int hashCode() { + return descriptor.hashCode(); + } + + abstract static class Builder> + implements InstrumentBuilder { + /* VisibleForTesting */ static final String ERROR_MESSAGE_INVALID_NAME = + "Name should be a ASCII string with a length no greater than " + + MetricsStringUtils.METRIC_NAME_MAX_LENGTH + + " characters."; + + private final String name; + private final InstrumentType instrumentType; + private final InstrumentValueType instrumentValueType; + private String description = ""; + private String unit = "1"; + + Builder(String name, InstrumentType instrumentType, InstrumentValueType instrumentValueType) { + Objects.requireNonNull(name, "name"); + Utils.checkArgument(MetricsStringUtils.isValidMetricName(name), ERROR_MESSAGE_INVALID_NAME); + this.name = name; + this.instrumentType = instrumentType; + this.instrumentValueType = instrumentValueType; + } + + @Override + public final B setDescription(String description) { + this.description = Objects.requireNonNull(description, "description"); + return getThis(); + } + + @Override + public final B setUnit(String unit) { + this.unit = Objects.requireNonNull(unit, "unit"); + return getThis(); + } + + abstract B getThis(); + + final InstrumentDescriptor buildDescriptor() { + return InstrumentDescriptor.create( + name, description, unit, instrumentType, instrumentValueType); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractLongAsynchronousInstrumentBuilder.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractLongAsynchronousInstrumentBuilder.java new file mode 100644 index 000000000..4fa0739a7 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractLongAsynchronousInstrumentBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.AsynchronousInstrument; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +abstract class AbstractLongAsynchronousInstrumentBuilder> + extends AbstractInstrument.Builder { + private final MeterProviderSharedState meterProviderSharedState; + private final MeterSharedState meterSharedState; + @Nullable private Consumer updater; + + AbstractLongAsynchronousInstrumentBuilder( + String name, + InstrumentType instrumentType, + InstrumentValueType instrumentValueType, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super(name, instrumentType, instrumentValueType); + this.meterProviderSharedState = meterProviderSharedState; + this.meterSharedState = meterSharedState; + } + + public B setUpdater(Consumer updater) { + this.updater = updater; + return getThis(); + } + + final I buildInstrument( + BiFunction instrumentFactory) { + InstrumentDescriptor descriptor = buildDescriptor(); + return meterSharedState + .getInstrumentRegistry() + .register( + instrumentFactory.apply( + descriptor, + AsynchronousInstrumentAccumulator.longAsynchronousAccumulator( + meterProviderSharedState, meterSharedState, descriptor, updater))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractSynchronousInstrument.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractSynchronousInstrument.java new file mode 100644 index 000000000..419659208 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractSynchronousInstrument.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.List; + +abstract class AbstractSynchronousInstrument extends AbstractInstrument { + private final SynchronousInstrumentAccumulator accumulator; + + AbstractSynchronousInstrument( + InstrumentDescriptor descriptor, SynchronousInstrumentAccumulator accumulator) { + super(descriptor); + this.accumulator = accumulator; + } + + @Override + final List collectAll(long epochNanos) { + return accumulator.collectAll(epochNanos); + } + + AggregatorHandle acquireHandle(Labels labels) { + return accumulator.bind(labels); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractSynchronousInstrumentBuilder.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractSynchronousInstrumentBuilder.java new file mode 100644 index 000000000..4e5f25e5b --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AbstractSynchronousInstrumentBuilder.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import java.util.function.BiFunction; + +abstract class AbstractSynchronousInstrumentBuilder< + B extends AbstractSynchronousInstrumentBuilder> + extends AbstractInstrument.Builder { + private final MeterProviderSharedState meterProviderSharedState; + private final MeterSharedState meterSharedState; + + AbstractSynchronousInstrumentBuilder( + String name, + InstrumentType instrumentType, + InstrumentValueType instrumentValueType, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super(name, instrumentType, instrumentValueType); + this.meterProviderSharedState = meterProviderSharedState; + this.meterSharedState = meterSharedState; + } + + final I buildInstrument( + BiFunction, I> instrumentFactory) { + InstrumentDescriptor descriptor = buildDescriptor(); + return meterSharedState + .getInstrumentRegistry() + .register( + instrumentFactory.apply( + descriptor, + SynchronousInstrumentAccumulator.create( + meterProviderSharedState, meterSharedState, descriptor))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AsynchronousInstrumentAccumulator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AsynchronousInstrumentAccumulator.java new file mode 100644 index 000000000..5ccbcd535 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/AsynchronousInstrumentAccumulator.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.AsynchronousInstrument; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.metrics.aggregator.Aggregator; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessor; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +final class AsynchronousInstrumentAccumulator extends AbstractAccumulator { + private final ReentrantLock collectLock = new ReentrantLock(); + private final InstrumentProcessor instrumentProcessor; + private final Runnable metricUpdater; + + static AsynchronousInstrumentAccumulator doubleAsynchronousAccumulator( + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState, + InstrumentDescriptor descriptor, + @Nullable Consumer metricUpdater) { + Aggregator aggregator = + getAggregator(meterProviderSharedState, meterSharedState, descriptor); + InstrumentProcessor instrumentProcessor = + new InstrumentProcessor<>(aggregator, meterProviderSharedState.getStartEpochNanos()); + // TODO: Decide what to do with null updater. + if (metricUpdater == null) { + return new AsynchronousInstrumentAccumulator(instrumentProcessor, () -> {}); + } + + LabelsProcessor labelsProcessor = + getLabelsProcessor(meterProviderSharedState, meterSharedState, descriptor); + AsynchronousInstrument.DoubleResult result = + (value, labels) -> + instrumentProcessor.batch( + labelsProcessor.onLabelsBound(Context.current(), labels), + aggregator.accumulateDouble(value)); + + return new AsynchronousInstrumentAccumulator( + instrumentProcessor, () -> metricUpdater.accept(result)); + } + + static AsynchronousInstrumentAccumulator longAsynchronousAccumulator( + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState, + InstrumentDescriptor descriptor, + @Nullable Consumer metricUpdater) { + Aggregator aggregator = + getAggregator(meterProviderSharedState, meterSharedState, descriptor); + InstrumentProcessor instrumentProcessor = + new InstrumentProcessor<>(aggregator, meterProviderSharedState.getStartEpochNanos()); + // TODO: Decide what to do with null updater. + if (metricUpdater == null) { + return new AsynchronousInstrumentAccumulator(instrumentProcessor, () -> {}); + } + + LabelsProcessor labelsProcessor = + getLabelsProcessor(meterProviderSharedState, meterSharedState, descriptor); + AsynchronousInstrument.LongResult result = + (value, labels) -> + instrumentProcessor.batch( + labelsProcessor.onLabelsBound(Context.current(), labels), + aggregator.accumulateLong(value)); + + return new AsynchronousInstrumentAccumulator( + instrumentProcessor, () -> metricUpdater.accept(result)); + } + + private AsynchronousInstrumentAccumulator( + InstrumentProcessor instrumentProcessor, Runnable metricUpdater) { + this.instrumentProcessor = instrumentProcessor; + this.metricUpdater = metricUpdater; + } + + @Override + List collectAll(long epochNanos) { + collectLock.lock(); + try { + metricUpdater.run(); + return instrumentProcessor.completeCollectionCycle(epochNanos); + } finally { + collectLock.unlock(); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/BatchRecorderSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/BatchRecorderSdk.java new file mode 100644 index 000000000..7dc09bb5f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/BatchRecorderSdk.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.BatchRecorder; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleValueRecorder; +import io.opentelemetry.api.metrics.Instrument; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongValueRecorder; +import io.opentelemetry.api.metrics.common.Labels; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TransferQueue; + +/** + * Minimal implementation of the {@link BatchRecorder} that simply redirects the calls to the + * instruments. + */ +final class BatchRecorderSdk implements BatchRecorder { + private final Labels labelSet; + + // todo: this queue is unbounded; should we make it bounded and drop recordings after it gets + // full? + private final TransferQueue pendingRecordings = new LinkedTransferQueue<>(); + + BatchRecorderSdk(String... keyValuePairs) { + this.labelSet = Labels.of(keyValuePairs); + } + + @Override + public BatchRecorder put(LongValueRecorder valueRecorder, long value) { + pendingRecordings.offer(new LongRecording(valueRecorder, value)); + return this; + } + + @Override + public BatchRecorder put(DoubleValueRecorder valueRecorder, double value) { + pendingRecordings.offer(new DoubleRecording(valueRecorder, value)); + return this; + } + + @Override + public BatchRecorder put(LongCounter counter, long value) { + pendingRecordings.offer(new LongRecording(counter, value)); + return this; + } + + @Override + public BatchRecorder put(DoubleCounter counter, double value) { + pendingRecordings.offer(new DoubleRecording(counter, value)); + return this; + } + + @Override + public BatchRecorder put(LongUpDownCounter upDownCounter, long value) { + pendingRecordings.offer(new LongRecording(upDownCounter, value)); + return this; + } + + @Override + public BatchRecorder put(DoubleUpDownCounter upDownCounter, double value) { + pendingRecordings.offer(new DoubleRecording(upDownCounter, value)); + return this; + } + + @Override + public void record() { + List recordings = new ArrayList<>(); + pendingRecordings.drainTo(recordings); + + recordings.forEach( + (recording) -> { + Instrument instrument = recording.getInstrument(); + if (instrument instanceof DoubleUpDownCounter) { + ((DoubleUpDownCounter) instrument).add(recording.getDoubleValue(), labelSet); + } else if (instrument instanceof DoubleCounter) { + ((DoubleCounter) instrument).add(recording.getDoubleValue(), labelSet); + } else if (instrument instanceof DoubleValueRecorder) { + ((DoubleValueRecorder) instrument).record(recording.getDoubleValue(), labelSet); + } else if (instrument instanceof LongUpDownCounter) { + ((LongUpDownCounter) instrument).add(recording.getLongValue(), labelSet); + } else if (instrument instanceof LongCounter) { + ((LongCounter) instrument).add(recording.getLongValue(), labelSet); + } else if (instrument instanceof LongValueRecorder) { + ((LongValueRecorder) instrument).record(recording.getLongValue(), labelSet); + } + }); + } + + private interface Recording { + Instrument getInstrument(); + + long getLongValue(); + + double getDoubleValue(); + } + + private static class LongRecording implements Recording { + private final Instrument instrument; + private final long value; + + private LongRecording(Instrument instrument, long value) { + this.instrument = instrument; + this.value = value; + } + + @Override + public Instrument getInstrument() { + return instrument; + } + + @Override + public long getLongValue() { + return value; + } + + @Override + public double getDoubleValue() { + throw new UnsupportedOperationException(); + } + } + + private static class DoubleRecording implements Recording { + private final Instrument instrument; + private final double value; + + private DoubleRecording(Instrument instrument, double value) { + this.instrument = instrument; + this.value = value; + } + + @Override + public Instrument getInstrument() { + return instrument; + } + + @Override + public long getLongValue() { + throw new UnsupportedOperationException(); + } + + @Override + public double getDoubleValue() { + return value; + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleCounterSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleCounterSdk.java new file mode 100644 index 000000000..7c0c8b52a --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleCounterSdk.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.BoundDoubleCounter; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleCounterBuilder; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class DoubleCounterSdk extends AbstractSynchronousInstrument implements DoubleCounter { + + private DoubleCounterSdk( + InstrumentDescriptor descriptor, SynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + @Override + public void add(double increment, Labels labels) { + AggregatorHandle aggregatorHandle = acquireHandle(labels); + try { + if (increment < 0) { + throw new IllegalArgumentException("Counters can only increase"); + } + aggregatorHandle.recordDouble(increment); + } finally { + aggregatorHandle.release(); + } + } + + @Override + public void add(double increment) { + add(increment, Labels.empty()); + } + + @Override + public BoundDoubleCounter bind(Labels labels) { + return new BoundInstrument(acquireHandle(labels)); + } + + static final class BoundInstrument implements BoundDoubleCounter { + private final AggregatorHandle aggregatorHandle; + + BoundInstrument(AggregatorHandle aggregatorHandle) { + this.aggregatorHandle = aggregatorHandle; + } + + @Override + public void add(double increment) { + if (increment < 0) { + throw new IllegalArgumentException("Counters can only increase"); + } + aggregatorHandle.recordDouble(increment); + } + + @Override + public void unbind() { + aggregatorHandle.release(); + } + } + + static final class Builder extends AbstractSynchronousInstrumentBuilder + implements DoubleCounterBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.COUNTER, + InstrumentValueType.DOUBLE, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public DoubleCounterSdk build() { + return buildInstrument(DoubleCounterSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleSumObserverSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleSumObserverSdk.java new file mode 100644 index 000000000..9d87bdbfb --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleSumObserverSdk.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.DoubleSumObserver; +import io.opentelemetry.api.metrics.DoubleSumObserverBuilder; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class DoubleSumObserverSdk extends AbstractAsynchronousInstrument + implements DoubleSumObserver { + + DoubleSumObserverSdk( + InstrumentDescriptor descriptor, AsynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + static final class Builder + extends AbstractDoubleAsynchronousInstrumentBuilder + implements DoubleSumObserverBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.SUM_OBSERVER, + InstrumentValueType.DOUBLE, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public DoubleSumObserverSdk build() { + return buildInstrument(DoubleSumObserverSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleUpDownCounterSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleUpDownCounterSdk.java new file mode 100644 index 000000000..9fdb1624f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleUpDownCounterSdk.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.BoundDoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class DoubleUpDownCounterSdk extends AbstractSynchronousInstrument + implements DoubleUpDownCounter { + + private DoubleUpDownCounterSdk( + InstrumentDescriptor descriptor, SynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + @Override + public void add(double increment, Labels labels) { + AggregatorHandle aggregatorHandle = acquireHandle(labels); + try { + aggregatorHandle.recordDouble(increment); + } finally { + aggregatorHandle.release(); + } + } + + @Override + public void add(double increment) { + add(increment, Labels.empty()); + } + + @Override + public BoundDoubleUpDownCounter bind(Labels labels) { + return new BoundInstrument(acquireHandle(labels)); + } + + static final class BoundInstrument implements BoundDoubleUpDownCounter { + private final AggregatorHandle aggregatorHandle; + + BoundInstrument(AggregatorHandle aggregatorHandle) { + this.aggregatorHandle = aggregatorHandle; + } + + @Override + public void add(double increment) { + aggregatorHandle.recordDouble(increment); + } + + @Override + public void unbind() { + aggregatorHandle.release(); + } + } + + static final class Builder + extends AbstractSynchronousInstrumentBuilder + implements DoubleUpDownCounterBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.UP_DOWN_COUNTER, + InstrumentValueType.DOUBLE, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public DoubleUpDownCounterSdk build() { + return buildInstrument(DoubleUpDownCounterSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleUpDownSumObserverSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleUpDownSumObserverSdk.java new file mode 100644 index 000000000..0ce56434f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleUpDownSumObserverSdk.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.DoubleUpDownSumObserver; +import io.opentelemetry.api.metrics.DoubleUpDownSumObserverBuilder; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class DoubleUpDownSumObserverSdk extends AbstractAsynchronousInstrument + implements DoubleUpDownSumObserver { + + DoubleUpDownSumObserverSdk( + InstrumentDescriptor descriptor, AsynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + static final class Builder + extends AbstractDoubleAsynchronousInstrumentBuilder + implements DoubleUpDownSumObserverBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.UP_DOWN_SUM_OBSERVER, + InstrumentValueType.DOUBLE, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public DoubleUpDownSumObserverSdk build() { + return buildInstrument(DoubleUpDownSumObserverSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleValueObserverSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleValueObserverSdk.java new file mode 100644 index 000000000..ef5af0303 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleValueObserverSdk.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.DoubleValueObserver; +import io.opentelemetry.api.metrics.DoubleValueObserverBuilder; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class DoubleValueObserverSdk extends AbstractAsynchronousInstrument + implements DoubleValueObserver { + + DoubleValueObserverSdk( + InstrumentDescriptor descriptor, AsynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + static final class Builder + extends AbstractDoubleAsynchronousInstrumentBuilder + implements DoubleValueObserverBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.VALUE_OBSERVER, + InstrumentValueType.DOUBLE, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public DoubleValueObserverSdk build() { + return buildInstrument(DoubleValueObserverSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleValueRecorderSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleValueRecorderSdk.java new file mode 100644 index 000000000..d6f4deab9 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/DoubleValueRecorderSdk.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.BoundDoubleValueRecorder; +import io.opentelemetry.api.metrics.DoubleValueRecorder; +import io.opentelemetry.api.metrics.DoubleValueRecorderBuilder; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class DoubleValueRecorderSdk extends AbstractSynchronousInstrument + implements DoubleValueRecorder { + + private DoubleValueRecorderSdk( + InstrumentDescriptor descriptor, SynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + @Override + public void record(double value, Labels labels) { + AggregatorHandle aggregatorHandle = acquireHandle(labels); + try { + aggregatorHandle.recordDouble(value); + } finally { + aggregatorHandle.release(); + } + } + + @Override + public void record(double value) { + record(value, Labels.empty()); + } + + @Override + public BoundDoubleValueRecorder bind(Labels labels) { + return new BoundInstrument(acquireHandle(labels)); + } + + static final class BoundInstrument implements BoundDoubleValueRecorder { + private final AggregatorHandle aggregatorHandle; + + BoundInstrument(AggregatorHandle aggregatorHandle) { + this.aggregatorHandle = aggregatorHandle; + } + + @Override + public void record(double value) { + aggregatorHandle.recordDouble(value); + } + + @Override + public void unbind() { + aggregatorHandle.release(); + } + } + + static final class Builder + extends AbstractSynchronousInstrumentBuilder + implements DoubleValueRecorderBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.VALUE_RECORDER, + InstrumentValueType.DOUBLE, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public DoubleValueRecorderSdk build() { + return buildInstrument(DoubleValueRecorderSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentProcessor.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentProcessor.java new file mode 100644 index 000000000..1cd45eeb4 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentProcessor.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.aggregator.Aggregator; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An {@code InstrumentProcessor} represents an internal instance of an {@code Accumulator} for a + * specific {code Instrument}. It records individual measurements (via the {@code Aggregator}). It + * batches together {@code Aggregator}s for the similar sets of labels. + * + *

    An entire collection cycle must be protected by a lock. A collection cycle is defined by + * multiple calls to {@code #batch(...)} followed by one {@code #completeCollectionCycle(...)}; + */ +final class InstrumentProcessor { + private final Aggregator aggregator; + private final long startEpochNanos; + private long lastEpochNanos; + private Map accumulationMap; + + InstrumentProcessor(Aggregator aggregator, long startEpochNanos) { + this.aggregator = aggregator; + this.startEpochNanos = startEpochNanos; + this.lastEpochNanos = startEpochNanos; + this.accumulationMap = new HashMap<>(); + } + + /** + * Batches multiple entries together that are part of the same metric. It may remove labels from + * the {@link Labels} and merge aggregations together. + * + * @param labelSet the {@link Labels} associated with this {@code Aggregator}. + * @param accumulation the accumulation produced by this instrument. + */ + void batch(Labels labelSet, T accumulation) { + T currentAccumulation = accumulationMap.get(labelSet); + if (currentAccumulation == null) { + accumulationMap.put(labelSet, accumulation); + return; + } + accumulationMap.put(labelSet, aggregator.merge(currentAccumulation, accumulation)); + } + + /** + * Ends the current collection cycle and returns the list of metrics batched in this Batcher. + * + *

    There may be more than one MetricData in case a multi aggregator is configured. + * + *

    Based on the configured options this method may reset the internal state to produce deltas, + * or keep the internal state to produce cumulative metrics. + * + * @return the list of metrics batched in this Batcher. + */ + List completeCollectionCycle(long epochNanos) { + if (accumulationMap.isEmpty()) { + return Collections.emptyList(); + } + + MetricData metricData = + aggregator.toMetricData(accumulationMap, startEpochNanos, lastEpochNanos, epochNanos); + + lastEpochNanos = epochNanos; + if (!aggregator.isStateful()) { + accumulationMap = new HashMap<>(); + } + + return metricData == null ? Collections.emptyList() : Collections.singletonList(metricData); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentRegistry.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentRegistry.java new file mode 100644 index 000000000..aaebd6a31 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/InstrumentRegistry.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Basic registry class for metrics instruments. The current implementation allows instruments to be + * registered only once for a given name. + * + *

    TODO: Discuss what is the right behavior when an already registered Instrument with the same + * name is present. TODO: Decide what is the identifier for an Instrument? Only name? + */ +final class InstrumentRegistry { + private final ConcurrentMap registry = new ConcurrentHashMap<>(); + + /** + * Registers the given {@code instrument} to this registry. Returns the registered instrument if + * no other instrument with the same name is registered or a previously registered instrument with + * same name and equal with the current instrument, otherwise throws an exception. + * + * @param instrument the newly created {@code Instrument}. + * @return the given instrument if no instrument with same name already registered, otherwise the + * previous registered instrument. + * @throws IllegalArgumentException if instrument cannot be registered. + */ + @SuppressWarnings("unchecked") + I register(I instrument) { + AbstractInstrument oldInstrument = + registry.putIfAbsent(instrument.getDescriptor().getName().toLowerCase(), instrument); + if (oldInstrument != null) { + if (!instrument.getClass().isInstance(oldInstrument) || !instrument.equals(oldInstrument)) { + throw new IllegalArgumentException( + "Instrument with same name and different descriptor already created."); + } + return (I) oldInstrument; + } + return instrument; + } + + /** + * Returns a {@code Collection} view of the registered instruments. + * + * @return a {@code Collection} view of the registered instruments. + */ + Collection getInstruments() { + return Collections.unmodifiableCollection(new ArrayList<>(registry.values())); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongCounterSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongCounterSdk.java new file mode 100644 index 000000000..79a6662b0 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongCounterSdk.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class LongCounterSdk extends AbstractSynchronousInstrument implements LongCounter { + + private LongCounterSdk( + InstrumentDescriptor descriptor, SynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + @Override + public void add(long increment, Labels labels) { + AggregatorHandle aggregatorHandle = acquireHandle(labels); + try { + if (increment < 0) { + throw new IllegalArgumentException("Counters can only increase"); + } + aggregatorHandle.recordLong(increment); + } finally { + aggregatorHandle.release(); + } + } + + @Override + public void add(long increment) { + add(increment, Labels.empty()); + } + + @Override + public BoundLongCounter bind(Labels labels) { + return new BoundInstrument(acquireHandle(labels)); + } + + static final class BoundInstrument implements BoundLongCounter { + private final AggregatorHandle aggregatorHandle; + + BoundInstrument(AggregatorHandle aggregatorHandle) { + this.aggregatorHandle = aggregatorHandle; + } + + @Override + public void add(long increment) { + if (increment < 0) { + throw new IllegalArgumentException("Counters can only increase"); + } + aggregatorHandle.recordLong(increment); + } + + @Override + public void unbind() { + aggregatorHandle.release(); + } + } + + static final class Builder extends AbstractSynchronousInstrumentBuilder + implements LongCounterBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.COUNTER, + InstrumentValueType.LONG, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public LongCounterSdk build() { + return buildInstrument(LongCounterSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongSumObserverSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongSumObserverSdk.java new file mode 100644 index 000000000..8ed14be05 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongSumObserverSdk.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.LongSumObserver; +import io.opentelemetry.api.metrics.LongSumObserverBuilder; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class LongSumObserverSdk extends AbstractAsynchronousInstrument implements LongSumObserver { + + LongSumObserverSdk( + InstrumentDescriptor descriptor, AsynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + static final class Builder + extends AbstractLongAsynchronousInstrumentBuilder + implements LongSumObserverBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.SUM_OBSERVER, + InstrumentValueType.LONG, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public LongSumObserverSdk build() { + return buildInstrument(LongSumObserverSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongUpDownCounterSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongUpDownCounterSdk.java new file mode 100644 index 000000000..975fcad08 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongUpDownCounterSdk.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.BoundLongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class LongUpDownCounterSdk extends AbstractSynchronousInstrument + implements LongUpDownCounter { + + private LongUpDownCounterSdk( + InstrumentDescriptor descriptor, SynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + @Override + public void add(long increment, Labels labels) { + AggregatorHandle aggregatorHandle = acquireHandle(labels); + try { + aggregatorHandle.recordLong(increment); + } finally { + aggregatorHandle.release(); + } + } + + @Override + public void add(long increment) { + add(increment, Labels.empty()); + } + + @Override + public BoundLongUpDownCounter bind(Labels labels) { + return new BoundInstrument(acquireHandle(labels)); + } + + static final class BoundInstrument implements BoundLongUpDownCounter { + private final AggregatorHandle aggregatorHandle; + + BoundInstrument(AggregatorHandle aggregatorHandle) { + this.aggregatorHandle = aggregatorHandle; + } + + @Override + public void add(long increment) { + aggregatorHandle.recordLong(increment); + } + + @Override + public void unbind() { + aggregatorHandle.release(); + } + } + + static final class Builder + extends AbstractSynchronousInstrumentBuilder + implements LongUpDownCounterBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.UP_DOWN_COUNTER, + InstrumentValueType.LONG, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public LongUpDownCounterSdk build() { + return buildInstrument(LongUpDownCounterSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongUpDownSumObserverSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongUpDownSumObserverSdk.java new file mode 100644 index 000000000..cb79e12c0 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongUpDownSumObserverSdk.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.LongUpDownSumObserver; +import io.opentelemetry.api.metrics.LongUpDownSumObserverBuilder; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class LongUpDownSumObserverSdk extends AbstractAsynchronousInstrument + implements LongUpDownSumObserver { + + LongUpDownSumObserverSdk( + InstrumentDescriptor descriptor, AsynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + static final class Builder + extends AbstractLongAsynchronousInstrumentBuilder + implements LongUpDownSumObserverBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.UP_DOWN_SUM_OBSERVER, + InstrumentValueType.LONG, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public LongUpDownSumObserverSdk build() { + return buildInstrument(LongUpDownSumObserverSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongValueObserverSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongValueObserverSdk.java new file mode 100644 index 000000000..59c44e118 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongValueObserverSdk.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.LongValueObserver; +import io.opentelemetry.api.metrics.LongValueObserverBuilder; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class LongValueObserverSdk extends AbstractAsynchronousInstrument + implements LongValueObserver { + + LongValueObserverSdk( + InstrumentDescriptor descriptor, AsynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + static final class Builder + extends AbstractLongAsynchronousInstrumentBuilder + implements LongValueObserverBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.VALUE_OBSERVER, + InstrumentValueType.LONG, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public LongValueObserverSdk build() { + return buildInstrument(LongValueObserverSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongValueRecorderSdk.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongValueRecorderSdk.java new file mode 100644 index 000000000..e8182cfe5 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/LongValueRecorderSdk.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.BoundLongValueRecorder; +import io.opentelemetry.api.metrics.LongValueRecorder; +import io.opentelemetry.api.metrics.LongValueRecorderBuilder; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; + +final class LongValueRecorderSdk extends AbstractSynchronousInstrument + implements LongValueRecorder { + + private LongValueRecorderSdk( + InstrumentDescriptor descriptor, SynchronousInstrumentAccumulator accumulator) { + super(descriptor, accumulator); + } + + @Override + public void record(long value, Labels labels) { + AggregatorHandle aggregatorHandle = acquireHandle(labels); + try { + aggregatorHandle.recordLong(value); + } finally { + aggregatorHandle.release(); + } + } + + @Override + public void record(long value) { + record(value, Labels.empty()); + } + + @Override + public BoundLongValueRecorder bind(Labels labels) { + return new BoundInstrument(acquireHandle(labels)); + } + + static final class BoundInstrument implements BoundLongValueRecorder { + private final AggregatorHandle aggregatorHandle; + + BoundInstrument(AggregatorHandle aggregatorHandle) { + this.aggregatorHandle = aggregatorHandle; + } + + @Override + public void record(long value) { + aggregatorHandle.recordLong(value); + } + + @Override + public void unbind() { + aggregatorHandle.release(); + } + } + + static final class Builder + extends AbstractSynchronousInstrumentBuilder + implements LongValueRecorderBuilder { + + Builder( + String name, + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState) { + super( + name, + InstrumentType.VALUE_RECORDER, + InstrumentValueType.LONG, + meterProviderSharedState, + meterSharedState); + } + + @Override + Builder getThis() { + return this; + } + + @Override + public LongValueRecorderSdk build() { + return buildInstrument(LongValueRecorderSdk::new); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterProviderSharedState.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterProviderSharedState.java new file mode 100644 index 000000000..cf2182840 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterProviderSharedState.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@Immutable +abstract class MeterProviderSharedState { + static MeterProviderSharedState create( + Clock clock, Resource resource, ViewRegistry viewRegistry) { + return new AutoValue_MeterProviderSharedState(clock, resource, viewRegistry, clock.now()); + } + + abstract Clock getClock(); + + abstract Resource getResource(); + + abstract ViewRegistry getViewRegistry(); + + abstract long getStartEpochNanos(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterSharedState.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterSharedState.java new file mode 100644 index 000000000..b73a32a3a --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/MeterSharedState.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@Immutable +abstract class MeterSharedState { + static MeterSharedState create(InstrumentationLibraryInfo instrumentationLibraryInfo) { + return new AutoValue_MeterSharedState(instrumentationLibraryInfo, new InstrumentRegistry()); + } + + abstract InstrumentationLibraryInfo getInstrumentationLibraryInfo(); + + abstract InstrumentRegistry getInstrumentRegistry(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java new file mode 100644 index 000000000..b4cabce43 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeter.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** {@link SdkMeter} is SDK implementation of {@link Meter}. */ +final class SdkMeter implements Meter { + private final MeterProviderSharedState meterProviderSharedState; + private final MeterSharedState meterSharedState; + + SdkMeter( + MeterProviderSharedState meterProviderSharedState, + InstrumentationLibraryInfo instrumentationLibraryInfo) { + this.meterProviderSharedState = meterProviderSharedState; + this.meterSharedState = MeterSharedState.create(instrumentationLibraryInfo); + } + + InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return meterSharedState.getInstrumentationLibraryInfo(); + } + + @Override + public DoubleCounterSdk.Builder doubleCounterBuilder(String name) { + return new DoubleCounterSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public LongCounterSdk.Builder longCounterBuilder(String name) { + return new LongCounterSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public DoubleUpDownCounterSdk.Builder doubleUpDownCounterBuilder(String name) { + return new DoubleUpDownCounterSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public LongUpDownCounterSdk.Builder longUpDownCounterBuilder(String name) { + return new LongUpDownCounterSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public DoubleValueRecorderSdk.Builder doubleValueRecorderBuilder(String name) { + return new DoubleValueRecorderSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public LongValueRecorderSdk.Builder longValueRecorderBuilder(String name) { + return new LongValueRecorderSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public DoubleSumObserverSdk.Builder doubleSumObserverBuilder(String name) { + return new DoubleSumObserverSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public LongSumObserverSdk.Builder longSumObserverBuilder(String name) { + return new LongSumObserverSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public DoubleUpDownSumObserverSdk.Builder doubleUpDownSumObserverBuilder(String name) { + return new DoubleUpDownSumObserverSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public LongUpDownSumObserverSdk.Builder longUpDownSumObserverBuilder(String name) { + return new LongUpDownSumObserverSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public DoubleValueObserverSdk.Builder doubleValueObserverBuilder(String name) { + return new DoubleValueObserverSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public LongValueObserverSdk.Builder longValueObserverBuilder(String name) { + return new LongValueObserverSdk.Builder(name, meterProviderSharedState, meterSharedState); + } + + @Override + public BatchRecorderSdk newBatchRecorder(String... keyValuePairs) { + return new BatchRecorderSdk(keyValuePairs); + } + + /** Collects all the metric recordings that changed since the previous call. */ + Collection collectAll(long epochNanos) { + InstrumentRegistry instrumentRegistry = meterSharedState.getInstrumentRegistry(); + Collection instruments = instrumentRegistry.getInstruments(); + List result = new ArrayList<>(instruments.size()); + for (AbstractInstrument instrument : instruments) { + result.addAll(instrument.collectAll(epochNanos)); + } + return result; + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java new file mode 100644 index 000000000..a420b865f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.internal.ComponentRegistry; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricProducer; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * {@code SdkMeterProvider} implementation for {@link MeterProvider}. + * + *

    This class is not intended to be used in application code and it is used only by {@link + * OpenTelemetry}. + * + *

    WARNING: A MetricProducer is stateful. It will only return changes since the last time it was + * accessed. This means that if more than one {@link + * io.opentelemetry.sdk.metrics.export.MetricExporter} has a handle to this MetricProducer, the two + * exporters will not receive copies of the same metric data to export. + */ +public final class SdkMeterProvider implements MeterProvider, MetricProducer { + + private static final Logger LOGGER = Logger.getLogger(SdkMeterProvider.class.getName()); + static final String DEFAULT_METER_NAME = "unknown"; + private final ComponentRegistry registry; + private final MeterProviderSharedState sharedState; + + SdkMeterProvider(Clock clock, Resource resource, ViewRegistry viewRegistry) { + this.sharedState = MeterProviderSharedState.create(clock, resource, viewRegistry); + this.registry = + new ComponentRegistry<>( + instrumentationLibraryInfo -> new SdkMeter(sharedState, instrumentationLibraryInfo)); + } + + @Override + public Meter get(String instrumentationName) { + return get(instrumentationName, null); + } + + @Override + public Meter get(String instrumentationName, @Nullable String instrumentationVersion) { + // Per the spec, both null and empty are "invalid" and a "default" should be used. + if (instrumentationName == null || instrumentationName.isEmpty()) { + LOGGER.fine("Meter requested without instrumentation name."); + instrumentationName = DEFAULT_METER_NAME; + } + return registry.get(instrumentationName, instrumentationVersion); + } + + @Override + public Collection collectAllMetrics() { + Collection meters = registry.getComponents(); + List result = new ArrayList<>(meters.size()); + for (SdkMeter meter : meters) { + result.addAll(meter.collectAll(sharedState.getClock().now())); + } + return Collections.unmodifiableCollection(result); + } + + /** + * Returns a new {@link SdkMeterProviderBuilder} for {@link SdkMeterProvider}. + * + * @return a new {@link SdkMeterProviderBuilder} for {@link SdkMeterProvider}. + */ + public static SdkMeterProviderBuilder builder() { + return new SdkMeterProviderBuilder(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java new file mode 100644 index 000000000..723bcbc9c --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.internal.SystemClock; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import io.opentelemetry.sdk.resources.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Builder class for the {@link SdkMeterProvider}. Has fully functional default implementations of + * all three required interfaces. + */ +public final class SdkMeterProviderBuilder { + + private Clock clock = SystemClock.getInstance(); + private Resource resource = Resource.getDefault(); + private final Map instrumentSelectorViews = new HashMap<>(); + + SdkMeterProviderBuilder() {} + + /** + * Assign a {@link Clock}. + * + * @param clock The clock to use for all temporal needs. + * @return this + */ + public SdkMeterProviderBuilder setClock(Clock clock) { + Objects.requireNonNull(clock, "clock"); + this.clock = clock; + return this; + } + + /** + * Assign a {@link Resource} to be attached to all Spans created by Tracers. + * + * @param resource A Resource implementation. + * @return this + */ + public SdkMeterProviderBuilder setResource(Resource resource) { + Objects.requireNonNull(resource, "resource"); + this.resource = resource; + return this; + } + + /** + * Register a view with the given {@link InstrumentSelector}. + * + *

    Example on how to register a view: + * + *

    {@code
    +   * // create a SdkMeterProviderBuilder
    +   * SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder();
    +   *
    +   * // create a selector to select which instruments to customize:
    +   * InstrumentSelector instrumentSelector = InstrumentSelector.builder()
    +   *   .setInstrumentType(InstrumentType.COUNTER)
    +   *   .build();
    +   *
    +   * // create a specification of how you want the metrics aggregated:
    +   * AggregatorFactory aggregatorFactory = AggregatorFactory.minMaxSumCount();
    +   *
    +   * // register the view with the SdkMeterProviderBuilder
    +   * meterProviderBuilder.registerView(instrumentSelector, View.builder()
    +   *   .setAggregatorFactory(aggregatorFactory).build());
    +   * }
    + * + * @since 1.1.0 + */ + public SdkMeterProviderBuilder registerView(InstrumentSelector selector, View view) { + Objects.requireNonNull(selector, "selector"); + Objects.requireNonNull(view, "view"); + instrumentSelectorViews.put(selector, view); + return this; + } + + /** + * Returns a new {@link SdkMeterProvider} built with the configuration of this {@link + * SdkMeterProviderBuilder} and registers it as the global {@link + * io.opentelemetry.api.metrics.MeterProvider}. + * + * @see GlobalMeterProvider + */ + public SdkMeterProvider buildAndRegisterGlobal() { + SdkMeterProvider meterProvider = build(); + GlobalMeterProvider.set(meterProvider); + return meterProvider; + } + + /** + * Returns a new {@link SdkMeterProvider} built with the configuration of this {@link + * SdkMeterProviderBuilder}. This provider is not registered as the global {@link + * io.opentelemetry.api.metrics.MeterProvider}. It is recommended that you register one provider + * using {@link SdkMeterProviderBuilder#buildAndRegisterGlobal()} for use by instrumentation when + * that requires access to a global instance of {@link + * io.opentelemetry.api.metrics.MeterProvider}. + * + * @see GlobalMeterProvider + */ + public SdkMeterProvider build() { + ViewRegistryBuilder viewRegistryBuilder = ViewRegistry.builder(); + instrumentSelectorViews.forEach(viewRegistryBuilder::addView); + ViewRegistry viewRegistry = viewRegistryBuilder.build(); + return new SdkMeterProvider(clock, resource, viewRegistry); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentAccumulator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentAccumulator.java new file mode 100644 index 000000000..90b08637b --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentAccumulator.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.metrics.aggregator.Aggregator; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessor; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +final class SynchronousInstrumentAccumulator extends AbstractAccumulator { + private final ConcurrentHashMap> aggregatorLabels; + private final ReentrantLock collectLock; + private final Aggregator aggregator; + private final InstrumentProcessor instrumentProcessor; + private final LabelsProcessor labelsProcessor; + + static SynchronousInstrumentAccumulator create( + MeterProviderSharedState meterProviderSharedState, + MeterSharedState meterSharedState, + InstrumentDescriptor descriptor) { + Aggregator aggregator = + getAggregator(meterProviderSharedState, meterSharedState, descriptor); + return new SynchronousInstrumentAccumulator<>( + aggregator, + new InstrumentProcessor<>(aggregator, meterProviderSharedState.getStartEpochNanos()), + getLabelsProcessor(meterProviderSharedState, meterSharedState, descriptor)); + } + + SynchronousInstrumentAccumulator( + Aggregator aggregator, + InstrumentProcessor instrumentProcessor, + LabelsProcessor labelsProcessor) { + aggregatorLabels = new ConcurrentHashMap<>(); + collectLock = new ReentrantLock(); + this.aggregator = aggregator; + this.instrumentProcessor = instrumentProcessor; + this.labelsProcessor = labelsProcessor; + } + + AggregatorHandle bind(Labels labels) { + Objects.requireNonNull(labels, "labels"); + labels = labelsProcessor.onLabelsBound(Context.current(), labels); + AggregatorHandle aggregatorHandle = aggregatorLabels.get(labels); + if (aggregatorHandle != null && aggregatorHandle.acquire()) { + // At this moment it is guaranteed that the Bound is in the map and will not be removed. + return aggregatorHandle; + } + + // Missing entry or no longer mapped, try to add a new entry. + aggregatorHandle = aggregator.createHandle(); + while (true) { + AggregatorHandle boundAggregatorHandle = + aggregatorLabels.putIfAbsent(labels, aggregatorHandle); + if (boundAggregatorHandle != null) { + if (boundAggregatorHandle.acquire()) { + // At this moment it is guaranteed that the Bound is in the map and will not be removed. + return boundAggregatorHandle; + } + // Try to remove the boundAggregator. This will race with the collect method, but only one + // will succeed. + aggregatorLabels.remove(labels, boundAggregatorHandle); + continue; + } + return aggregatorHandle; + } + } + + @Override + List collectAll(long epochNanos) { + collectLock.lock(); + try { + for (Map.Entry> entry : aggregatorLabels.entrySet()) { + boolean unmappedEntry = entry.getValue().tryUnmap(); + if (unmappedEntry) { + // If able to unmap then remove the record from the current Map. This can race with the + // acquire but because we requested a specific value only one will succeed. + aggregatorLabels.remove(entry.getKey(), entry.getValue()); + } + T accumulation = entry.getValue().accumulateThenReset(); + if (accumulation == null) { + continue; + } + instrumentProcessor.batch(entry.getKey(), accumulation); + } + return instrumentProcessor.completeCollectionCycle(epochNanos); + } finally { + collectLock.unlock(); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewRegistry.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewRegistry.java new file mode 100644 index 000000000..5f778fbd1 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewRegistry.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.view.View; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; +import javax.annotation.concurrent.Immutable; + +/** + * Central location for Views to be registered. Registration of a view is done via the {@link + * SdkMeterProviderBuilder}. + */ +@Immutable +final class ViewRegistry { + static final View CUMULATIVE_SUM = + View.builder() + .setAggregatorFactory(AggregatorFactory.sum(AggregationTemporality.CUMULATIVE)) + .build(); + static final View SUMMARY = + View.builder().setAggregatorFactory(AggregatorFactory.minMaxSumCount()).build(); + static final View LAST_VALUE = + View.builder().setAggregatorFactory(AggregatorFactory.lastValue()).build(); + + private final EnumMap> configuration; + + ViewRegistry(EnumMap> configuration) { + this.configuration = new EnumMap<>(InstrumentType.class); + // make a copy for safety + configuration.forEach( + (instrumentType, patternViewLinkedHashMap) -> + this.configuration.put(instrumentType, new LinkedHashMap<>(patternViewLinkedHashMap))); + } + + static ViewRegistryBuilder builder() { + return new ViewRegistryBuilder(); + } + + View findView(InstrumentDescriptor descriptor) { + LinkedHashMap configPerType = configuration.get(descriptor.getType()); + for (Map.Entry entry : configPerType.entrySet()) { + if (entry.getKey().matcher(descriptor.getName()).matches()) { + return entry.getValue(); + } + } + + return getDefaultSpecification(descriptor); + } + + private static View getDefaultSpecification(InstrumentDescriptor descriptor) { + switch (descriptor.getType()) { + case COUNTER: + case UP_DOWN_COUNTER: + case SUM_OBSERVER: + case UP_DOWN_SUM_OBSERVER: + return CUMULATIVE_SUM; + case VALUE_RECORDER: + return SUMMARY; + case VALUE_OBSERVER: + return LAST_VALUE; + } + throw new IllegalArgumentException("Unknown descriptor type: " + descriptor.getType()); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewRegistryBuilder.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewRegistryBuilder.java new file mode 100644 index 000000000..3131cc08c --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewRegistryBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.regex.Pattern; + +class ViewRegistryBuilder { + private final EnumMap> configuration = + new EnumMap<>(InstrumentType.class); + private static final LinkedHashMap EMPTY_CONFIG = new LinkedHashMap<>(); + + ViewRegistryBuilder() { + for (InstrumentType type : InstrumentType.values()) { + configuration.put(type, EMPTY_CONFIG); + } + } + + ViewRegistry build() { + return new ViewRegistry(configuration); + } + + ViewRegistryBuilder addView(InstrumentSelector selector, View view) { + LinkedHashMap parentConfiguration = + configuration.get(selector.getInstrumentType()); + configuration.put( + selector.getInstrumentType(), + newLinkedHashMap(selector.getInstrumentNamePattern(), view, parentConfiguration)); + return this; + } + + private static LinkedHashMap newLinkedHashMap( + Pattern pattern, View view, LinkedHashMap parentConfiguration) { + LinkedHashMap result = new LinkedHashMap<>(); + result.put(pattern, view); + result.putAll(parentConfiguration); + return result; + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractAggregator.java new file mode 100644 index 000000000..ecb16938d --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractAggregator.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.resources.Resource; + +public abstract class AbstractAggregator implements Aggregator { + private final Resource resource; + private final InstrumentationLibraryInfo instrumentationLibraryInfo; + private final InstrumentDescriptor instrumentDescriptor; + private final boolean stateful; + + protected AbstractAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor instrumentDescriptor, + boolean stateful) { + this.resource = resource; + this.instrumentationLibraryInfo = instrumentationLibraryInfo; + this.instrumentDescriptor = instrumentDescriptor; + this.stateful = stateful; + } + + @Override + public boolean isStateful() { + return stateful; + } + + protected final Resource getResource() { + return resource; + } + + protected final InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return instrumentationLibraryInfo; + } + + protected final InstrumentDescriptor getInstrumentDescriptor() { + return instrumentDescriptor; + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractMinMaxSumCountAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractMinMaxSumCountAggregator.java new file mode 100644 index 000000000..300e66e91 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractMinMaxSumCountAggregator.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Map; + +abstract class AbstractMinMaxSumCountAggregator + extends AbstractAggregator { + + AbstractMinMaxSumCountAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor instrumentDescriptor) { + super(resource, instrumentationLibraryInfo, instrumentDescriptor, /* stateful= */ false); + } + + @Override + public final MinMaxSumCountAccumulation merge( + MinMaxSumCountAccumulation a1, MinMaxSumCountAccumulation a2) { + return MinMaxSumCountAccumulation.create( + a1.getCount() + a2.getCount(), + a1.getSum() + a2.getSum(), + Math.min(a1.getMin(), a2.getMin()), + Math.max(a1.getMax(), a2.getMax())); + } + + @Override + public final MetricData toMetricData( + Map accumulationByLabels, + long startEpochNanos, + long lastCollectionEpoch, + long epochNanos) { + return MetricData.createDoubleSummary( + getResource(), + getInstrumentationLibraryInfo(), + getInstrumentDescriptor().getName(), + getInstrumentDescriptor().getDescription(), + getInstrumentDescriptor().getUnit(), + DoubleSummaryData.create( + MetricDataUtils.toDoubleSummaryPointList( + accumulationByLabels, lastCollectionEpoch, epochNanos))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractSumAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractSumAggregator.java new file mode 100644 index 000000000..92dbe4b38 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AbstractSumAggregator.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.resources.Resource; + +abstract class AbstractSumAggregator extends AbstractAggregator { + private final boolean isMonotonic; + private final AggregationTemporality temporality; + private final MergeStrategy mergeStrategy; + + AbstractSumAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor instrumentDescriptor, + AggregationTemporality temporality) { + super( + resource, + instrumentationLibraryInfo, + instrumentDescriptor, + resolveStateful(instrumentDescriptor.getType(), temporality)); + InstrumentType type = instrumentDescriptor.getType(); + this.isMonotonic = type == InstrumentType.COUNTER || type == InstrumentType.SUM_OBSERVER; + this.temporality = temporality; + this.mergeStrategy = resolveMergeStrategy(type, temporality); + } + + /** + * Resolve whether the aggregator should be stateful. For the special case {@link + * InstrumentType#SUM_OBSERVER} and {@link InstrumentType#UP_DOWN_SUM_OBSERVER} instruments, state + * is required if temporality is {@link AggregationTemporality#DELTA}. Because the observed values + * are cumulative sums, we must maintain state to compute delta sums between collections. For + * other instruments, state is required if temporality is {@link + * AggregationTemporality#CUMULATIVE}. + * + * @param instrumentType the instrument type + * @param temporality the temporality + * @return whether the aggregator is stateful + */ + private static boolean resolveStateful( + InstrumentType instrumentType, AggregationTemporality temporality) { + if (instrumentType == InstrumentType.SUM_OBSERVER + || instrumentType == InstrumentType.UP_DOWN_SUM_OBSERVER) { + return temporality == AggregationTemporality.DELTA; + } else { + return temporality == AggregationTemporality.CUMULATIVE; + } + } + + /** + * Resolve the aggregator merge strategy. The merge strategy is SUM in all cases except where + * temporality is {@link AggregationTemporality#DELTA} and instrument type is {@link + * InstrumentType#SUM_OBSERVER} or {@link InstrumentType#UP_DOWN_SUM_OBSERVER}. In these special + * cases, the observed values are cumulative sums so we must take a diff to compute the delta sum. + * + * @param instrumentType the instrument type + * @param temporality the temporality + * @return the merge strategy + */ + // Visible for testing + static MergeStrategy resolveMergeStrategy( + InstrumentType instrumentType, AggregationTemporality temporality) { + if ((instrumentType == InstrumentType.SUM_OBSERVER + || instrumentType == InstrumentType.UP_DOWN_SUM_OBSERVER) + && temporality == AggregationTemporality.DELTA) { + return MergeStrategy.DIFF; + } else { + return MergeStrategy.SUM; + } + } + + @Override + public final T merge(T previousAccumulation, T accumulation) { + switch (mergeStrategy) { + case SUM: + return mergeSum(previousAccumulation, accumulation); + case DIFF: + return mergeDiff(previousAccumulation, accumulation); + } + throw new IllegalStateException("Unsupported merge strategy: " + mergeStrategy.name()); + } + + abstract T mergeSum(T previousAccumulation, T accumulation); + + abstract T mergeDiff(T previousAccumulation, T accumulation); + + final boolean isMonotonic() { + return isMonotonic; + } + + final AggregationTemporality temporality() { + return temporality; + } + + enum MergeStrategy { + SUM, + DIFF + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/Aggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/Aggregator.java new file mode 100644 index 000000000..a3e0340c3 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/Aggregator.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import java.util.Map; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Aggregator represents the abstract class for all the available aggregations that can be computed + * during the accumulation phase for all the instrument. + * + *

    The synchronous instruments will create an {@link AggregatorHandle} to record individual + * measurements synchronously, and for asynchronous the {@link #accumulateDouble(double)} or {@link + * #accumulateLong(long)} will be used when reading values from the instrument callbacks. + */ +@Immutable +public interface Aggregator { + /** + * Returns a new {@link AggregatorHandle}. This MUST by used by the synchronous to aggregate + * recorded measurements during the collection cycle. + * + * @return a new {@link AggregatorHandle}. + */ + AggregatorHandle createHandle(); + + /** + * Returns a new {@code Accumulation} for the given value. This MUST be used by the asynchronous + * instruments to create {@code Accumulation} that are passed to the processor. + * + * @param value the given value to be used to create the {@code Accumulation}. + * @return a new {@code Accumulation} for the given value. + */ + default T accumulateLong(long value) { + throw new UnsupportedOperationException( + "This aggregator does not support recording long values."); + } + + /** + * Returns a new {@code Accumulation} for the given value. This MUST be used by the asynchronous + * instruments to create {@code Accumulation} that are passed to the processor. + * + * @param value the given value to be used to create the {@code Accumulation}. + * @return a new {@code Accumulation} for the given value. + */ + default T accumulateDouble(double value) { + throw new UnsupportedOperationException( + "This aggregator does not support recording double values."); + } + + /** + * Returns the result of the merge of the given accumulations. + * + * @param previousAccumulation the previously captured accumulation + * @param accumulation the newly captured accumulation + * @return the result of the merge of the given accumulations. + */ + T merge(T previousAccumulation, T accumulation); + + /** + * Returns {@code true} if the processor needs to keep the previous collected state in order to + * compute the desired metric. + * + * @return {@code true} if the processor needs to keep the previous collected state. + */ + boolean isStateful(); + + /** + * Returns the {@link MetricData} that this {@code Aggregation} will produce. + * + * @param accumulationByLabels the map of Labels to Accumulation. + * @param startEpochNanos the startEpochNanos for the {@code Point}. + * @param epochNanos the epochNanos for the {@code Point}. + * @return the {@link MetricDataType} that this {@code Aggregation} will produce. + */ + @Nullable + MetricData toMetricData( + Map accumulationByLabels, + long startEpochNanos, + long lastCollectionEpoch, + long epochNanos); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorFactory.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorFactory.java new file mode 100644 index 000000000..d66711e49 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorFactory.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.resources.Resource; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** Factory class for {@link Aggregator}. */ +@Immutable +public interface AggregatorFactory { + /** + * Returns an {@code AggregationFactory} that calculates sum of recorded measurements. + * + *

    This factory produces {@link Aggregator} that will always produce Sum metrics, the + * monotonicity is determined based on the instrument type (for Counter and SumObserver will be + * monotonic, otherwise not). + * + * @param alwaysCumulative configures to always produce {@link AggregationTemporality#CUMULATIVE} + * if {@code true} OR {@link AggregationTemporality#DELTA} for all types except SumObserver + * and UpDownSumObserver which will always produce {@link AggregationTemporality#CUMULATIVE}. + * @return an {@code AggregationFactory} that calculates sum of recorded measurements. + * @deprecated Use {@link AggregatorFactory#sum(AggregationTemporality)} + */ + @Deprecated + static AggregatorFactory sum(boolean alwaysCumulative) { + return new SumAggregatorFactory( + alwaysCumulative ? AggregationTemporality.CUMULATIVE : AggregationTemporality.DELTA); + } + + /** + * Returns an {@code AggregationFactory} that calculates sum of recorded measurements. + * + *

    This factory produces {@link Aggregator} that will always produce Sum metrics, the + * monotonicity is determined based on the instrument type (for Counter and SumObserver will be + * monotonic, otherwise not). + * + * @param temporality configures what temporality to be produced for the Sum metrics. + * @return an {@code AggregationFactory} that calculates sum of recorded measurements. + * @since 1.2.0 + */ + static AggregatorFactory sum(AggregationTemporality temporality) { + return new SumAggregatorFactory(temporality); + } + + /** + * Returns an {@code AggregationFactory} that calculates count of recorded measurements (the + * number of recorded measurements). + * + *

    This factory produces {@link Aggregator} that will always produce monotonic Sum metrics + * independent of the instrument type. The sum represents the number of measurements recorded. + * + * @param temporality configures what temporality to be produced for the Sum metrics. + * @return an {@code AggregationFactory} that calculates count of recorded measurements (the + * number of recorded * measurements). + */ + static AggregatorFactory count(AggregationTemporality temporality) { + return new CountAggregatorFactory(temporality); + } + + /** + * Returns an {@code AggregationFactory} that calculates the last value of all recorded + * measurements. + * + *

    This factory produces {@link Aggregator} that will always produce gauge metrics independent + * of the instrument type. + * + *

    Limitation: The current implementation does not store a time when the value was recorded, so + * merging multiple LastValueAggregators will not preserve the ordering of records. + * + * @return an {@code AggregationFactory} that calculates the last value of all recorded + * measurements. + */ + static AggregatorFactory lastValue() { + return LastValueAggregatorFactory.INSTANCE; + } + + /** + * Returns an {@code AggregationFactory} that calculates a simple summary of all recorded + * measurements. The summary consists of the count of measurements, the sum of all measurements, + * the maximum value recorded and the minimum value recorded. + * + *

    This factory produces {@link Aggregator} that will always produce double summary metrics + * independent of the instrument type. + * + * @return an {@code AggregationFactory} that calculates a simple summary of all recorded + * measurements. + */ + static AggregatorFactory minMaxSumCount() { + return MinMaxSumCountAggregatorFactory.INSTANCE; + } + + /** + * Returns an {@code AggregatorFactory} that calculates an approximation of the distribution of + * the measurements taken. + * + * @param temporality configures what temporality to be produced for the Histogram metrics. + * @param boundaries configures the fixed bucket boundaries. + * @return an {@code AggregationFactory} that calculates histogram of recorded measurements. + * @since 1.1.0 + */ + static AggregatorFactory histogram(List boundaries, AggregationTemporality temporality) { + return new HistogramAggregatorFactory(boundaries, temporality); + } + + /** + * Returns a new {@link Aggregator}. + * + * @param resource the Resource associated with the {@code Instrument} that will record + * measurements. + * @param instrumentationLibraryInfo the InstrumentationLibraryInfo associated with the {@code + * Instrument} that will record measurements. + * @param descriptor the descriptor of the {@code Instrument} that will record measurements. + * @return a new {@link Aggregator}. + */ + Aggregator create( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorHandle.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorHandle.java new file mode 100644 index 000000000..6b6114be8 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorHandle.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Aggregator represents the abstract class that is used for synchronous instruments. It must be + * thread-safe and avoid locking when possible, because values are recorded synchronously on the + * calling thread. + * + *

    An {@link AggregatorHandle} must be created for every unique {@code LabelSet} recorded, and + * can be referenced by the bound instruments. + * + *

    It atomically counts the number of references (usages) while also keeping a state of + * mapped/unmapped into an external map. It uses an atomic value where the least significant bit is + * used to keep the state of mapping ('1' is used for unmapped and '0' is for mapped) and the rest + * of the bits are used for reference (usage) counting. + */ +@ThreadSafe +public abstract class AggregatorHandle { + // Atomically counts the number of references (usages) while also keeping a state of + // mapped/unmapped into a registry map. + private final AtomicLong refCountMapped; + // Note: This is not 100% thread-safe. There is a race condition where recordings can + // be made in the moment between the reset and the setting of this field's value. In those + // cases, it is possible that a recording could be missed in a given recording interval, but + // it should be picked up in the next, assuming that more recordings are being made. + private volatile boolean hasRecordings = false; + + protected AggregatorHandle() { + // Start with this binding already bound. + this.refCountMapped = new AtomicLong(2); + } + + /** + * Acquires this {@code Aggregator} for use. Returns {@code true} if the entry is still mapped and + * increases the reference usages, if unmapped returns {@code false}. + * + * @return {@code true} if successful. + */ + public final boolean acquire() { + // Every reference adds/removes 2 instead of 1 to avoid changing the mapping bit. + return (refCountMapped.addAndGet(2L) & 1L) == 0; + } + + /** Release this {@code Aggregator}. It decreases the reference usage. */ + public final void release() { + // Every reference adds/removes 2 instead of 1 to avoid changing the mapping bit. + refCountMapped.getAndAdd(-2L); + } + + /** + * Flips the mapped bit to "unmapped" state and returns true if both of the following conditions + * are true upon entry to this function: 1) There are no active references; 2) The mapped bit is + * in "mapped" state; otherwise no changes are done to mapped bit and false is returned. + * + * @return {@code true} if successful. + */ + public final boolean tryUnmap() { + if (refCountMapped.get() != 0) { + // Still references (usages) to this bound or already unmapped. + return false; + } + return refCountMapped.compareAndSet(0L, 1L); + } + + /** + * Returns the current value into as {@link T} and resets the current value in this {@code + * Aggregator}. + */ + @Nullable + public final T accumulateThenReset() { + if (!hasRecordings) { + return null; + } + hasRecordings = false; + return doAccumulateThenReset(); + } + + /** Implementation of the {@code accumulateThenReset}. */ + protected abstract T doAccumulateThenReset(); + + /** + * Updates the current aggregator with a newly recorded {@code long} value. + * + * @param value the new {@code long} value to be added. + */ + public final void recordLong(long value) { + doRecordLong(value); + hasRecordings = true; + } + + /** + * Concrete Aggregator instances should implement this method in order support recordings of long + * values. + */ + protected void doRecordLong(long value) { + throw new UnsupportedOperationException( + "This aggregator does not support recording long values."); + } + + /** + * Updates the current aggregator with a newly recorded {@code double} value. + * + * @param value the new {@code double} value to be added. + */ + public final void recordDouble(double value) { + doRecordDouble(value); + hasRecordings = true; + } + + /** + * Concrete Aggregator instances should implement this method in order support recordings of + * double values. + */ + protected void doRecordDouble(double value) { + throw new UnsupportedOperationException( + "This aggregator does not support recording double values."); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregator.java new file mode 100644 index 000000000..3bb289bdb --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregator.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Map; +import java.util.concurrent.atomic.LongAdder; +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +final class CountAggregator extends AbstractAggregator { + private final AggregationTemporality temporality; + + CountAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor, + AggregationTemporality temporality) { + super( + resource, + instrumentationLibraryInfo, + descriptor, + temporality == AggregationTemporality.CUMULATIVE); + this.temporality = temporality; + } + + @Override + public AggregatorHandle createHandle() { + return new Handle(); + } + + @Override + public Long accumulateDouble(double value) { + return 1L; + } + + @Override + public Long accumulateLong(long value) { + return 1L; + } + + @Override + public Long merge(Long a1, Long a2) { + return a1 + a2; + } + + @Override + public MetricData toMetricData( + Map accumulationByLabels, + long startEpochNanos, + long lastCollectionEpoch, + long epochNanos) { + return MetricData.createLongSum( + getResource(), + getInstrumentationLibraryInfo(), + getInstrumentDescriptor().getName(), + getInstrumentDescriptor().getDescription(), + "1", + LongSumData.create( + /* isMonotonic= */ true, + temporality, + MetricDataUtils.toLongPointList( + accumulationByLabels, + temporality == AggregationTemporality.CUMULATIVE + ? startEpochNanos + : lastCollectionEpoch, + epochNanos))); + } + + static final class Handle extends AggregatorHandle { + private final LongAdder current = new LongAdder(); + + private Handle() {} + + @Override + protected void doRecordLong(long value) { + current.add(1); + } + + @Override + protected void doRecordDouble(double value) { + current.add(1); + } + + @Override + protected Long doAccumulateThenReset() { + return current.sumThenReset(); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregatorFactory.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregatorFactory.java new file mode 100644 index 000000000..75d0a712a --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregatorFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.resources.Resource; + +final class CountAggregatorFactory implements AggregatorFactory { + private final AggregationTemporality temporality; + + CountAggregatorFactory(AggregationTemporality temporality) { + this.temporality = temporality; + } + + @Override + @SuppressWarnings("unchecked") + public Aggregator create( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + return (Aggregator) + new CountAggregator(resource, instrumentationLibraryInfo, descriptor, temporality); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramAggregator.java new file mode 100644 index 000000000..f476a2d9b --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramAggregator.java @@ -0,0 +1,158 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +final class DoubleHistogramAggregator extends AbstractAggregator { + private final double[] boundaries; + + // a cache for converting to MetricData + private final List boundaryList; + + DoubleHistogramAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor instrumentDescriptor, + double[] boundaries, + boolean stateful) { + super(resource, instrumentationLibraryInfo, instrumentDescriptor, stateful); + this.boundaries = boundaries; + + List boundaryList = new ArrayList<>(this.boundaries.length); + for (double v : this.boundaries) { + boundaryList.add(v); + } + this.boundaryList = Collections.unmodifiableList(boundaryList); + } + + @Override + public AggregatorHandle createHandle() { + return new Handle(this.boundaries); + } + + /** + * Return the result of the merge of two histogram accumulations. As long as one Aggregator + * instance produces all Accumulations with constant boundaries we don't need to worry about + * merging accumulations with different boundaries. + */ + @Override + public final HistogramAccumulation merge(HistogramAccumulation x, HistogramAccumulation y) { + long[] mergedCounts = new long[x.getCounts().length]; + for (int i = 0; i < x.getCounts().length; ++i) { + mergedCounts[i] = x.getCounts()[i] + y.getCounts()[i]; + } + return HistogramAccumulation.create(x.getSum() + y.getSum(), mergedCounts); + } + + @Override + public final MetricData toMetricData( + Map accumulationByLabels, + long startEpochNanos, + long lastCollectionEpoch, + long epochNanos) { + return MetricData.createDoubleHistogram( + getResource(), + getInstrumentationLibraryInfo(), + getInstrumentDescriptor().getName(), + getInstrumentDescriptor().getDescription(), + getInstrumentDescriptor().getUnit(), + DoubleHistogramData.create( + isStateful() ? AggregationTemporality.CUMULATIVE : AggregationTemporality.DELTA, + MetricDataUtils.toDoubleHistogramPointList( + accumulationByLabels, + isStateful() ? startEpochNanos : lastCollectionEpoch, + epochNanos, + boundaryList))); + } + + @Override + public HistogramAccumulation accumulateDouble(double value) { + long[] counts = new long[this.boundaries.length + 1]; + counts[findBucketIndex(this.boundaries, value)] = 1; + return HistogramAccumulation.create(value, counts); + } + + @Override + public HistogramAccumulation accumulateLong(long value) { + return accumulateDouble((double) value); + } + + // Benchmark shows that linear search performs better than binary search with ordinary + // buckets. + private static int findBucketIndex(double[] boundaries, double value) { + for (int i = 0; i < boundaries.length; ++i) { + if (value <= boundaries[i]) { + return i; + } + } + return boundaries.length; + } + + static final class Handle extends AggregatorHandle { + // read-only + private final double[] boundaries; + + @GuardedBy("lock") + private double sum; + + @GuardedBy("lock") + private final long[] counts; + + private final ReentrantLock lock = new ReentrantLock(); + + Handle(double[] boundaries) { + this.boundaries = boundaries; + this.counts = new long[this.boundaries.length + 1]; + this.sum = 0; + } + + @Override + protected HistogramAccumulation doAccumulateThenReset() { + lock.lock(); + try { + HistogramAccumulation acc = + HistogramAccumulation.create(sum, Arrays.copyOf(counts, counts.length)); + this.sum = 0; + Arrays.fill(this.counts, 0); + return acc; + } finally { + lock.unlock(); + } + } + + @Override + protected void doRecordDouble(double value) { + int bucketIndex = findBucketIndex(this.boundaries, value); + + lock.lock(); + try { + this.sum += value; + this.counts[bucketIndex]++; + } finally { + lock.unlock(); + } + } + + @Override + protected void doRecordLong(long value) { + doRecordDouble((double) value); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleLastValueAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleLastValueAggregator.java new file mode 100644 index 000000000..f4ac43800 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleLastValueAggregator.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Aggregator that aggregates recorded values by storing the last recorded value. + * + *

    Limitation: The current implementation does not store a time when the value was recorded, so + * merging multiple LastValueAggregators will not preserve the ordering of records. This is not a + * problem because LastValueAggregator is currently only available for Observers which record all + * values once. + */ +@ThreadSafe +final class DoubleLastValueAggregator extends AbstractAggregator { + DoubleLastValueAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + super(resource, instrumentationLibraryInfo, descriptor, /* stateful= */ true); + } + + @Override + public AggregatorHandle createHandle() { + return new Handle(); + } + + @Override + public Double accumulateDouble(double value) { + return value; + } + + @Override + public Double merge(Double a1, Double a2) { + // TODO: Define the order between accumulation. + return a2; + } + + @Override + public MetricData toMetricData( + Map accumulationByLabels, + long startEpochNanos, + long lastCollectionEpoch, + long epochNanos) { + return MetricData.createDoubleGauge( + getResource(), + getInstrumentationLibraryInfo(), + getInstrumentDescriptor().getName(), + getInstrumentDescriptor().getDescription(), + getInstrumentDescriptor().getUnit(), + DoubleGaugeData.create( + MetricDataUtils.toDoublePointList(accumulationByLabels, 0, epochNanos))); + } + + static final class Handle extends AggregatorHandle { + @Nullable private static final Double DEFAULT_VALUE = null; + private final AtomicReference current = new AtomicReference<>(DEFAULT_VALUE); + + private Handle() {} + + @Override + protected Double doAccumulateThenReset() { + return this.current.getAndSet(DEFAULT_VALUE); + } + + @Override + protected void doRecordDouble(double value) { + current.set(value); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountAggregator.java new file mode 100644 index 000000000..58dabbd65 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountAggregator.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.resources.Resource; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +final class DoubleMinMaxSumCountAggregator extends AbstractMinMaxSumCountAggregator { + DoubleMinMaxSumCountAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + super(resource, instrumentationLibraryInfo, descriptor); + } + + @Override + public AggregatorHandle createHandle() { + return new Handle(); + } + + @Override + public MinMaxSumCountAccumulation accumulateDouble(double value) { + return MinMaxSumCountAccumulation.create(1, value, value, value); + } + + static final class Handle extends AggregatorHandle { + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + // The current value. This controls its own internal thread-safety via method access. Don't + // try to use its fields directly. + @GuardedBy("lock") + private final DoubleState current = new DoubleState(); + + @Override + protected MinMaxSumCountAccumulation doAccumulateThenReset() { + lock.writeLock().lock(); + try { + MinMaxSumCountAccumulation toReturn = + MinMaxSumCountAccumulation.create(current.count, current.sum, current.min, current.max); + current.reset(); + return toReturn; + } finally { + lock.writeLock().unlock(); + } + } + + @Override + protected void doRecordDouble(double value) { + lock.writeLock().lock(); + try { + current.record(value); + } finally { + lock.writeLock().unlock(); + } + } + + private static final class DoubleState { + private long count; + private double sum; + private double min; + private double max; + + public DoubleState() { + reset(); + } + + private void reset() { + this.sum = 0; + this.count = 0; + this.min = Double.POSITIVE_INFINITY; + this.max = Double.NEGATIVE_INFINITY; + } + + public void record(double value) { + count++; + sum += value; + min = Math.min(value, min); + max = Math.max(value, max); + } + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleSumAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleSumAggregator.java new file mode 100644 index 000000000..0bd152206 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/DoubleSumAggregator.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Map; +import java.util.concurrent.atomic.DoubleAdder; + +final class DoubleSumAggregator extends AbstractSumAggregator { + DoubleSumAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor, + AggregationTemporality temporality) { + super(resource, instrumentationLibraryInfo, descriptor, temporality); + } + + @Override + public AggregatorHandle createHandle() { + return new Handle(); + } + + @Override + public Double accumulateDouble(double value) { + return value; + } + + @Override + Double mergeSum(Double previousAccumulation, Double accumulation) { + return previousAccumulation + accumulation; + } + + @Override + Double mergeDiff(Double previousAccumulation, Double accumulation) { + return accumulation - previousAccumulation; + } + + @Override + public MetricData toMetricData( + Map accumulationByLabels, + long startEpochNanos, + long lastCollectionEpoch, + long epochNanos) { + return MetricData.createDoubleSum( + getResource(), + getInstrumentationLibraryInfo(), + getInstrumentDescriptor().getName(), + getInstrumentDescriptor().getDescription(), + getInstrumentDescriptor().getUnit(), + DoubleSumData.create( + isMonotonic(), + temporality(), + MetricDataUtils.toDoublePointList( + accumulationByLabels, + temporality() == AggregationTemporality.CUMULATIVE + ? startEpochNanos + : lastCollectionEpoch, + epochNanos))); + } + + static final class Handle extends AggregatorHandle { + private final DoubleAdder current = new DoubleAdder(); + + @Override + protected Double doAccumulateThenReset() { + return this.current.sumThenReset(); + } + + @Override + protected void doRecordDouble(double value) { + current.add(value); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/HistogramAccumulation.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/HistogramAccumulation.java new file mode 100644 index 000000000..7a8557f98 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/HistogramAccumulation.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import com.google.auto.value.AutoValue; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +abstract class HistogramAccumulation { + /** + * Creates a new {@link HistogramAccumulation} with the given values. Assume `counts` is read-only + * so we don't need a defensive-copy here. + * + * @return a new {@link HistogramAccumulation} with the given values. + */ + static HistogramAccumulation create(double sum, long[] counts) { + return new AutoValue_HistogramAccumulation(sum, counts); + } + + HistogramAccumulation() {} + + /** + * The sum of all measurements recorded. + * + * @return the sum of recorded measurements. + */ + abstract double getSum(); + + /** + * The counts in each bucket. The returned type is a mutable object, but it should be fine because + * the class is only used internally. + * + * @return the counts in each bucket. do not mutate the returned object. + */ + @SuppressWarnings("mutable") + abstract long[] getCounts(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/HistogramAggregatorFactory.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/HistogramAggregatorFactory.java new file mode 100644 index 000000000..1ce974d08 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/HistogramAggregatorFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.resources.Resource; +import java.util.List; + +final class HistogramAggregatorFactory implements AggregatorFactory { + private final double[] boundaries; + private final AggregationTemporality temporality; + + HistogramAggregatorFactory(List boundaries, AggregationTemporality temporality) { + this.boundaries = boundaries.stream().mapToDouble(i -> i).toArray(); + this.temporality = temporality; + + for (double v : this.boundaries) { + if (Double.isNaN(v)) { + throw new IllegalArgumentException("invalid bucket boundary: NaN"); + } + } + for (int i = 1; i < this.boundaries.length; ++i) { + if (this.boundaries[i - 1] >= this.boundaries[i]) { + throw new IllegalArgumentException( + "invalid bucket boundary: " + this.boundaries[i - 1] + " >= " + this.boundaries[i]); + } + } + if (this.boundaries.length > 0) { + if (this.boundaries[0] == Double.NEGATIVE_INFINITY) { + throw new IllegalArgumentException("invalid bucket boundary: -Inf"); + } + if (this.boundaries[this.boundaries.length - 1] == Double.POSITIVE_INFINITY) { + throw new IllegalArgumentException("invalid bucket boundary: +Inf"); + } + } + } + + @Override + @SuppressWarnings("unchecked") + public Aggregator create( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + final boolean stateful = this.temporality == AggregationTemporality.CUMULATIVE; + switch (descriptor.getValueType()) { + case LONG: + case DOUBLE: + return (Aggregator) + new DoubleHistogramAggregator( + resource, instrumentationLibraryInfo, descriptor, this.boundaries, stateful); + } + throw new IllegalArgumentException("Invalid instrument value type"); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LastValueAggregatorFactory.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LastValueAggregatorFactory.java new file mode 100644 index 000000000..62a0c611f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LastValueAggregatorFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.resources.Resource; + +final class LastValueAggregatorFactory implements AggregatorFactory { + static final AggregatorFactory INSTANCE = new LastValueAggregatorFactory(); + + private LastValueAggregatorFactory() {} + + @Override + @SuppressWarnings("unchecked") + public Aggregator create( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + switch (descriptor.getValueType()) { + case LONG: + return (Aggregator) + new LongLastValueAggregator(resource, instrumentationLibraryInfo, descriptor); + case DOUBLE: + return (Aggregator) + new DoubleLastValueAggregator(resource, instrumentationLibraryInfo, descriptor); + } + throw new IllegalArgumentException("Invalid instrument value type"); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongLastValueAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongLastValueAggregator.java new file mode 100644 index 000000000..b0bd666ba --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongLastValueAggregator.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; + +/** + * Aggregator that aggregates recorded values by storing the last recorded value. + * + *

    Limitation: The current implementation does not store a time when the value was recorded, so + * merging multiple LastValueAggregators will not preserve the ordering of records. This is not a + * problem because LastValueAggregator is currently only available for Observers which record all + * values once. + */ +final class LongLastValueAggregator extends AbstractAggregator { + LongLastValueAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + super(resource, instrumentationLibraryInfo, descriptor, /* stateful= */ false); + } + + @Override + public AggregatorHandle createHandle() { + return new Handle(); + } + + @Override + public Long accumulateLong(long value) { + return value; + } + + @Override + public Long merge(Long a1, Long a2) { + // TODO: Define the order between accumulation. + return a2; + } + + @Override + public MetricData toMetricData( + Map accumulationByLabels, + long startEpochNanos, + long lastCollectionEpoch, + long epochNanos) { + return MetricData.createLongGauge( + getResource(), + getInstrumentationLibraryInfo(), + getInstrumentDescriptor().getName(), + getInstrumentDescriptor().getDescription(), + getInstrumentDescriptor().getUnit(), + LongGaugeData.create(MetricDataUtils.toLongPointList(accumulationByLabels, 0, epochNanos))); + } + + static final class Handle extends AggregatorHandle { + @Nullable private static final Long DEFAULT_VALUE = null; + private final AtomicReference current = new AtomicReference<>(DEFAULT_VALUE); + + @Override + protected Long doAccumulateThenReset() { + return this.current.getAndSet(DEFAULT_VALUE); + } + + @Override + protected void doRecordLong(long value) { + current.set(value); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountAggregator.java new file mode 100644 index 000000000..855411ed1 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountAggregator.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.resources.Resource; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +final class LongMinMaxSumCountAggregator extends AbstractMinMaxSumCountAggregator { + LongMinMaxSumCountAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + super(resource, instrumentationLibraryInfo, descriptor); + } + + @Override + public AggregatorHandle createHandle() { + return new Handle(); + } + + @Override + public MinMaxSumCountAccumulation accumulateLong(long value) { + return MinMaxSumCountAccumulation.create(1, value, value, value); + } + + static final class Handle extends AggregatorHandle { + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + // The current value. This controls its own internal thread-safety via method access. Don't + // try to use its fields directly. + @GuardedBy("lock") + private final LongState current = new LongState(); + + @Override + protected MinMaxSumCountAccumulation doAccumulateThenReset() { + lock.writeLock().lock(); + try { + MinMaxSumCountAccumulation toReturn = + MinMaxSumCountAccumulation.create(current.count, current.sum, current.min, current.max); + current.reset(); + return toReturn; + } finally { + lock.writeLock().unlock(); + } + } + + @Override + protected void doRecordLong(long value) { + lock.writeLock().lock(); + try { + current.record(value); + } finally { + lock.writeLock().unlock(); + } + } + + private static final class LongState { + private long count; + private long sum; + private long min; + private long max; + + public LongState() { + reset(); + } + + private void reset() { + this.sum = 0; + this.count = 0; + this.min = Long.MAX_VALUE; + this.max = Long.MIN_VALUE; + } + + public void record(long value) { + count++; + sum += value; + min = Math.min(value, min); + max = Math.max(value, max); + } + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongSumAggregator.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongSumAggregator.java new file mode 100644 index 000000000..52a1667dc --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/LongSumAggregator.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Map; +import java.util.concurrent.atomic.LongAdder; + +final class LongSumAggregator extends AbstractSumAggregator { + + LongSumAggregator( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor, + AggregationTemporality temporality) { + super(resource, instrumentationLibraryInfo, descriptor, temporality); + } + + @Override + public AggregatorHandle createHandle() { + return new Handle(); + } + + @Override + public Long accumulateLong(long value) { + return value; + } + + @Override + Long mergeSum(Long previousAccumulation, Long accumulation) { + return previousAccumulation + accumulation; + } + + @Override + Long mergeDiff(Long previousAccumulation, Long accumulation) { + return accumulation - previousAccumulation; + } + + @Override + public MetricData toMetricData( + Map accumulationByLabels, + long startEpochNanos, + long lastCollectionEpoch, + long epochNanos) { + InstrumentDescriptor descriptor = getInstrumentDescriptor(); + return MetricData.createLongSum( + getResource(), + getInstrumentationLibraryInfo(), + descriptor.getName(), + descriptor.getDescription(), + descriptor.getUnit(), + LongSumData.create( + isMonotonic(), + temporality(), + MetricDataUtils.toLongPointList( + accumulationByLabels, + temporality() == AggregationTemporality.CUMULATIVE + ? startEpochNanos + : lastCollectionEpoch, + epochNanos))); + } + + static final class Handle extends AggregatorHandle { + private final LongAdder current = new LongAdder(); + + @Override + protected Long doAccumulateThenReset() { + return this.current.sumThenReset(); + } + + @Override + public void doRecordLong(long value) { + current.add(value); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MetricDataUtils.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MetricDataUtils.java new file mode 100644 index 000000000..dd7a23381 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MetricDataUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.data.DoubleHistogramPointData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +final class MetricDataUtils { + private MetricDataUtils() {} + + static List toLongPointList( + Map accumulationMap, long startEpochNanos, long epochNanos) { + List points = new ArrayList<>(accumulationMap.size()); + accumulationMap.forEach( + (labels, accumulation) -> + points.add(LongPointData.create(startEpochNanos, epochNanos, labels, accumulation))); + return points; + } + + static List toDoublePointList( + Map accumulationMap, long startEpochNanos, long epochNanos) { + List points = new ArrayList<>(accumulationMap.size()); + accumulationMap.forEach( + (labels, accumulation) -> + points.add(DoublePointData.create(startEpochNanos, epochNanos, labels, accumulation))); + return points; + } + + static List toDoubleSummaryPointList( + Map accumulationMap, + long startEpochNanos, + long epochNanos) { + List points = new ArrayList<>(accumulationMap.size()); + accumulationMap.forEach( + (labels, aggregator) -> + points.add(aggregator.toPoint(startEpochNanos, epochNanos, labels))); + return points; + } + + static List toDoubleHistogramPointList( + Map accumulationMap, + long startEpochNanos, + long epochNanos, + List boundaries) { + List points = new ArrayList<>(accumulationMap.size()); + accumulationMap.forEach( + (labels, aggregator) -> { + List counts = new ArrayList<>(aggregator.getCounts().length); + for (long v : aggregator.getCounts()) { + counts.add(v); + } + points.add( + DoubleHistogramPointData.create( + startEpochNanos, epochNanos, labels, aggregator.getSum(), boundaries, counts)); + }); + return points; + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAccumulation.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAccumulation.java new file mode 100644 index 000000000..daab1d258 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAccumulation.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import java.util.Arrays; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +abstract class MinMaxSumCountAccumulation { + /** + * Creates a new {@link MinMaxSumCountAccumulation} with the given values. + * + * @param count the number of measurements. + * @param sum the sum of the measurements. + * @param min the min value out of all measurements. + * @param max the max value out of all measurements. + * @return a new {@link MinMaxSumCountAccumulation} with the given values. + */ + static MinMaxSumCountAccumulation create(long count, double sum, double min, double max) { + return new AutoValue_MinMaxSumCountAccumulation(count, sum, min, max); + } + + MinMaxSumCountAccumulation() {} + + /** + * Returns the count (number of measurements) stored by this accumulation. + * + * @return the count stored by this accumulation. + */ + abstract long getCount(); + + /** + * Returns the sum (sum of measurements) stored by this accumulation. + * + * @return the sum stored by this accumulation. + */ + abstract double getSum(); + + /** + * Returns the min (minimum of all measurements) stored by this accumulation. + * + * @return the min stored by this accumulation. + */ + abstract double getMin(); + + /** + * Returns the max (maximum of all measurements) stored by this accumulation. + * + * @return the max stored by this accumulation. + */ + abstract double getMax(); + + final DoubleSummaryPointData toPoint(long startEpochNanos, long epochNanos, Labels labels) { + return DoubleSummaryPointData.create( + startEpochNanos, + epochNanos, + labels, + getCount(), + getSum(), + Arrays.asList( + ValueAtPercentile.create(0.0, getMin()), ValueAtPercentile.create(100.0, getMax()))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAggregatorFactory.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAggregatorFactory.java new file mode 100644 index 000000000..70b108622 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAggregatorFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.resources.Resource; + +final class MinMaxSumCountAggregatorFactory implements AggregatorFactory { + static final AggregatorFactory INSTANCE = new MinMaxSumCountAggregatorFactory(); + + private MinMaxSumCountAggregatorFactory() {} + + @Override + @SuppressWarnings("unchecked") + public Aggregator create( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + switch (descriptor.getValueType()) { + case LONG: + return (Aggregator) + new LongMinMaxSumCountAggregator(resource, instrumentationLibraryInfo, descriptor); + case DOUBLE: + return (Aggregator) + new DoubleMinMaxSumCountAggregator(resource, instrumentationLibraryInfo, descriptor); + } + throw new IllegalArgumentException("Invalid instrument value type"); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/SumAggregatorFactory.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/SumAggregatorFactory.java new file mode 100644 index 000000000..ce8a25efd --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/SumAggregatorFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.resources.Resource; + +final class SumAggregatorFactory implements AggregatorFactory { + + private final AggregationTemporality temporality; + + SumAggregatorFactory(AggregationTemporality temporality) { + this.temporality = temporality; + } + + @Override + @SuppressWarnings("unchecked") + public Aggregator create( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor) { + switch (descriptor.getValueType()) { + case LONG: + return (Aggregator) + new LongSumAggregator(resource, instrumentationLibraryInfo, descriptor, temporality); + case DOUBLE: + return (Aggregator) + new DoubleSumAggregator(resource, instrumentationLibraryInfo, descriptor, temporality); + } + throw new IllegalArgumentException("Invalid instrument value type"); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/package-info.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/package-info.java new file mode 100644 index 000000000..538a83d35 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/aggregator/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Metric aggregators. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.metrics.aggregator; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentDescriptor.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentDescriptor.java new file mode 100644 index 000000000..6ecaaab71 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentDescriptor.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.common; + +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@Immutable +public abstract class InstrumentDescriptor { + public static InstrumentDescriptor create( + String name, + String description, + String unit, + InstrumentType type, + InstrumentValueType valueType) { + return new AutoValue_InstrumentDescriptor(name, description, unit, type, valueType); + } + + public abstract String getName(); + + public abstract String getDescription(); + + public abstract String getUnit(); + + public abstract InstrumentType getType(); + + public abstract InstrumentValueType getValueType(); + + @Memoized + @Override + public abstract int hashCode(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentType.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentType.java new file mode 100644 index 000000000..e584e0c7c --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentType.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.common; + +/** All instrument types available in the metric package. */ +public enum InstrumentType { + COUNTER, + UP_DOWN_COUNTER, + VALUE_RECORDER, + SUM_OBSERVER, + UP_DOWN_SUM_OBSERVER, + VALUE_OBSERVER, +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentValueType.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentValueType.java new file mode 100644 index 000000000..24af6c559 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/InstrumentValueType.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.common; + +/** All possible types for the values recorded via the instruments. */ +public enum InstrumentValueType { + LONG, + DOUBLE, +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/package-info.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/package-info.java new file mode 100644 index 000000000..055f841e3 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/common/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Common utilities used by metrics. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.metrics.common; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/AggregationTemporality.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/AggregationTemporality.java new file mode 100644 index 000000000..5a019ba98 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/AggregationTemporality.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import io.opentelemetry.api.metrics.Instrument; + +/** An enumeration which describes the time period over which metrics should be aggregated. */ +public enum AggregationTemporality { + /** Metrics will be aggregated only over the most recent collection interval. */ + DELTA, + /** Metrics will be aggregated over the lifetime of the associated {@link Instrument}. */ + CUMULATIVE +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/Data.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/Data.java new file mode 100644 index 000000000..a8e733ac0 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/Data.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +@Immutable +interface Data { + /** + * Returns the data {@link PointData}s for this metric. + * + * @return the data {@link PointData}s for this metric, or empty {@code Collection} if no points. + */ + Collection getPoints(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleGaugeData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleGaugeData.java new file mode 100644 index 000000000..1378b1c70 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleGaugeData.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +public abstract class DoubleGaugeData implements Data { + public static DoubleGaugeData create(Collection points) { + return new AutoValue_DoubleGaugeData(points); + } + + DoubleGaugeData() {} + + @Override + public abstract Collection getPoints(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleHistogramData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleHistogramData.java new file mode 100644 index 000000000..a5f90422f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleHistogramData.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +public abstract class DoubleHistogramData implements Data { + DoubleHistogramData() {} + + public static DoubleHistogramData create( + AggregationTemporality temporality, Collection points) { + return new AutoValue_DoubleHistogramData(temporality, points); + } + + /** + * Returns the {@code AggregationTemporality} of this metric, + * + *

    AggregationTemporality describes if the aggregator reports delta changes since last report + * time, or cumulative changes since a fixed start time. + * + * @return the {@code AggregationTemporality} of this metric + */ + public abstract AggregationTemporality getAggregationTemporality(); + + @Override + public abstract Collection getPoints(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleHistogramPointData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleHistogramPointData.java new file mode 100644 index 000000000..22e5e1346 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleHistogramPointData.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.metrics.common.Labels; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * DoubleHistogramPointData represents an approximate representation of the distribution of + * measurements. + */ +@Immutable +@AutoValue +public abstract class DoubleHistogramPointData implements PointData { + /** + * Creates a DoubleHistogramPointData. For a Histogram with N defined boundaries, there should be + * N+1 counts. + * + * @return a DoubleHistogramPointData. + * @throws IllegalArgumentException if the given boundaries/counts were invalid + */ + public static DoubleHistogramPointData create( + long startEpochNanos, + long epochNanos, + Labels labels, + double sum, + List boundaries, + List counts) { + if (counts.size() != boundaries.size() + 1) { + throw new IllegalArgumentException( + "invalid counts: size should be " + + (boundaries.size() + 1) + + " instead of " + + counts.size()); + } + if (!isStrictlyIncreasing(boundaries)) { + throw new IllegalArgumentException("invalid boundaries: " + boundaries); + } + if (!boundaries.isEmpty() + && (boundaries.get(0).isInfinite() || boundaries.get(boundaries.size() - 1).isInfinite())) { + throw new IllegalArgumentException("invalid boundaries: contains explicit +/-Inf"); + } + + long totalCount = 0; + for (long c : counts) { + totalCount += c; + } + return new AutoValue_DoubleHistogramPointData( + startEpochNanos, + epochNanos, + labels, + sum, + totalCount, + Collections.unmodifiableList(new ArrayList<>(boundaries)), + Collections.unmodifiableList(new ArrayList<>(counts))); + } + + DoubleHistogramPointData() {} + + /** + * The sum of all measurements recorded. + * + * @return the sum of recorded measurements. + */ + public abstract double getSum(); + + /** + * The number of measurements taken. + * + * @return the count of recorded measurements. + */ + public abstract long getCount(); + + /** + * The bucket boundaries. For a Histogram with N defined boundaries, e.g, [x, y, z]. There are N+1 + * counts: (-inf, x], (x, y], (y, z], (z, +inf). + * + * @return the read-only bucket boundaries in increasing order. do not mutate the returned + * object. + */ + public abstract List getBoundaries(); + + /** + * The counts in each bucket. + * + * @return the read-only counts in each bucket. do not mutate the returned object. + */ + public abstract List getCounts(); + + private static boolean isStrictlyIncreasing(List xs) { + for (int i = 0; i < xs.size() - 1; i++) { + if (xs.get(i).compareTo(xs.get(i + 1)) >= 0) { + return false; + } + } + return true; + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoublePointData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoublePointData.java new file mode 100644 index 000000000..9f5d1af10 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoublePointData.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.Immutable; + +/** + * DoublePoint is a single data point in a timeseries that describes the time-varying value of a + * double metric. + */ +@Immutable +@AutoValue +public abstract class DoublePointData implements PointData { + public static DoublePointData create( + long startEpochNanos, long epochNanos, Labels labels, double value) { + return new AutoValue_DoublePointData(startEpochNanos, epochNanos, labels, value); + } + + DoublePointData() {} + + /** + * Returns the value of the data point. + * + * @return the value of the data point. + */ + public abstract double getValue(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSumData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSumData.java new file mode 100644 index 000000000..235fa49a2 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSumData.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +public abstract class DoubleSumData implements SumData { + DoubleSumData() {} + + public static DoubleSumData create( + boolean isMonotonic, AggregationTemporality temporality, Collection points) { + return new AutoValue_DoubleSumData(points, isMonotonic, temporality); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSummaryData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSummaryData.java new file mode 100644 index 000000000..0188cc142 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSummaryData.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +public abstract class DoubleSummaryData implements Data { + DoubleSummaryData() {} + + public static DoubleSummaryData create(Collection points) { + return new AutoValue_DoubleSummaryData(points); + } + + @Override + public abstract Collection getPoints(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSummaryPointData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSummaryPointData.java new file mode 100644 index 000000000..78165e77f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/DoubleSummaryPointData.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.metrics.common.Labels; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * SummaryPoint is a single data point that summarizes the values in a time series of numeric + * values. + */ +@Immutable +@AutoValue +public abstract class DoubleSummaryPointData implements PointData { + public static DoubleSummaryPointData create( + long startEpochNanos, + long epochNanos, + Labels labels, + long count, + double sum, + List percentileValues) { + return new AutoValue_DoubleSummaryPointData( + startEpochNanos, epochNanos, labels, count, sum, percentileValues); + } + + DoubleSummaryPointData() {} + + /** + * The number of values that are being summarized. + * + * @return the number of values that are being summarized. + */ + public abstract long getCount(); + + /** + * The sum of all the values that are being summarized. + * + * @return the sum of the values that are being summarized. + */ + public abstract double getSum(); + + /** + * Percentile values in the summarization. Note: a percentile 0.0 represents the minimum value in + * the distribution. + * + * @return the percentiles values. + */ + public abstract List getPercentileValues(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongGaugeData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongGaugeData.java new file mode 100644 index 000000000..703c5e614 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongGaugeData.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +public abstract class LongGaugeData implements Data { + public static LongGaugeData create(Collection points) { + return new AutoValue_LongGaugeData(points); + } + + LongGaugeData() {} + + @Override + public abstract Collection getPoints(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongPointData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongPointData.java new file mode 100644 index 000000000..5d10f5269 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongPointData.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.Immutable; + +/** + * LongPoint is a single data point in a timeseries that describes the time-varying values of a + * int64 metric. + * + *

    In the proto definition this is called Int64Point. + */ +@Immutable +@AutoValue +public abstract class LongPointData implements PointData { + + LongPointData() {} + + /** + * Returns the value of the data point. + * + * @return the value of the data point. + */ + public abstract long getValue(); + + public static LongPointData create( + long startEpochNanos, long epochNanos, Labels labels, long value) { + return new AutoValue_LongPointData(startEpochNanos, epochNanos, labels, value); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongSumData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongSumData.java new file mode 100644 index 000000000..dd2836f6c --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/LongSumData.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +public abstract class LongSumData implements SumData { + public static LongSumData create( + boolean isMonotonic, AggregationTemporality temporality, Collection points) { + return new AutoValue_LongSumData(points, isMonotonic, temporality); + } + + LongSumData() {} +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/MetricData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/MetricData.java new file mode 100644 index 000000000..61df82f5d --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/MetricData.java @@ -0,0 +1,306 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import javax.annotation.concurrent.Immutable; + +/** + * A {@link MetricData} represents the data exported as part of aggregating one {@code Instrument}. + */ +@Immutable +@AutoValue +public abstract class MetricData { + private static final DoubleGaugeData DEFAULT_DOUBLE_GAUGE_DATA = + DoubleGaugeData.create(Collections.emptyList()); + private static final LongGaugeData DEFAULT_LONG_GAUGE_DATA = + LongGaugeData.create(Collections.emptyList()); + private static final DoubleSumData DEFAULT_DOUBLE_SUM_DATA = + DoubleSumData.create( + /* isMonotonic= */ false, AggregationTemporality.CUMULATIVE, Collections.emptyList()); + private static final LongSumData DEFAULT_LONG_SUM_DATA = + LongSumData.create( + /* isMonotonic= */ false, AggregationTemporality.CUMULATIVE, Collections.emptyList()); + private static final DoubleSummaryData DEFAULT_DOUBLE_SUMMARY_DATA = + DoubleSummaryData.create(Collections.emptyList()); + private static final DoubleHistogramData DEFAULT_DOUBLE_HISTOGRAM_DATA = + DoubleHistogramData.create(AggregationTemporality.CUMULATIVE, Collections.emptyList()); + + /** + * Returns a new MetricData wih a {@link MetricDataType#DOUBLE_GAUGE} type. + * + * @return a new MetricData wih a {@link MetricDataType#DOUBLE_GAUGE} type. + */ + public static MetricData createDoubleGauge( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + String name, + String description, + String unit, + DoubleGaugeData data) { + return new AutoValue_MetricData( + resource, + instrumentationLibraryInfo, + name, + description, + unit, + MetricDataType.DOUBLE_GAUGE, + data); + } + + /** + * Returns a new MetricData wih a {@link MetricDataType#LONG_GAUGE} type. + * + * @return a new MetricData wih a {@link MetricDataType#LONG_GAUGE} type. + */ + public static MetricData createLongGauge( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + String name, + String description, + String unit, + LongGaugeData data) { + return new AutoValue_MetricData( + resource, + instrumentationLibraryInfo, + name, + description, + unit, + MetricDataType.LONG_GAUGE, + data); + } + + /** + * Returns a new MetricData wih a {@link MetricDataType#DOUBLE_SUM} type. + * + * @return a new MetricData wih a {@link MetricDataType#DOUBLE_SUM} type. + */ + public static MetricData createDoubleSum( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + String name, + String description, + String unit, + DoubleSumData data) { + return new AutoValue_MetricData( + resource, + instrumentationLibraryInfo, + name, + description, + unit, + MetricDataType.DOUBLE_SUM, + data); + } + + /** + * Returns a new MetricData wih a {@link MetricDataType#LONG_SUM} type. + * + * @return a new MetricData wih a {@link MetricDataType#LONG_SUM} type. + */ + public static MetricData createLongSum( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + String name, + String description, + String unit, + LongSumData data) { + return new AutoValue_MetricData( + resource, + instrumentationLibraryInfo, + name, + description, + unit, + MetricDataType.LONG_SUM, + data); + } + + /** + * Returns a new MetricData wih a {@link MetricDataType#SUMMARY} type. + * + * @return a new MetricData wih a {@link MetricDataType#SUMMARY} type. + */ + public static MetricData createDoubleSummary( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + String name, + String description, + String unit, + DoubleSummaryData data) { + return new AutoValue_MetricData( + resource, + instrumentationLibraryInfo, + name, + description, + unit, + MetricDataType.SUMMARY, + data); + } + + /** + * Returns a new MetricData with a {@link MetricDataType#HISTOGRAM} type. + * + * @return a new MetricData wih a {@link MetricDataType#HISTOGRAM} type. + */ + public static MetricData createDoubleHistogram( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + String name, + String description, + String unit, + DoubleHistogramData data) { + return new AutoValue_MetricData( + resource, + instrumentationLibraryInfo, + name, + description, + unit, + MetricDataType.HISTOGRAM, + data); + } + + MetricData() {} + + /** + * Returns the resource of this {@code MetricData}. + * + * @return the resource of this {@code MetricData}. + */ + public abstract Resource getResource(); + + /** + * Returns the instrumentation library specified when creating the {@code Meter} which created the + * {@code Instrument} that produces {@code MetricData}. + * + * @return an instance of {@link InstrumentationLibraryInfo} + */ + public abstract InstrumentationLibraryInfo getInstrumentationLibraryInfo(); + + /** + * Returns the metric name. + * + * @return the metric name. + */ + public abstract String getName(); + + /** + * Returns the description of this metric. + * + * @return the description of this metric. + */ + public abstract String getDescription(); + + /** + * Returns the unit of this metric. + * + * @return the unit of this metric. + */ + public abstract String getUnit(); + + /** + * Returns the type of this metric. + * + * @return the type of this metric. + */ + public abstract MetricDataType getType(); + + abstract Data getData(); + + /** + * Returns {@code true} if there are no points associated with this metric. + * + * @return {@code true} if there are no points associated with this metric. + */ + public boolean isEmpty() { + return getData().getPoints().isEmpty(); + } + + /** + * Returns the {@code DoubleGaugeData} if type is {@link MetricDataType#DOUBLE_GAUGE}, otherwise a + * default empty data. + * + * @return the {@code DoubleGaugeData} if type is {@link MetricDataType#DOUBLE_GAUGE}, otherwise a + * default empty data. + */ + public final DoubleGaugeData getDoubleGaugeData() { + if (getType() == MetricDataType.DOUBLE_GAUGE) { + return (DoubleGaugeData) getData(); + } + return DEFAULT_DOUBLE_GAUGE_DATA; + } + + /** + * Returns the {@code LongGaugeData} if type is {@link MetricDataType#LONG_GAUGE}, otherwise a + * default empty data. + * + * @return the {@code LongGaugeData} if type is {@link MetricDataType#LONG_GAUGE}, otherwise a + * default empty data. + */ + public final LongGaugeData getLongGaugeData() { + if (getType() == MetricDataType.LONG_GAUGE) { + return (LongGaugeData) getData(); + } + return DEFAULT_LONG_GAUGE_DATA; + } + + /** + * Returns the {@code DoubleSumData} if type is {@link MetricDataType#DOUBLE_SUM}, otherwise a + * default empty data. + * + * @return the {@code DoubleSumData} if type is {@link MetricDataType#DOUBLE_SUM}, otherwise a + * default empty data. + */ + public final DoubleSumData getDoubleSumData() { + if (getType() == MetricDataType.DOUBLE_SUM) { + return (DoubleSumData) getData(); + } + return DEFAULT_DOUBLE_SUM_DATA; + } + + /** + * Returns the {@code LongSumData} if type is {@link MetricDataType#LONG_SUM}, otherwise a default + * empty data. + * + * @return the {@code LongSumData} if type is {@link MetricDataType#LONG_SUM}, otherwise a default + * empty data. + */ + public final LongSumData getLongSumData() { + if (getType() == MetricDataType.LONG_SUM) { + return (LongSumData) getData(); + } + return DEFAULT_LONG_SUM_DATA; + } + + /** + * Returns the {@code DoubleSummaryData} if type is {@link MetricDataType#SUMMARY}, otherwise a + * default empty data. + * + * @return the {@code DoubleSummaryData} if type is {@link MetricDataType#SUMMARY}, otherwise a + * default * empty data. + */ + public final DoubleSummaryData getDoubleSummaryData() { + if (getType() == MetricDataType.SUMMARY) { + return (DoubleSummaryData) getData(); + } + return DEFAULT_DOUBLE_SUMMARY_DATA; + } + + /** + * Returns the {@code DoubleHistogramData} if type is {@link MetricDataType#HISTOGRAM}, otherwise + * a default empty data. + * + * @return the {@code DoubleHistogramData} if type is {@link MetricDataType#HISTOGRAM}, otherwise + * a default empty data. + */ + public final DoubleHistogramData getDoubleHistogramData() { + if (getType() == MetricDataType.HISTOGRAM) { + return (DoubleHistogramData) getData(); + } + return DEFAULT_DOUBLE_HISTOGRAM_DATA; + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/MetricDataType.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/MetricDataType.java new file mode 100644 index 000000000..074980784 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/MetricDataType.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +/** The kind of metric. It describes how the data is reported. */ +public enum MetricDataType { + /** + * A Gauge represents a measurement of a long value at a moment in time. Generally only one + * instance of a given Gauge metric will be reported per reporting interval. + */ + LONG_GAUGE, + + /** + * A Gauge represents a measurement of a double value at a moment in time. Generally only one + * instance of a given Gauge metric will be reported per reporting interval. + */ + DOUBLE_GAUGE, + + /** A sum of non negative long (int64) values. Reports {@link LongSumData} data. */ + LONG_SUM, + + /** A sum of non negative double values. Reports {@link DoubleSumData} data. */ + DOUBLE_SUM, + + /** + * A Summary of measurements of numeric values, containing the minimum value recorded, the maximum + * value recorded, the sum of all measurements and the total number of measurements recorded. + */ + SUMMARY, + + /** + * A Histogram represents an approximate representation of the distribution of measurements + * recorded. + */ + HISTOGRAM, +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/PointData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/PointData.java new file mode 100644 index 000000000..aec315ef1 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/PointData.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import io.opentelemetry.api.metrics.common.Labels; +import javax.annotation.concurrent.Immutable; + +@Immutable +public interface PointData { + /** + * Returns the start epoch timestamp in nanos of this {@code Instrument}, usually the time when + * the metric was created or an aggregation was enabled. + * + * @return the start epoch timestamp in nanos. + */ + long getStartEpochNanos(); + + /** + * Returns the epoch timestamp in nanos when data were collected, usually it represents the moment + * when {@code Instrument.getData()} was called. + * + * @return the epoch timestamp in nanos. + */ + long getEpochNanos(); + + /** + * Returns the labels associated with this {@code Point}. + * + * @return the labels associated with this {@code Point}. + */ + Labels getLabels(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/SumData.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/SumData.java new file mode 100644 index 000000000..fc4e0b079 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/SumData.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import javax.annotation.concurrent.Immutable; + +@Immutable +interface SumData extends Data { + /** + * Returns "true" if the sum is monotonic. + * + * @return "true" if the sum is monotonic + */ + boolean isMonotonic(); + + /** + * Returns the {@code AggregationTemporality} of this metric, + * + *

    AggregationTemporality describes if the aggregator reports delta changes since last report + * time, or cumulative changes since a fixed start time. + * + * @return the {@code AggregationTemporality} of this metric + */ + AggregationTemporality getAggregationTemporality(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/ValueAtPercentile.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/ValueAtPercentile.java new file mode 100644 index 000000000..25c429620 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/ValueAtPercentile.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import com.google.auto.value.AutoValue; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +public abstract class ValueAtPercentile { + public static ValueAtPercentile create(double percentile, double value) { + return new AutoValue_ValueAtPercentile(percentile, value); + } + + ValueAtPercentile() {} + + /** + * The percentile of a distribution. Must be in the interval [0.0, 100.0]. + * + * @return the percentile. + */ + public abstract double getPercentile(); + + /** + * The value at the given percentile of a distribution. + * + * @return the value at the percentile. + */ + public abstract double getValue(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/package-info.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/package-info.java new file mode 100644 index 000000000..528673ec4 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/data/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** The data format to model metrics for export. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.metrics.data; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReader.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReader.java new file mode 100644 index 000000000..3312859f4 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReader.java @@ -0,0 +1,188 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.concurrent.Immutable; + +/** + * Wraps a list of {@link MetricProducer}s and automatically reads and exports the metrics every + * export interval. Metrics may also be dropped when it becomes time to export again, and there is + * an export in progress. + */ +public final class IntervalMetricReader { + private static final Logger logger = Logger.getLogger(IntervalMetricReader.class.getName()); + + private final Exporter exporter; + private final ScheduledExecutorService scheduler; + + private volatile ScheduledFuture scheduledFuture; + private final Object lock = new Object(); + + /** Stops the scheduled task and calls export one more time. */ + public CompletableResultCode shutdown() { + final CompletableResultCode result = new CompletableResultCode(); + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + } + scheduler.shutdown(); + try { + scheduler.awaitTermination(5, TimeUnit.SECONDS); + final CompletableResultCode flushResult = exporter.doRun(); + flushResult.join(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // force a shutdown if the export hasn't finished. + scheduler.shutdownNow(); + // reset the interrupted status + Thread.currentThread().interrupt(); + } finally { + final CompletableResultCode shutdownResult = exporter.shutdown(); + shutdownResult.whenComplete( + () -> { + if (!shutdownResult.isSuccess()) { + result.fail(); + } else { + result.succeed(); + } + }); + } + return result; + } + + /** + * Returns a new {@link IntervalMetricReaderBuilder} for {@link IntervalMetricReader}. + * + * @return a new {@link IntervalMetricReaderBuilder} for {@link IntervalMetricReader}. + */ + public static IntervalMetricReaderBuilder builder() { + return new IntervalMetricReaderBuilder(InternalState.builder()); + } + + IntervalMetricReader(InternalState internalState) { + this( + internalState, + Executors.newScheduledThreadPool(1, new DaemonThreadFactory("IntervalMetricReader"))); + } + + // visible for testing + IntervalMetricReader(InternalState internalState, ScheduledExecutorService intervalMetricReader) { + this.exporter = new Exporter(internalState); + this.scheduler = intervalMetricReader; + } + + /** + * Starts this {@link IntervalMetricReader} to report to the configured exporter. + * + * @return this for fluent usage along with the builder. + */ + public IntervalMetricReader start() { + synchronized (lock) { + if (scheduledFuture != null) { + return this; + } + scheduledFuture = + scheduler.scheduleAtFixedRate( + exporter, + exporter.internalState.getExportIntervalMillis(), + exporter.internalState.getExportIntervalMillis(), + TimeUnit.MILLISECONDS); + return this; + } + } + + private static final class Exporter implements Runnable { + + private final InternalState internalState; + private final AtomicBoolean exportAvailable = new AtomicBoolean(true); + + private Exporter(InternalState internalState) { + this.internalState = internalState; + } + + @Override + public void run() { + // Ignore the CompletableResultCode from doRun() in order to keep run() asynchronous + doRun(); + } + + CompletableResultCode doRun() { + final CompletableResultCode flushResult = new CompletableResultCode(); + if (exportAvailable.compareAndSet(true, false)) { + try { + List metricsList = new ArrayList<>(); + for (MetricProducer metricProducer : internalState.getMetricProducers()) { + metricsList.addAll(metricProducer.collectAllMetrics()); + } + final CompletableResultCode result = + internalState.getMetricExporter().export(Collections.unmodifiableList(metricsList)); + result.whenComplete( + () -> { + if (!result.isSuccess()) { + logger.log(Level.FINE, "Exporter failed"); + } + flushResult.succeed(); + exportAvailable.set(true); + }); + } catch (Throwable t) { + exportAvailable.set(true); + logger.log(Level.WARNING, "Exporter threw an Exception", t); + flushResult.fail(); + } + } else { + logger.log(Level.FINE, "Exporter busy. Dropping metrics."); + flushResult.fail(); + } + return flushResult; + } + + CompletableResultCode shutdown() { + return internalState.getMetricExporter().shutdown(); + } + } + + @AutoValue + @Immutable + abstract static class InternalState { + static final long DEFAULT_INTERVAL_MILLIS = 60_000; + + abstract MetricExporter getMetricExporter(); + + abstract long getExportIntervalMillis(); + + abstract Collection getMetricProducers(); + + static Builder builder() { + return new AutoValue_IntervalMetricReader_InternalState.Builder() + .setExportIntervalMillis(DEFAULT_INTERVAL_MILLIS); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setExportIntervalMillis(long exportIntervalMillis); + + abstract Builder setMetricExporter(MetricExporter metricExporter); + + abstract Builder setMetricProducers(Collection metricProducers); + + abstract InternalState build(); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderBuilder.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderBuilder.java new file mode 100644 index 000000000..7fd383329 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderBuilder.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import io.opentelemetry.api.internal.Utils; +import java.util.Collection; + +/** Builder for {@link IntervalMetricReader}. */ +public final class IntervalMetricReaderBuilder { + private final IntervalMetricReader.InternalState.Builder optionsBuilder; + + IntervalMetricReaderBuilder(IntervalMetricReader.InternalState.Builder optionsBuilder) { + this.optionsBuilder = optionsBuilder; + } + + /** + * Sets the export interval. + * + * @param exportIntervalMillis the export interval between pushes to the exporter. + * @return this. + */ + public IntervalMetricReaderBuilder setExportIntervalMillis(long exportIntervalMillis) { + optionsBuilder.setExportIntervalMillis(exportIntervalMillis); + return this; + } + + /** + * Sets the exporter to be called when export metrics. + * + * @param metricExporter the {@link MetricExporter} to be called when export metrics. + * @return this. + */ + public IntervalMetricReaderBuilder setMetricExporter(MetricExporter metricExporter) { + optionsBuilder.setMetricExporter(metricExporter); + return this; + } + + /** + * Sets a collection of {@link MetricProducer} from where the metrics should be read. + * + * @param metricProducers a collection of {@link MetricProducer} from where the metrics should be + * read. + * @return this. + */ + public IntervalMetricReaderBuilder setMetricProducers( + Collection metricProducers) { + optionsBuilder.setMetricProducers(metricProducers); + return this; + } + + /** + * Builds a new {@link IntervalMetricReader} with current settings. Does not start the background + * thread. Please call {@link IntervalMetricReader#start()} to do that. + * + * @return a {@code IntervalMetricReader}. + */ + public IntervalMetricReader build() { + IntervalMetricReader.InternalState internalState = optionsBuilder.build(); + Utils.checkArgument( + internalState.getExportIntervalMillis() > 0, "Export interval must be positive"); + + return new IntervalMetricReader(internalState); + } + + /** + * Builds a new {@link IntervalMetricReader} with current settings and starts the background + * thread running. + * + * @return a {@code IntervalMetricReader}. + */ + public IntervalMetricReader buildAndStart() { + return build().start(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricExporter.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricExporter.java new file mode 100644 index 000000000..baf401b39 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricExporter.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.Collection; + +/** + * {@code MetricExporter} is the interface that all "push based" metric libraries should use to + * export metrics to the OpenTelemetry exporters. + * + *

    All OpenTelemetry exporters should allow access to a {@code MetricExporter} instance. + */ +public interface MetricExporter { + + /** + * Exports the collection of given {@link MetricData}. Note that export operations can be + * performed simultaneously depending on the type of metric reader being used. However, the {@link + * IntervalMetricReader} will ensure that only one export can occur at a time. + * + * @param metrics the collection of {@link MetricData} to be exported. + * @return the result of the export, which is often an asynchronous operation. + */ + CompletableResultCode export(Collection metrics); + + /** + * Exports the collection of {@link MetricData} that have not yet been exported. Note that flush + * operations can be performed simultaneously depending on the type of metric reader being used. + * However, the {@link IntervalMetricReader} will ensure that only one export can occur at a time. + * + * @return the result of the flush, which is often an asynchronous operation. + */ + CompletableResultCode flush(); + + /** + * Called when the associated IntervalMetricReader is shutdown. + * + * @return a {@link CompletableResultCode} which is completed when shutdown completes. + */ + CompletableResultCode shutdown(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricProducer.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricProducer.java new file mode 100644 index 000000000..a6c003185 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/MetricProducer.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.Collection; +import javax.annotation.concurrent.ThreadSafe; + +/** + * {@code MetricProducer} is the interface that is used to make metric data available to the + * OpenTelemetry exporters. Implementations should be stateful, in that each call to {@link + * #collectAllMetrics()} will return any metric generated since the last call was made. + * + *

    Implementations must be thread-safe. + */ +@ThreadSafe +public interface MetricProducer { + /** + * Returns a collection of produced {@link MetricData}s to be exported. This will only be those + * metrics that have been produced since the last time this method was called. + * + * @return a collection of produced {@link MetricData}s to be exported. + */ + Collection collectAllMetrics(); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/README.md b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/README.md new file mode 100644 index 000000000..429be48d2 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/README.md @@ -0,0 +1,112 @@ +# OpenTelemetry metrics export framework + +The metrics world split between "pushed based" and "pull based" libraries and backends, and because +of this, the OpenTelemetry export framework needs to address all the possible combinations. + +To achieve the support for "pushed based" and "pull based" libraries the OpenTelemetry defines two +interfaces that helps with this: +* MetricProducer - is the interface that a "pull based" library should implement in order to make +data available to OpenTelemetry exporters. +* MetricExporter - is an interface that every OpenTelemetry exporter should implement in order to +allow "push based" libraries to push metrics to the backend. + +Here are some examples on how different libraries will interact with pull/push backends. + +**Push backend:** + +```java +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.MetricProducer; + +/** + * Simple implementation of the MetricExporter that pushes data to the backend. + */ +public final class PushMetricExporter implements MetricExporter { + @Override + ResultCode export(Collection metrics) { + // A "push based" library calls to export metrics + return pushToBackend(metrics); + } +} + +/** + * Class that periodically reads from all MetricProducers and pushes metrics using the + * PushMetricExporter. + */ +public final class PushExporter { + private final PushMetricExporter metricExporter; + // IntervalMetricReader reads metrics from all producers periodically. + private final IntervalMetricReader intervalMetricReader; + + public PushExporter(Collection producers) { + metricExporter = new PushMetricExporter(); + intervalMetricReader = + IntervalMetricReader.builder() + .readEnvironment() // Read configuration from environment variables + .readSystemProperties() // Read configuration from system properties + .setExportIntervalMillis(100_000) + .setMetricExporter(metricExporter) + .setMetricProducers(Collections.singletonList(producers)) + .build(); + } + + // Can be accessed by any "push based" library to export metrics. + public MetricExporter getMetricExporter() { + return metricExporter; + } +} +``` + +**Pull backend:** + +```java +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.MetricProducer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * Simple implementation of the MetricExporter that stores data in memory and makes them available + * via MetricProducer interface. + */ +public final class PullMetricExporter implements MetricExporter, MetricProducer { + private final List metricsBuffer = new ArrayList<>(); + + @Override + synchronized ResultCode export(Collection metrics) { + metricsBuffer.addAll(metrics); + return ResultCode.SUCCESS; + } + + synchronized Collection getAllMetrics() { + List ret = metricsBuffer; + metricsBuffer = new ArrayList<>(); + return ret; + } +} + +public final class PullExporter { + private final PullMetricExporter metricExporter; + private final Collection producers; + + public PushExporter(Collection producers) { + metricExporter = new PullMetricExporter(); + producers = Collections.unmodifiableCollection(new ArrayList<>(producers)); + } + + // Can be accessed by any "push based" library to export metrics. + public MetricExporter getMetricExporter() { + return metricExporter; + } + + private void onPullRequest() { + // Iterate over all producers and the PullMetricExporter and export all metrics. + for (MetricProducer metricProducer : producers) { + Collection metrics = metricProducer.getAllMetrics(); + // Do something with metrics + } + } +} +``` diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/package-info.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/package-info.java new file mode 100644 index 000000000..8e0a2efc4 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/package-info.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Utilities that allow to export metrics to the OpenTelemetry exporters. + * + *

    Contents

    + * + *
      + *
    • {@link io.opentelemetry.sdk.metrics.export.IntervalMetricReader} + *
    • {@link io.opentelemetry.sdk.metrics.export.MetricExporter} + *
    • {@link io.opentelemetry.sdk.metrics.export.MetricProducer} + *
    + * + *

    Configuration options for {@link io.opentelemetry.sdk.metrics.export.IntervalMetricReader} can + * be read from system properties, environment variables, or {@link java.util.Properties} objects. + * + *

    For system properties and {@link java.util.Properties} objects, {@link + * io.opentelemetry.sdk.metrics.export.IntervalMetricReader} will look for the following names: + * + *

      + *
    • {@code otel.imr.export.interval}: sets the export interval between pushes to the exporter. + *
    + * + *

    For environment variables, {@link io.opentelemetry.sdk.metrics.export.IntervalMetricReader} + * will look for the following names: + * + *

      + *
    • {@code OTEL_IMR_EXPORT_INTERVAL}: sets the export interval between pushes to the exporter. + *
    + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.metrics.export; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/package-info.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/package-info.java new file mode 100644 index 000000000..683f8f5f5 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The SDK implementation of metrics. + * + * @see io.opentelemetry.sdk.metrics.SdkMeterProvider + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.metrics; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/LabelsProcessor.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/LabelsProcessor.java new file mode 100644 index 000000000..7869c44b5 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/LabelsProcessor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.processor; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.context.Context; + +/** + * Labels processor is an abstraction to manipulate instrument labels during metrics capture + * process. + */ +public interface LabelsProcessor { + /** + * Called when bound synchronous instrument is created or metrics are recorded for non-bound + * synchronous instrument. Allows to manipulate labels which this instrument is bound to in case + * of binding operation or labels used for recording values in case of non-bound synchronous + * instrument. Particular use case includes enriching labels and/or adding more labels depending + * on the Context + * + *

    Please note, this is an experimental API. In case of bound instruments, it will be only + * invoked upon instrument binding and not when measurements are recorded. + * + * @param ctx context of the operation + * @param labels immutable labels. When processors are chained output labels of the previous one + * is passed as an input to the next one. Last labels returned by a chain of processors are + * used for bind() operation. + * @return labels to be used as an input to the next processor in chain or bind() operation if + * this is the last processor + */ + Labels onLabelsBound(Context ctx, Labels labels); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/LabelsProcessorFactory.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/LabelsProcessorFactory.java new file mode 100644 index 000000000..3cf916f67 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/LabelsProcessorFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.processor; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.resources.Resource; + +public interface LabelsProcessorFactory { + static LabelsProcessorFactory noop() { + return (resource, instrumentationLibraryInfo, descriptor) -> new NoopLabelsProcessor(); + } + + /** + * Returns a new {@link LabelsProcessorFactory}. + * + * @return new {@link LabelsProcessorFactory} + */ + LabelsProcessor create( + Resource resource, + InstrumentationLibraryInfo instrumentationLibraryInfo, + InstrumentDescriptor descriptor); +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/NoopLabelsProcessor.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/NoopLabelsProcessor.java new file mode 100644 index 000000000..f2c3bf507 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/processor/NoopLabelsProcessor.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.processor; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.context.Context; + +public class NoopLabelsProcessor implements LabelsProcessor { + + @Override + public Labels onLabelsBound(Context c, Labels labels) { + return labels; + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricExporter.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricExporter.java new file mode 100644 index 000000000..0a419038b --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricExporter.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.testing; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A {@link MetricExporter} implementation that can be used to test OpenTelemetry integration. + * + *

    Can be created using {@code InMemoryMetricExporter.create()} + * + *

    Example usage: + * + *

    
    + * public class InMemoryMetricExporterExample {
    + *
    + *   // creating InMemoryMetricExporter
    + *   private final InMemoryMetricExporter exporter = InMemoryMetricExporter.create();
    + *   private final MeterSdkProvider meterSdkProvider = OpenTelemetrySdk.getMeterProvider();
    + *   private final Meter meter = meterSdkProvider.get("InMemoryMetricExporterExample");
    + *   private IntervalMetricReader intervalMetricReader;
    + *
    + *   void setup() {
    + *     intervalMetricReader =
    + *         IntervalMetricReader.builder()
    + *             .setMetricExporter(exporter)
    + *             .setMetricProducers(Collections.singletonList(meterSdkProvider.getMetricProducer()))
    + *             .setExportIntervalMillis(1000)
    + *             .build();
    + *   }
    + *
    + *   LongCounter generateLongCounterMeter(String name) {
    + *     return meter.longCounterBuilder(name).setDescription("Sample LongCounter").build();
    + *   }
    + *
    + *   public static void main(String[] args) throws InterruptedException {
    + *     InMemoryMetricExporterExample example = new InMemoryMetricExporterExample();
    + *     example.setup();
    + *     example.generateLongCounterMeter("counter-1");
    + *   }
    + * }
    + * 
    + */ +public final class InMemoryMetricExporter implements MetricExporter { + + // using LinkedBlockingQueue to avoid manual locks for thread-safe operations + private final Queue finishedMetricItems = new LinkedBlockingQueue<>(); + private boolean isStopped = false; + + private InMemoryMetricExporter() {} + + /** + * Returns a new instance of the {@code InMemoryMetricExporter}. + * + * @return a new instance of the {@code InMemoryMetricExporter}. + */ + public static InMemoryMetricExporter create() { + return new InMemoryMetricExporter(); + } + + /** + * Returns a {@code List} of the finished {@code Metric}s, represented by {@code MetricData}. + * + * @return a {@code List} of the finished {@code Metric}s. + */ + public List getFinishedMetricItems() { + return Collections.unmodifiableList(new ArrayList<>(finishedMetricItems)); + } + + /** + * Clears the internal {@code List} of finished {@code Metric}s. + * + *

    Does not reset the state of this exporter if already shutdown. + */ + public void reset() { + finishedMetricItems.clear(); + } + + /** + * Exports the collection of {@code Metric}s into the inmemory queue. + * + *

    If this is called after {@code shutdown}, this will return {@code ResultCode.FAILURE}. + */ + @Override + public CompletableResultCode export(Collection metrics) { + if (isStopped) { + return CompletableResultCode.ofFailure(); + } + finishedMetricItems.addAll(metrics); + return CompletableResultCode.ofSuccess(); + } + + /** + * The InMemory exporter does not batch metrics, so this method will immediately return with + * success. + * + * @return always Success + */ + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + /** + * Clears the internal {@code List} of finished {@code Metric}s. + * + *

    Any subsequent call to export() function on this MetricExporter, will return {@code + * CompletableResultCode.ofFailure()} + */ + @Override + public CompletableResultCode shutdown() { + isStopped = true; + finishedMetricItems.clear(); + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/package-info.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/package-info.java new file mode 100644 index 000000000..334639b99 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/testing/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.metrics.testing; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/InstrumentSelector.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/InstrumentSelector.java new file mode 100644 index 000000000..9caccdabc --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/InstrumentSelector.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.view; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import java.util.Objects; +import java.util.regex.Pattern; +import javax.annotation.concurrent.Immutable; + +/** + * Provides means for selecting one ore more {@link io.opentelemetry.api.metrics.Instrument}s. Used + * for configuring aggregations for the specified instruments. + */ +@AutoValue +@Immutable +public abstract class InstrumentSelector { + private static final Pattern MATCH_ALL = Pattern.compile(".*"); + + /** + * Returns a new {@link Builder} for {@link InstrumentSelector}. + * + * @return a new {@link Builder} for {@link InstrumentSelector}. + */ + public static Builder builder() { + return new AutoValue_InstrumentSelector.Builder().setInstrumentNamePattern(MATCH_ALL); + } + + /** + * Returns {@link InstrumentType} that should be selected. If null, then this specifier will not + * be used. + */ + public abstract InstrumentType getInstrumentType(); + + /** + * Returns the {@link Pattern} generated by the provided {@code regex} in the {@link Builder}, or + * {@code Pattern.compile(".*")} if none was specified. + */ + public abstract Pattern getInstrumentNamePattern(); + + /** Builder for {@link InstrumentSelector} instances. */ + @AutoValue.Builder + public abstract static class Builder { + /** Sets a specifier for {@link InstrumentType}. */ + public abstract Builder setInstrumentType(InstrumentType instrumentType); + + abstract Builder setInstrumentNamePattern(Pattern instrumentNamePattern); + + /** Sets a specifier for selecting Instruments by name. */ + public final Builder setInstrumentNameRegex(String regex) { + return setInstrumentNamePattern(Pattern.compile(Objects.requireNonNull(regex, "regex"))); + } + + /** Returns an InstrumentSelector instance with the content of this builder. */ + public abstract InstrumentSelector build(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/View.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/View.java new file mode 100644 index 000000000..3591b1703 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/View.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.view; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessorFactory; +import javax.annotation.concurrent.Immutable; + +/** TODO: javadoc. */ +@AutoValue +@Immutable +public abstract class View { + public abstract AggregatorFactory getAggregatorFactory(); + + public abstract LabelsProcessorFactory getLabelsProcessorFactory(); + + public static ViewBuilder builder() { + return new ViewBuilder(); + } + + static View create( + AggregatorFactory aggregatorFactory, LabelsProcessorFactory labelsProcessorFactory) { + return new AutoValue_View(aggregatorFactory, labelsProcessorFactory); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/ViewBuilder.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/ViewBuilder.java new file mode 100644 index 000000000..3d524f2ad --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/ViewBuilder.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.view; + +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessorFactory; + +public final class ViewBuilder { + private AggregatorFactory aggregatorFactory; + private LabelsProcessorFactory labelsProcessorFactory = LabelsProcessorFactory.noop(); + + ViewBuilder() {} + + /** + * sets {@link AggregatorFactory}. + * + * @param aggregatorFactory aggregator factory. + * @return this Builder. + */ + public ViewBuilder setAggregatorFactory(AggregatorFactory aggregatorFactory) { + this.aggregatorFactory = aggregatorFactory; + return this; + } + + /** + * sets {@link LabelsProcessorFactory}. + * + * @param labelsProcessorFactory labels processor factory. + * @return this Builder. + */ + public ViewBuilder setLabelsProcessorFactory(LabelsProcessorFactory labelsProcessorFactory) { + this.labelsProcessorFactory = labelsProcessorFactory; + return this; + } + + public View build() { + return View.create(this.aggregatorFactory, this.labelsProcessorFactory); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/package-info.java b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/package-info.java new file mode 100644 index 000000000..648a170ac --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/view/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Metric views. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.metrics.view; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AbstractInstrumentBuilderTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AbstractInstrumentBuilderTest.java new file mode 100644 index 000000000..99c1442da --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AbstractInstrumentBuilderTest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.sdk.metrics.AbstractInstrument.Builder.ERROR_MESSAGE_INVALID_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.internal.MetricsStringUtils; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AbstractInstrument.Builder}. */ +class AbstractInstrumentBuilderTest { + + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String UNIT = "1"; + + @Test + void preventNull_Name() { + assertThatThrownBy(() -> new TestInstrumentBuilder(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("name"); + } + + @Test + void preventEmpty_Name() { + assertThatThrownBy(() -> new TestInstrumentBuilder("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Name"); + } + + @Test + void checkCorrect_Name() { + new TestInstrumentBuilder("a"); + new TestInstrumentBuilder("METRIC_name"); + new TestInstrumentBuilder("metric.name_01"); + new TestInstrumentBuilder("metric_name.01"); + assertThatThrownBy(() -> new TestInstrumentBuilder("01.metric_name_01")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Name"); + } + + @Test + void preventNonPrintableName() { + assertThatThrownBy(() -> new TestInstrumentBuilder("\2").build()) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void preventTooLongName() { + char[] chars = new char[MetricsStringUtils.METRIC_NAME_MAX_LENGTH + 1]; + Arrays.fill(chars, 'a'); + String longName = String.valueOf(chars); + assertThatThrownBy(() -> new TestInstrumentBuilder(longName).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE_INVALID_NAME); + } + + @Test + void preventNull_Description() { + assertThatThrownBy(() -> new TestInstrumentBuilder(NAME).setDescription(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("description"); + } + + @Test + void preventNull_Unit() { + assertThatThrownBy(() -> new TestInstrumentBuilder(NAME).setUnit(null).build()) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + } + + @Test + void defaultValue() { + TestInstrumentBuilder testInstrumentBuilder = new TestInstrumentBuilder(NAME); + TestInstrument testInstrument = testInstrumentBuilder.build(); + assertThat(testInstrument).isInstanceOf(TestInstrument.class); + assertThat(testInstrument.getDescriptor().getName()).isEqualTo(NAME); + assertThat(testInstrument.getDescriptor().getDescription()).isEmpty(); + assertThat(testInstrument.getDescriptor().getUnit()).isEqualTo("1"); + } + + @Test + void setAndGetValues() { + TestInstrumentBuilder testInstrumentBuilder = + new TestInstrumentBuilder(NAME).setDescription(DESCRIPTION).setUnit(UNIT); + + TestInstrument testInstrument = testInstrumentBuilder.build(); + assertThat(testInstrument).isInstanceOf(TestInstrument.class); + assertThat(testInstrument.getDescriptor().getName()).isEqualTo(NAME); + assertThat(testInstrument.getDescriptor().getDescription()).isEqualTo(DESCRIPTION); + assertThat(testInstrument.getDescriptor().getUnit()).isEqualTo(UNIT); + assertThat(testInstrument.getDescriptor().getType()).isEqualTo(InstrumentType.UP_DOWN_COUNTER); + assertThat(testInstrument.getDescriptor().getValueType()).isEqualTo(InstrumentValueType.LONG); + } + + private static final class TestInstrumentBuilder + extends AbstractInstrument.Builder { + TestInstrumentBuilder(String name) { + super(name, InstrumentType.UP_DOWN_COUNTER, InstrumentValueType.LONG); + } + + @Override + TestInstrumentBuilder getThis() { + return this; + } + + @Override + public TestInstrument build() { + return new TestInstrument(buildDescriptor()); + } + } + + private static final class TestInstrument extends AbstractInstrument { + TestInstrument(InstrumentDescriptor descriptor) { + super(descriptor); + } + + @Override + List collectAll(long epochNanos) { + return Collections.emptyList(); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AbstractInstrumentTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AbstractInstrumentTest.java new file mode 100644 index 000000000..1315218a6 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AbstractInstrumentTest.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AbstractInstrument}. */ +class AbstractInstrumentTest { + private static final InstrumentDescriptor INSTRUMENT_DESCRIPTOR = + InstrumentDescriptor.create( + "name", "description", "1", InstrumentType.COUNTER, InstrumentValueType.LONG); + + @Test + void getValues() { + TestInstrument testInstrument = new TestInstrument(INSTRUMENT_DESCRIPTOR); + assertThat(testInstrument.getDescriptor()).isSameAs(INSTRUMENT_DESCRIPTOR); + } + + private static final class TestInstrument extends AbstractInstrument { + TestInstrument(InstrumentDescriptor descriptor) { + super(descriptor); + } + + @Override + List collectAll(long epochNanos) { + return Collections.emptyList(); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AsynchronousInstrumentAccumulatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AsynchronousInstrumentAccumulatorTest.java new file mode 100644 index 000000000..8bd661782 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/AsynchronousInstrumentAccumulatorTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessor; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class AsynchronousInstrumentAccumulatorTest { + private final TestClock testClock = TestClock.create(); + private MeterProviderSharedState meterProviderSharedState; + private final MeterSharedState meterSharedState = + MeterSharedState.create(InstrumentationLibraryInfo.empty()); + private LabelsProcessor spyLabelProcessor; + + @BeforeEach + void setup() { + spyLabelProcessor = + Mockito.spy( + // note: can't convert to a lambda here because Mockito gets grumpy + new LabelsProcessor() { + @Override + public Labels onLabelsBound(Context ctx, Labels labels) { + return labels.toBuilder().build(); + } + }); + ViewRegistry viewRegistry = + ViewRegistry.builder() + .addView( + InstrumentSelector.builder() + .setInstrumentType(InstrumentType.VALUE_OBSERVER) + .build(), + View.builder() + .setAggregatorFactory(AggregatorFactory.lastValue()) + .setLabelsProcessorFactory( + (resource, instrumentationLibraryInfo, descriptor) -> spyLabelProcessor) + .build()) + .build(); + + meterProviderSharedState = + MeterProviderSharedState.create(testClock, Resource.empty(), viewRegistry); + } + + @Test + void doubleAsynchronousAccumulator_LabelsProcessor_used() { + AsynchronousInstrumentAccumulator.doubleAsynchronousAccumulator( + meterProviderSharedState, + meterSharedState, + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_OBSERVER, + InstrumentValueType.DOUBLE), + value -> value.observe(1.0, Labels.empty())) + .collectAll(testClock.nanoTime()); + Mockito.verify(spyLabelProcessor).onLabelsBound(Context.current(), Labels.empty()); + } + + @Test + void longAsynchronousAccumulator_LabelsProcessor_used() { + AsynchronousInstrumentAccumulator.longAsynchronousAccumulator( + meterProviderSharedState, + meterSharedState, + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_OBSERVER, + InstrumentValueType.LONG), + value -> value.observe(1, Labels.empty())) + .collectAll(testClock.nanoTime()); + Mockito.verify(spyLabelProcessor).onLabelsBound(Context.current(), Labels.empty()); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/BatchRecorderSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/BatchRecorderSdkTest.java new file mode 100644 index 000000000..e1cf31c5f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/BatchRecorderSdkTest.java @@ -0,0 +1,223 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BatchRecorder; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleValueRecorder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongValueRecorder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link BatchRecorderSdk}. */ +class BatchRecorderSdkTest { + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(BatchRecorderSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void batchRecorder_badLabelSet() { + assertThatThrownBy(() -> sdkMeter.newBatchRecorder("key").record()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("key/value"); + } + + @Test + void batchRecorder() { + DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testDoubleCounter").build(); + LongCounter longCounter = sdkMeter.longCounterBuilder("testLongCounter").build(); + DoubleUpDownCounter doubleUpDownCounter = + sdkMeter.doubleUpDownCounterBuilder("testDoubleUpDownCounter").build(); + LongUpDownCounter longUpDownCounter = + sdkMeter.longUpDownCounterBuilder("testLongUpDownCounter").build(); + DoubleValueRecorder doubleValueRecorder = + sdkMeter.doubleValueRecorderBuilder("testDoubleValueRecorder").build(); + LongValueRecorder longValueRecorder = + sdkMeter.longValueRecorderBuilder("testLongValueRecorder").build(); + Labels labelSet = Labels.of("key", "value"); + + BatchRecorder batchRecorder = sdkMeter.newBatchRecorder("key", "value"); + + batchRecorder + .put(longCounter, 12) + .put(doubleUpDownCounter, -12.1d) + .put(longUpDownCounter, -12) + .put(doubleCounter, 12.1d) + .put(doubleCounter, 12.1d) + .put(longValueRecorder, 13) + .put(doubleValueRecorder, 13.1d); + + // until record() is called, nothing should be recorded. + Collection preRecord = sdkMeterProvider.collectAllMetrics(); + preRecord.forEach(metricData -> assertThat(metricData.isEmpty()).isTrue()); + + batchRecorder.record(); + + assertBatchRecordings( + doubleCounter, + longCounter, + doubleUpDownCounter, + longUpDownCounter, + doubleValueRecorder, + longValueRecorder, + labelSet, + /* shouldHaveDeltas=*/ true); + + // a second record, with no recordings added should not change any of the values. + batchRecorder.record(); + assertBatchRecordings( + doubleCounter, + longCounter, + doubleUpDownCounter, + longUpDownCounter, + doubleValueRecorder, + longValueRecorder, + labelSet, + /* shouldHaveDeltas=*/ false); + } + + private void assertBatchRecordings( + DoubleCounter doubleCounter, + LongCounter longCounter, + DoubleUpDownCounter doubleUpDownCounter, + LongUpDownCounter longUpDownCounter, + DoubleValueRecorder doubleValueRecorder, + LongValueRecorder longValueRecorder, + Labels labelSet, + boolean shouldHaveDeltas) { + assertThat(((AbstractInstrument) doubleCounter).collectAll(testClock.now())) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now(), testClock.now(), labelSet, 24.2d))))); + assertThat(((AbstractInstrument) longCounter).collectAll(testClock.now())) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create(testClock.now(), testClock.now(), labelSet, 12))))); + assertThat(((AbstractInstrument) doubleUpDownCounter).collectAll(testClock.now())) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleUpDownCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now(), testClock.now(), labelSet, -12.1d))))); + assertThat(((AbstractInstrument) longUpDownCounter).collectAll(testClock.now())) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create(testClock.now(), testClock.now(), labelSet, -12))))); + + if (shouldHaveDeltas) { + assertThat(((AbstractInstrument) doubleValueRecorder).collectAll(testClock.now())) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleValueRecorder", + "", + "1", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + labelSet, + 1, + 13.1d, + Arrays.asList( + ValueAtPercentile.create(0.0, 13.1), + ValueAtPercentile.create(100.0, 13.1))))))); + } else { + assertThat(((AbstractInstrument) doubleValueRecorder).collectAll(testClock.now())).isEmpty(); + } + + if (shouldHaveDeltas) { + assertThat(((AbstractInstrument) longValueRecorder).collectAll(testClock.now())) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongValueRecorder", + "", + "1", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + labelSet, + 1, + 13, + Arrays.asList( + ValueAtPercentile.create(0.0, 13), + ValueAtPercentile.create(100.0, 13))))))); + } else { + assertThat(((AbstractInstrument) longValueRecorder).collectAll(testClock.now())).isEmpty(); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleCounterSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleCounterSdkTest.java new file mode 100644 index 000000000..c30da5b6e --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleCounterSdkTest.java @@ -0,0 +1,301 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BoundDoubleCounter; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DoubleCounterSdk}. */ +class DoubleCounterSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(DoubleCounterSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void add_PreventNullLabels() { + assertThatThrownBy(() -> sdkMeter.doubleCounterBuilder("testCounter").build().add(1.0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> sdkMeter.doubleCounterBuilder("testCounter").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void collectMetrics_NoRecords() { + DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testCounter").build(); + BoundDoubleCounter bound = doubleCounter.bind(Labels.of("foo", "bar")); + try { + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } finally { + bound.unbind(); + } + } + + @Test + void collectMetrics_WithEmptyLabel() { + DoubleCounter doubleCounter = + sdkMeter + .doubleCounterBuilder("testCounter") + .setDescription("description") + .setUnit("ms") + .build(); + testClock.advanceNanos(SECOND_NANOS); + doubleCounter.add(12d, Labels.empty()); + doubleCounter.add(12d); + // TODO: This is not perfect because we compare double values using direct equal, maybe worth + // changing to do a proper comparison for double values, here and everywhere else. + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "description", + "ms", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.empty(), + 24))))); + } + + @Test + void collectMetrics_WithMultipleCollects() { + long startTime = testClock.now(); + DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testCounter").build(); + BoundDoubleCounter bound = doubleCounter.bind(Labels.of("K", "V")); + try { + // Do some records using bounds and direct calls and bindings. + doubleCounter.add(12.1d, Labels.empty()); + bound.add(123.3d); + doubleCounter.add(21.4d, Labels.empty()); + // Advancing time here should not matter. + testClock.advanceNanos(SECOND_NANOS); + bound.add(321.5d); + doubleCounter.add(111.1d, Labels.of("K", "V")); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + DoublePointData.create( + startTime, testClock.now(), Labels.of("K", "V"), 555.9d), + DoublePointData.create( + startTime, testClock.now(), Labels.empty(), 33.5d))))); + + // Repeat to prove we keep previous values. + testClock.advanceNanos(SECOND_NANOS); + bound.add(222d); + doubleCounter.add(11d, Labels.empty()); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + DoublePointData.create( + startTime, testClock.now(), Labels.of("K", "V"), 777.9d), + DoublePointData.create( + startTime, testClock.now(), Labels.empty(), 44.5d))))); + } finally { + bound.unbind(); + } + } + + @Test + void doubleCounterAdd_Monotonicity() { + DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testCounter").build(); + + assertThatThrownBy(() -> doubleCounter.add(-45.77d, Labels.empty())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void boundDoubleCounterAdd_Monotonicity() { + DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testCounter").build(); + + assertThatThrownBy(() -> doubleCounter.bind(Labels.empty()).add(-9.3)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void stressTest() { + final DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testCounter").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((DoubleCounterSdk) doubleCounter) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, 2, new OperationUpdaterDirectCall(doubleCounter, "K", "V"))); + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, 2, new OperationUpdaterWithBinding(doubleCounter.bind(Labels.of("K", "V"))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now(), testClock.now(), Labels.of("K", "V"), 80_000))))); + } + + @Test + void stressTest_WithDifferentLabelSet() { + final String[] keys = {"Key_1", "Key_2", "Key_3", "Key_4"}; + final String[] values = {"Value_1", "Value_2", "Value_3", "Value_4"}; + final DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testCounter").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((DoubleCounterSdk) doubleCounter) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, 1, new OperationUpdaterDirectCall(doubleCounter, keys[i], values[i]))); + + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, + 1, + new OperationUpdaterWithBinding(doubleCounter.bind(Labels.of(keys[i], values[i]))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + DoublePointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[0], values[0]), + 40_000), + DoublePointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[1], values[1]), + 40_000), + DoublePointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[2], values[2]), + 40_000), + DoublePointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[3], values[3]), + 40_000))))); + } + + private static class OperationUpdaterWithBinding extends OperationUpdater { + private final BoundDoubleCounter boundDoubleCounter; + + private OperationUpdaterWithBinding(BoundDoubleCounter boundDoubleCounter) { + this.boundDoubleCounter = boundDoubleCounter; + } + + @Override + void update() { + boundDoubleCounter.add(9.0); + } + + @Override + void cleanup() { + boundDoubleCounter.unbind(); + } + } + + private static class OperationUpdaterDirectCall extends OperationUpdater { + + private final DoubleCounter doubleCounter; + private final String key; + private final String value; + + private OperationUpdaterDirectCall(DoubleCounter doubleCounter, String key, String value) { + this.doubleCounter = doubleCounter; + this.key = key; + this.value = value; + } + + @Override + void update() { + doubleCounter.add(11.0, Labels.of(key, value)); + } + + @Override + void cleanup() {} + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleSumObserverSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleSumObserverSdkTest.java new file mode 100644 index 000000000..7308d0fe9 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleSumObserverSdkTest.java @@ -0,0 +1,167 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessorFactory; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DoubleSumObserverSdk}. */ +class DoubleSumObserverSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(DoubleSumObserverSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProviderBuilder sdkMeterProviderBuilder = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE); + + @Test + void collectMetrics_NoCallback() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .doubleSumObserverBuilder("testObserver") + .setDescription("My own DoubleSumObserver") + .setUnit("ms") + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_NoRecords() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .doubleSumObserverBuilder("testObserver") + .setDescription("My own DoubleSumObserver") + .setUnit("ms") + .setUpdater(result -> {}) + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_WithOneRecord() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .doubleSumObserverBuilder("testObserver") + .setDescription("My own DoubleSumObserver") + .setUnit("ms") + .setUpdater(result -> result.observe(12.1d, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "My own DoubleSumObserver", + "ms", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12.1d))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "My own DoubleSumObserver", + "ms", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now() - 2 * SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12.1d))))); + } + + @Test + void collectMetrics_DeltaSumAggregator() { + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder + .registerView( + InstrumentSelector.builder().setInstrumentType(InstrumentType.SUM_OBSERVER).build(), + View.builder() + .setLabelsProcessorFactory(LabelsProcessorFactory.noop()) + .setAggregatorFactory(AggregatorFactory.sum(AggregationTemporality.DELTA)) + .build()) + .build(); + sdkMeterProvider + .get(getClass().getName()) + .doubleSumObserverBuilder("testObserver") + .setDescription("My own DoubleSumObserver") + .setUnit("ms") + .setUpdater(result -> result.observe(12.1d, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "My own DoubleSumObserver", + "ms", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + DoublePointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12.1d))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "My own DoubleSumObserver", + "ms", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + DoublePointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 0))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleUpDownCounterSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleUpDownCounterSdkTest.java new file mode 100644 index 000000000..5c0d223a0 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleUpDownCounterSdkTest.java @@ -0,0 +1,296 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BoundDoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DoubleUpDownCounterSdk}. */ +class DoubleUpDownCounterSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(DoubleUpDownCounterSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void add_PreventNullLabels() { + assertThatThrownBy( + () -> sdkMeter.doubleUpDownCounterBuilder("testUpDownCounter").build().add(1.0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy( + () -> sdkMeter.doubleUpDownCounterBuilder("testUpDownCounter").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void collectMetrics_NoRecords() { + DoubleUpDownCounter doubleUpDownCounter = + sdkMeter.doubleUpDownCounterBuilder("testUpDownCounter").build(); + BoundDoubleUpDownCounter bound = doubleUpDownCounter.bind(Labels.of("foo", "bar")); + try { + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } finally { + bound.unbind(); + } + } + + @Test + void collectMetrics_WithEmptyLabel() { + DoubleUpDownCounter doubleUpDownCounter = + sdkMeter + .doubleUpDownCounterBuilder("testUpDownCounter") + .setDescription("description") + .setUnit("ms") + .build(); + testClock.advanceNanos(SECOND_NANOS); + doubleUpDownCounter.add(12d, Labels.empty()); + doubleUpDownCounter.add(12d); + // TODO: This is not perfect because we compare double values using direct equal, maybe worth + // changing to do a proper comparison for double values, here and everywhere else. + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "description", + "ms", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.empty(), + 24))))); + } + + @Test + void collectMetrics_WithMultipleCollects() { + long startTime = testClock.now(); + DoubleUpDownCounter doubleUpDownCounter = + sdkMeter.doubleUpDownCounterBuilder("testUpDownCounter").build(); + BoundDoubleUpDownCounter bound = doubleUpDownCounter.bind(Labels.of("K", "V")); + try { + // Do some records using bounds and direct calls and bindings. + doubleUpDownCounter.add(12.1d, Labels.empty()); + bound.add(123.3d); + doubleUpDownCounter.add(21.4d, Labels.empty()); + // Advancing time here should not matter. + testClock.advanceNanos(SECOND_NANOS); + bound.add(321.5d); + doubleUpDownCounter.add(111.1d, Labels.of("K", "V")); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + DoublePointData.create( + startTime, testClock.now(), Labels.of("K", "V"), 555.9d), + DoublePointData.create( + startTime, testClock.now(), Labels.empty(), 33.5d))))); + + // Repeat to prove we keep previous values. + testClock.advanceNanos(SECOND_NANOS); + bound.add(222d); + doubleUpDownCounter.add(11d, Labels.empty()); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + DoublePointData.create( + startTime, testClock.now(), Labels.of("K", "V"), 777.9d), + DoublePointData.create( + startTime, testClock.now(), Labels.empty(), 44.5d))))); + } finally { + bound.unbind(); + } + } + + @Test + void stressTest() { + final DoubleUpDownCounter doubleUpDownCounter = + sdkMeter.doubleUpDownCounterBuilder("testUpDownCounter").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((DoubleUpDownCounterSdk) doubleUpDownCounter) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, 2, new OperationUpdaterDirectCall(doubleUpDownCounter, "K", "V"))); + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, + 2, + new OperationUpdaterWithBinding(doubleUpDownCounter.bind(Labels.of("K", "V"))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now(), testClock.now(), Labels.of("K", "V"), 80_000))))); + } + + @Test + void stressTest_WithDifferentLabelSet() { + final String[] keys = {"Key_1", "Key_2", "Key_3", "Key_4"}; + final String[] values = {"Value_1", "Value_2", "Value_3", "Value_4"}; + final DoubleUpDownCounter doubleUpDownCounter = + sdkMeter.doubleUpDownCounterBuilder("testUpDownCounter").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((DoubleUpDownCounterSdk) doubleUpDownCounter) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, 1, new OperationUpdaterDirectCall(doubleUpDownCounter, keys[i], values[i]))); + + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, + 1, + new OperationUpdaterWithBinding( + doubleUpDownCounter.bind(Labels.of(keys[i], values[i]))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + DoublePointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[0], values[0]), + 40_000), + DoublePointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[1], values[1]), + 40_000), + DoublePointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[2], values[2]), + 40_000), + DoublePointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[3], values[3]), + 40_000))))); + } + + private static class OperationUpdaterWithBinding extends OperationUpdater { + private final BoundDoubleUpDownCounter boundDoubleUpDownCounter; + + private OperationUpdaterWithBinding(BoundDoubleUpDownCounter boundDoubleUpDownCounter) { + this.boundDoubleUpDownCounter = boundDoubleUpDownCounter; + } + + @Override + void update() { + boundDoubleUpDownCounter.add(9.0); + } + + @Override + void cleanup() { + boundDoubleUpDownCounter.unbind(); + } + } + + private static class OperationUpdaterDirectCall extends OperationUpdater { + + private final DoubleUpDownCounter doubleUpDownCounter; + private final String key; + private final String value; + + private OperationUpdaterDirectCall( + DoubleUpDownCounter doubleUpDownCounter, String key, String value) { + this.doubleUpDownCounter = doubleUpDownCounter; + this.key = key; + this.value = value; + } + + @Override + void update() { + doubleUpDownCounter.add(11.0, Labels.of(key, value)); + } + + @Override + void cleanup() {} + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleUpDownSumObserverSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleUpDownSumObserverSdkTest.java new file mode 100644 index 000000000..817413d16 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleUpDownSumObserverSdkTest.java @@ -0,0 +1,165 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessorFactory; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DoubleUpDownSumObserverSdk}. */ +class DoubleUpDownSumObserverSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(DoubleUpDownSumObserverSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProviderBuilder sdkMeterProviderBuilder = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE); + + @Test + void collectMetrics_NoCallback() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .doubleUpDownSumObserverBuilder("testObserver") + .setDescription("My own DoubleUpDownSumObserver") + .setUnit("ms") + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_NoRecords() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .doubleUpDownSumObserverBuilder("testObserver") + .setDescription("My own DoubleUpDownSumObserver") + .setUnit("ms") + .setUpdater(result -> {}) + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_WithOneRecord() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .doubleUpDownSumObserverBuilder("testObserver") + .setUpdater(result -> result.observe(12.1d, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12.1d))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now() - 2 * SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12.1d))))); + } + + @Test + void collectMetrics_DeltaSumAggregator() { + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder + .registerView( + InstrumentSelector.builder() + .setInstrumentType(InstrumentType.UP_DOWN_SUM_OBSERVER) + .build(), + View.builder() + .setLabelsProcessorFactory(LabelsProcessorFactory.noop()) + .setAggregatorFactory(AggregatorFactory.sum(AggregationTemporality.DELTA)) + .build()) + .build(); + sdkMeterProvider + .get(getClass().getName()) + .doubleUpDownSumObserverBuilder("testObserver") + .setUpdater(result -> result.observe(12.1d, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.DELTA, + Collections.singletonList( + DoublePointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12.1d))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.DELTA, + Collections.singletonList( + DoublePointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 0))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleValueObserverSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleValueObserverSdkTest.java new file mode 100644 index 000000000..6fcfe177e --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleValueObserverSdkTest.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DoubleValueObserverSdk}. */ +class DoubleValueObserverSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(DoubleValueObserverSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void collectMetrics_NoCallback() { + sdkMeter + .doubleValueObserverBuilder("testObserver") + .setDescription("My own DoubleValueObserver") + .setUnit("ms") + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_NoRecords() { + sdkMeter + .doubleValueObserverBuilder("testObserver") + .setDescription("My own DoubleValueObserver") + .setUnit("ms") + .setUpdater(result -> {}) + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_WithOneRecord() { + sdkMeter + .doubleValueObserverBuilder("testObserver") + .setDescription("My own DoubleValueObserver") + .setUnit("ms") + .setUpdater(result -> result.observe(12.1d, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleGauge( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "My own DoubleValueObserver", + "ms", + DoubleGaugeData.create( + Collections.singletonList( + DoublePointData.create(0, testClock.now(), Labels.of("k", "v"), 12.1d))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleGauge( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "My own DoubleValueObserver", + "ms", + DoubleGaugeData.create( + Collections.singletonList( + DoublePointData.create(0, testClock.now(), Labels.of("k", "v"), 12.1d))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleValueRecorderSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleValueRecorderSdkTest.java new file mode 100644 index 000000000..69be83b1b --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/DoubleValueRecorderSdkTest.java @@ -0,0 +1,323 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BoundDoubleValueRecorder; +import io.opentelemetry.api.metrics.DoubleValueRecorder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DoubleValueRecorderSdk}. */ +class DoubleValueRecorderSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(DoubleValueRecorderSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void record_PreventNullLabels() { + assertThatThrownBy( + () -> sdkMeter.doubleValueRecorderBuilder("testRecorder").build().record(1.0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> sdkMeter.doubleValueRecorderBuilder("testRecorder").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void collectMetrics_NoRecords() { + DoubleValueRecorder doubleRecorder = + sdkMeter.doubleValueRecorderBuilder("testRecorder").build(); + BoundDoubleValueRecorder bound = doubleRecorder.bind(Labels.of("key", "value")); + try { + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } finally { + bound.unbind(); + } + } + + @Test + void collectMetrics_WithEmptyLabel() { + DoubleValueRecorder doubleRecorder = + sdkMeter + .doubleValueRecorderBuilder("testRecorder") + .setDescription("description") + .setUnit("ms") + .build(); + testClock.advanceNanos(SECOND_NANOS); + doubleRecorder.record(12d, Labels.empty()); + doubleRecorder.record(12d); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "description", + "ms", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.empty(), + 2, + 24d, + valueAtPercentiles(12d, 12d)))))); + } + + @Test + void collectMetrics_WithMultipleCollects() { + long startTime = testClock.now(); + DoubleValueRecorder doubleRecorder = + sdkMeter.doubleValueRecorderBuilder("testRecorder").build(); + BoundDoubleValueRecorder bound = doubleRecorder.bind(Labels.of("K", "V")); + try { + // Do some records using bounds and direct calls and bindings. + doubleRecorder.record(12.1d, Labels.empty()); + bound.record(123.3d); + doubleRecorder.record(-13.1d, Labels.empty()); + // Advancing time here should not matter. + testClock.advanceNanos(SECOND_NANOS); + bound.record(321.5d); + doubleRecorder.record(-121.5d, Labels.of("K", "V")); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "", + "1", + DoubleSummaryData.create( + Arrays.asList( + DoubleSummaryPointData.create( + startTime, + testClock.now(), + Labels.of("K", "V"), + 3, + 323.3d, + valueAtPercentiles(-121.5d, 321.5d)), + DoubleSummaryPointData.create( + startTime, + testClock.now(), + Labels.empty(), + 2, + -1.0d, + valueAtPercentiles(-13.1d, 12.1d)))))); + + // Repeat to prove we don't keep previous values. + testClock.advanceNanos(SECOND_NANOS); + bound.record(222d); + doubleRecorder.record(17d, Labels.empty()); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "", + "1", + DoubleSummaryData.create( + Arrays.asList( + DoubleSummaryPointData.create( + startTime + SECOND_NANOS, + testClock.now(), + Labels.of("K", "V"), + 1, + 222.0d, + valueAtPercentiles(222.0, 222.0d)), + DoubleSummaryPointData.create( + startTime + SECOND_NANOS, + testClock.now(), + Labels.empty(), + 1, + 17.0d, + valueAtPercentiles(17d, 17d)))))); + } finally { + bound.unbind(); + } + } + + @Test + void stressTest() { + final DoubleValueRecorder doubleRecorder = + sdkMeter.doubleValueRecorderBuilder("testRecorder").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((DoubleValueRecorderSdk) doubleRecorder) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, + 2, + new DoubleValueRecorderSdkTest.OperationUpdaterDirectCall(doubleRecorder, "K", "V"))); + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, 2, new OperationUpdaterWithBinding(doubleRecorder.bind(Labels.of("K", "V"))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "", + "1", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of("K", "V"), + 8_000, + 80_000, + valueAtPercentiles(9.0, 11.0)))))); + } + + @Test + void stressTest_WithDifferentLabelSet() { + final String[] keys = {"Key_1", "Key_2", "Key_3", "Key_4"}; + final String[] values = {"Value_1", "Value_2", "Value_3", "Value_4"}; + final DoubleValueRecorder doubleRecorder = + sdkMeter.doubleValueRecorderBuilder("testRecorder").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((DoubleValueRecorderSdk) doubleRecorder) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, + 1, + new DoubleValueRecorderSdkTest.OperationUpdaterDirectCall( + doubleRecorder, keys[i], values[i]))); + + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, + 1, + new OperationUpdaterWithBinding(doubleRecorder.bind(Labels.of(keys[i], values[i]))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "", + "1", + DoubleSummaryData.create( + Arrays.asList( + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[0], values[0]), + 4_000, + 40_000d, + valueAtPercentiles(9.0, 11.0)), + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[1], values[1]), + 4_000, + 40_000d, + valueAtPercentiles(9.0, 11.0)), + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[2], values[2]), + 4_000, + 40_000d, + valueAtPercentiles(9.0, 11.0)), + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[3], values[3]), + 4_000, + 40_000d, + valueAtPercentiles(9.0, 11.0)))))); + } + + private static class OperationUpdaterWithBinding extends OperationUpdater { + private final BoundDoubleValueRecorder boundDoubleValueRecorder; + + private OperationUpdaterWithBinding(BoundDoubleValueRecorder boundDoubleValueRecorder) { + this.boundDoubleValueRecorder = boundDoubleValueRecorder; + } + + @Override + void update() { + boundDoubleValueRecorder.record(11.0); + } + + @Override + void cleanup() { + boundDoubleValueRecorder.unbind(); + } + } + + private static class OperationUpdaterDirectCall extends OperationUpdater { + private final DoubleValueRecorder doubleValueRecorder; + private final String key; + private final String value; + + private OperationUpdaterDirectCall( + DoubleValueRecorder doubleValueRecorder, String key, String value) { + this.doubleValueRecorder = doubleValueRecorder; + this.key = key; + this.value = value; + } + + @Override + void update() { + doubleValueRecorder.record(9.0, Labels.of(key, value)); + } + + @Override + void cleanup() {} + } + + private static List valueAtPercentiles(double min, double max) { + return Arrays.asList(ValueAtPercentile.create(0, min), ValueAtPercentile.create(100, max)); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentRegistryTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentRegistryTest.java new file mode 100644 index 000000000..8412b4cf2 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentRegistryTest.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link InstrumentRegistry}. */ +class InstrumentRegistryTest { + private static final InstrumentDescriptor INSTRUMENT_DESCRIPTOR = + InstrumentDescriptor.create( + "name", "description", "1", InstrumentType.COUNTER, InstrumentValueType.LONG); + private static final InstrumentDescriptor OTHER_INSTRUMENT_DESCRIPTOR = + InstrumentDescriptor.create( + "name", "other_description", "1", InstrumentType.COUNTER, InstrumentValueType.LONG); + + @Test + void register() { + MeterSharedState meterSharedState = MeterSharedState.create(InstrumentationLibraryInfo.empty()); + TestInstrument testInstrument = new TestInstrument(INSTRUMENT_DESCRIPTOR); + assertThat(meterSharedState.getInstrumentRegistry().register(testInstrument)) + .isSameAs(testInstrument); + assertThat(meterSharedState.getInstrumentRegistry().register(testInstrument)) + .isSameAs(testInstrument); + assertThat( + meterSharedState + .getInstrumentRegistry() + .register(new TestInstrument(INSTRUMENT_DESCRIPTOR))) + .isSameAs(testInstrument); + } + + @Test + void register_OtherDescriptor() { + MeterSharedState meterSharedState = MeterSharedState.create(InstrumentationLibraryInfo.empty()); + TestInstrument testInstrument = new TestInstrument(INSTRUMENT_DESCRIPTOR); + assertThat(meterSharedState.getInstrumentRegistry().register(testInstrument)) + .isSameAs(testInstrument); + + assertThatThrownBy( + () -> + meterSharedState + .getInstrumentRegistry() + .register(new TestInstrument(OTHER_INSTRUMENT_DESCRIPTOR))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void register_OtherInstance() { + MeterSharedState meterSharedState = MeterSharedState.create(InstrumentationLibraryInfo.empty()); + TestInstrument testInstrument = new TestInstrument(INSTRUMENT_DESCRIPTOR); + assertThat(meterSharedState.getInstrumentRegistry().register(testInstrument)) + .isSameAs(testInstrument); + + assertThatThrownBy( + () -> + meterSharedState + .getInstrumentRegistry() + .register(new OtherTestInstrument(INSTRUMENT_DESCRIPTOR))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + private static final class TestInstrument extends AbstractInstrument { + TestInstrument(InstrumentDescriptor descriptor) { + super(descriptor); + } + + @Override + List collectAll(long epochNanos) { + return Collections.emptyList(); + } + } + + private static final class OtherTestInstrument extends AbstractInstrument { + OtherTestInstrument(InstrumentDescriptor descriptor) { + super(descriptor); + } + + @Override + List collectAll(long epochNanos) { + return Collections.emptyList(); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongCounterSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongCounterSdkTest.java new file mode 100644 index 000000000..a9f88e6ac --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongCounterSdkTest.java @@ -0,0 +1,297 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongCounterSdk}. */ +class LongCounterSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(LongCounterSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void add_PreventNullLabels() { + assertThatThrownBy(() -> sdkMeter.longCounterBuilder("testCounter").build().add(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> sdkMeter.longCounterBuilder("testCounter").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void collectMetrics_NoRecords() { + LongCounter longCounter = sdkMeter.longCounterBuilder("testCounter").build(); + BoundLongCounter bound = longCounter.bind(Labels.of("foo", "bar")); + try { + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } finally { + bound.unbind(); + } + } + + @Test + void collectMetrics_WithEmptyLabels() { + LongCounter longCounter = + sdkMeter + .longCounterBuilder("testCounter") + .setDescription("description") + .setUnit("By") + .build(); + testClock.advanceNanos(SECOND_NANOS); + longCounter.add(12, Labels.empty()); + longCounter.add(12); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "description", + "By", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.empty(), + 24))))); + } + + @Test + void collectMetrics_WithMultipleCollects() { + long startTime = testClock.now(); + LongCounter longCounter = sdkMeter.longCounterBuilder("testCounter").build(); + BoundLongCounter bound = longCounter.bind(Labels.of("K", "V")); + try { + // Do some records using bounds and direct calls and bindings. + longCounter.add(12, Labels.empty()); + bound.add(123); + longCounter.add(21, Labels.empty()); + // Advancing time here should not matter. + testClock.advanceNanos(SECOND_NANOS); + bound.add(321); + longCounter.add(111, Labels.of("K", "V")); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + LongPointData.create( + startTime, testClock.now(), Labels.of("K", "V"), 555), + LongPointData.create(startTime, testClock.now(), Labels.empty(), 33))))); + + // Repeat to prove we keep previous values. + testClock.advanceNanos(SECOND_NANOS); + bound.add(222); + longCounter.add(11, Labels.empty()); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + LongPointData.create( + startTime, testClock.now(), Labels.of("K", "V"), 777), + LongPointData.create(startTime, testClock.now(), Labels.empty(), 44))))); + } finally { + bound.unbind(); + } + } + + @Test + void longCounterAdd_MonotonicityCheck() { + LongCounter longCounter = sdkMeter.longCounterBuilder("testCounter").build(); + + assertThatThrownBy(() -> longCounter.add(-45, Labels.empty())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void boundLongCounterAdd_MonotonicityCheck() { + LongCounter longCounter = sdkMeter.longCounterBuilder("testCounter").build(); + + assertThatThrownBy(() -> longCounter.bind(Labels.empty()).add(-9)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void stressTest() { + final LongCounter longCounter = sdkMeter.longCounterBuilder("testCounter").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((LongCounterSdk) longCounter) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, 1, new OperationUpdaterDirectCall(longCounter, "K", "V"))); + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, 1, new OperationUpdaterWithBinding(longCounter.bind(Labels.of("K", "V"))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now(), testClock.now(), Labels.of("K", "V"), 160_000))))); + } + + @Test + void stressTest_WithDifferentLabelSet() { + final String[] keys = {"Key_1", "Key_2", "Key_3", "Key_4"}; + final String[] values = {"Value_1", "Value_2", "Value_3", "Value_4"}; + final LongCounter longCounter = sdkMeter.longCounterBuilder("testCounter").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((LongCounterSdk) longCounter) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, 2, new OperationUpdaterDirectCall(longCounter, keys[i], values[i]))); + + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, + 2, + new OperationUpdaterWithBinding(longCounter.bind(Labels.of(keys[i], values[i]))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + LongPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[0], values[0]), + 20_000), + LongPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[1], values[1]), + 20_000), + LongPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[2], values[2]), + 20_000), + LongPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[3], values[3]), + 20_000))))); + } + + private static class OperationUpdaterWithBinding extends OperationUpdater { + private final BoundLongCounter boundLongCounter; + + private OperationUpdaterWithBinding(BoundLongCounter boundLongCounter) { + this.boundLongCounter = boundLongCounter; + } + + @Override + void update() { + boundLongCounter.add(9); + } + + @Override + void cleanup() { + boundLongCounter.unbind(); + } + } + + private static class OperationUpdaterDirectCall extends OperationUpdater { + + private final LongCounter longCounter; + private final String key; + private final String value; + + private OperationUpdaterDirectCall(LongCounter longCounter, String key, String value) { + this.longCounter = longCounter; + this.key = key; + this.value = value; + } + + @Override + void update() { + longCounter.add(11, Labels.of(key, value)); + } + + @Override + void cleanup() {} + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongSumObserverSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongSumObserverSdkTest.java new file mode 100644 index 000000000..0e5505171 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongSumObserverSdkTest.java @@ -0,0 +1,163 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessorFactory; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongSumObserverSdk}. */ +class LongSumObserverSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(LongSumObserverSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProviderBuilder sdkMeterProviderBuilder = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE); + + @Test + void collectMetrics_NoCallback() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .longSumObserverBuilder("testObserver") + .setDescription("My own LongSumObserver") + .setUnit("ms") + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_NoRecords() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .longSumObserverBuilder("testObserver") + .setDescription("My own LongSumObserver") + .setUnit("ms") + .setUpdater(result -> {}) + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_WithOneRecord() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .longSumObserverBuilder("testObserver") + .setUpdater(result -> result.observe(12, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 2 * SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12))))); + } + + @Test + void collectMetrics_DeltaSumAggregator() { + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder + .registerView( + InstrumentSelector.builder().setInstrumentType(InstrumentType.SUM_OBSERVER).build(), + View.builder() + .setLabelsProcessorFactory(LabelsProcessorFactory.noop()) + .setAggregatorFactory(AggregatorFactory.sum(AggregationTemporality.DELTA)) + .build()) + .build(); + sdkMeterProvider + .get(getClass().getName()) + .longSumObserverBuilder("testObserver") + .setUpdater(result -> result.observe(12, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 0))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongUpDownCounterSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongUpDownCounterSdkTest.java new file mode 100644 index 000000000..6ad296092 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongUpDownCounterSdkTest.java @@ -0,0 +1,290 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BoundLongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongUpDownCounterSdk}. */ +class LongUpDownCounterSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(LongUpDownCounterSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void add_PreventNullLabels() { + assertThatThrownBy(() -> sdkMeter.longUpDownCounterBuilder("testCounter").build().add(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy( + () -> sdkMeter.longUpDownCounterBuilder("testUpDownCounter").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void collectMetrics_NoRecords() { + LongUpDownCounter longUpDownCounter = + sdkMeter.longUpDownCounterBuilder("testUpDownCounter").build(); + BoundLongUpDownCounter bound = longUpDownCounter.bind(Labels.of("foo", "bar")); + try { + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } finally { + bound.unbind(); + } + } + + @Test + void collectMetrics_WithEmptyLabel() { + LongUpDownCounter longUpDownCounter = + sdkMeter + .longUpDownCounterBuilder("testUpDownCounter") + .setDescription("description") + .setUnit("By") + .build(); + testClock.advanceNanos(SECOND_NANOS); + longUpDownCounter.add(12, Labels.empty()); + longUpDownCounter.add(12); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "description", + "By", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.empty(), + 24))))); + } + + @Test + void collectMetrics_WithMultipleCollects() { + long startTime = testClock.now(); + LongUpDownCounter longUpDownCounter = + sdkMeter.longUpDownCounterBuilder("testUpDownCounter").build(); + BoundLongUpDownCounter bound = longUpDownCounter.bind(Labels.of("K", "V")); + try { + // Do some records using bounds and direct calls and bindings. + longUpDownCounter.add(12, Labels.empty()); + bound.add(123); + longUpDownCounter.add(21, Labels.empty()); + // Advancing time here should not matter. + testClock.advanceNanos(SECOND_NANOS); + bound.add(321); + longUpDownCounter.add(111, Labels.of("K", "V")); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + LongPointData.create( + startTime, testClock.now(), Labels.of("K", "V"), 555), + LongPointData.create(startTime, testClock.now(), Labels.empty(), 33))))); + + // Repeat to prove we keep previous values. + testClock.advanceNanos(SECOND_NANOS); + bound.add(222); + longUpDownCounter.add(11, Labels.empty()); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + LongPointData.create( + startTime, testClock.now(), Labels.of("K", "V"), 777), + LongPointData.create(startTime, testClock.now(), Labels.empty(), 44))))); + } finally { + bound.unbind(); + } + } + + @Test + void stressTest() { + final LongUpDownCounter longUpDownCounter = + sdkMeter.longUpDownCounterBuilder("testUpDownCounter").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((LongUpDownCounterSdk) longUpDownCounter) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, 1, new OperationUpdaterDirectCall(longUpDownCounter, "K", "V"))); + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, + 1, + new OperationUpdaterWithBinding(longUpDownCounter.bind(Labels.of("K", "V"))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now(), testClock.now(), Labels.of("K", "V"), 160_000))))); + } + + @Test + void stressTest_WithDifferentLabelSet() { + final String[] keys = {"Key_1", "Key_2", "Key_3", "Key_4"}; + final String[] values = {"Value_1", "Value_2", "Value_3", "Value_4"}; + final LongUpDownCounter longUpDownCounter = + sdkMeter.longUpDownCounterBuilder("testUpDownCounter").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((LongUpDownCounterSdk) longUpDownCounter) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, 2, new OperationUpdaterDirectCall(longUpDownCounter, keys[i], values[i]))); + + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, + 2, + new OperationUpdaterWithBinding( + longUpDownCounter.bind(Labels.of(keys[i], values[i]))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Arrays.asList( + LongPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[0], values[0]), + 20_000), + LongPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[1], values[1]), + 20_000), + LongPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[2], values[2]), + 20_000), + LongPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[3], values[3]), + 20_000))))); + } + + private static class OperationUpdaterWithBinding extends OperationUpdater { + private final BoundLongUpDownCounter boundLongUpDownCounter; + + private OperationUpdaterWithBinding(BoundLongUpDownCounter boundLongUpDownCounter) { + this.boundLongUpDownCounter = boundLongUpDownCounter; + } + + @Override + void update() { + boundLongUpDownCounter.add(9); + } + + @Override + void cleanup() { + boundLongUpDownCounter.unbind(); + } + } + + private static class OperationUpdaterDirectCall extends OperationUpdater { + + private final LongUpDownCounter longUpDownCounter; + private final String key; + private final String value; + + private OperationUpdaterDirectCall( + LongUpDownCounter longUpDownCounter, String key, String value) { + this.longUpDownCounter = longUpDownCounter; + this.key = key; + this.value = value; + } + + @Override + void update() { + longUpDownCounter.add(11, Labels.of(key, value)); + } + + @Override + void cleanup() {} + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongUpDownSumObserverSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongUpDownSumObserverSdkTest.java new file mode 100644 index 000000000..0eb422919 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongUpDownSumObserverSdkTest.java @@ -0,0 +1,165 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessorFactory; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongUpDownSumObserverSdk}. */ +class LongUpDownSumObserverSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(LongUpDownSumObserverSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProviderBuilder sdkMeterProviderBuilder = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE); + + @Test + void collectMetrics_NoCallback() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .longUpDownSumObserverBuilder("testObserver") + .setDescription("My own LongUpDownSumObserver") + .setUnit("ms") + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_NoRecords() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .longUpDownSumObserverBuilder("testObserver") + .setDescription("My own LongUpDownSumObserver") + .setUnit("ms") + .setUpdater(result -> {}) + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_WithOneRecord() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + sdkMeterProvider + .get(getClass().getName()) + .longUpDownSumObserverBuilder("testObserver") + .setUpdater(result -> result.observe(12, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 2 * SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12))))); + } + + @Test + void collectMetrics_DeltaSumAggregator() { + SdkMeterProvider sdkMeterProvider = + sdkMeterProviderBuilder + .registerView( + InstrumentSelector.builder() + .setInstrumentType(InstrumentType.UP_DOWN_SUM_OBSERVER) + .build(), + View.builder() + .setLabelsProcessorFactory(LabelsProcessorFactory.noop()) + .setAggregatorFactory(AggregatorFactory.sum(AggregationTemporality.DELTA)) + .build()) + .build(); + sdkMeterProvider + .get(getClass().getName()) + .longUpDownSumObserverBuilder("testObserver") + .setUpdater(result -> result.observe(12, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 12))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.of("k", "v"), + 0))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongValueObserverSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongValueObserverSdkTest.java new file mode 100644 index 000000000..a9a06237b --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongValueObserverSdkTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongValueObserverSdk}. */ +class LongValueObserverSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(LongValueObserverSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void collectMetrics_NoCallback() { + sdkMeter + .longValueObserverBuilder("testObserver") + .setDescription("My own LongValueObserver") + .setUnit("ms") + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_NoRecords() { + sdkMeter + .longValueObserverBuilder("testObserver") + .setDescription("My own LongValueObserver") + .setUnit("ms") + .setUpdater(result -> {}) + .build(); + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } + + @Test + void collectMetrics_WithOneRecord() { + sdkMeter + .longValueObserverBuilder("testObserver") + .setUpdater(result -> result.observe(12, Labels.of("k", "v"))) + .build(); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongGauge( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongGaugeData.create( + Collections.singletonList( + LongPointData.create(0, testClock.now(), Labels.of("k", "v"), 12))))); + testClock.advanceNanos(SECOND_NANOS); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createLongGauge( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testObserver", + "", + "1", + LongGaugeData.create( + Collections.singletonList( + LongPointData.create(0, testClock.now(), Labels.of("k", "v"), 12))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongValueRecorderSdkTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongValueRecorderSdkTest.java new file mode 100644 index 000000000..b1be0626b --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/LongValueRecorderSdkTest.java @@ -0,0 +1,325 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BoundLongValueRecorder; +import io.opentelemetry.api.metrics.LongValueRecorder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongValueRecorderSdk}. */ +class LongValueRecorderSdkTest { + private static final long SECOND_NANOS = 1_000_000_000; + private static final Resource RESOURCE = + Resource.create(Attributes.of(stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(LongValueRecorderSdkTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE).build(); + private final Meter sdkMeter = sdkMeterProvider.get(getClass().getName()); + + @Test + void record_PreventNullLabels() { + assertThatThrownBy( + () -> sdkMeter.longValueRecorderBuilder("testRecorder").build().record(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void bound_PreventNullLabels() { + assertThatThrownBy(() -> sdkMeter.longValueRecorderBuilder("testRecorder").build().bind(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("labels"); + } + + @Test + void collectMetrics_NoRecords() { + LongValueRecorder longRecorder = sdkMeter.longValueRecorderBuilder("testRecorder").build(); + BoundLongValueRecorder bound = longRecorder.bind(Labels.of("key", "value")); + try { + assertThat(sdkMeterProvider.collectAllMetrics()).isEmpty(); + } finally { + bound.unbind(); + } + } + + @Test + void collectMetrics_WithEmptyLabel() { + LongValueRecorder longRecorder = + sdkMeter + .longValueRecorderBuilder("testRecorder") + .setDescription("description") + .setUnit("By") + .build(); + testClock.advanceNanos(SECOND_NANOS); + longRecorder.record(12, Labels.empty()); + longRecorder.record(12); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "description", + "By", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + testClock.now() - SECOND_NANOS, + testClock.now(), + Labels.empty(), + 2, + 24, + valueAtPercentiles(12, 12)))))); + } + + @Test + void collectMetrics_WithMultipleCollects() { + long startTime = testClock.now(); + LongValueRecorder longRecorder = sdkMeter.longValueRecorderBuilder("testRecorder").build(); + BoundLongValueRecorder bound = longRecorder.bind(Labels.of("K", "V")); + try { + // Do some records using bounds and direct calls and bindings. + longRecorder.record(12, Labels.empty()); + bound.record(123); + longRecorder.record(-14, Labels.empty()); + // Advancing time here should not matter. + testClock.advanceNanos(SECOND_NANOS); + bound.record(321); + longRecorder.record(-121, Labels.of("K", "V")); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "", + "1", + DoubleSummaryData.create( + Arrays.asList( + DoubleSummaryPointData.create( + startTime, + testClock.now(), + Labels.of("K", "V"), + 3, + 323, + valueAtPercentiles(-121, 321)), + DoubleSummaryPointData.create( + startTime, + testClock.now(), + Labels.empty(), + 2, + -2, + valueAtPercentiles(-14, 12)))))); + + // Repeat to prove we don't keep previous values. + testClock.advanceNanos(SECOND_NANOS); + bound.record(222); + longRecorder.record(17, Labels.empty()); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "", + "1", + DoubleSummaryData.create( + Arrays.asList( + DoubleSummaryPointData.create( + startTime + SECOND_NANOS, + testClock.now(), + Labels.of("K", "V"), + 1, + 222, + valueAtPercentiles(222, 222)), + DoubleSummaryPointData.create( + startTime + SECOND_NANOS, + testClock.now(), + Labels.empty(), + 1, + 17, + valueAtPercentiles(17, 17)))))); + } finally { + bound.unbind(); + } + } + + @Test + void stressTest() { + final LongValueRecorder longRecorder = + sdkMeter.longValueRecorderBuilder("testRecorder").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((LongValueRecorderSdk) longRecorder) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, + 1, + new LongValueRecorderSdkTest.OperationUpdaterDirectCall(longRecorder, "K", "V"))); + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 2_000, + 1, + new LongValueRecorderSdkTest.OperationUpdaterWithBinding( + longRecorder.bind(Labels.of("K", "V"))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "", + "1", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of("K", "V"), + 16_000, + 160_000, + valueAtPercentiles(9, 11)))))); + } + + @Test + void stressTest_WithDifferentLabelSet() { + final String[] keys = {"Key_1", "Key_2", "Key_3", "Key_4"}; + final String[] values = {"Value_1", "Value_2", "Value_3", "Value_4"}; + final LongValueRecorder longRecorder = + sdkMeter.longValueRecorderBuilder("testRecorder").build(); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder() + .setInstrument((LongValueRecorderSdk) longRecorder) + .setCollectionIntervalMs(100); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, + 2, + new LongValueRecorderSdkTest.OperationUpdaterDirectCall( + longRecorder, keys[i], values[i]))); + + stressTestBuilder.addOperation( + StressTestRunner.Operation.create( + 1_000, + 2, + new LongValueRecorderSdkTest.OperationUpdaterWithBinding( + longRecorder.bind(Labels.of(keys[i], values[i]))))); + } + + stressTestBuilder.build().run(); + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactly( + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testRecorder", + "", + "1", + DoubleSummaryData.create( + Arrays.asList( + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[0], values[0]), + 2_000, + 20_000, + valueAtPercentiles(9, 11)), + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[1], values[1]), + 2_000, + 20_000, + valueAtPercentiles(9, 11)), + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[2], values[2]), + 2_000, + 20_000, + valueAtPercentiles(9, 11)), + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.of(keys[3], values[3]), + 2_000, + 20_000, + valueAtPercentiles(9, 11)))))); + } + + private static class OperationUpdaterWithBinding extends OperationUpdater { + private final BoundLongValueRecorder boundLongValueRecorder; + + private OperationUpdaterWithBinding(BoundLongValueRecorder boundLongValueRecorder) { + this.boundLongValueRecorder = boundLongValueRecorder; + } + + @Override + void update() { + boundLongValueRecorder.record(9); + } + + @Override + void cleanup() { + boundLongValueRecorder.unbind(); + } + } + + private static class OperationUpdaterDirectCall extends OperationUpdater { + + private final LongValueRecorder longRecorder; + private final String key; + private final String value; + + private OperationUpdaterDirectCall(LongValueRecorder longRecorder, String key, String value) { + this.longRecorder = longRecorder; + this.key = key; + this.value = value; + } + + @Override + void update() { + longRecorder.record(11, Labels.of(key, value)); + } + + @Override + void cleanup() {} + } + + private static List valueAtPercentiles(double min, double max) { + return Arrays.asList(ValueAtPercentile.create(0, min), ValueAtPercentile.create(100, max)); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java new file mode 100644 index 000000000..5501e16c2 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilderTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.Test; + +class SdkMeterProviderBuilderTest { + + @Test + void buildAndRegisterGlobal() { + SdkMeterProvider meterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + try { + assertThat(GlobalMeterProvider.get()).isSameAs(meterProvider); + } finally { + GlobalMeterProvider.set(null); + } + } + + @Test + void defaultResource() { + SdkMeterProvider meterProvider = SdkMeterProvider.builder().build(); + + assertThat(meterProvider) + .extracting("sharedState") + .hasFieldOrPropertyWithValue("resource", Resource.getDefault()); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderTest.java new file mode 100644 index 000000000..e6bdabbd5 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterProviderTest.java @@ -0,0 +1,692 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleValueRecorder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongValueRecorder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryData; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +public class SdkMeterProviderTest { + private static final Resource RESOURCE = + Resource.create(Attributes.of(AttributeKey.stringKey("resource_key"), "resource_value")); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create(SdkMeterProviderTest.class.getName(), null); + private final TestClock testClock = TestClock.create(); + private final SdkMeterProviderBuilder sdkMeterProviderBuilder = + SdkMeterProvider.builder().setClock(testClock).setResource(RESOURCE); + + @Test + void defaultMeterName() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + assertThat(sdkMeterProvider.get(null)).isSameAs(sdkMeterProvider.get("unknown")); + } + + @Test + void collectAllSyncInstruments() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); + LongCounter longCounter = sdkMeter.longCounterBuilder("testLongCounter").build(); + longCounter.add(10, Labels.empty()); + LongUpDownCounter longUpDownCounter = + sdkMeter.longUpDownCounterBuilder("testLongUpDownCounter").build(); + longUpDownCounter.add(-10, Labels.empty()); + LongValueRecorder longValueRecorder = + sdkMeter.longValueRecorderBuilder("testLongValueRecorder").build(); + longValueRecorder.record(10, Labels.empty()); + DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testDoubleCounter").build(); + doubleCounter.add(10.1, Labels.empty()); + DoubleUpDownCounter doubleUpDownCounter = + sdkMeter.doubleUpDownCounterBuilder("testDoubleUpDownCounter").build(); + doubleUpDownCounter.add(-10.1, Labels.empty()); + DoubleValueRecorder doubleValueRecorder = + sdkMeter.doubleValueRecorderBuilder("testDoubleValueRecorder").build(); + doubleValueRecorder.record(10.1, Labels.empty()); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now(), testClock.now(), Labels.empty(), 10)))), + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now(), testClock.now(), Labels.empty(), 10.1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now(), testClock.now(), Labels.empty(), -10)))), + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleUpDownCounter", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now(), testClock.now(), Labels.empty(), -10.1)))), + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongValueRecorder", + "", + "1", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.empty(), + 1, + 10, + Arrays.asList( + ValueAtPercentile.create(0, 10), + ValueAtPercentile.create(100, 10)))))), + MetricData.createDoubleSummary( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleValueRecorder", + "", + "1", + DoubleSummaryData.create( + Collections.singletonList( + DoubleSummaryPointData.create( + testClock.now(), + testClock.now(), + Labels.empty(), + 1, + 10.1d, + Arrays.asList( + ValueAtPercentile.create(0, 10.1d), + ValueAtPercentile.create(100, 10.1d))))))); + } + + @Test + void collectAllSyncInstruments_OverwriteTemporality() { + sdkMeterProviderBuilder.registerView( + InstrumentSelector.builder().setInstrumentType(InstrumentType.COUNTER).build(), + View.builder() + .setAggregatorFactory(AggregatorFactory.sum(AggregationTemporality.DELTA)) + .build()); + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); + + LongCounter longCounter = sdkMeter.longCounterBuilder("testLongCounter").build(); + longCounter.add(10, Labels.empty()); + testClock.advanceNanos(50); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 10))))); + + longCounter.add(10, Labels.empty()); + testClock.advanceNanos(50); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 10))))); + } + + @Test + void collectAllSyncInstruments_DeltaCount() { + registerViewForAllTypes( + sdkMeterProviderBuilder, AggregatorFactory.count(AggregationTemporality.DELTA)); + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); + LongCounter longCounter = sdkMeter.longCounterBuilder("testLongCounter").build(); + longCounter.add(10, Labels.empty()); + LongUpDownCounter longUpDownCounter = + sdkMeter.longUpDownCounterBuilder("testLongUpDownCounter").build(); + longUpDownCounter.add(-10, Labels.empty()); + LongValueRecorder longValueRecorder = + sdkMeter.longValueRecorderBuilder("testLongValueRecorder").build(); + longValueRecorder.record(10, Labels.empty()); + DoubleCounter doubleCounter = sdkMeter.doubleCounterBuilder("testDoubleCounter").build(); + doubleCounter.add(10.1, Labels.empty()); + DoubleUpDownCounter doubleUpDownCounter = + sdkMeter.doubleUpDownCounterBuilder("testDoubleUpDownCounter").build(); + doubleUpDownCounter.add(-10.1, Labels.empty()); + DoubleValueRecorder doubleValueRecorder = + sdkMeter.doubleValueRecorderBuilder("testDoubleValueRecorder").build(); + doubleValueRecorder.record(10.1, Labels.empty()); + + testClock.advanceNanos(50); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongValueRecorder", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleValueRecorder", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1))))); + + testClock.advanceNanos(50); + + longCounter.add(10, Labels.empty()); + longUpDownCounter.add(-10, Labels.empty()); + longValueRecorder.record(10, Labels.empty()); + doubleCounter.add(10.1, Labels.empty()); + doubleUpDownCounter.add(-10.1, Labels.empty()); + doubleValueRecorder.record(10.1, Labels.empty()); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleUpDownCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongValueRecorder", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleValueRecorder", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1))))); + } + + @Test + void collectAllAsyncInstruments() { + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); + sdkMeter + .longSumObserverBuilder("testLongSumObserver") + .setUpdater(longResult -> longResult.observe(10, Labels.empty())) + .build(); + sdkMeter + .longUpDownSumObserverBuilder("testLongUpDownSumObserver") + .setUpdater(longResult -> longResult.observe(-10, Labels.empty())) + .build(); + sdkMeter + .longValueObserverBuilder("testLongValueObserver") + .setUpdater(longResult -> longResult.observe(10, Labels.empty())) + .build(); + + sdkMeter + .doubleSumObserverBuilder("testDoubleSumObserver") + .setUpdater(doubleResult -> doubleResult.observe(10.1, Labels.empty())) + .build(); + sdkMeter + .doubleUpDownSumObserverBuilder("testDoubleUpDownSumObserver") + .setUpdater(doubleResult -> doubleResult.observe(-10.1, Labels.empty())) + .build(); + sdkMeter + .doubleValueObserverBuilder("testDoubleValueObserver") + .setUpdater(doubleResult -> doubleResult.observe(10.1, Labels.empty())) + .build(); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now(), testClock.now(), Labels.empty(), 10)))), + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleSumObserver", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now(), testClock.now(), Labels.empty(), 10.1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongUpDownSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now(), testClock.now(), Labels.empty(), -10)))), + MetricData.createDoubleSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleUpDownSumObserver", + "", + "1", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create( + testClock.now(), testClock.now(), Labels.empty(), -10.1)))), + MetricData.createLongGauge( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongValueObserver", + "", + "1", + LongGaugeData.create( + Collections.singletonList( + LongPointData.create(0, testClock.now(), Labels.empty(), 10)))), + MetricData.createDoubleGauge( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleValueObserver", + "", + "1", + DoubleGaugeData.create( + Collections.singletonList( + DoublePointData.create(0, testClock.now(), Labels.empty(), 10.1))))); + } + + @Test + void collectAllAsyncInstruments_CumulativeCount() { + registerViewForAllTypes( + sdkMeterProviderBuilder, AggregatorFactory.count(AggregationTemporality.CUMULATIVE)); + SdkMeterProvider sdkMeterProvider = sdkMeterProviderBuilder.build(); + Meter sdkMeter = sdkMeterProvider.get(SdkMeterProviderTest.class.getName()); + sdkMeter + .longSumObserverBuilder("testLongSumObserver") + .setUpdater(longResult -> longResult.observe(10, Labels.empty())) + .build(); + sdkMeter + .longUpDownSumObserverBuilder("testLongUpDownSumObserver") + .setUpdater(longResult -> longResult.observe(-10, Labels.empty())) + .build(); + sdkMeter + .longValueObserverBuilder("testLongValueObserver") + .setUpdater(longResult -> longResult.observe(10, Labels.empty())) + .build(); + + sdkMeter + .doubleSumObserverBuilder("testDoubleSumObserver") + .setUpdater(doubleResult -> doubleResult.observe(10.1, Labels.empty())) + .build(); + sdkMeter + .doubleUpDownSumObserverBuilder("testDoubleUpDownSumObserver") + .setUpdater(doubleResult -> doubleResult.observe(-10.1, Labels.empty())) + .build(); + sdkMeter + .doubleValueObserverBuilder("testDoubleValueObserver") + .setUpdater(doubleResult -> doubleResult.observe(10.1, Labels.empty())) + .build(); + + testClock.advanceNanos(50); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongUpDownSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleUpDownSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongValueObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleValueObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 50, testClock.now(), Labels.empty(), 1))))); + + testClock.advanceNanos(50); + + assertThat(sdkMeterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 100, testClock.now(), Labels.empty(), 2)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 100, testClock.now(), Labels.empty(), 2)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongUpDownSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 100, testClock.now(), Labels.empty(), 2)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleUpDownSumObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 100, testClock.now(), Labels.empty(), 2)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testLongValueObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 100, testClock.now(), Labels.empty(), 2)))), + MetricData.createLongSum( + RESOURCE, + INSTRUMENTATION_LIBRARY_INFO, + "testDoubleValueObserver", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now() - 100, testClock.now(), Labels.empty(), 2))))); + } + + private static void registerViewForAllTypes( + SdkMeterProviderBuilder meterProviderBuilder, AggregatorFactory factory) { + for (InstrumentType instrumentType : InstrumentType.values()) { + meterProviderBuilder.registerView( + InstrumentSelector.builder().setInstrumentType(instrumentType).build(), + View.builder().setAggregatorFactory(factory).build()); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterRegistryTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterRegistryTest.java new file mode 100644 index 000000000..9dd3dc947 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterRegistryTest.java @@ -0,0 +1,138 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link SdkMeterProvider}. */ +class SdkMeterRegistryTest { + private final TestClock testClock = TestClock.create(); + private final SdkMeterProvider meterProvider = + SdkMeterProvider.builder().setClock(testClock).setResource(Resource.empty()).build(); + + @Test + void builder_HappyPath() { + assertThat( + SdkMeterProvider.builder() + .setClock(mock(Clock.class)) + .setResource(Resource.empty()) + .build()) + .isNotNull(); + } + + @Test + void builder_NullClock() { + assertThatThrownBy(() -> SdkMeterProvider.builder().setClock(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("clock"); + } + + @Test + void builder_NullResource() { + assertThatThrownBy(() -> SdkMeterProvider.builder().setResource(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("resource"); + } + + @Test + void defaultGet() { + assertThat(meterProvider.get("test")).isInstanceOf(SdkMeter.class); + } + + @Test + void getSameInstanceForSameName_WithoutVersion() { + assertThat(meterProvider.get("test")).isSameAs(meterProvider.get("test")); + assertThat(meterProvider.get("test")).isSameAs(meterProvider.get("test", null)); + } + + @Test + void getSameInstanceForSameName_WithVersion() { + assertThat(meterProvider.get("test", "version")).isSameAs(meterProvider.get("test", "version")); + } + + @Test + void propagatesInstrumentationLibraryInfoToMeter() { + InstrumentationLibraryInfo expected = + InstrumentationLibraryInfo.create("theName", "theVersion"); + SdkMeter meter = (SdkMeter) meterProvider.get(expected.getName(), expected.getVersion()); + assertThat(meter.getInstrumentationLibraryInfo()).isEqualTo(expected); + } + + @Test + void metricProducer_GetAllMetrics() { + Meter sdkMeter1 = meterProvider.get("io.opentelemetry.sdk.metrics.MeterSdkRegistryTest_1"); + LongCounter longCounter1 = sdkMeter1.longCounterBuilder("testLongCounter").build(); + longCounter1.add(10, Labels.empty()); + Meter sdkMeter2 = meterProvider.get("io.opentelemetry.sdk.metrics.MeterSdkRegistryTest_2"); + LongCounter longCounter2 = sdkMeter2.longCounterBuilder("testLongCounter").build(); + longCounter2.add(10, Labels.empty()); + + assertThat(meterProvider.collectAllMetrics()) + .containsExactlyInAnyOrder( + MetricData.createLongSum( + Resource.empty(), + ((SdkMeter) sdkMeter1).getInstrumentationLibraryInfo(), + "testLongCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now(), testClock.now(), Labels.empty(), 10)))), + MetricData.createLongSum( + Resource.empty(), + ((SdkMeter) sdkMeter2).getInstrumentationLibraryInfo(), + "testLongCounter", + "", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + testClock.now(), testClock.now(), Labels.empty(), 10))))); + } + + @Test + void suppliesDefaultMeterForNullName() { + SdkMeter meter = (SdkMeter) meterProvider.get(null); + assertThat(meter.getInstrumentationLibraryInfo().getName()) + .isEqualTo(SdkMeterProvider.DEFAULT_METER_NAME); + + meter = (SdkMeter) meterProvider.get(null, null); + assertThat(meter.getInstrumentationLibraryInfo().getName()) + .isEqualTo(SdkMeterProvider.DEFAULT_METER_NAME); + } + + @Test + void suppliesDefaultMeterForEmptyName() { + SdkMeter meter = (SdkMeter) meterProvider.get(""); + assertThat(meter.getInstrumentationLibraryInfo().getName()) + .isEqualTo(SdkMeterProvider.DEFAULT_METER_NAME); + + meter = (SdkMeter) meterProvider.get("", ""); + assertThat(meter.getInstrumentationLibraryInfo().getName()) + .isEqualTo(SdkMeterProvider.DEFAULT_METER_NAME); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterTest.java new file mode 100644 index 000000000..a58924948 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SdkMeterTest.java @@ -0,0 +1,376 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.metrics.BatchRecorder; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleSumObserver; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownSumObserver; +import io.opentelemetry.api.metrics.DoubleValueObserver; +import io.opentelemetry.api.metrics.DoubleValueRecorder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongSumObserver; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownSumObserver; +import io.opentelemetry.api.metrics.LongValueObserver; +import io.opentelemetry.api.metrics.LongValueRecorder; +import io.opentelemetry.api.metrics.Meter; +import org.junit.jupiter.api.Test; + +class SdkMeterTest { + private final SdkMeterProvider testMeterProvider = SdkMeterProvider.builder().build(); + private final Meter sdkMeter = testMeterProvider.get(getClass().getName()); + + @Test + void testLongCounter() { + LongCounter longCounter = + sdkMeter + .longCounterBuilder("testLongCounter") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(longCounter).isNotNull(); + + assertThat( + sdkMeter + .longCounterBuilder("testLongCounter") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(longCounter); + + assertThatThrownBy(() -> sdkMeter.longCounterBuilder("testLongCounter").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy(() -> sdkMeter.longCounterBuilder("testLongCounter".toUpperCase()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testLongUpDownCounter() { + LongUpDownCounter longUpDownCounter = + sdkMeter + .longUpDownCounterBuilder("testLongUpDownCounter") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(longUpDownCounter).isNotNull(); + + assertThat( + sdkMeter + .longUpDownCounterBuilder("testLongUpDownCounter") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(longUpDownCounter); + + assertThatThrownBy(() -> sdkMeter.longUpDownCounterBuilder("testLongUpDownCounter").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> sdkMeter.longUpDownCounterBuilder("testLongUpDownCounter".toUpperCase()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testLongValueRecorder() { + LongValueRecorder longValueRecorder = + sdkMeter + .longValueRecorderBuilder("testLongValueRecorder") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(longValueRecorder).isNotNull(); + + assertThat( + sdkMeter + .longValueRecorderBuilder("testLongValueRecorder") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(longValueRecorder); + + assertThatThrownBy(() -> sdkMeter.longValueRecorderBuilder("testLongValueRecorder").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> sdkMeter.longValueRecorderBuilder("testLongValueRecorder".toUpperCase()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testLongValueObserver() { + LongValueObserver longValueObserver = + sdkMeter + .longValueObserverBuilder("longValueObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(longValueObserver).isNotNull(); + + assertThat( + sdkMeter + .longValueObserverBuilder("longValueObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(longValueObserver); + + assertThatThrownBy(() -> sdkMeter.longValueObserverBuilder("longValueObserver").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> sdkMeter.longValueObserverBuilder("longValueObserver".toUpperCase()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testLongSumObserver() { + LongSumObserver longObserver = + sdkMeter + .longSumObserverBuilder("testLongSumObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(longObserver).isNotNull(); + + assertThat( + sdkMeter + .longSumObserverBuilder("testLongSumObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(longObserver); + + assertThatThrownBy(() -> sdkMeter.longSumObserverBuilder("testLongSumObserver").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + + assertThatThrownBy( + () -> sdkMeter.longSumObserverBuilder("testLongSumObserver".toUpperCase()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testLongUpDownSumObserver() { + LongUpDownSumObserver longObserver = + sdkMeter + .longUpDownSumObserverBuilder("testLongUpDownSumObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(longObserver).isNotNull(); + + assertThat( + sdkMeter + .longUpDownSumObserverBuilder("testLongUpDownSumObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(longObserver); + + assertThatThrownBy( + () -> sdkMeter.longUpDownSumObserverBuilder("testLongUpDownSumObserver").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + + assertThatThrownBy( + () -> + sdkMeter + .longUpDownSumObserverBuilder("testLongUpDownSumObserver".toUpperCase()) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testDoubleCounter() { + DoubleCounter doubleCounter = + sdkMeter + .doubleCounterBuilder("testDoubleCounter") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(doubleCounter).isNotNull(); + + assertThat( + sdkMeter + .doubleCounterBuilder("testDoubleCounter") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(doubleCounter); + + assertThatThrownBy(() -> sdkMeter.doubleCounterBuilder("testDoubleCounter").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> sdkMeter.doubleCounterBuilder("testDoubleCounter".toUpperCase()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testDoubleUpDownCounter() { + DoubleUpDownCounter doubleUpDownCounter = + sdkMeter + .doubleUpDownCounterBuilder("testDoubleUpDownCounter") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(doubleUpDownCounter).isNotNull(); + + assertThat( + sdkMeter + .doubleUpDownCounterBuilder("testDoubleUpDownCounter") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(doubleUpDownCounter); + + assertThatThrownBy(() -> sdkMeter.doubleUpDownCounterBuilder("testDoubleUpDownCounter").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> + sdkMeter + .doubleUpDownCounterBuilder("testDoubleUpDownCounter".toUpperCase()) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testDoubleValueRecorder() { + DoubleValueRecorder doubleValueRecorder = + sdkMeter + .doubleValueRecorderBuilder("testDoubleValueRecorder") + .setDescription("My very own ValueRecorder") + .setUnit("metric tonnes") + .build(); + assertThat(doubleValueRecorder).isNotNull(); + + assertThat( + sdkMeter + .doubleValueRecorderBuilder("testDoubleValueRecorder") + .setDescription("My very own ValueRecorder") + .setUnit("metric tonnes") + .build()) + .isSameAs(doubleValueRecorder); + + assertThatThrownBy(() -> sdkMeter.doubleValueRecorderBuilder("testDoubleValueRecorder").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> + sdkMeter + .doubleValueRecorderBuilder("testDoubleValueRecorder".toUpperCase()) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testDoubleSumObserver() { + DoubleSumObserver doubleObserver = + sdkMeter + .doubleSumObserverBuilder("testDoubleSumObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(doubleObserver).isNotNull(); + + assertThat( + sdkMeter + .doubleSumObserverBuilder("testDoubleSumObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(doubleObserver); + + assertThatThrownBy(() -> sdkMeter.doubleSumObserverBuilder("testDoubleSumObserver").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> sdkMeter.doubleSumObserverBuilder("testDoubleSumObserver".toUpperCase()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testDoubleUpDownSumObserver() { + DoubleUpDownSumObserver doubleObserver = + sdkMeter + .doubleUpDownSumObserverBuilder("testDoubleUpDownSumObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(doubleObserver).isNotNull(); + + assertThat( + sdkMeter + .doubleUpDownSumObserverBuilder("testDoubleUpDownSumObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(doubleObserver); + + assertThatThrownBy( + () -> sdkMeter.doubleUpDownSumObserverBuilder("testDoubleUpDownSumObserver").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> + sdkMeter + .doubleUpDownSumObserverBuilder("testDoubleUpDownSumObserver".toUpperCase()) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testDoubleValueObserver() { + DoubleValueObserver doubleValueObserver = + sdkMeter + .doubleValueObserverBuilder("doubleValueObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build(); + assertThat(doubleValueObserver).isNotNull(); + + assertThat( + sdkMeter + .doubleValueObserverBuilder("doubleValueObserver") + .setDescription("My very own counter") + .setUnit("metric tonnes") + .build()) + .isSameAs(doubleValueObserver); + + assertThatThrownBy(() -> sdkMeter.doubleValueObserverBuilder("doubleValueObserver").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + assertThatThrownBy( + () -> sdkMeter.doubleValueObserverBuilder("doubleValueObserver".toUpperCase()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Instrument with same name and different descriptor already created."); + } + + @Test + void testBatchRecorder() { + BatchRecorder batchRecorder = sdkMeter.newBatchRecorder("key", "value"); + assertThat(batchRecorder).isNotNull(); + assertThat(batchRecorder).isInstanceOf(BatchRecorderSdk.class); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/StressTestRunner.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/StressTestRunner.java new file mode 100644 index 000000000..79f62a6db --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/StressTestRunner.java @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Uninterruptibles; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@Immutable +abstract class StressTestRunner { + abstract ImmutableList getOperations(); + + abstract AbstractInstrument getInstrument(); + + abstract int getCollectionIntervalMs(); + + final void run() { + List operations = getOperations(); + int numThreads = operations.size(); + final CountDownLatch countDownLatch = new CountDownLatch(numThreads); + Thread collectionThread = + new Thread( + () -> { + // While workers still work, do collections. + while (countDownLatch.getCount() != 0) { + Uninterruptibles.sleepUninterruptibly(Duration.ofMillis(getCollectionIntervalMs())); + } + }); + List operationThreads = new ArrayList<>(numThreads); + for (final Operation operation : operations) { + operationThreads.add( + new Thread( + () -> { + for (int i = 0; i < operation.getNumOperations(); i++) { + operation.getUpdater().update(); + Uninterruptibles.sleepUninterruptibly( + Duration.ofMillis(operation.getOperationDelayMs())); + } + countDownLatch.countDown(); + })); + } + + // Start collection thread then the rest of the worker threads. + collectionThread.start(); + for (Thread thread : operationThreads) { + thread.start(); + } + + // Wait for all the thread to finish. + for (Thread thread : operationThreads) { + Uninterruptibles.joinUninterruptibly(thread); + } + Uninterruptibles.joinUninterruptibly(collectionThread); + } + + static Builder builder() { + return new AutoValue_StressTestRunner.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + // TODO: Change this to MeterSdk when collect is available for the entire Meter. + abstract Builder setInstrument(AbstractInstrument meterSdk); + + abstract ImmutableList.Builder operationsBuilder(); + + abstract Builder setCollectionIntervalMs(int collectionInterval); + + Builder addOperation(final Operation operation) { + operationsBuilder().add(operation); + return this; + } + + public abstract StressTestRunner build(); + } + + @AutoValue + @Immutable + abstract static class Operation { + + abstract int getNumOperations(); + + abstract int getOperationDelayMs(); + + abstract OperationUpdater getUpdater(); + + static Operation create(int numOperations, int operationDelayMs, OperationUpdater updater) { + return new AutoValue_StressTestRunner_Operation(numOperations, operationDelayMs, updater); + } + } + + abstract static class OperationUpdater { + + /** Called every operation. */ + abstract void update(); + + /** Called after all operations are completed. */ + abstract void cleanup(); + } + + StressTestRunner() {} +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentAccumulatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentAccumulatorTest.java new file mode 100644 index 000000000..5a61cabc3 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/SynchronousInstrumentAccumulatorTest.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.metrics.aggregator.Aggregator; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.aggregator.AggregatorHandle; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessor; +import io.opentelemetry.sdk.metrics.processor.LabelsProcessorFactory; +import io.opentelemetry.sdk.resources.Resource; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class SynchronousInstrumentAccumulatorTest { + private static final InstrumentDescriptor DESCRIPTOR = + InstrumentDescriptor.create( + "name", "description", "unit", InstrumentType.COUNTER, InstrumentValueType.DOUBLE); + private final TestClock testClock = TestClock.create(); + private final Aggregator aggregator = + AggregatorFactory.lastValue() + .create(Resource.empty(), InstrumentationLibraryInfo.create("test", "1.0"), DESCRIPTOR); + private final LabelsProcessor labelsProcessor = + LabelsProcessorFactory.noop() + .create(Resource.empty(), InstrumentationLibraryInfo.create("test", "1.0"), DESCRIPTOR); + + @Test + void labelsProcessor_used() { + LabelsProcessor spyLabelsProcessor = Mockito.spy(this.labelsProcessor); + SynchronousInstrumentAccumulator accumulator = + new SynchronousInstrumentAccumulator<>( + aggregator, new InstrumentProcessor<>(aggregator, testClock.now()), spyLabelsProcessor); + accumulator.bind(Labels.empty()); + Mockito.verify(spyLabelsProcessor).onLabelsBound(Context.current(), Labels.empty()); + } + + @Test + void labelsProcessor_applied() { + final Labels labels = Labels.of("K", "V"); + LabelsProcessor labelsProcessor = + new LabelsProcessor() { + @Override + public Labels onLabelsBound(Context ctx, Labels lbls) { + return lbls.toBuilder().put("modifiedK", "modifiedV").build(); + } + }; + LabelsProcessor spyLabelsProcessor = Mockito.spy(labelsProcessor); + SynchronousInstrumentAccumulator accumulator = + new SynchronousInstrumentAccumulator<>( + aggregator, new InstrumentProcessor<>(aggregator, testClock.now()), spyLabelsProcessor); + AggregatorHandle aggregatorHandle = accumulator.bind(labels); + aggregatorHandle.recordDouble(1); + List md = accumulator.collectAll(testClock.now()); + md.stream() + .flatMap(m -> m.getLongGaugeData().getPoints().stream()) + .forEach( + p -> + assertThat(p.getLabels().asMap()) + .isEqualTo(labels.toBuilder().put("modifiedK", "modifiedV").build().asMap())); + } + + @Test + void sameAggregator_ForSameLabelSet() { + SynchronousInstrumentAccumulator accumulator = + new SynchronousInstrumentAccumulator<>( + aggregator, new InstrumentProcessor<>(aggregator, testClock.now()), labelsProcessor); + AggregatorHandle aggregatorHandle = accumulator.bind(Labels.of("K", "V")); + AggregatorHandle duplicateAggregatorHandle = accumulator.bind(Labels.of("K", "V")); + try { + assertThat(duplicateAggregatorHandle).isSameAs(aggregatorHandle); + accumulator.collectAll(testClock.now()); + AggregatorHandle anotherDuplicateAggregatorHandle = accumulator.bind(Labels.of("K", "V")); + try { + assertThat(anotherDuplicateAggregatorHandle).isSameAs(aggregatorHandle); + } finally { + anotherDuplicateAggregatorHandle.release(); + } + } finally { + duplicateAggregatorHandle.release(); + aggregatorHandle.release(); + } + + // At this point we should be able to unmap because all references are gone. Because this is an + // internal detail we cannot call collectAll after this anymore. + assertThat(aggregatorHandle.tryUnmap()).isTrue(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/ViewRegistryTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/ViewRegistryTest.java new file mode 100644 index 000000000..008aff882 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/ViewRegistryTest.java @@ -0,0 +1,175 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.metrics.aggregator.AggregatorFactory; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.view.InstrumentSelector; +import io.opentelemetry.sdk.metrics.view.View; +import org.junit.jupiter.api.Test; + +class ViewRegistryTest { + @Test + void selection_onType() { + AggregatorFactory factory = AggregatorFactory.lastValue(); + View view = View.builder().setAggregatorFactory(factory).build(); + + ViewRegistry viewRegistry = + ViewRegistry.builder() + .addView( + InstrumentSelector.builder() + .setInstrumentType(InstrumentType.COUNTER) + .setInstrumentNameRegex(".*") + .build(), + view) + .build(); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "", "", "", InstrumentType.COUNTER, InstrumentValueType.LONG))) + .isEqualTo(view); + // this one hasn't been configured, so it gets the default still. + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "", "", "", InstrumentType.UP_DOWN_COUNTER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.CUMULATIVE_SUM); + } + + @Test + void selection_onName() { + AggregatorFactory factory = AggregatorFactory.lastValue(); + View view = View.builder().setAggregatorFactory(factory).build(); + + ViewRegistry viewRegistry = + ViewRegistry.builder() + .addView( + InstrumentSelector.builder() + .setInstrumentType(InstrumentType.COUNTER) + .setInstrumentNameRegex("overridden") + .build(), + view) + .build(); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "overridden", "", "", InstrumentType.COUNTER, InstrumentValueType.LONG))) + .isSameAs(view); + // this one hasn't been configured, so it gets the default still. + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "default", "", "", InstrumentType.COUNTER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.CUMULATIVE_SUM); + } + + @Test + void selection_LastAddedViewWins() { + AggregatorFactory factory1 = AggregatorFactory.lastValue(); + View view1 = View.builder().setAggregatorFactory(factory1).build(); + AggregatorFactory factory2 = AggregatorFactory.minMaxSumCount(); + View view2 = View.builder().setAggregatorFactory(factory2).build(); + + ViewRegistry viewRegistry = + ViewRegistry.builder() + .addView( + InstrumentSelector.builder() + .setInstrumentType(InstrumentType.COUNTER) + .setInstrumentNameRegex(".*") + .build(), + view1) + .addView( + InstrumentSelector.builder() + .setInstrumentType(InstrumentType.COUNTER) + .setInstrumentNameRegex("overridden") + .build(), + view2) + .build(); + + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "overridden", "", "", InstrumentType.COUNTER, InstrumentValueType.LONG))) + .isEqualTo(view2); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "default", "", "", InstrumentType.COUNTER, InstrumentValueType.LONG))) + .isEqualTo(view1); + } + + @Test + void selection_regex() { + AggregatorFactory factory = AggregatorFactory.lastValue(); + View view = View.builder().setAggregatorFactory(factory).build(); + + ViewRegistry viewRegistry = + ViewRegistry.builder() + .addView( + InstrumentSelector.builder() + .setInstrumentNameRegex("overrid(es|den)") + .setInstrumentType(InstrumentType.COUNTER) + .build(), + view) + .build(); + + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "overridden", "", "", InstrumentType.COUNTER, InstrumentValueType.LONG))) + .isEqualTo(view); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "overrides", "", "", InstrumentType.COUNTER, InstrumentValueType.LONG))) + .isEqualTo(view); + // this one hasn't been configured, so it gets the default still.. + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "default", "", "", InstrumentType.UP_DOWN_COUNTER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.CUMULATIVE_SUM); + } + + @Test + void defaults() { + ViewRegistry viewRegistry = ViewRegistry.builder().build(); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "", "", "", InstrumentType.COUNTER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.CUMULATIVE_SUM); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "", "", "", InstrumentType.UP_DOWN_COUNTER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.CUMULATIVE_SUM); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "", "", "", InstrumentType.VALUE_RECORDER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.SUMMARY); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "", "", "", InstrumentType.SUM_OBSERVER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.CUMULATIVE_SUM); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "", "", "", InstrumentType.VALUE_OBSERVER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.LAST_VALUE); + assertThat( + viewRegistry.findView( + InstrumentDescriptor.create( + "", "", "", InstrumentType.UP_DOWN_SUM_OBSERVER, InstrumentValueType.LONG))) + .isSameAs(ViewRegistry.CUMULATIVE_SUM); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorFactoryTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorFactoryTest.java new file mode 100644 index 000000000..df1f668bc --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorFactoryTest.java @@ -0,0 +1,211 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class AggregatorFactoryTest { + @Test + void getCountAggregatorFactory() { + AggregatorFactory count = AggregatorFactory.count(AggregationTemporality.CUMULATIVE); + assertThat( + count.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.COUNTER, + InstrumentValueType.LONG))) + .isInstanceOf(CountAggregator.class); + assertThat( + count.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.COUNTER, + InstrumentValueType.DOUBLE))) + .isInstanceOf(CountAggregator.class); + } + + @Test + void getLastValueAggregatorFactory() { + AggregatorFactory lastValue = AggregatorFactory.lastValue(); + assertThat( + lastValue.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.COUNTER, + InstrumentValueType.LONG))) + .isInstanceOf(LongLastValueAggregator.class); + assertThat( + lastValue.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.COUNTER, + InstrumentValueType.DOUBLE))) + .isInstanceOf(DoubleLastValueAggregator.class); + } + + @Test + void getMinMaxSumCountAggregatorFactory() { + AggregatorFactory minMaxSumCount = AggregatorFactory.minMaxSumCount(); + assertThat( + minMaxSumCount.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.COUNTER, + InstrumentValueType.LONG))) + .isInstanceOf(LongMinMaxSumCountAggregator.class); + assertThat( + minMaxSumCount.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.COUNTER, + InstrumentValueType.DOUBLE))) + .isInstanceOf(DoubleMinMaxSumCountAggregator.class); + } + + @Test + void getSumAggregatorFactory() { + AggregatorFactory sum = AggregatorFactory.sum(AggregationTemporality.DELTA); + assertThat( + sum.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.COUNTER, + InstrumentValueType.LONG))) + .isInstanceOf(LongSumAggregator.class); + assertThat( + sum.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.COUNTER, + InstrumentValueType.DOUBLE))) + .isInstanceOf(DoubleSumAggregator.class); + } + + @Test + void getHistogramAggregatorFactory() { + AggregatorFactory histogram = + AggregatorFactory.histogram(Collections.singletonList(1.0), AggregationTemporality.DELTA); + assertThat( + histogram.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.LONG))) + .isInstanceOf(DoubleHistogramAggregator.class); + assertThat( + histogram.create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.DOUBLE))) + .isInstanceOf(DoubleHistogramAggregator.class); + + assertThat( + histogram + .create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.LONG)) + .isStateful()) + .isFalse(); + assertThat( + AggregatorFactory.histogram( + Collections.singletonList(1.0), AggregationTemporality.CUMULATIVE) + .create( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.DOUBLE)) + .isStateful()) + .isTrue(); + + assertThatThrownBy( + () -> + AggregatorFactory.histogram( + Collections.singletonList(Double.NEGATIVE_INFINITY), + AggregationTemporality.DELTA)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid bucket boundary: -Inf"); + assertThatThrownBy( + () -> + AggregatorFactory.histogram( + Arrays.asList(1.0, Double.POSITIVE_INFINITY), AggregationTemporality.DELTA)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid bucket boundary: +Inf"); + assertThatThrownBy( + () -> + AggregatorFactory.histogram( + Arrays.asList(1.0, Double.NaN), AggregationTemporality.DELTA)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid bucket boundary: NaN"); + assertThatThrownBy( + () -> + AggregatorFactory.histogram( + Arrays.asList(2.0, 1.0, 3.0), AggregationTemporality.DELTA)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid bucket boundary: 2.0 >= 1.0"); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorHandleTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorHandleTest.java new file mode 100644 index 000000000..1f5441320 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/AggregatorHandleTest.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.util.concurrent.AtomicDouble; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +public class AggregatorHandleTest { + + @Test + void acquireMapped() { + TestAggregatorHandle testAggregator = new TestAggregatorHandle(); + assertThat(testAggregator.acquire()).isTrue(); + testAggregator.release(); + assertThat(testAggregator.acquire()).isTrue(); + assertThat(testAggregator.acquire()).isTrue(); + testAggregator.release(); + assertThat(testAggregator.acquire()).isTrue(); + testAggregator.release(); + testAggregator.release(); + } + + @Test + void tryUnmap_AcquiredHandler() { + TestAggregatorHandle testAggregator = new TestAggregatorHandle(); + assertThat(testAggregator.acquire()).isTrue(); + assertThat(testAggregator.tryUnmap()).isFalse(); + testAggregator.release(); + // The aggregator is by default acquired, so need an extra release. + assertThat(testAggregator.tryUnmap()).isFalse(); + testAggregator.release(); + assertThat(testAggregator.tryUnmap()).isTrue(); + } + + @Test + void tryUnmap_AcquiredHandler_MultipleTimes() { + TestAggregatorHandle testAggregator = new TestAggregatorHandle(); + assertThat(testAggregator.acquire()).isTrue(); + assertThat(testAggregator.acquire()).isTrue(); + assertThat(testAggregator.acquire()).isTrue(); + assertThat(testAggregator.tryUnmap()).isFalse(); + testAggregator.release(); + assertThat(testAggregator.acquire()).isTrue(); + assertThat(testAggregator.tryUnmap()).isFalse(); + testAggregator.release(); + assertThat(testAggregator.tryUnmap()).isFalse(); + testAggregator.release(); + assertThat(testAggregator.tryUnmap()).isFalse(); + testAggregator.release(); + // The aggregator is by default acquired, so need an extra release. + assertThat(testAggregator.tryUnmap()).isFalse(); + testAggregator.release(); + assertThat(testAggregator.tryUnmap()).isTrue(); + } + + @Test + void bind_ThenUnmap_ThenTryToBind() { + TestAggregatorHandle testAggregator = new TestAggregatorHandle(); + testAggregator.release(); + assertThat(testAggregator.tryUnmap()).isTrue(); + assertThat(testAggregator.acquire()).isFalse(); + testAggregator.release(); + } + + @Test + void testRecordings() { + TestAggregatorHandle testAggregator = new TestAggregatorHandle(); + + testAggregator.recordLong(22); + assertThat(testAggregator.recordedLong.get()).isEqualTo(22); + assertThat(testAggregator.recordedDouble.get()).isEqualTo(0); + + testAggregator.accumulateThenReset(); + assertThat(testAggregator.recordedLong.get()).isEqualTo(0); + assertThat(testAggregator.recordedDouble.get()).isEqualTo(0); + + testAggregator.recordDouble(33.55); + assertThat(testAggregator.recordedLong.get()).isEqualTo(0); + assertThat(testAggregator.recordedDouble.get()).isEqualTo(33.55); + + testAggregator.accumulateThenReset(); + assertThat(testAggregator.recordedLong.get()).isEqualTo(0); + assertThat(testAggregator.recordedDouble.get()).isEqualTo(0); + } + + private static class TestAggregatorHandle extends AggregatorHandle { + final AtomicLong recordedLong = new AtomicLong(); + final AtomicDouble recordedDouble = new AtomicDouble(); + + @Nullable + @Override + protected Void doAccumulateThenReset() { + recordedLong.set(0); + recordedDouble.set(0); + return null; + } + + @Override + protected void doRecordLong(long value) { + recordedLong.set(value); + } + + @Override + protected void doRecordDouble(double value) { + recordedDouble.set(value); + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregatorTest.java new file mode 100644 index 000000000..d87045088 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/CountAggregatorTest.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link CountAggregator}. */ +class CountAggregatorTest { + private static final CountAggregator cumulativeAggregator = + new CountAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.LONG), + AggregationTemporality.CUMULATIVE); + private static final CountAggregator deltaAggregator = + new CountAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.LONG), + AggregationTemporality.DELTA); + + @Test + void createHandle() { + assertThat(cumulativeAggregator.createHandle()).isInstanceOf(CountAggregator.Handle.class); + } + + @Test + void toPoint() { + AggregatorHandle aggregatorHandle = cumulativeAggregator.createHandle(); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void recordLongOperations() { + AggregatorHandle aggregatorHandle = cumulativeAggregator.createHandle(); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(12); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(2); + } + + @Test + void recordDoubleOperations() { + AggregatorHandle aggregatorHandle = cumulativeAggregator.createHandle(); + aggregatorHandle.recordDouble(12.3); + aggregatorHandle.recordDouble(12.3); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(2); + } + + @Test + void toMetricData_CumulativeTemporality() { + AggregatorHandle aggregatorHandle = cumulativeAggregator.createHandle(); + aggregatorHandle.recordLong(10); + + MetricData metricData = + cumulativeAggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData) + .isEqualTo( + MetricData.createLongSum( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList(LongPointData.create(0, 100, Labels.empty(), 1))))); + } + + @Test + void toMetricData_DeltaTemporality() { + AggregatorHandle aggregatorHandle = deltaAggregator.createHandle(); + aggregatorHandle.recordLong(10); + + MetricData metricData = + deltaAggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData) + .isEqualTo( + MetricData.createLongSum( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.DELTA, + Collections.singletonList(LongPointData.create(10, 100, Labels.empty(), 1))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramAggregatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramAggregatorTest.java new file mode 100644 index 000000000..efbc387ad --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleHistogramAggregatorTest.java @@ -0,0 +1,165 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +public class DoubleHistogramAggregatorTest { + private static final double[] boundaries = new double[] {10.0, 100.0, 1000.0}; + private static final DoubleHistogramAggregator aggregator = + new DoubleHistogramAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.LONG), + boundaries, + /* stateful= */ false); + + @Test + void createHandle() { + assertThat(aggregator.createHandle()).isInstanceOf(DoubleHistogramAggregator.Handle.class); + } + + @Test + void testRecordings() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(20); + aggregatorHandle.recordLong(5); + aggregatorHandle.recordLong(150); + aggregatorHandle.recordLong(2000); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(HistogramAccumulation.create(2175, new long[] {1, 1, 1, 1})); + } + + @Test + void toAccumulationAndReset() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordLong(100); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(HistogramAccumulation.create(100, new long[] {0, 1, 0, 0})); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordLong(0); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(HistogramAccumulation.create(0, new long[] {1, 0, 0, 0})); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void accumulateData() { + assertThat(aggregator.accumulateDouble(11.1)) + .isEqualTo(HistogramAccumulation.create(11.1, new long[] {0, 1, 0, 0})); + assertThat(aggregator.accumulateLong(10)) + .isEqualTo(HistogramAccumulation.create(10.0, new long[] {1, 0, 0, 0})); + } + + @Test + void toMetricData() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(10); + + MetricData metricData = + aggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData).isNotNull(); + assertThat(metricData.getType()).isEqualTo(MetricDataType.HISTOGRAM); + assertThat(metricData.getDoubleHistogramData().getAggregationTemporality()) + .isEqualTo(AggregationTemporality.DELTA); + } + + @Test + void testHistogramCounts() { + assertThat(aggregator.accumulateDouble(1.1).getCounts().length) + .isEqualTo(boundaries.length + 1); + assertThat(aggregator.accumulateLong(1).getCounts().length).isEqualTo(boundaries.length + 1); + + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordDouble(1.1); + HistogramAccumulation histogramAccumulation = aggregatorHandle.accumulateThenReset(); + assertThat(histogramAccumulation).isNotNull(); + assertThat(histogramAccumulation.getCounts().length).isEqualTo(boundaries.length + 1); + } + + @Test + void testMultithreadedUpdates() throws InterruptedException { + final AggregatorHandle aggregatorHandle = aggregator.createHandle(); + final Histogram summarizer = new Histogram(); + final ImmutableList updates = + ImmutableList.of(1L, 2L, 3L, 5L, 7L, 11L, 13L, 17L, 19L, 23L); + final int numberOfThreads = updates.size(); + final int numberOfUpdates = 10000; + final ThreadPoolExecutor executor = + (ThreadPoolExecutor) Executors.newFixedThreadPool(numberOfThreads); + + executor.invokeAll( + updates.stream() + .map( + v -> + Executors.callable( + () -> { + for (int j = 0; j < numberOfUpdates; j++) { + aggregatorHandle.recordLong(v); + if (ThreadLocalRandom.current().nextInt(10) == 0) { + summarizer.process(aggregatorHandle.accumulateThenReset()); + } + } + })) + .collect(Collectors.toList())); + + // make sure everything gets merged when all the aggregation is done. + summarizer.process(aggregatorHandle.accumulateThenReset()); + + assertThat(summarizer.accumulation) + .isEqualTo(HistogramAccumulation.create(1010000, new long[] {50000, 50000, 0, 0})); + } + + private static final class Histogram { + private final Object mutex = new Object(); + + @Nullable private HistogramAccumulation accumulation; + + void process(@Nullable HistogramAccumulation other) { + if (other == null) { + return; + } + + synchronized (mutex) { + if (accumulation == null) { + accumulation = other; + return; + } + accumulation = aggregator.merge(accumulation, other); + } + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleLastValueAggregatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleLastValueAggregatorTest.java new file mode 100644 index 000000000..6c3b9697f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleLastValueAggregatorTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.DoubleGaugeData; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AggregatorHandle}. */ +class DoubleLastValueAggregatorTest { + private static final DoubleLastValueAggregator aggregator = + new DoubleLastValueAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_OBSERVER, + InstrumentValueType.DOUBLE)); + + @Test + void createHandle() { + assertThat(aggregator.createHandle()).isInstanceOf(DoubleLastValueAggregator.Handle.class); + } + + @Test + void multipleRecords() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordDouble(12.1); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(12.1); + aggregatorHandle.recordDouble(13.1); + aggregatorHandle.recordDouble(14.1); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(14.1); + } + + @Test + void toAccumulationAndReset() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordDouble(13.1); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(13.1); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordDouble(12.1); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(12.1); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void toMetricData() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordDouble(10); + + MetricData metricData = + aggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData) + .isEqualTo( + MetricData.createDoubleGauge( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "unit", + DoubleGaugeData.create( + Collections.singletonList( + DoublePointData.create(0, 100, Labels.empty(), 10))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountAggregatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountAggregatorTest.java new file mode 100644 index 000000000..74d29f978 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleMinMaxSumCountAggregatorTest.java @@ -0,0 +1,161 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class DoubleMinMaxSumCountAggregatorTest { + private static final DoubleMinMaxSumCountAggregator aggregator = + new DoubleMinMaxSumCountAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.DOUBLE)); + + @Test + void createHandle() { + assertThat(aggregator.createHandle()).isInstanceOf(DoubleMinMaxSumCountAggregator.Handle.class); + } + + @Test + void testRecordings() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + + aggregatorHandle.recordDouble(100); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, 100, 100, 100)); + + aggregatorHandle.recordDouble(200); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, 200, 200, 200)); + + aggregatorHandle.recordDouble(-75); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, -75, -75, -75)); + } + + @Test + void toAccumulationAndReset() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordDouble(100); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, 100, 100, 100)); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordDouble(100); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, 100, 100, 100)); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void toMetricData() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordDouble(10); + + MetricData metricData = + aggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData).isNotNull(); + assertThat(metricData.getType()).isEqualTo(MetricDataType.SUMMARY); + } + + @Test + void testMultithreadedUpdates() throws Exception { + final AggregatorHandle aggregatorHandle = aggregator.createHandle(); + final Summary summarizer = new Summary(); + int numberOfThreads = 10; + final double[] updates = new double[] {1, 2, 3, 5, 7, 11, 13, 17, 19, 23}; + final int numberOfUpdates = 1000; + final CountDownLatch starter = new CountDownLatch(numberOfThreads); + List workers = new ArrayList<>(); + for (int i = 0; i < numberOfThreads; i++) { + final int index = i; + Thread t = + new Thread( + () -> { + double update = updates[index]; + try { + starter.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + for (int j = 0; j < numberOfUpdates; j++) { + aggregatorHandle.recordDouble(update); + if (ThreadLocalRandom.current().nextInt(10) == 0) { + summarizer.process(aggregatorHandle.accumulateThenReset()); + } + } + }); + workers.add(t); + t.start(); + } + for (int i = 0; i <= numberOfThreads; i++) { + starter.countDown(); + } + + for (Thread worker : workers) { + worker.join(); + } + // make sure everything gets merged when all the aggregation is done. + summarizer.process(aggregatorHandle.accumulateThenReset()); + + assertThat(summarizer.accumulation) + .isEqualTo( + MinMaxSumCountAccumulation.create(numberOfThreads * numberOfUpdates, 101000, 1, 23)); + } + + private static final class Summary { + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + @GuardedBy("lock") + @Nullable + private MinMaxSumCountAccumulation accumulation; + + void process(@Nullable MinMaxSumCountAccumulation other) { + if (other == null) { + return; + } + lock.writeLock().lock(); + try { + if (accumulation == null) { + accumulation = other; + return; + } + accumulation = aggregator.merge(accumulation, other); + } finally { + lock.writeLock().unlock(); + } + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleSumAggregatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleSumAggregatorTest.java new file mode 100644 index 000000000..0448e36f2 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/DoubleSumAggregatorTest.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.aggregator.AbstractSumAggregator.MergeStrategy; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.DoubleSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link DoubleSumAggregator}. */ +class DoubleSumAggregatorTest { + private static final DoubleSumAggregator aggregator = + new DoubleSumAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", "description", "unit", InstrumentType.COUNTER, InstrumentValueType.DOUBLE), + AggregationTemporality.CUMULATIVE); + + @Test + void createHandle() { + assertThat(aggregator.createHandle()).isInstanceOf(DoubleSumAggregator.Handle.class); + } + + @Test + void multipleRecords() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordDouble(12.1); + aggregatorHandle.recordDouble(12.1); + aggregatorHandle.recordDouble(12.1); + aggregatorHandle.recordDouble(12.1); + aggregatorHandle.recordDouble(12.1); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(12.1 * 5); + } + + @Test + void multipleRecords_WithNegatives() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordDouble(12); + aggregatorHandle.recordDouble(12); + aggregatorHandle.recordDouble(-23); + aggregatorHandle.recordDouble(12); + aggregatorHandle.recordDouble(12); + aggregatorHandle.recordDouble(-11); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(14); + } + + @Test + void toAccumulationAndReset() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordDouble(13); + aggregatorHandle.recordDouble(12); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(25); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordDouble(12); + aggregatorHandle.recordDouble(-25); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(-13); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void merge() { + for (InstrumentType instrumentType : InstrumentType.values()) { + for (AggregationTemporality temporality : AggregationTemporality.values()) { + DoubleSumAggregator aggregator = + new DoubleSumAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", "description", "unit", instrumentType, InstrumentValueType.LONG), + temporality); + MergeStrategy expectedMergeStrategy = + AbstractSumAggregator.resolveMergeStrategy(instrumentType, temporality); + double merged = aggregator.merge(1.0d, 2.0d); + assertThat(merged) + .withFailMessage( + "Invalid merge result for instrumentType %s, temporality %s: %s", + instrumentType, temporality, merged) + .isEqualTo(expectedMergeStrategy == MergeStrategy.SUM ? 3.0d : 1.0d); + } + } + } + + @Test + void toMetricData() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordDouble(10); + + MetricData metricData = + aggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData) + .isEqualTo( + MetricData.createDoubleSum( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "unit", + DoubleSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + DoublePointData.create(0, 100, Labels.empty(), 10))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongLastValueAggregatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongLastValueAggregatorTest.java new file mode 100644 index 000000000..9b6e0cbdb --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongLastValueAggregatorTest.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.LongGaugeData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongLastValueAggregator}. */ +class LongLastValueAggregatorTest { + private static final LongLastValueAggregator aggregator = + new LongLastValueAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_OBSERVER, + InstrumentValueType.LONG)); + + @Test + void createHandle() { + assertThat(aggregator.createHandle()).isInstanceOf(LongLastValueAggregator.Handle.class); + } + + @Test + void multipleRecords() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(12); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(12L); + aggregatorHandle.recordLong(13); + aggregatorHandle.recordLong(14); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(14L); + } + + @Test + void toAccumulationAndReset() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordLong(13); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(13L); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordLong(12); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(12L); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void toMetricData() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(10); + + MetricData metricData = + aggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData) + .isEqualTo( + MetricData.createLongGauge( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "unit", + LongGaugeData.create( + Collections.singletonList(LongPointData.create(0, 100, Labels.empty(), 10))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountAggregatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountAggregatorTest.java new file mode 100644 index 000000000..81a76999e --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongMinMaxSumCountAggregatorTest.java @@ -0,0 +1,158 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class LongMinMaxSumCountAggregatorTest { + private static final LongMinMaxSumCountAggregator aggregator = + new LongMinMaxSumCountAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", + "description", + "unit", + InstrumentType.VALUE_RECORDER, + InstrumentValueType.LONG)); + + @Test + void createHandle() { + assertThat(aggregator.createHandle()).isInstanceOf(LongMinMaxSumCountAggregator.Handle.class); + } + + @Test + void testRecordings() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(100); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, 100, 100, 100)); + aggregatorHandle.recordLong(200); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, 200, 200, 200)); + aggregatorHandle.recordLong(-75); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, -75, -75, -75)); + } + + @Test + void toAccumulationAndReset() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordLong(100); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, 100, 100, 100)); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordLong(100); + assertThat(aggregatorHandle.accumulateThenReset()) + .isEqualTo(MinMaxSumCountAccumulation.create(1, 100, 100, 100)); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void toMetricData() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(10); + + MetricData metricData = + aggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData).isNotNull(); + assertThat(metricData.getType()).isEqualTo(MetricDataType.SUMMARY); + } + + @Test + void testMultithreadedUpdates() throws Exception { + final AggregatorHandle aggregatorHandle = aggregator.createHandle(); + final Summary summarizer = new Summary(); + int numberOfThreads = 10; + final long[] updates = new long[] {1, 2, 3, 5, 7, 11, 13, 17, 19, 23}; + final int numberOfUpdates = 1000; + final CountDownLatch starter = new CountDownLatch(numberOfThreads); + List workers = new ArrayList<>(); + for (int i = 0; i < numberOfThreads; i++) { + final int index = i; + Thread t = + new Thread( + () -> { + long update = updates[index]; + try { + starter.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + for (int j = 0; j < numberOfUpdates; j++) { + aggregatorHandle.recordLong(update); + if (ThreadLocalRandom.current().nextInt(10) == 0) { + summarizer.process(aggregatorHandle.accumulateThenReset()); + } + } + }); + workers.add(t); + t.start(); + } + for (int i = 0; i <= numberOfThreads; i++) { + starter.countDown(); + } + + for (Thread worker : workers) { + worker.join(); + } + // make sure everything gets merged when all the aggregation is done. + summarizer.process(aggregatorHandle.accumulateThenReset()); + + assertThat(summarizer.accumulation) + .isEqualTo( + MinMaxSumCountAccumulation.create(numberOfThreads * numberOfUpdates, 101000, 1, 23)); + } + + private static final class Summary { + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + @GuardedBy("lock") + @Nullable + private MinMaxSumCountAccumulation accumulation; + + void process(@Nullable MinMaxSumCountAccumulation other) { + if (other == null) { + return; + } + lock.writeLock().lock(); + try { + if (accumulation == null) { + accumulation = other; + return; + } + accumulation = aggregator.merge(accumulation, other); + } finally { + lock.writeLock().unlock(); + } + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongSumAggregatorTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongSumAggregatorTest.java new file mode 100644 index 000000000..283c04e8b --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/LongSumAggregatorTest.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.aggregator.AbstractSumAggregator.MergeStrategy; +import io.opentelemetry.sdk.metrics.common.InstrumentDescriptor; +import io.opentelemetry.sdk.metrics.common.InstrumentType; +import io.opentelemetry.sdk.metrics.common.InstrumentValueType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link LongSumAggregator}. */ +class LongSumAggregatorTest { + private static final LongSumAggregator aggregator = + new LongSumAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", "description", "unit", InstrumentType.COUNTER, InstrumentValueType.LONG), + AggregationTemporality.CUMULATIVE); + + @Test + void createHandle() { + assertThat(aggregator.createHandle()).isInstanceOf(LongSumAggregator.Handle.class); + } + + @Test + void multipleRecords() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(12); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(12 * 5); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void multipleRecords_WithNegatives() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(-23); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(-11); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(14); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void toAccumulationAndReset() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordLong(13); + aggregatorHandle.recordLong(12); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(25); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + + aggregatorHandle.recordLong(12); + aggregatorHandle.recordLong(-25); + assertThat(aggregatorHandle.accumulateThenReset()).isEqualTo(-13); + assertThat(aggregatorHandle.accumulateThenReset()).isNull(); + } + + @Test + void merge() { + for (InstrumentType instrumentType : InstrumentType.values()) { + for (AggregationTemporality temporality : AggregationTemporality.values()) { + LongSumAggregator aggregator = + new LongSumAggregator( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + InstrumentDescriptor.create( + "name", "description", "unit", instrumentType, InstrumentValueType.LONG), + temporality); + MergeStrategy expectedMergeStrategy = + AbstractSumAggregator.resolveMergeStrategy(instrumentType, temporality); + long merged = aggregator.merge(1L, 2L); + assertThat(merged) + .withFailMessage( + "Invalid merge result for instrumentType %s, temporality %s: %s", + instrumentType, temporality, merged) + .isEqualTo(expectedMergeStrategy == MergeStrategy.SUM ? 3 : 1); + } + } + } + + @Test + void toMetricData() { + AggregatorHandle aggregatorHandle = aggregator.createHandle(); + aggregatorHandle.recordLong(10); + + MetricData metricData = + aggregator.toMetricData( + Collections.singletonMap(Labels.empty(), aggregatorHandle.accumulateThenReset()), + 0, + 10, + 100); + assertThat(metricData) + .isEqualTo( + MetricData.createLongSum( + Resource.getDefault(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "unit", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList(LongPointData.create(0, 100, Labels.empty(), 10))))); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAccumulationTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAccumulationTest.java new file mode 100644 index 000000000..b033c1a8f --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/aggregator/MinMaxSumCountAccumulationTest.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.aggregator; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.metrics.data.DoubleSummaryPointData; +import io.opentelemetry.sdk.metrics.data.ValueAtPercentile; +import org.junit.jupiter.api.Test; + +class MinMaxSumCountAccumulationTest { + @Test + void toPoint() { + MinMaxSumCountAccumulation accumulation = MinMaxSumCountAccumulation.create(12, 25, 1, 3); + DoubleSummaryPointData point = getPoint(accumulation); + assertThat(point.getCount()).isEqualTo(12); + assertThat(point.getSum()).isEqualTo(25); + assertThat(point.getPercentileValues()).hasSize(2); + assertThat(point.getPercentileValues().get(0)).isEqualTo(ValueAtPercentile.create(0.0, 1)); + assertThat(point.getPercentileValues().get(1)).isEqualTo(ValueAtPercentile.create(100.0, 3)); + } + + private static DoubleSummaryPointData getPoint(MinMaxSumCountAccumulation accumulation) { + DoubleSummaryPointData point = accumulation.toPoint(12345, 12358, Labels.of("key", "value")); + assertThat(point).isNotNull(); + assertThat(point.getStartEpochNanos()).isEqualTo(12345); + assertThat(point.getEpochNanos()).isEqualTo(12358); + assertThat(point.getLabels().size()).isEqualTo(1); + assertThat(point.getLabels().get("key")).isEqualTo("value"); + assertThat(point).isInstanceOf(DoubleSummaryPointData.class); + return point; + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/data/MetricDataTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/data/MetricDataTest.java new file mode 100644 index 000000000..eb44b5705 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/data/MetricDataTest.java @@ -0,0 +1,240 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link io.opentelemetry.sdk.metrics.data.MetricData}. */ +class MetricDataTest { + private static final long START_EPOCH_NANOS = TimeUnit.MILLISECONDS.toNanos(1000); + private static final long EPOCH_NANOS = TimeUnit.MILLISECONDS.toNanos(2000); + private static final long LONG_VALUE = 10; + private static final double DOUBLE_VALUE = 1.234; + private static final ValueAtPercentile MINIMUM_VALUE = + ValueAtPercentile.create(0.0, DOUBLE_VALUE); + private static final ValueAtPercentile MAXIMUM_VALUE = + ValueAtPercentile.create(100.0, DOUBLE_VALUE); + private static final LongPointData LONG_POINT = + LongPointData.create(START_EPOCH_NANOS, EPOCH_NANOS, Labels.of("key", "value"), LONG_VALUE); + private static final DoublePointData DOUBLE_POINT = + DoublePointData.create( + START_EPOCH_NANOS, EPOCH_NANOS, Labels.of("key", "value"), DOUBLE_VALUE); + private static final DoubleSummaryPointData SUMMARY_POINT = + DoubleSummaryPointData.create( + START_EPOCH_NANOS, + EPOCH_NANOS, + Labels.of("key", "value"), + LONG_VALUE, + DOUBLE_VALUE, + Arrays.asList( + ValueAtPercentile.create(0.0, DOUBLE_VALUE), + ValueAtPercentile.create(100, DOUBLE_VALUE))); + private static final DoubleHistogramPointData HISTOGRAM_POINT = + DoubleHistogramPointData.create( + START_EPOCH_NANOS, + EPOCH_NANOS, + Labels.of("key", "value"), + DOUBLE_VALUE, + ImmutableList.of(1.0), + ImmutableList.of(1L, 1L)); + + @Test + void metricData_Getters() { + MetricData metricData = + MetricData.createDoubleGauge( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + DoubleGaugeData.create(Collections.emptyList())); + assertThat(metricData.getName()).isEqualTo("metric_name"); + assertThat(metricData.getDescription()).isEqualTo("metric_description"); + assertThat(metricData.getUnit()).isEqualTo("ms"); + assertThat(metricData.getType()).isEqualTo(MetricDataType.DOUBLE_GAUGE); + assertThat(metricData.getResource()).isEqualTo(Resource.empty()); + assertThat(metricData.getInstrumentationLibraryInfo()) + .isEqualTo(InstrumentationLibraryInfo.empty()); + assertThat(metricData.isEmpty()).isTrue(); + } + + @Test + void metricData_LongPoints() { + assertThat(LONG_POINT.getStartEpochNanos()).isEqualTo(START_EPOCH_NANOS); + assertThat(LONG_POINT.getEpochNanos()).isEqualTo(EPOCH_NANOS); + assertThat(LONG_POINT.getLabels().size()).isEqualTo(1); + assertThat(LONG_POINT.getLabels().get("key")).isEqualTo("value"); + assertThat(LONG_POINT.getValue()).isEqualTo(LONG_VALUE); + MetricData metricData = + MetricData.createLongGauge( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + LongGaugeData.create(Collections.singletonList(LONG_POINT))); + assertThat(metricData.isEmpty()).isFalse(); + assertThat(metricData.getLongGaugeData().getPoints()).containsExactly(LONG_POINT); + metricData = + MetricData.createLongSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + LongSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList(LONG_POINT))); + assertThat(metricData.isEmpty()).isFalse(); + assertThat(metricData.getLongSumData().getPoints()).containsExactly(LONG_POINT); + } + + @Test + void metricData_DoublePoints() { + assertThat(DOUBLE_POINT.getStartEpochNanos()).isEqualTo(START_EPOCH_NANOS); + assertThat(DOUBLE_POINT.getEpochNanos()).isEqualTo(EPOCH_NANOS); + assertThat(DOUBLE_POINT.getLabels().size()).isEqualTo(1); + assertThat(DOUBLE_POINT.getLabels().get("key")).isEqualTo("value"); + assertThat(DOUBLE_POINT.getValue()).isEqualTo(DOUBLE_VALUE); + MetricData metricData = + MetricData.createDoubleGauge( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + DoubleGaugeData.create(Collections.singletonList(DOUBLE_POINT))); + assertThat(metricData.isEmpty()).isFalse(); + assertThat(metricData.getDoubleGaugeData().getPoints()).containsExactly(DOUBLE_POINT); + metricData = + MetricData.createDoubleSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + DoubleSumData.create( + /* isMonotonic= */ false, + AggregationTemporality.CUMULATIVE, + Collections.singletonList(DOUBLE_POINT))); + assertThat(metricData.isEmpty()).isFalse(); + assertThat(metricData.getDoubleSumData().getPoints()).containsExactly(DOUBLE_POINT); + } + + @Test + void metricData_SummaryPoints() { + assertThat(SUMMARY_POINT.getStartEpochNanos()).isEqualTo(START_EPOCH_NANOS); + assertThat(SUMMARY_POINT.getEpochNanos()).isEqualTo(EPOCH_NANOS); + assertThat(SUMMARY_POINT.getLabels().size()).isEqualTo(1); + assertThat(SUMMARY_POINT.getLabels().get("key")).isEqualTo("value"); + assertThat(SUMMARY_POINT.getCount()).isEqualTo(LONG_VALUE); + assertThat(SUMMARY_POINT.getSum()).isEqualTo(DOUBLE_VALUE); + assertThat(SUMMARY_POINT.getPercentileValues()) + .isEqualTo(Arrays.asList(MINIMUM_VALUE, MAXIMUM_VALUE)); + MetricData metricData = + MetricData.createDoubleSummary( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + DoubleSummaryData.create(Collections.singletonList(SUMMARY_POINT))); + assertThat(metricData.getDoubleSummaryData().getPoints()).containsExactly(SUMMARY_POINT); + } + + @Test + void metricData_HistogramPoints() { + assertThat(HISTOGRAM_POINT.getStartEpochNanos()).isEqualTo(START_EPOCH_NANOS); + assertThat(HISTOGRAM_POINT.getEpochNanos()).isEqualTo(EPOCH_NANOS); + assertThat(HISTOGRAM_POINT.getLabels().size()).isEqualTo(1); + assertThat(HISTOGRAM_POINT.getLabels().get("key")).isEqualTo("value"); + assertThat(HISTOGRAM_POINT.getCount()).isEqualTo(2L); + assertThat(HISTOGRAM_POINT.getSum()).isEqualTo(DOUBLE_VALUE); + assertThat(HISTOGRAM_POINT.getBoundaries()).isEqualTo(ImmutableList.of(1.0)); + assertThat(HISTOGRAM_POINT.getCounts()).isEqualTo(ImmutableList.of(1L, 1L)); + + MetricData metricData = + MetricData.createDoubleHistogram( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + DoubleHistogramData.create( + AggregationTemporality.DELTA, Collections.singleton(HISTOGRAM_POINT))); + assertThat(metricData.getDoubleHistogramData().getPoints()).containsExactly(HISTOGRAM_POINT); + + assertThatThrownBy( + () -> + DoubleHistogramPointData.create( + 0, 0, Labels.empty(), 0.0, ImmutableList.of(), ImmutableList.of())) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy( + () -> + DoubleHistogramPointData.create( + 0, + 0, + Labels.empty(), + 0.0, + ImmutableList.of(1.0, 1.0), + ImmutableList.of(0L, 0L, 0L))) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy( + () -> + DoubleHistogramPointData.create( + 0, + 0, + Labels.empty(), + 0.0, + ImmutableList.of(Double.NEGATIVE_INFINITY), + ImmutableList.of(0L, 0L))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void metricData_GetDefault() { + MetricData metricData = + MetricData.createDoubleSummary( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + DoubleSummaryData.create(Collections.singletonList(SUMMARY_POINT))); + assertThat(metricData.getDoubleGaugeData().getPoints()).isEmpty(); + assertThat(metricData.getLongGaugeData().getPoints()).isEmpty(); + assertThat(metricData.getDoubleSumData().getPoints()).isEmpty(); + assertThat(metricData.getLongGaugeData().getPoints()).isEmpty(); + assertThat(metricData.getDoubleHistogramData().getPoints()).isEmpty(); + assertThat(metricData.getDoubleSummaryData().getPoints()).containsExactly(SUMMARY_POINT); + + metricData = + MetricData.createDoubleGauge( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "metric_name", + "metric_description", + "ms", + DoubleGaugeData.create(Collections.singletonList(DOUBLE_POINT))); + assertThat(metricData.getDoubleGaugeData().getPoints()).containsExactly(DOUBLE_POINT); + assertThat(metricData.getLongGaugeData().getPoints()).isEmpty(); + assertThat(metricData.getDoubleSumData().getPoints()).isEmpty(); + assertThat(metricData.getLongGaugeData().getPoints()).isEmpty(); + assertThat(metricData.getDoubleHistogramData().getPoints()).isEmpty(); + assertThat(metricData.getDoubleSummaryData().getPoints()).isEmpty(); + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderTest.java new file mode 100644 index 000000000..0148fd260 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/export/IntervalMetricReaderTest.java @@ -0,0 +1,203 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.export; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class IntervalMetricReaderTest { + private static final List LONG_POINT_LIST = + Collections.singletonList(LongPointData.create(1000, 3000, Labels.empty(), 1234567)); + + private static final MetricData METRIC_DATA = + MetricData.createLongSum( + Resource.empty(), + InstrumentationLibraryInfo.create("IntervalMetricReaderTest", null), + "my metric", + "my metric description", + "us", + LongSumData.create( + /* isMonotonic= */ true, AggregationTemporality.CUMULATIVE, LONG_POINT_LIST)); + + @Mock private MetricProducer metricProducer; + + @BeforeEach + void setup() { + when(metricProducer.collectAllMetrics()).thenReturn(Collections.singletonList(METRIC_DATA)); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void startOnlyOnce() { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + + ScheduledFuture mock = mock(ScheduledFuture.class); + when(scheduler.scheduleAtFixedRate(any(), anyLong(), anyLong(), any())).thenReturn(mock); + + IntervalMetricReader intervalMetricReader = + new IntervalMetricReader( + IntervalMetricReader.InternalState.builder() + .setMetricProducers(Collections.emptyList()) + .setMetricExporter(mock(MetricExporter.class)) + .build(), + scheduler); + + intervalMetricReader.start(); + intervalMetricReader.start(); + + verify(scheduler, times(1)).scheduleAtFixedRate(any(), anyLong(), anyLong(), any()); + } + + @Test + void intervalExport() throws Exception { + WaitingMetricExporter waitingMetricExporter = new WaitingMetricExporter(); + IntervalMetricReader intervalMetricReader = + IntervalMetricReader.builder() + .setExportIntervalMillis(100) + .setMetricExporter(waitingMetricExporter) + .setMetricProducers(Collections.singletonList(metricProducer)) + .buildAndStart(); + + try { + assertThat(waitingMetricExporter.waitForNumberOfExports(1)) + .containsExactly(Collections.singletonList(METRIC_DATA)); + + assertThat(waitingMetricExporter.waitForNumberOfExports(2)) + .containsExactly( + Collections.singletonList(METRIC_DATA), Collections.singletonList(METRIC_DATA)); + } finally { + intervalMetricReader.shutdown(); + } + } + + @Test + @Timeout(2) + public void intervalExport_exporterThrowsException() throws Exception { + WaitingMetricExporter waitingMetricExporter = new WaitingMetricExporter(/* shouldThrow=*/ true); + IntervalMetricReader intervalMetricReader = + IntervalMetricReader.builder() + .setExportIntervalMillis(100) + .setMetricExporter(waitingMetricExporter) + .setMetricProducers(Collections.singletonList(metricProducer)) + .buildAndStart(); + + try { + assertThat(waitingMetricExporter.waitForNumberOfExports(2)) + .containsExactly( + Collections.singletonList(METRIC_DATA), Collections.singletonList(METRIC_DATA)); + } finally { + intervalMetricReader.shutdown(); + } + } + + @Test + void oneLastExportAfterShutdown() throws Exception { + WaitingMetricExporter waitingMetricExporter = new WaitingMetricExporter(); + IntervalMetricReader intervalMetricReader = + IntervalMetricReader.builder() + .setExportIntervalMillis(100_000) + .setMetricExporter(waitingMetricExporter) + .setMetricProducers(Collections.singletonList(metricProducer)) + .buildAndStart(); + + // Assume that this will be called in less than 100 seconds. + intervalMetricReader.shutdown(); + + // This export was called during shutdown. + assertThat(waitingMetricExporter.waitForNumberOfExports(1)) + .containsExactly(Collections.singletonList(METRIC_DATA)); + + assertThat(waitingMetricExporter.hasShutdown.get()).isTrue(); + } + + private static class WaitingMetricExporter implements MetricExporter { + + private final AtomicBoolean hasShutdown = new AtomicBoolean(false); + private final boolean shouldThrow; + private final BlockingQueue> queue = new LinkedBlockingQueue<>(); + private final List exportTimes = Collections.synchronizedList(new ArrayList<>()); + + private WaitingMetricExporter() { + this(false); + } + + private WaitingMetricExporter(boolean shouldThrow) { + this.shouldThrow = shouldThrow; + } + + @Override + public CompletableResultCode export(Collection metricList) { + exportTimes.add(System.currentTimeMillis()); + queue.offer(new ArrayList<>(metricList)); + + if (shouldThrow) { + throw new RuntimeException("Export Failed!"); + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + hasShutdown.set(true); + return CompletableResultCode.ofSuccess(); + } + + /** + * Waits until export is called for numberOfExports times. Returns the list of exported lists of + * metrics. + */ + @Nullable + List> waitForNumberOfExports(int numberOfExports) throws Exception { + List> result = new ArrayList<>(); + while (result.size() < numberOfExports) { + List export = queue.poll(5, TimeUnit.SECONDS); + assertThat(export).isNotNull(); + result.add(export); + } + return result; + } + } +} diff --git a/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricExporterTest.java b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricExporterTest.java new file mode 100644 index 000000000..dc8576232 --- /dev/null +++ b/opentelemetry-java/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/testing/InMemoryMetricExporterTest.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.metrics.testing; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.LongSumData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link InMemoryMetricExporter}. */ +class InMemoryMetricExporterTest { + + private final InMemoryMetricExporter exporter = InMemoryMetricExporter.create(); + + private static MetricData generateFakeMetric() { + long startNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + long endNs = startNs + TimeUnit.MILLISECONDS.toNanos(900); + return MetricData.createLongSum( + Resource.empty(), + InstrumentationLibraryInfo.empty(), + "name", + "description", + "1", + LongSumData.create( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create(startNs, endNs, Labels.of("k", "v"), 5)))); + } + + @Test + void test_getFinishedMetricItems() { + List metrics = new ArrayList(); + metrics.add(generateFakeMetric()); + metrics.add(generateFakeMetric()); + metrics.add(generateFakeMetric()); + + assertThat(exporter.export(metrics).isSuccess()).isTrue(); + List metricItems = exporter.getFinishedMetricItems(); + assertThat(metricItems).isNotNull(); + assertThat(metricItems.size()).isEqualTo(3); + } + + @Test + void test_reset() { + List metrics = new ArrayList(); + metrics.add(generateFakeMetric()); + metrics.add(generateFakeMetric()); + metrics.add(generateFakeMetric()); + + assertThat(exporter.export(metrics).isSuccess()).isTrue(); + List metricItems = exporter.getFinishedMetricItems(); + assertThat(metricItems).isNotNull(); + assertThat(metricItems.size()).isEqualTo(3); + exporter.reset(); + metricItems = exporter.getFinishedMetricItems(); + assertThat(metricItems).isNotNull(); + assertThat(metricItems.size()).isEqualTo(0); + } + + @Test + void test_shutdown() { + List metrics = new ArrayList(); + metrics.add(generateFakeMetric()); + metrics.add(generateFakeMetric()); + metrics.add(generateFakeMetric()); + + assertThat(exporter.export(metrics).isSuccess()).isTrue(); + exporter.shutdown(); + List metricItems = exporter.getFinishedMetricItems(); + assertThat(metricItems).isNotNull(); + assertThat(metricItems.size()).isEqualTo(0); + } + + @Test + void testShutdown_export() { + List metrics = new ArrayList(); + metrics.add(generateFakeMetric()); + metrics.add(generateFakeMetric()); + metrics.add(generateFakeMetric()); + + assertThat(exporter.export(metrics).isSuccess()).isTrue(); + exporter.shutdown(); + assertThat(exporter.export(metrics).isSuccess()).isFalse(); + } + + @Test + void test_flush() { + assertThat(exporter.flush().isSuccess()).isTrue(); + } +} diff --git a/opentelemetry-java/sdk/testing/build.gradle.kts b/opentelemetry-java/sdk/testing/build.gradle.kts new file mode 100644 index 000000000..70aa6a417 --- /dev/null +++ b/opentelemetry-java/sdk/testing/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java-library") + id("maven-publish") +} + +description = "OpenTelemetry SDK Testing utilities" +extra["moduleName"] = "io.opentelemetry.sdk.testing" + +dependencies { + api(project(":api:all")) + api(project(":sdk:all")) + + compileOnly("org.assertj:assertj-core") + compileOnly("junit:junit") + compileOnly("org.junit.jupiter:junit-jupiter-api") + + annotationProcessor("com.google.auto.value:auto-value") + + testImplementation("junit:junit") +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributesAssert.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributesAssert.java new file mode 100644 index 000000000..bb19d5100 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/AttributesAssert.java @@ -0,0 +1,164 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.assertj; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import org.assertj.core.api.AbstractAssert; + +/** Assertions for {@link Attributes}. */ +public final class AttributesAssert extends AbstractAssert { + AttributesAssert(Attributes actual) { + super(actual, AttributesAssert.class); + } + + /** Asserts the attributes have the given key and value. */ + public AttributesAssert containsEntry(AttributeKey key, T value) { + isNotNull(); + T actualValue = actual.get(key); + if (!Objects.equals(actualValue, value)) { + failWithActualExpectedAndMessage( + actualValue, + value, + "Expected attributes to have key <%s> with value <%s> but was value <%s>", + key, + value, + actualValue); + } + return this; + } + + /** Asserts the attributes have the given key and string value. */ + public AttributesAssert containsEntry(String key, String value) { + return containsEntry(AttributeKey.stringKey(key), value); + } + + /** Asserts the attributes have the given key and boolean value. */ + public AttributesAssert containsEntry(String key, boolean value) { + return containsEntry(AttributeKey.booleanKey(key), value); + } + + /** Asserts the attributes have the given key and long value. */ + public AttributesAssert containsEntry(String key, long value) { + return containsEntry(AttributeKey.longKey(key), value); + } + + /** Asserts the attributes have the given key and double value. */ + public AttributesAssert containsEntry(String key, double value) { + return containsEntry(AttributeKey.doubleKey(key), value); + } + + /** Asserts the attributes have the given key and string array value. */ + public AttributesAssert containsEntry(String key, String... value) { + return containsEntry(AttributeKey.stringArrayKey(key), Arrays.asList(value)); + } + + /** Asserts the attributes have the given key and boolean array value. */ + public AttributesAssert containsEntry(String key, Boolean... value) { + return containsEntry(AttributeKey.booleanArrayKey(key), Arrays.asList(value)); + } + + /** Asserts the attributes have the given key and long array value. */ + public AttributesAssert containsEntry(String key, Long... value) { + return containsEntry(AttributeKey.longArrayKey(key), Arrays.asList(value)); + } + + /** Asserts the attributes have the given key and double array value. */ + public AttributesAssert containsEntry(String key, Double... value) { + return containsEntry(AttributeKey.doubleArrayKey(key), Arrays.asList(value)); + } + + /** Asserts the attributes have the given key and string array value. */ + public AttributesAssert containsEntryWithStringValuesOf(String key, Iterable value) { + isNotNull(); + List actualValue = actual.get(AttributeKey.stringArrayKey(key)); + assertThat(actualValue) + .withFailMessage( + "Expected attributes to have key <%s> with value <%s> but was <%s>", + key, value, actualValue) + .containsExactlyElementsOf(value); + return this; + } + + /** Asserts the attributes have the given key and boolean array value. */ + public AttributesAssert containsEntryWithBooleanValuesOf(String key, Iterable value) { + isNotNull(); + List actualValue = actual.get(AttributeKey.booleanArrayKey(key)); + assertThat(actualValue) + .withFailMessage( + "Expected attributes to have key <%s> with value <%s> but was <%s>", + key, value, actualValue) + .containsExactlyElementsOf(value); + return this; + } + + /** Asserts the attributes have the given key and long array value. */ + public AttributesAssert containsEntryWithLongValuesOf(String key, Iterable value) { + isNotNull(); + List actualValue = actual.get(AttributeKey.longArrayKey(key)); + assertThat(actualValue) + .withFailMessage( + "Expected attributes to have key <%s> with value <%s> but was <%s>", + key, value, actualValue) + .containsExactlyElementsOf(value); + return this; + } + + /** Asserts the attributes have the given key and double array value. */ + public AttributesAssert containsEntryWithDoubleValuesOf(String key, Iterable value) { + isNotNull(); + List actualValue = actual.get(AttributeKey.doubleArrayKey(key)); + assertThat(actualValue) + .withFailMessage( + "Expected attributes to have key <%s> with value <%s> but was <%s>", + key, value, actualValue) + .containsExactlyElementsOf(value); + return this; + } + + /** Asserts the attributes have the given key with a value satisfying the given condition. */ + public AttributesAssert hasEntrySatisfying(AttributeKey key, Consumer valueCondition) { + isNotNull(); + assertThat(actual.get(key)).as("value").satisfies(valueCondition); + return this; + } + + /** Asserts the attributes only contain the given entries. */ + // NB: SafeVarArgs requires final on the method even if it's on the class. + @SafeVarargs + @SuppressWarnings("varargs") + public final AttributesAssert containsOnly(Map.Entry, ?>... entries) { + isNotNull(); + assertThat(actual.asMap()).containsOnly(entries); + return this; + } + + /** Asserts the attributes have no entries. */ + public AttributesAssert isEmpty() { + return isEqualTo(Attributes.empty()); + } + + /** Asserts the number of attributes in the collection. */ + public AttributesAssert hasSize(int numberOfEntries) { + int size = actual.size(); + if (size != numberOfEntries) { + failWithActualExpectedAndMessage( + size, + numberOfEntries, + "Expected attributes to have <%s> entries but actually has <%s>", + numberOfEntries, + size); + } + return this; + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/EventDataAssert.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/EventDataAssert.java new file mode 100644 index 000000000..d7e9d7e05 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/EventDataAssert.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.assertj; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.data.EventData; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.assertj.core.api.AbstractAssert; + +/** Assertions for {@link EventData}. */ +public final class EventDataAssert extends AbstractAssert { + EventDataAssert(EventData actual) { + super(actual, EventDataAssert.class); + } + + /** Asserts the event has the given name. */ + public EventDataAssert hasName(String name) { + isNotNull(); + if (!actual.getName().equals(name)) { + failWithActualExpectedAndMessage( + actual.getName(), + name, + "Expected event to have name <%s> but was <%s>", + name, + actual.getName()); + } + return this; + } + + /** Asserts the event has the given timestamp, in nanos. */ + public EventDataAssert hasTimestamp(long timestampNanos) { + isNotNull(); + if (actual.getEpochNanos() != timestampNanos) { + failWithActualExpectedAndMessage( + actual.getEpochNanos(), + timestampNanos, + "Expected event [%s] to have timestamp <%s> nanos but was <%s>", + actual.getName(), + timestampNanos, + actual.getEpochNanos()); + } + return this; + } + + /** Asserts the event has the given timestamp. */ + @SuppressWarnings("PreferJavaTimeOverload") + public EventDataAssert hasTimestamp(long timestamp, TimeUnit unit) { + return hasTimestamp(unit.toNanos(timestamp)); + } + + /** Asserts the event has the given timestamp, in nanos. */ + public EventDataAssert hasTimestamp(Instant timestamp) { + return hasTimestamp(TimeUnit.SECONDS.toNanos(timestamp.getEpochSecond()) + timestamp.getNano()); + } + + /** Asserts the event has the given attributes. */ + public EventDataAssert hasAttributes(Attributes attributes) { + isNotNull(); + if (!actual.getAttributes().equals(attributes)) { + failWithActualExpectedAndMessage( + actual.getAttributes(), + attributes, + "Expected event [%s] to have attributes <%s> but was <%s>", + actual.getName(), + attributes, + actual.getAttributes()); + } + return this; + } + + /** Asserts the event has attributes satisfying the given condition. */ + public EventDataAssert hasAttributesSatisfying(Consumer attributes) { + isNotNull(); + assertThat(actual.getAttributes()).as("attributes").satisfies(attributes); + return this; + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertions.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertions.java new file mode 100644 index 000000000..dea827f5b --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertions.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.assertj; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; + +/** + * Entry point for assertion methods for OpenTelemetry types. To use type-specific assertions, + * static import any {@code assertThat} method in this class instead of {@code + * Assertions.assertThat}. + */ +public final class OpenTelemetryAssertions extends Assertions { + + /** Returns an assertion for {@link Attributes}. */ + public static AttributesAssert assertThat(Attributes attributes) { + return new AttributesAssert(attributes); + } + + /** Returns an assertion for {@link SpanDataAssert}. */ + public static SpanDataAssert assertThat(SpanData spanData) { + return new SpanDataAssert(spanData); + } + + /** Returns an assertion for {@link EventDataAssert}. */ + public static EventDataAssert assertThat(EventData eventData) { + return new EventDataAssert(eventData); + } + + /** + * Returns an attribute entry with a String value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry, String> attributeEntry(String key, String value) { + return new AbstractMap.SimpleImmutableEntry<>(AttributeKey.stringKey(key), value); + } + + /** + * Returns an attribute entry with a boolean value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry, Boolean> attributeEntry( + String key, boolean value) { + return new AbstractMap.SimpleImmutableEntry<>(AttributeKey.booleanKey(key), value); + } + + /** + * Returns an attribute entry with a long value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry, Long> attributeEntry(String key, long value) { + return new AbstractMap.SimpleImmutableEntry<>(AttributeKey.longKey(key), value); + } + + /** + * Returns an attribute entry with a double value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry, Double> attributeEntry(String key, double value) { + return new AbstractMap.SimpleImmutableEntry<>(AttributeKey.doubleKey(key), value); + } + + /** + * Returns an attribute entry with a String array value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry>, List> attributeEntry( + String key, String... value) { + return new AbstractMap.SimpleImmutableEntry<>( + AttributeKey.stringArrayKey(key), Arrays.asList(value)); + } + + /** + * Returns an attribute entry with a boolean array value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry>, List> attributeEntry( + String key, boolean... value) { + return new AbstractMap.SimpleImmutableEntry<>(AttributeKey.booleanArrayKey(key), toList(value)); + } + + /** + * Returns an attribute entry with a long array value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry>, List> attributeEntry( + String key, long... value) { + return new AbstractMap.SimpleImmutableEntry<>( + AttributeKey.longArrayKey(key), Arrays.stream(value).boxed().collect(Collectors.toList())); + } + + /** + * Returns an attribute entry with a double array value for use with {@link + * AttributesAssert#containsOnly(java.util.Map.Entry[])}. + */ + public static Map.Entry>, List> attributeEntry( + String key, double... value) { + return new AbstractMap.SimpleImmutableEntry<>( + AttributeKey.doubleArrayKey(key), + Arrays.stream(value).boxed().collect(Collectors.toList())); + } + + private static List toList(boolean... values) { + Boolean[] boxed = new Boolean[values.length]; + for (int i = 0; i < values.length; i++) { + boxed[i] = values[i]; + } + return Arrays.asList(boxed); + } + + private OpenTelemetryAssertions() {} +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/SpanDataAssert.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/SpanDataAssert.java new file mode 100644 index 000000000..840df5885 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/SpanDataAssert.java @@ -0,0 +1,379 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.assertj; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.assertj.core.api.AbstractAssert; + +/** Assertions for an exported {@link SpanData}. */ +public final class SpanDataAssert extends AbstractAssert { + + SpanDataAssert(SpanData actual) { + super(actual, SpanDataAssert.class); + } + + /** Asserts the span has the given trace ID. */ + public SpanDataAssert hasTraceId(String traceId) { + isNotNull(); + if (!actual.getTraceId().equals(traceId)) { + failWithActualExpectedAndMessage( + actual.getTraceId(), + traceId, + "Expected span [%s] to have trace ID <%s> but was <%s>", + actual.getName(), + traceId, + actual.getTraceId()); + } + return this; + } + + /** Asserts the span has the given span ID. */ + public SpanDataAssert hasSpanId(String spanId) { + isNotNull(); + if (!actual.getSpanId().equals(spanId)) { + failWithActualExpectedAndMessage( + actual.getSpanId(), + spanId, + "Expected span [%s] to have span ID <%s> but was <%s>", + actual.getName(), + spanId, + actual.getSpanId()); + } + return this; + } + + /** Asserts the span is sampled. */ + public SpanDataAssert isSampled() { + isNotNull(); + if (!actual.getSpanContext().isSampled()) { + failWithMessage("Expected span [%s] to be sampled but was not.", actual.getName()); + } + return this; + } + + /** Asserts the span is not sampled. */ + public SpanDataAssert isNotSampled() { + isNotNull(); + if (actual.getSpanContext().isSampled()) { + failWithMessage("Expected span [%s] to not be sampled but it was.", actual.getName()); + } + return this; + } + + /** Asserts the span has the given {@link TraceState}. */ + public SpanDataAssert hasTraceState(TraceState traceState) { + isNotNull(); + if (!actual.getSpanContext().getTraceState().equals(traceState)) { + failWithActualExpectedAndMessage( + actual.getSpanContext().getTraceState(), + traceState, + "Expected span [%s] to have trace state <%s> but was <%s>", + actual.getName(), + traceState, + actual.getSpanContext().getTraceState()); + } + return this; + } + + /** Asserts the span has the given parent span ID. */ + public SpanDataAssert hasParentSpanId(String parentSpanId) { + isNotNull(); + String actualParentSpanId = actual.getParentSpanId(); + if (!actualParentSpanId.equals(parentSpanId)) { + failWithActualExpectedAndMessage( + actualParentSpanId, + parentSpanId, + "Expected span [%s] to have parent span ID <%s> but was <%s>", + actual.getName(), + parentSpanId, + actualParentSpanId); + } + return this; + } + + /** Asserts the span has the given {@link Resource}. */ + public SpanDataAssert hasResource(Resource resource) { + isNotNull(); + if (!actual.getResource().equals(resource)) { + failWithActualExpectedAndMessage( + actual.getResource(), + resource, + "Expected span [%s] to have resource <%s> but was <%s>", + actual.getName(), + resource, + actual.getResource()); + } + return this; + } + + /** Asserts the span has the given {@link InstrumentationLibraryInfo}. */ + public SpanDataAssert hasInstrumentationLibraryInfo( + InstrumentationLibraryInfo instrumentationLibraryInfo) { + isNotNull(); + if (!actual.getInstrumentationLibraryInfo().equals(instrumentationLibraryInfo)) { + failWithActualExpectedAndMessage( + actual.getInstrumentationLibraryInfo(), + instrumentationLibraryInfo, + "Expected span [%s] to have instrumentation library info <%s> but was <%s>", + actual.getName(), + instrumentationLibraryInfo, + actual.getInstrumentationLibraryInfo()); + } + return this; + } + + /** Asserts the span has the given name. */ + public SpanDataAssert hasName(String name) { + isNotNull(); + if (!actual.getName().equals(name)) { + failWithActualExpectedAndMessage( + actual.getName(), + name, + "Expected span to have name <%s> but was <%s>", + name, + actual.getName()); + } + return this; + } + + /** Asserts the span has the given kind. */ + public SpanDataAssert hasKind(SpanKind kind) { + isNotNull(); + if (!actual.getKind().equals(kind)) { + failWithActualExpectedAndMessage( + actual.getKind(), + kind, + "Expected span [%s] to have kind <%s> but was <%s>", + actual.getName(), + kind, + actual.getKind()); + } + return this; + } + + /** Asserts the span starts at the given epoch timestamp, in nanos. */ + public SpanDataAssert startsAt(long startEpochNanos) { + isNotNull(); + if (actual.getStartEpochNanos() != startEpochNanos) { + failWithActualExpectedAndMessage( + actual.getStartEpochNanos(), + startEpochNanos, + "Expected span [%s] to have start epoch <%s> nanos but was <%s>", + actual.getName(), + startEpochNanos, + actual.getStartEpochNanos()); + } + return this; + } + + /** Asserts the span starts at the given epoch timestamp. */ + @SuppressWarnings("PreferJavaTimeOverload") + public SpanDataAssert startsAt(long startEpoch, TimeUnit unit) { + return startsAt(unit.toNanos(startEpoch)); + } + + /** Asserts the span starts at the given epoch timestamp. */ + public SpanDataAssert startsAt(Instant timestamp) { + return startsAt(toNanos(timestamp)); + } + + /** Asserts the span has the given attributes. */ + public SpanDataAssert hasAttributes(Attributes attributes) { + isNotNull(); + if (!attributesAreEqual(attributes)) { + failWithActualExpectedAndMessage( + actual.getAttributes(), + attributes, + "Expected span [%s] to have attributes <%s> but was <%s>", + actual.getName(), + attributes, + actual.getAttributes()); + } + return this; + } + + private boolean attributesAreEqual(Attributes attributes) { + // compare as maps, since implementations do not have equals that work correctly across + // implementations. + return actual.getAttributes().asMap().equals(attributes.asMap()); + } + + /** Asserts the span has attributes satisfying the given condition. */ + public SpanDataAssert hasAttributesSatisfying(Consumer attributes) { + isNotNull(); + assertThat(actual.getAttributes()).as("attributes").satisfies(attributes); + return this; + } + + /** Asserts the span has the given events. */ + public SpanDataAssert hasEvents(Iterable events) { + isNotNull(); + assertThat(actual.getEvents()) + .withFailMessage( + "Expected span [%s] to have events <%s> but was <%s>", + actual.getName(), events, actual.getEvents()) + .containsExactlyInAnyOrderElementsOf(events); + return this; + } + + /** Asserts the span has the given events. */ + public SpanDataAssert hasEvents(EventData... events) { + return hasEvents(Arrays.asList(events)); + } + + /** Asserts the span has events satisfying the given condition. */ + public SpanDataAssert hasEventsSatisfying(Consumer> condition) { + isNotNull(); + assertThat(actual.getEvents()).satisfies(condition); + return this; + } + + /** Asserts the span has the given links. */ + public SpanDataAssert hasLinks(Iterable links) { + isNotNull(); + assertThat(actual.getLinks()) + .withFailMessage( + "Expected span [%s] to have links <%s> but was <%s>", + actual.getName(), links, actual.getLinks()) + .containsExactlyInAnyOrderElementsOf(links); + return this; + } + + /** Asserts the span has the given links. */ + public SpanDataAssert hasLinks(LinkData... links) { + return hasLinks(Arrays.asList(links)); + } + + /** Asserts the span has events satisfying the given condition. */ + public SpanDataAssert hasLinksSatisfying(Consumer> condition) { + isNotNull(); + assertThat(actual.getLinks()).satisfies(condition); + return this; + } + + /** Asserts the span has the given {@link StatusData}. */ + public SpanDataAssert hasStatus(StatusData status) { + isNotNull(); + if (!actual.getStatus().equals(status)) { + failWithActualExpectedAndMessage( + actual.getStatus(), + status, + "Expected span [%s] to have status <%s> but was <%s>", + actual.getName(), + status, + actual.getStatus()); + } + return this; + } + + /** Asserts the span ends at the given epoch timestamp, in nanos. */ + public SpanDataAssert endsAt(long endEpochNanos) { + isNotNull(); + if (actual.getEndEpochNanos() != endEpochNanos) { + failWithActualExpectedAndMessage( + actual.getEndEpochNanos(), + endEpochNanos, + "Expected span [%s] to have end epoch <%s> nanos but was <%s>", + actual.getName(), + endEpochNanos, + actual.getEndEpochNanos()); + } + return this; + } + + /** Asserts the span ends at the given epoch timestamp. */ + @SuppressWarnings("PreferJavaTimeOverload") + public SpanDataAssert endsAt(long startEpoch, TimeUnit unit) { + return endsAt(unit.toNanos(startEpoch)); + } + + /** Asserts the span ends at the given epoch timestamp. */ + public SpanDataAssert endsAt(Instant timestamp) { + return endsAt(toNanos(timestamp)); + } + + /** Asserts the span has ended. */ + public SpanDataAssert hasEnded() { + isNotNull(); + if (!actual.hasEnded()) { + failWithMessage("Expected span [%s] to have ended but did not", actual.getName()); + } + return this; + } + + /** Asserts the span has not ended. */ + public SpanDataAssert hasNotEnded() { + isNotNull(); + if (actual.hasEnded()) { + failWithMessage("Expected span [%s] to have not ended but did has", actual.getName()); + } + return this; + } + + /** Asserts the span has the given total recorded events. */ + public SpanDataAssert hasTotalRecordedEvents(int totalRecordedEvents) { + isNotNull(); + if (actual.getTotalRecordedEvents() != totalRecordedEvents) { + failWithActualExpectedAndMessage( + actual.getTotalRecordedEvents(), + totalRecordedEvents, + "Expected span [%s] to have recorded <%s> total events but did not", + actual.getName(), + totalRecordedEvents, + actual.getTotalRecordedEvents()); + } + return this; + } + + /** Asserts the span has the given total recorded links. */ + public SpanDataAssert hasTotalRecordedLinks(int totalRecordedLinks) { + isNotNull(); + if (actual.getTotalRecordedLinks() != totalRecordedLinks) { + failWithActualExpectedAndMessage( + actual.getTotalRecordedLinks(), + totalRecordedLinks, + "Expected span [%s] to have recorded <%s> total links but did not", + actual.getName(), + totalRecordedLinks, + actual.getTotalRecordedLinks()); + } + return this; + } + + /** Asserts the span has the given total attributes. */ + public SpanDataAssert hasTotalAttributeCount(int totalAttributeCount) { + isNotNull(); + if (actual.getTotalAttributeCount() != totalAttributeCount) { + failWithActualExpectedAndMessage( + actual.getTotalAttributeCount(), + totalAttributeCount, + "Expected span [%s] to have recorded <%s> total attributes but did not", + actual.getName(), + totalAttributeCount, + actual.getTotalAttributeCount()); + } + return this; + } + + private static long toNanos(Instant timestamp) { + return TimeUnit.SECONDS.toNanos(timestamp.getEpochSecond()) + timestamp.getNano(); + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/TraceAssert.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/TraceAssert.java new file mode 100644 index 000000000..41e62cf81 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/TraceAssert.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.assertj; + +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.assertj.core.api.AbstractIterableAssert; + +/** Assertions for an exported trace, a list of {@link SpanData} with the same trace ID. */ +public final class TraceAssert + extends AbstractIterableAssert, SpanData, SpanDataAssert> { + + TraceAssert(List spanData) { + super(spanData, TraceAssert.class); + } + + /** Asserts that the trace has the given trace ID. */ + public TraceAssert hasTraceId(String traceId) { + isNotNull(); + isNotEmpty(); + + String actualTraceId = actual.get(0).getTraceId(); + if (!actualTraceId.equals(traceId)) { + failWithActualExpectedAndMessage( + actualTraceId, + traceId, + "Expected trace to have trace ID <%s> but was <%s>", + traceId, + actualTraceId); + } + return this; + } + + /** + * Asserts that the trace under assertion has the same number of spans as provided {@code + * assertions} and executes each {@link SpanDataAssert} in {@code assertions} in order with the + * corresponding span. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final TraceAssert hasSpansSatisfyingExactly(Consumer... assertions) { + hasSize(assertions.length); + zipSatisfy( + Arrays.asList(assertions), (span, assertion) -> assertion.accept(new SpanDataAssert(span))); + return this; + } + + @Override + protected SpanDataAssert toAssert(SpanData value, String description) { + return new SpanDataAssert(value).as(description); + } + + @Override + protected TraceAssert newAbstractIterableAssert(Iterable iterable) { + return new TraceAssert( + StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList())); + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/TracesAssert.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/TracesAssert.java new file mode 100644 index 000000000..2fbfa8c40 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/TracesAssert.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.assertj; + +import static java.util.stream.Collectors.toList; + +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.StreamSupport; +import org.assertj.core.api.AbstractIterableAssert; + +/** Assertions for a list of exported traces. */ +public final class TracesAssert + extends AbstractIterableAssert< + TracesAssert, List>, List, TraceAssert> { + + /** + * Returns an assertion for a list of traces. The traces must already be grouped into {@code + * List} where each list has spans with the same trace ID. + */ + public static TracesAssert assertThat(Collection> traces) { + for (List trace : traces) { + if (trace.stream().map(SpanData::getTraceId).distinct().count() != 1) { + throw new IllegalArgumentException( + "trace does not have consistent trace IDs, group spans into traces before calling " + + "this function: " + + trace); + } + } + return new TracesAssert(new ArrayList<>(traces)); + } + + TracesAssert(List> lists) { + super(lists, TracesAssert.class); + } + + /** + * Asserts that the traces under assertion have the same number of traces as provided {@code + * assertions} and executes each {@link TracesAssert} in {@code assertions} in order with the + * corresponding trace. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final TracesAssert hasTracesSatisfyingExactly(Consumer... assertions) { + hasSize(assertions.length); + zipSatisfy( + Arrays.asList(assertions), (trace, assertion) -> assertion.accept(new TraceAssert(trace))); + return this; + } + + @Override + protected TraceAssert toAssert(List value, String description) { + return new TraceAssert(value).as(description); + } + + @Override + protected TracesAssert newAbstractIterableAssert(Iterable> iterable) { + return new TracesAssert(StreamSupport.stream(iterable.spliterator(), false).collect(toList())); + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/package-info.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/package-info.java new file mode 100644 index 000000000..c2388a7b6 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.testing.assertj; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/context/SettableContextStorageProvider.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/context/SettableContextStorageProvider.java new file mode 100644 index 000000000..807d5e63f --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/context/SettableContextStorageProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.context; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.ContextStorageProvider; +import io.opentelemetry.context.Scope; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +/** A {@link ContextStorageProvider} which can have it's {@link ContextStorage} set at any time. */ +public final class SettableContextStorageProvider implements ContextStorageProvider { + @Override + public ContextStorage get() { + return SettableContextStorage.INSTANCE; + } + + /** Sets the {@link ContextStorage} to use for future context operations. */ + public static void setContextStorage(ContextStorage storage) { + SettableContextStorage.delegate = storage; + } + + /** Returns the current {@link ContextStorage}. */ + public static ContextStorage getContextStorage() { + return SettableContextStorage.delegate; + } + + private enum SettableContextStorage implements ContextStorage { + INSTANCE; + + private static volatile ContextStorage delegate = createStorage(); + + @Override + public Scope attach(Context toAttach) { + return delegate.attach(toAttach); + } + + @Override + public Context current() { + return delegate.current(); + } + + // We reimplement provider lookup, ignoring the settable provider. It's clunky but allows + // reconfiguring only in test, not in production. + private static ContextStorage createStorage() { + List providers = new ArrayList<>(); + for (ContextStorageProvider provider : ServiceLoader.load(ContextStorageProvider.class)) { + if (provider.getClass().equals(SettableContextStorageProvider.class)) { + continue; + } + providers.add(provider); + } + + if (providers.isEmpty()) { + return ContextStorage.defaultStorage(); + } + + String providerClassName = + System.getProperty("io.opentelemetry.context.contextStorageProvider", ""); + if (providerClassName.isEmpty()) { + if (providers.size() == 1) { + return providers.get(0).get(); + } + return ContextStorage.defaultStorage(); + } + + for (ContextStorageProvider provider : providers) { + if (provider.getClass().getName().equals(providerClassName)) { + return provider.get(); + } + } + return ContextStorage.defaultStorage(); + } + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/context/package-info.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/context/package-info.java new file mode 100644 index 000000000..20c4f1342 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/context/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.testing.context; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/exporter/InMemorySpanExporter.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/exporter/InMemorySpanExporter.java new file mode 100644 index 000000000..966aeed53 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/exporter/InMemorySpanExporter.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.exporter; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * A {@link SpanExporter} implementation that can be used to test OpenTelemetry integration. + * + *

    Example usage: + * + *

    {@code
    + * // class MyClassTest {
    + * //   private final Tracer tracer = new TracerSdk();
    + * //   private final InMemorySpanExporter testExporter = InMemorySpanExporter.create();
    + * //
    + * //   @Before
    + * //   public void setup() {
    + * //     tracer.addSpanProcessor(SimpleSampledSpansProcessor.builder(testExporter).build());
    + * //   }
    + * //
    + * //   @Test
    + * //   public void getFinishedSpanData() {
    + * //     tracer.spanBuilder("span").startSpan().end();
    + * //
    + * //     List spanItems = exporter.getFinishedSpanItems();
    + * //     assertThat(spanItems).isNotNull();
    + * //     assertThat(spanItems.size()).isEqualTo(1);
    + * //     assertThat(spanItems.get(0).getName()).isEqualTo("span");
    + * //   }
    + * // }
    + * }
    + */ +public final class InMemorySpanExporter implements SpanExporter { + private final List finishedSpanItems = new ArrayList<>(); + private boolean isStopped = false; + + /** + * Returns a new instance of the {@code InMemorySpanExporter}. + * + * @return a new instance of the {@code InMemorySpanExporter}. + */ + public static InMemorySpanExporter create() { + return new InMemorySpanExporter(); + } + + /** + * Returns a {@code List} of the finished {@code Span}s, represented by {@code SpanData}. + * + * @return a {@code List} of the finished {@code Span}s. + */ + public List getFinishedSpanItems() { + synchronized (this) { + return Collections.unmodifiableList(new ArrayList<>(finishedSpanItems)); + } + } + + /** + * Clears the internal {@code List} of finished {@code Span}s. + * + *

    Does not reset the state of this exporter if already shutdown. + */ + public void reset() { + synchronized (this) { + finishedSpanItems.clear(); + } + } + + @Override + public CompletableResultCode export(Collection spans) { + synchronized (this) { + if (isStopped) { + return CompletableResultCode.ofFailure(); + } + finishedSpanItems.addAll(spans); + } + return CompletableResultCode.ofSuccess(); + } + + /** + * The InMemory exporter does not batch spans, so this method will immediately return with + * success. + * + * @return always Success + */ + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + /** + * Clears the internal {@code List} of finished {@code SpanData}s. + * + *

    Any subsequent call to export() function on this SpanExporter, will return {@code + * CompletableResultCode.ofFailure()} + */ + @Override + public CompletableResultCode shutdown() { + synchronized (this) { + finishedSpanItems.clear(); + isStopped = true; + } + return CompletableResultCode.ofSuccess(); + } + + private InMemorySpanExporter() {} +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/exporter/package-info.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/exporter/package-info.java new file mode 100644 index 000000000..6481ee4c3 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/exporter/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.testing.exporter; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit4/OpenTelemetryRule.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit4/OpenTelemetryRule.java new file mode 100644 index 000000000..03a2cba01 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit4/OpenTelemetryRule.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.junit4; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.List; +import org.junit.rules.ExternalResource; + +/** + * A JUnit4 rule which sets up the {@link OpenTelemetrySdk} for testing, resetting state between + * tests. This rule cannot be used with {@link org.junit.ClassRule}. + * + *

    {@code
    + * // public class CoolTest {
    + * //   @Rule
    + * //   public OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
    + * //
    + * //   private Tracer tracer;
    + * //
    + * //   @Before
    + * //   public void setUp() {
    + * //       tracer = otelTesting.getOpenTelemetry().getTracer("test");
    + * //   }
    + * //
    + * //   @Test
    + * //   public void test() {
    + * //     tracer.spanBuilder("name").startSpan().end();
    + * //     assertThat(otelTesting.getSpans()).containsExactly(expected);
    + * //   }
    + * //  }
    + * }
    + */ +public final class OpenTelemetryRule extends ExternalResource { + + /** + * Returns a {@link OpenTelemetryRule} with a default SDK initialized with an in-memory span + * exporter and W3C trace context propagation. + */ + public static OpenTelemetryRule create() { + InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .setTracerProvider(tracerProvider) + .build(); + + return new OpenTelemetryRule(openTelemetry, spanExporter); + } + + private final OpenTelemetrySdk openTelemetry; + private final InMemorySpanExporter spanExporter; + + private OpenTelemetryRule(OpenTelemetrySdk openTelemetry, InMemorySpanExporter spanExporter) { + this.openTelemetry = openTelemetry; + this.spanExporter = spanExporter; + } + + /** Returns the {@link OpenTelemetrySdk} created by this extension. */ + public OpenTelemetry getOpenTelemetry() { + return openTelemetry; + } + + /** Returns all the exported {@link SpanData} so far. */ + public List getSpans() { + return spanExporter.getFinishedSpanItems(); + } + + /** + * Clears the collected exported {@link SpanData}. Consider making your test smaller instead of + * manually clearing state using this method. + */ + public void clearSpans() { + spanExporter.reset(); + } + + @Override + protected void before() { + GlobalOpenTelemetry.resetForTest(); + GlobalOpenTelemetry.set(openTelemetry); + clearSpans(); + } + + @Override + protected void after() { + GlobalOpenTelemetry.resetForTest(); + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit4/package-info.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit4/package-info.java new file mode 100644 index 000000000..9540f063c --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit4/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.testing.junit4; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtension.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtension.java new file mode 100644 index 000000000..55110c47d --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtension.java @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.junit5; + +import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.assertj.TracesAssert; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * A JUnit5 extension which sets up the {@link OpenTelemetrySdk} for testing, resetting state + * between tests. + * + *
    {@code
    + * // class CoolTest {
    + * //   @RegisterExtension
    + * //   static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
    + * //
    + * //   private final Tracer tracer = otelTesting.getOpenTelemetry().getTracer("test");
    + * //
    + * //   @Test
    + * //   void test() {
    + * //     tracer.spanBuilder("name").startSpan().end();
    + * //     assertThat(otelTesting.getSpans()).containsExactly(expected);
    + * //   }
    + * //  }
    + * }
    + */ +public final class OpenTelemetryExtension + implements BeforeEachCallback, BeforeAllCallback, AfterAllCallback { + + /** + * Returns a {@link OpenTelemetryExtension} with a default SDK initialized with an in-memory span + * exporter and W3C trace context propagation. + */ + public static OpenTelemetryExtension create() { + InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .setTracerProvider(tracerProvider) + .build(); + + return new OpenTelemetryExtension(openTelemetry, spanExporter); + } + + private final OpenTelemetrySdk openTelemetry; + private final InMemorySpanExporter spanExporter; + + private OpenTelemetryExtension( + OpenTelemetrySdk openTelemetry, InMemorySpanExporter spanExporter) { + this.openTelemetry = openTelemetry; + this.spanExporter = spanExporter; + } + + /** Returns the {@link OpenTelemetrySdk} created by this extension. */ + public OpenTelemetry getOpenTelemetry() { + return openTelemetry; + } + + /** Returns all the exported {@link SpanData} so far. */ + public List getSpans() { + return spanExporter.getFinishedSpanItems(); + } + + /** + * Returns a {@link TracesAssert} for asserting on the currently exported traces. This method + * requires AssertJ to be on the classpath. + */ + public TracesAssert assertTraces() { + Map> traces = + getSpans().stream() + .collect( + Collectors.groupingBy( + SpanData::getTraceId, LinkedHashMap::new, Collectors.toList())); + for (List trace : traces.values()) { + trace.sort(Comparator.comparing(SpanData::getStartEpochNanos)); + } + return assertThat(traces.values()); + } + + /** + * Clears the collected exported {@link SpanData}. Consider making your test smaller instead of + * manually clearing state using this method. + */ + public void clearSpans() { + spanExporter.reset(); + } + + @Override + public void beforeEach(ExtensionContext context) { + clearSpans(); + } + + @Override + public void beforeAll(ExtensionContext context) { + GlobalOpenTelemetry.resetForTest(); + GlobalOpenTelemetry.set(openTelemetry); + } + + @Override + public void afterAll(ExtensionContext context) { + GlobalOpenTelemetry.resetForTest(); + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit5/package-info.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit5/package-info.java new file mode 100644 index 000000000..04ae5caeb --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/junit5/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.testing.junit5; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/trace/TestSpanData.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/trace/TestSpanData.java new file mode 100644 index 000000000..0cbf5af6d --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/trace/TestSpanData.java @@ -0,0 +1,217 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.trace; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * Immutable representation of all data collected by the {@link io.opentelemetry.api.trace.Span} + * class. + */ +@Immutable +@AutoValue +public abstract class TestSpanData implements SpanData { + + /** + * Creates a new Builder for creating an SpanData instance. + * + * @return a new Builder. + */ + public static Builder builder() { + return new AutoValue_TestSpanData.Builder() + .setSpanContext(SpanContext.getInvalid()) + .setParentSpanContext(SpanContext.getInvalid()) + .setInstrumentationLibraryInfo(InstrumentationLibraryInfo.empty()) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setAttributes(Attributes.empty()) + .setEvents(Collections.emptyList()) + .setTotalRecordedEvents(0) + .setResource(Resource.empty()) + .setTotalAttributeCount(0); + } + + TestSpanData() {} + + abstract boolean getInternalHasEnded(); + + @Override + public final boolean hasEnded() { + return getInternalHasEnded(); + } + + /** A {@code Builder} class for {@link TestSpanData}. */ + @AutoValue.Builder + public abstract static class Builder { + + abstract TestSpanData autoBuild(); + + abstract List getEvents(); + + abstract List getLinks(); + + /** + * Create a new SpanData instance from the data in this. + * + * @return a new SpanData instance + */ + public TestSpanData build() { + // make unmodifiable copies of any collections + setEvents(Collections.unmodifiableList(new ArrayList<>(getEvents()))); + setLinks(Collections.unmodifiableList(new ArrayList<>(getLinks()))); + return autoBuild(); + } + + /** + * Set the {@code SpanContext} on this builder. + * + * @param spanContext the {@code SpanContext}. + * @return this builder (for chaining). + */ + public abstract Builder setSpanContext(SpanContext spanContext); + + /** + * The parent span context associated for this span, which may be null. + * + * @param parentSpanContext the SpanId of the parent + * @return this. + */ + public abstract Builder setParentSpanContext(SpanContext parentSpanContext); + + /** + * Set the {@link Resource} associated with this span. Must not be null. + * + * @param resource the Resource that generated this span. + * @return this + */ + public abstract Builder setResource(Resource resource); + + /** + * Sets the instrumentation library of the tracer which created this span. Must not be null. + * + * @param instrumentationLibraryInfo the instrumentation library of the tracer which created + * this span. + * @return this + */ + public abstract Builder setInstrumentationLibraryInfo( + InstrumentationLibraryInfo instrumentationLibraryInfo); + + /** + * Set the name of the span. Must not be null. + * + * @param name the name. + * @return this + */ + public abstract Builder setName(String name); + + /** + * Set the start timestamp of the span. + * + * @param epochNanos the start epoch timestamp in nanos. + * @return this + */ + public abstract Builder setStartEpochNanos(long epochNanos); + + /** + * Set the end timestamp of the span. + * + * @param epochNanos the end epoch timestamp in nanos. + * @return this + */ + public abstract Builder setEndEpochNanos(long epochNanos); + + /** + * Set the attributes that are associated with this span, in the form of {@link Attributes}. + * + * @param attributes {@link Attributes} for this span. + * @return this + * @see Attributes + */ + public abstract Builder setAttributes(Attributes attributes); + + /** + * Set timed events that are associated with this span. Must not be null, may be empty. + * + * @param events A List<Event> of events associated with this span. + * @return this + * @see EventData + */ + public abstract Builder setEvents(List events); + + /** + * Set the status for this span. Must not be null. + * + * @param status The Status of this span. + * @return this + */ + public abstract Builder setStatus(StatusData status); + + /** + * Set the kind of span. Must not be null. + * + * @param kind The Kind of span. + * @return this + */ + public abstract Builder setKind(SpanKind kind); + + /** + * Set the links associated with this span. Must not be null, may be empty. + * + * @param links A List<Link> + * @return this + */ + public abstract Builder setLinks(List links); + + abstract Builder setInternalHasEnded(boolean hasEnded); + + /** + * Sets to true if the span has been ended. + * + * @param hasEnded A boolean indicating if the span has been ended. + * @return this + */ + public final Builder setHasEnded(boolean hasEnded) { + return setInternalHasEnded(hasEnded); + } + + /** + * Set the total number of events recorded on this span. + * + * @param totalRecordedEvents The total number of events recorded. + * @return this + */ + public abstract Builder setTotalRecordedEvents(int totalRecordedEvents); + + /** + * Set the total number of links recorded on this span. + * + * @param totalRecordedLinks The total number of links recorded. + * @return this + */ + public abstract Builder setTotalRecordedLinks(int totalRecordedLinks); + + /** + * Set the total number of attributes recorded on this span. + * + * @param totalAttributeCount The total number of attributes recorded. + * @return this + */ + public abstract Builder setTotalAttributeCount(int totalAttributeCount); + } +} diff --git a/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/trace/package-info.java b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/trace/package-info.java new file mode 100644 index 000000000..d250803c4 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/trace/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.testing.trace; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/testing/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider b/opentelemetry-java/sdk/testing/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider new file mode 100644 index 000000000..a31f46f93 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider @@ -0,0 +1 @@ +io.opentelemetry.sdk.testing.context.SettableContextStorageProvider diff --git a/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertionsTest.java b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertionsTest.java new file mode 100644 index 000000000..33044fd2f --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/OpenTelemetryAssertionsTest.java @@ -0,0 +1,314 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.assertj; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("PreferJavaTimeOverload") +class OpenTelemetryAssertionsTest { + private static final String TRACE_ID = "00000000000000010000000000000002"; + private static final String SPAN_ID1 = "0000000000000003"; + private static final String SPAN_ID2 = "0000000000000004"; + private static final TraceState TRACE_STATE = TraceState.builder().put("cat", "meow").build(); + private static final Resource RESOURCE = + Resource.create(Attributes.builder().put("dog", "bark").build()); + private static final InstrumentationLibraryInfo INSTRUMENTATION_LIBRARY_INFO = + InstrumentationLibraryInfo.create("opentelemetry", "1.0"); + private static final Attributes ATTRIBUTES = + Attributes.builder() + .put("bear", "mya") + .put("warm", true) + .put("temperature", 30) + .put("length", 1.2) + .put("colors", "red", "blue") + .put("conditions", false, true) + .put("scores", 0L, 1L) + .put("coins", 0.01, 0.05, 0.1) + .build(); + private static final List EVENTS = + Arrays.asList( + EventData.create(10, "event", Attributes.empty()), + EventData.create( + 20, "event2", Attributes.builder().put("cookie monster", "yum").build())); + private static final List LINKS = + Arrays.asList( + LinkData.create( + SpanContext.create( + TRACE_ID, SPAN_ID1, TraceFlags.getDefault(), TraceState.getDefault())), + LinkData.create( + SpanContext.create(TRACE_ID, SPAN_ID2, TraceFlags.getSampled(), TRACE_STATE), + Attributes.empty(), + 100)); + + private static final TestSpanData SPAN1; + private static final TestSpanData SPAN2; + + static { + TestSpanData.Builder spanDataBuilder = + TestSpanData.builder() + .setParentSpanContext( + SpanContext.create( + TRACE_ID, SPAN_ID2, TraceFlags.getDefault(), TraceState.getDefault())) + .setResource(RESOURCE) + .setInstrumentationLibraryInfo(INSTRUMENTATION_LIBRARY_INFO) + .setName("span") + .setKind(SpanKind.CLIENT) + .setStartEpochNanos(100) + .setAttributes(ATTRIBUTES) + .setEvents(EVENTS) + .setLinks(LINKS) + .setStatus(StatusData.ok()) + .setEndEpochNanos(200) + .setHasEnded(true) + .setTotalRecordedEvents(300) + .setTotalRecordedLinks(400) + .setTotalAttributeCount(500); + + SPAN1 = + spanDataBuilder + .setSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID1, TraceFlags.getSampled(), TRACE_STATE)) + .build(); + + SPAN2 = + spanDataBuilder + .setSpanContext( + SpanContext.create(TRACE_ID, SPAN_ID1, TraceFlags.getDefault(), TRACE_STATE)) + .setHasEnded(false) + .build(); + } + + @Test + void passing() { + assertThat(SPAN1) + .hasTraceId(TRACE_ID) + .hasSpanId(SPAN_ID1) + .isSampled() + .hasTraceState(TRACE_STATE) + .hasParentSpanId(SPAN_ID2) + .hasResource(RESOURCE) + .hasInstrumentationLibraryInfo(INSTRUMENTATION_LIBRARY_INFO) + .hasName("span") + .hasKind(SpanKind.CLIENT) + .startsAt(100) + .startsAt(100, TimeUnit.NANOSECONDS) + .startsAt(Instant.ofEpochSecond(0, 100)) + .hasAttributes(ATTRIBUTES) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .hasSize(8) + .containsEntry(AttributeKey.stringKey("bear"), "mya") + .hasEntrySatisfying( + AttributeKey.stringKey("bear"), value -> assertThat(value).hasSize(3)) + .containsEntry("bear", "mya") + .containsEntry("warm", true) + .containsEntry("temperature", 30) + .containsEntry("length", 1.2) + .containsEntry("colors", "red", "blue") + .containsEntryWithStringValuesOf("colors", Arrays.asList("red", "blue")) + .containsEntry("conditions", false, true) + .containsEntryWithBooleanValuesOf("conditions", Arrays.asList(false, true)) + .containsEntry("scores", 0L, 1L) + .containsEntryWithLongValuesOf("scores", Arrays.asList(0L, 1L)) + .containsEntry("coins", 0.01, 0.05, 0.1) + .containsEntryWithDoubleValuesOf("coins", Arrays.asList(0.01, 0.05, 0.1)) + .containsOnly( + attributeEntry("bear", "mya"), + attributeEntry("warm", true), + attributeEntry("temperature", 30), + attributeEntry("length", 1.2), + attributeEntry("colors", "red", "blue"), + attributeEntry("conditions", false, true), + attributeEntry("scores", 0L, 1L), + attributeEntry("coins", 0.01, 0.05, 0.1))) + .hasEvents(EVENTS) + .hasEvents(EVENTS.toArray(new EventData[0])) + .hasEventsSatisfying( + events -> { + assertThat(events).hasSize(EVENTS.size()); + assertThat(events.get(0)) + .hasName("event") + .hasTimestamp(10) + .hasTimestamp(10, TimeUnit.NANOSECONDS) + .hasTimestamp(Instant.ofEpochSecond(0, 10)) + .hasAttributes(Attributes.empty()) + .hasAttributesSatisfying( + attributes -> assertThat(attributes).isEqualTo(Attributes.empty())) + .hasAttributesSatisfying(attributes -> assertThat(attributes).isEmpty()); + }) + .hasLinks(LINKS) + .hasLinks(LINKS.toArray(new LinkData[0])) + .hasLinksSatisfying(links -> assertThat(links).hasSize(LINKS.size())) + .hasStatus(StatusData.ok()) + .endsAt(200) + .endsAt(200, TimeUnit.NANOSECONDS) + .endsAt(Instant.ofEpochSecond(0, 200)) + .hasEnded() + .hasTotalRecordedEvents(300) + .hasTotalRecordedLinks(400) + .hasTotalAttributeCount(500); + + assertThat(RESOURCE.getAttributes()).containsOnly(entry(AttributeKey.stringKey("dog"), "bark")); + } + + @Test + void failure() { + assertThatThrownBy(() -> assertThat(SPAN1).hasTraceId("foo")) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasSpanId("foo")).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).isNotSampled()).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasTraceState(TraceState.getDefault())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasParentSpanId("foo")) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasResource(Resource.empty())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1).hasInstrumentationLibraryInfo(InstrumentationLibraryInfo.empty())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasName("foo")).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasKind(SpanKind.SERVER)) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).startsAt(10)).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).startsAt(10, TimeUnit.NANOSECONDS)) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).startsAt(Instant.EPOCH)) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasAttributes(Attributes.empty())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasAttributesSatisfying( + attributes -> assertThat(attributes).containsEntry("cat", "bark"))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasAttributesSatisfying(attributes -> assertThat(attributes).isEmpty())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasAttributesSatisfying(attributes -> assertThat(attributes).hasSize(33))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .hasEntrySatisfying( + AttributeKey.stringKey("bear"), + value -> assertThat(value).hasSize(2)))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasEvents()).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasEvents(Collections.emptyList())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> assertThat(SPAN1).hasEventsSatisfying(events -> assertThat(events).isEmpty())); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasEventsSatisfying(events -> assertThat(events.get(0)).hasName("notevent"))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasEventsSatisfying(events -> assertThat(events.get(0)).hasTimestamp(1))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasEventsSatisfying( + events -> assertThat(events.get(0)).hasTimestamp(1, TimeUnit.NANOSECONDS))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasEventsSatisfying( + events -> + assertThat(events.get(0)).hasTimestamp(Instant.ofEpochSecond(0, 1)))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasEventsSatisfying( + events -> + assertThat(events.get(0)).hasAttributes(RESOURCE.getAttributes()))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SPAN1) + .hasEventsSatisfying( + events -> + assertThat(events.get(0)) + .hasAttributesSatisfying( + attributes -> + assertThat(attributes).containsEntry("dogs", "meow")))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasLinks()).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasLinks(Collections.emptyList())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> assertThat(SPAN1).hasLinksSatisfying(links -> assertThat(links).isEmpty())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasStatus(StatusData.error())) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).endsAt(10)).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).endsAt(10, TimeUnit.NANOSECONDS)) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).endsAt(Instant.EPOCH)) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasNotEnded()).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasTotalRecordedEvents(1)) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasTotalRecordedLinks(1)) + .isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN1).hasTotalAttributeCount(1)) + .isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> assertThat(SPAN2).isSampled()).isInstanceOf(AssertionError.class); + assertThatThrownBy(() -> assertThat(SPAN2).hasEnded()).isInstanceOf(AssertionError.class); + + assertThatThrownBy( + () -> + assertThat(RESOURCE.getAttributes()) + .containsOnly( + entry(AttributeKey.stringKey("dog"), "bark"), + entry(AttributeKey.stringKey("cat"), "meow"))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(RESOURCE.getAttributes()) + .containsOnly(entry(AttributeKey.stringKey("cat"), "meow"))) + .isInstanceOf(AssertionError.class); + } +} diff --git a/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/exporter/InMemorySpanExporterTest.java b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/exporter/InMemorySpanExporterTest.java new file mode 100644 index 000000000..a9e340f4b --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/exporter/InMemorySpanExporterTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.exporter; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link InMemorySpanExporter}. */ +class InMemorySpanExporterTest { + private final InMemorySpanExporter exporter = InMemorySpanExporter.create(); + + private SdkTracerProvider tracerProvider; + private Tracer tracer; + + @BeforeEach + void setup() { + tracerProvider = + SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + tracer = tracerProvider.get("InMemorySpanExporterTest"); + } + + @AfterEach + void tearDown() { + tracerProvider.shutdown(); + } + + @Test + void getFinishedSpanItems() { + tracer.spanBuilder("one").startSpan().end(); + tracer.spanBuilder("two").startSpan().end(); + tracer.spanBuilder("three").startSpan().end(); + + List spanItems = exporter.getFinishedSpanItems(); + assertThat(spanItems).isNotNull(); + assertThat(spanItems.size()).isEqualTo(3); + assertThat(spanItems.get(0).getName()).isEqualTo("one"); + assertThat(spanItems.get(1).getName()).isEqualTo("two"); + assertThat(spanItems.get(2).getName()).isEqualTo("three"); + } + + @Test + void reset() { + tracer.spanBuilder("one").startSpan().end(); + tracer.spanBuilder("two").startSpan().end(); + tracer.spanBuilder("three").startSpan().end(); + List spanItems = exporter.getFinishedSpanItems(); + assertThat(spanItems).isNotNull(); + assertThat(spanItems.size()).isEqualTo(3); + // Reset then expect no items in memory. + exporter.reset(); + assertThat(exporter.getFinishedSpanItems()).isEmpty(); + } + + @Test + void shutdown() { + tracer.spanBuilder("one").startSpan().end(); + tracer.spanBuilder("two").startSpan().end(); + tracer.spanBuilder("three").startSpan().end(); + List spanItems = exporter.getFinishedSpanItems(); + assertThat(spanItems).isNotNull(); + assertThat(spanItems.size()).isEqualTo(3); + // Shutdown then expect no items in memory. + exporter.shutdown(); + assertThat(exporter.getFinishedSpanItems()).isEmpty(); + // Cannot add new elements after the shutdown. + tracer.spanBuilder("one").startSpan().end(); + assertThat(exporter.getFinishedSpanItems()).isEmpty(); + } + + @Test + void export_ReturnCode() { + assertThat(exporter.export(Collections.singletonList(makeBasicSpan())).isSuccess()).isTrue(); + exporter.shutdown(); + // After shutdown no more export. + assertThat(exporter.export(Collections.singletonList(makeBasicSpan())).isSuccess()).isFalse(); + exporter.reset(); + // Reset does not do anything if already shutdown. + assertThat(exporter.export(Collections.singletonList(makeBasicSpan())).isSuccess()).isFalse(); + } + + static SpanData makeBasicSpan() { + return TestSpanData.builder() + .setHasEnded(true) + .setName("span") + .setKind(SpanKind.SERVER) + .setStartEpochNanos(100_000_000_100L) + .setStatus(StatusData.ok()) + .setEndEpochNanos(200_000_000_200L) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .build(); + } +} diff --git a/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit4/OpenTelemetryRuleTest.java b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit4/OpenTelemetryRuleTest.java new file mode 100644 index 000000000..daa776522 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit4/OpenTelemetryRuleTest.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.junit4; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Tracer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +public class OpenTelemetryRuleTest { + + @Rule public OpenTelemetryRule otelTesting = OpenTelemetryRule.create(); + + private Tracer tracer; + + @Before + public void setup() { + tracer = otelTesting.getOpenTelemetry().getTracer("test"); + } + + @Test + public void exportSpan() { + tracer.spanBuilder("test").startSpan().end(); + + assertThat(otelTesting.getSpans()) + .singleElement() + .satisfies(span -> assertThat(span.getName()).isEqualTo("test")); + // Spans cleared between tests, not when retrieving + assertThat(otelTesting.getSpans()) + .singleElement() + .satisfies(span -> assertThat(span.getName()).isEqualTo("test")); + } + + // We have two tests to verify spans get cleared up between tests. + @Test + public void exportSpanAgain() { + tracer.spanBuilder("test").startSpan().end(); + + assertThat(otelTesting.getSpans()) + .singleElement() + .satisfies(span -> assertThat(span.getName()).isEqualTo("test")); + // Spans cleared between tests, not when retrieving + assertThat(otelTesting.getSpans()) + .singleElement() + .satisfies(span -> assertThat(span.getName()).isEqualTo("test")); + } +} diff --git a/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtensionTest.java b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtensionTest.java new file mode 100644 index 000000000..2c0aa20f6 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtensionTest.java @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.junit5; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.LinkedHashMap; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OpenTelemetryExtensionTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = otelTesting.getOpenTelemetry().getTracer("test"); + + @Test + public void exportSpan() { + tracer.spanBuilder("test").startSpan().end(); + + assertThat(otelTesting.getSpans()) + .singleElement() + .satisfies(span -> assertThat(span.getName()).isEqualTo("test")); + // Spans cleared between tests, not when retrieving + assertThat(otelTesting.getSpans()) + .singleElement() + .satisfies(span -> assertThat(span.getName()).isEqualTo("test")); + } + + // We have two tests to verify spans get cleared up between tests. + @Test + public void exportSpanAgain() { + tracer.spanBuilder("test").startSpan().end(); + + assertThat(otelTesting.getSpans()) + .singleElement() + .satisfies(span -> assertThat(span.getName()).isEqualTo("test")); + // Spans cleared between tests, not when retrieving + assertThat(otelTesting.getSpans()) + .singleElement() + .satisfies(span -> assertThat(span.getName()).isEqualTo("test")); + } + + @Test + public void exportTraces() { + Span span = tracer.spanBuilder("testa1").startSpan(); + try (Scope ignored = span.makeCurrent()) { + tracer.spanBuilder("testa2").startSpan().end(); + } finally { + span.end(); + } + + span = tracer.spanBuilder("testb1").startSpan(); + try (Scope ignored = span.makeCurrent()) { + tracer.spanBuilder("testb2").startSpan().end(); + } finally { + span.end(); + } + + String traceId = + otelTesting.getSpans().stream() + .collect( + Collectors.groupingBy( + SpanData::getTraceId, LinkedHashMap::new, Collectors.toList())) + .values() + .stream() + .findFirst() + .get() + .get(0) + .getTraceId(); + + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace + .hasTraceId(traceId) + .hasSpansSatisfyingExactly(s -> s.hasName("testa1"), s -> s.hasName("testa2")) + .first() + .hasName("testa1"), + trace -> + trace + .hasSpansSatisfyingExactly(s -> s.hasName("testb1"), s -> s.hasName("testb2")) + .filteredOn(s -> s.getName().endsWith("1")) + .hasSize(1)); + + assertThatThrownBy( + () -> + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly(trace -> trace.hasTraceId("foo"))) + .isInstanceOf(AssertionError.class); + + otelTesting + .assertTraces() + .first() + .hasSpansSatisfyingExactly(s -> s.hasName("testa1"), s -> s.hasName("testa2")); + otelTesting + .assertTraces() + .filteredOn(trace -> trace.size() == 2) + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly(s -> s.hasName("testa1"), s -> s.hasName("testa2")), + trace -> + trace.hasSpansSatisfyingExactly( + s -> s.hasName("testb1"), s -> s.hasName("testb2"))); + } +} diff --git a/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/trace/TestSpanDataTest.java b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/trace/TestSpanDataTest.java new file mode 100644 index 000000000..3d7cbdef9 --- /dev/null +++ b/opentelemetry-java/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/trace/TestSpanDataTest.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.trace; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class TestSpanDataTest { + + private static final long START_EPOCH_NANOS = TimeUnit.SECONDS.toNanos(3000) + 200; + private static final long END_EPOCH_NANOS = TimeUnit.SECONDS.toNanos(3001) + 255; + + @Test + void defaultValues() { + SpanData spanData = createBasicSpanBuilder().build(); + + assertThat(SpanId.isValid(spanData.getParentSpanId())).isFalse(); + assertThat(spanData.getAttributes()).isEqualTo(Attributes.empty()); + assertThat(spanData.getEvents()).isEqualTo(emptyList()); + assertThat(spanData.getLinks()).isEqualTo(emptyList()); + assertThat(spanData.getInstrumentationLibraryInfo()) + .isSameAs(InstrumentationLibraryInfo.empty()); + } + + @Test + void unmodifiableLinks() { + SpanData spanData = createSpanDataWithMutableCollections(); + + assertThatThrownBy(() -> spanData.getLinks().add(emptyLink())) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void unmodifiableTimedEvents() { + SpanData spanData = createSpanDataWithMutableCollections(); + + assertThatThrownBy( + () -> spanData.getEvents().add(EventData.create(1234, "foo", Attributes.empty()))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void defaultTotalAttributeCountIsZero() { + SpanData spanData = createSpanDataWithMutableCollections(); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(0); + } + + @Test + void canSetTotalAttributeCountWithBuilder() { + SpanData spanData = createBasicSpanBuilder().setTotalAttributeCount(123).build(); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(123); + } + + @Test + void link_defaultTotalAttributeCountIsZero() { + LinkData link = LinkData.create(SpanContext.getInvalid()); + assertThat(link.getTotalAttributeCount()).isEqualTo(0); + } + + @Test + void link_canSetTotalAttributeCount() { + LinkData link = LinkData.create(SpanContext.getInvalid()); + assertThat(link.getTotalAttributeCount()).isEqualTo(0); + } + + @Test + void timedEvent_defaultTotalAttributeCountIsZero() { + EventData event = EventData.create(START_EPOCH_NANOS, "foo", Attributes.empty()); + assertThat(event.getTotalAttributeCount()).isEqualTo(0); + } + + @Test + void timedEvent_canSetTotalAttributeCount() { + EventData event = EventData.create(START_EPOCH_NANOS, "foo", Attributes.empty(), 123); + assertThat(event.getTotalAttributeCount()).isEqualTo(123); + } + + private static SpanData createSpanDataWithMutableCollections() { + return createBasicSpanBuilder() + .setLinks(new ArrayList<>()) + .setEvents(new ArrayList<>()) + .build(); + } + + private static LinkData emptyLink() { + return LinkData.create(SpanContext.getInvalid()); + } + + private static TestSpanData.Builder createBasicSpanBuilder() { + return TestSpanData.builder() + .setHasEnded(true) + .setName("spanName") + .setStartEpochNanos(START_EPOCH_NANOS) + .setEndEpochNanos(END_EPOCH_NANOS) + .setKind(SpanKind.SERVER) + .setStatus(StatusData.ok()) + .setTotalRecordedEvents(0) + .setTotalRecordedLinks(0); + } +} diff --git a/opentelemetry-java/sdk/trace-shaded-deps/build.gradle.kts b/opentelemetry-java/sdk/trace-shaded-deps/build.gradle.kts new file mode 100644 index 000000000..6ca213973 --- /dev/null +++ b/opentelemetry-java/sdk/trace-shaded-deps/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + `java-library` + + id("com.github.johnrengelman.shadow") +} + +// This project is not published, it is bundled into :sdk:trace + +description = "Internal use only - shaded dependencies of OpenTelemetry SDK for Tracing" +extra["moduleName"] = "io.opentelemetry.sdk.trace.internal" + +dependencies { + implementation("org.jctools:jctools-core") +} + +tasks { + shadowJar { + minimize() + + relocate("org.jctools", "io.opentelemetry.internal.shaded.jctools") + } +} diff --git a/opentelemetry-java/sdk/trace-shaded-deps/src/main/java/io/opentelemetry/sdk/trace/internal/JcTools.java b/opentelemetry-java/sdk/trace-shaded-deps/src/main/java/io/opentelemetry/sdk/trace/internal/JcTools.java new file mode 100644 index 000000000..255d04138 --- /dev/null +++ b/opentelemetry-java/sdk/trace-shaded-deps/src/main/java/io/opentelemetry/sdk/trace/internal/JcTools.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import java.util.Queue; +import org.jctools.queues.MessagePassingQueue; +import org.jctools.queues.MpscArrayQueue; + +/** Internal accessor of JCTools package for fast queues. */ +public final class JcTools { + + /** + * Returns a new {@link Queue} appropriate for use with multiple producers and a single consumer. + */ + public static Queue newMpscArrayQueue(int capacity) { + return new MpscArrayQueue<>(capacity); + } + + /** + * Returns the capacity of the {@link Queue}, which must be a JcTools queue. We cast to the + * implementation so callers do not need to use the shaded classes. + */ + public static long capacity(Queue queue) { + return ((MessagePassingQueue) queue).capacity(); + } + + private JcTools() {} +} diff --git a/opentelemetry-java/sdk/trace/build.gradle.kts b/opentelemetry-java/sdk/trace/build.gradle.kts new file mode 100644 index 000000000..a7525c51d --- /dev/null +++ b/opentelemetry-java/sdk/trace/build.gradle.kts @@ -0,0 +1,78 @@ +plugins { + id("java-library") + id("maven-publish") + + id("me.champeau.jmh") + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry SDK For Tracing" +extra["moduleName"] = "io.opentelemetry.sdk.trace" + +evaluationDependsOn(":sdk:trace-shaded-deps") + +dependencies { + api(project(":api:all")) + api(project(":sdk:common")) + + compileOnly(project(":sdk:trace-shaded-deps")) + + implementation(project(":api:metrics")) + implementation(project(":semconv")) + + annotationProcessor("com.google.auto.value:auto-value") + + testAnnotationProcessor("com.google.auto.value:auto-value") + + testImplementation(project(":sdk:testing")) + testImplementation("com.google.guava:guava") + + jmh(project(":sdk:metrics")) + jmh(project(":sdk:trace-shaded-deps")) + jmh(project(":sdk:testing")) { + // JMH doesn"t handle dependencies that are duplicated between the main and jmh + // configurations properly, but luckily here it"s simple enough to just exclude transitive + // dependencies. + isTransitive = false + } + jmh(project(":exporters:jaeger-thrift")) + jmh(project(":exporters:otlp:trace")) { + // The opentelemetry-exporter-otlp-trace depends on this project itself. So don"t pull in + // the transitive dependencies. + isTransitive = false + } + // explicitly adding the opentelemetry-exporter-otlp dependencies + jmh(project(":exporters:otlp:common")) { + isTransitive = false + } + jmh(project(":proto")) + + jmh("com.google.guava:guava") + jmh("io.grpc:grpc-api") + jmh("io.grpc:grpc-netty-shaded") + jmh("org.testcontainers:testcontainers") // testContainer for OTLP collector +} + +sourceSets { + main { + output.dir("build/generated/properties", "builtBy" to "generateVersionResource") + } +} + +tasks { + register("generateVersionResource") { + val propertiesDir = file("build/generated/properties/io/opentelemetry/sdk/trace") + outputs.dir(propertiesDir) + + doLast { + File(propertiesDir, "version.properties").writeText("sdk.version=${project.version}") + } + } + + jar { + inputs.files(project(":sdk:trace-shaded-deps").file("src")) + val shadowJar = project(":sdk:trace-shaded-deps").tasks.named("shadowJar") + from(zipTree(shadowJar.get().archiveFile)) + dependsOn(shadowJar) + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/ExceptionBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/ExceptionBenchmark.java new file mode 100644 index 000000000..438deb7b6 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/ExceptionBenchmark.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.samplers.Sampler; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class ExceptionBenchmark { + private static SpanBuilder spanBuilder; + + @Setup(Level.Trial) + public final void setup() { + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder().setSampler(Sampler.alwaysOn()).build(); + + Tracer tracer = tracerProvider.get("benchmarkTracer"); + spanBuilder = tracer.spanBuilder("benchmarkSpanBuilder"); + } + + @Benchmark + @Threads(value = 1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @BenchmarkMode(Mode.AverageTime) + public Span createSpan() { + Span span = spanBuilder.startSpan(); + span.end(); + return span; + } + + @Benchmark + @Threads(value = 1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @BenchmarkMode(Mode.AverageTime) + public Span createSpanAndRecordException() { + Span span = spanBuilder.startSpan(); + span.recordException(new RuntimeException()); + span.end(); + return span; + } + + @Benchmark + @Threads(value = 1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @BenchmarkMode(Mode.AverageTime) + public RuntimeException createException() { + return new RuntimeException(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/ExporterBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/ExporterBenchmark.java new file mode 100644 index 000000000..c1837a2f8 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/ExporterBenchmark.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.exporter.jaeger.thrift.JaegerThriftSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.time.Duration; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class ExporterBenchmark { + private ExporterBenchmark() {} + + @State(Scope.Benchmark) + public abstract static class AbstractProcessorBenchmark { + private static final DockerImageName OTLP_COLLECTOR_IMAGE = + DockerImageName.parse("otel/opentelemetry-collector-dev:latest"); + protected static final int OTLP_PORT = 5678; + protected static final int JAEGER_PORT = 14268; + private static final int HEALTH_CHECK_PORT = 13133; + protected SdkSpanBuilder sdkSpanBuilder; + + protected abstract SpanExporter createExporter(GenericContainer collector); + + @Setup(Level.Trial) + public void setup() { + // Configuring the collector test-container + GenericContainer collector = + new GenericContainer<>(OTLP_COLLECTOR_IMAGE) + .withExposedPorts(OTLP_PORT, HEALTH_CHECK_PORT, JAEGER_PORT) + .waitingFor(Wait.forHttp("/").forPort(HEALTH_CHECK_PORT)) + .withCopyFileToContainer( + MountableFile.forClasspathResource("/otel.yaml"), "/etc/otel.yaml") + .withCommand("--config /etc/otel.yaml"); + + collector.start(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOn()) + .addSpanProcessor(SimpleSpanProcessor.create(createExporter(collector))) + .build(); + + Tracer tracerSdk = tracerProvider.get("PipelineBenchmarkTracer"); + sdkSpanBuilder = (SdkSpanBuilder) tracerSdk.spanBuilder("PipelineBenchmarkSpan"); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork(1) + @Threads(1) + public Span createAndExportSpan() { + Span span = sdkSpanBuilder.startSpan(); + span.end(); + return span; + } + } + + public static class OtlpBenchmark extends AbstractProcessorBenchmark { + @Override + protected OtlpGrpcSpanExporter createExporter(GenericContainer collector) { + String host = collector.getHost(); + int port = collector.getMappedPort(OTLP_PORT); + return OtlpGrpcSpanExporter.builder() + .setEndpoint("http://" + host + ":" + port) + .setTimeout(Duration.ofSeconds(50)) + .build(); + } + } + + public static class JaegerBenchmark extends AbstractProcessorBenchmark { + @Override + protected JaegerThriftSpanExporter createExporter(GenericContainer collector) { + String host = collector.getHost(); + int port = collector.getMappedPort(JAEGER_PORT); + return JaegerThriftSpanExporter.builder() + .setEndpoint("http://" + host + ":" + port + "/api/traces") + .build(); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/FillSpanBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/FillSpanBenchmark.java new file mode 100644 index 000000000..245b13afa --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/FillSpanBenchmark.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanBuilder; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@Threads(value = 1) +@Fork(3) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 20, time = 1) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class FillSpanBenchmark { + + private static final SpanBuilder spanBuilder = + SdkTracerProvider.builder().build().get("benchmark").spanBuilder("benchmark"); + + private static final AttributeKey KEY1 = AttributeKey.stringKey("key1"); + private static final AttributeKey KEY2 = AttributeKey.stringKey("key2"); + private static final AttributeKey KEY3 = AttributeKey.stringKey("key3"); + private static final AttributeKey KEY4 = AttributeKey.stringKey("key4"); + + @Benchmark + public void setFourAttributes() { + spanBuilder + .startSpan() + .setAttribute(KEY1, "value1") + .setAttribute(KEY2, "value2") + .setAttribute(KEY3, "value3") + .setAttribute(KEY4, "value4"); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/PrintThrowableBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/PrintThrowableBenchmark.java new file mode 100644 index 000000000..153581fe7 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/PrintThrowableBenchmark.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import com.google.common.io.CharStreams; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@Threads(value = 1) +@Fork(3) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 20, time = 1) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class PrintThrowableBenchmark { + + private static final int THROWABLE_SIZE = 50; + + private static final Throwable THROWABLE; + + static { + Throwable throwable = null; + try { + throwAfter(THROWABLE_SIZE); + } catch (Throwable t) { + throwable = t; + } + if (throwable == null) { + throw new AssertionError(); + } + THROWABLE = throwable; + } + + private static void throwAfter(int count) { + if (count == THROWABLE_SIZE) { + throw new AssertionError("threw"); + } else { + throwAfter(count + 1); + } + } + + /** Measures performance of {@link StringBuilder} + Guava {@link CharStreams}. */ + @Benchmark + public String normalPrintWriter() { + StringBuilder sb = new StringBuilder(); + PrintWriter writer = new PrintWriter(CharStreams.asWriter(sb)); + THROWABLE.printStackTrace(writer); + return sb.toString(); + } + + /** Measures performance of JDK {@link StringWriter}. */ + @Benchmark + public String stringWriter() { + StringWriter sw = new StringWriter(); + PrintWriter writer = new PrintWriter(sw); + THROWABLE.printStackTrace(writer); + return sw.toString(); + } + + /** Measures performance of a {@link PrintStream}. */ + @Benchmark + public String printStream() throws Exception { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream stream = new PrintStream(bos); + THROWABLE.printStackTrace(stream); + return bos.toString(StandardCharsets.UTF_8.name()); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/SpanBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/SpanBenchmark.java new file mode 100644 index 000000000..c8063e1bb --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/SpanBenchmark.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class SpanBenchmark { + private static SdkSpanBuilder sdkSpanBuilder; + private final Resource serviceResource = + Resource.create( + Attributes.builder() + .put("service.name", "benchmark1") + .put("service.version", "123.456.89") + .put("service.instance.id", "123ab456-a123-12ab-12ab-12340a1abc12") + .build()); + + @Setup(Level.Trial) + public final void setup() { + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setResource(serviceResource) + .setSampler(Sampler.alwaysOn()) + .build(); + + Tracer tracerSdk = tracerProvider.get("benchmarkTracer"); + sdkSpanBuilder = + (SdkSpanBuilder) + tracerSdk.spanBuilder("benchmarkSpanBuilder").setAttribute("longAttribute", 33L); + } + + @Benchmark + @Threads(value = 1) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void simpleSpanStartAddEventEnd_01Thread() { + doSpanWork(); + } + + @Benchmark + @Threads(value = 5) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void simpleSpanStartAddEventEnd_05Threads() { + doSpanWork(); + } + + @Benchmark + @Threads(value = 2) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void simpleSpanStartAddEventEnd_02Threads() { + doSpanWork(); + } + + @Benchmark + @Threads(value = 10) + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public void simpleSpanStartAddEventEnd_10Threads() { + doSpanWork(); + } + + private static void doSpanWork() { + Span span = sdkSpanBuilder.startSpan(); + span.addEvent("testEvent"); + span.end(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/SpanPipelineBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/SpanPipelineBenchmark.java new file mode 100644 index 000000000..03684d1a3 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/SpanPipelineBenchmark.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +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.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class SpanPipelineBenchmark { + private SpanPipelineBenchmark() {} + + @State(Scope.Benchmark) + public abstract static class AbstractProcessorBenchmark { + private static final DockerImageName OTLP_COLLECTOR_IMAGE = + DockerImageName.parse("otel/opentelemetry-collector-dev:latest"); + private static final int EXPOSED_PORT = 5678; + private static final int HEALTH_CHECK_PORT = 13133; + private Tracer tracer; + private SdkTracerProvider tracerProvider; + + protected abstract SpanProcessor getSpanProcessor(String collectorAddress); + + protected abstract void runThePipeline(); + + protected void doWork() { + for (int j = 0; j < 100; j++) { + Span span = tracer.spanBuilder("PipelineBenchmarkSpan " + j).startSpan(); + for (int i = 0; i < 10; i++) { + span.setAttribute("benchmarkAttribute_" + i, "benchmarkAttrValue_" + i); + } + span.end(); + } + // we flush the SDK in order to make sure that the BatchSpanProcessor doesn't drop spans. + // this means that this benchmark is mostly useful for measuring allocations, not throughput. + tracerProvider.forceFlush().join(1, TimeUnit.SECONDS); + } + + @Setup(Level.Trial) + @SuppressWarnings("SystemOut") + public void setup() { + // Configuring the collector test-container + GenericContainer collector = + new GenericContainer<>(OTLP_COLLECTOR_IMAGE) + .withExposedPorts(EXPOSED_PORT, HEALTH_CHECK_PORT) + .waitingFor(Wait.forHttp("/").forPort(HEALTH_CHECK_PORT)) + .withCopyFileToContainer( + MountableFile.forClasspathResource("/otel.yaml"), "/etc/otel.yaml") + .withCommand("--config /etc/otel.yaml"); + + collector.start(); + + SpanProcessor spanProcessor = makeSpanProcessor(collector); + + tracerProvider = + SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOn()) + .addSpanProcessor(spanProcessor) + .build(); + + tracer = tracerProvider.get("PipelineBenchmarkTracer"); + } + + private SpanProcessor makeSpanProcessor(GenericContainer collector) { + try { + String host = collector.getHost(); + Integer port = collector.getMappedPort(EXPOSED_PORT); + String address = new URL("http", host, port, "").toString(); + return getSpanProcessor(address); + } catch (MalformedURLException e) { + throw new IllegalStateException("can't make a url", e); + } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 15, time = 1) + @OutputTimeUnit(TimeUnit.SECONDS) + @Fork(1) + @Threads(1) + public void measureSpanPipeline() { + runThePipeline(); + } + } + + public static class SimpleSpanProcessorBenchmark extends AbstractProcessorBenchmark { + @Override + protected SpanProcessor getSpanProcessor(String collectorAddress) { + return SimpleSpanProcessor.create( + OtlpGrpcSpanExporter.builder() + .setEndpoint(collectorAddress) + .setTimeout(Duration.ofSeconds(50)) + .build()); + } + + @Override + protected void runThePipeline() { + doWork(); + } + } + + public static class BatchSpanProcessorBenchmark extends AbstractProcessorBenchmark { + + @Override + protected SpanProcessor getSpanProcessor(String collectorAddress) { + return BatchSpanProcessor.builder( + OtlpGrpcSpanExporter.builder() + .setEndpoint(collectorAddress) + .setTimeout(Duration.ofSeconds(50)) + .build()) + .build(); + } + + @Override + protected void runThePipeline() { + doWork(); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/StatusBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/StatusBenchmark.java new file mode 100644 index 000000000..c0eab7146 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/StatusBenchmark.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.trace.data.StatusData; +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.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@Threads(value = 1) +@BenchmarkMode(Mode.AverageTime) +@Fork(3) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class StatusBenchmark { + + @Benchmark + public StatusData createWithoutDescription() { + return StatusData.create(StatusCode.UNSET, ""); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorBenchmark.java new file mode 100644 index 000000000..736f9612a --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorBenchmark.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class BatchSpanProcessorBenchmark { + + @Param({"0", "1", "5"}) + private int delayMs; + + @Param({"1000", "2000", "5000"}) + private int spanCount; + + private List spans; + + private BatchSpanProcessor processor; + + @Setup(Level.Trial) + public final void setup() { + SpanExporter exporter = new DelayingSpanExporter(delayMs); + processor = BatchSpanProcessor.builder(exporter).build(); + + ImmutableList.Builder spans = ImmutableList.builderWithExpectedSize(spanCount); + Tracer tracer = SdkTracerProvider.builder().build().get("benchmarkTracer"); + for (int i = 0; i < spanCount; i++) { + spans.add(tracer.spanBuilder("span").startSpan()); + } + this.spans = spans.build(); + } + + @TearDown(Level.Trial) + public final void tearDown() { + processor.shutdown().join(10, TimeUnit.SECONDS); + } + + /** Export spans through {@link BatchSpanProcessor}. */ + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export() { + for (Span span : spans) { + processor.onEnd((ReadableSpan) span); + } + processor.forceFlush().join(10, TimeUnit.MINUTES); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorCpuBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorCpuBenchmark.java new file mode 100644 index 000000000..918ef2fad --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorCpuBenchmark.java @@ -0,0 +1,164 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/* + * Run this along with a profiler to measure the CPU usage of BatchSpanProcessor's exporter thread. + */ +public class BatchSpanProcessorCpuBenchmark { + @State(Scope.Benchmark) + public static class BenchmarkState { + private SdkMeterProvider sdkMeterProvider; + private BatchSpanProcessor processor; + private Tracer tracer; + private int numThreads = 1; + + @Param({"1"}) + private int delayMs; + + private long exportedSpans; + private long droppedSpans; + + @Setup(Level.Iteration) + public final void setup() { + sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + SpanExporter exporter = new DelayingSpanExporter(delayMs); + processor = BatchSpanProcessor.builder(exporter).build(); + tracer = + SdkTracerProvider.builder().addSpanProcessor(processor).build().get("benchmarkTracer"); + } + + @TearDown(Level.Iteration) + public final void recordMetrics() { + BatchSpanProcessorMetrics metrics = + new BatchSpanProcessorMetrics(sdkMeterProvider.collectAllMetrics(), numThreads); + exportedSpans = metrics.exportedSpans(); + droppedSpans = metrics.droppedSpans(); + } + + @TearDown(Level.Iteration) + public final void tearDown() { + processor.shutdown().join(10, TimeUnit.SECONDS); + } + } + + @State(Scope.Thread) + @AuxCounters(AuxCounters.Type.OPERATIONS) + public static class ThreadState { + BenchmarkState benchmarkState; + + @TearDown(Level.Iteration) + public final void recordMetrics(BenchmarkState benchmarkState) { + this.benchmarkState = benchmarkState; + } + + public long exportedSpans() { + return benchmarkState.exportedSpans; + } + + public long droppedSpans() { + return benchmarkState.droppedSpans; + } + } + + private static void doWork(BenchmarkState benchmarkState) { + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + // This sleep is essential to maintain a steady state of the benchmark run by generating 10k + // spans per second per thread. Without this JMH outer loop consumes as much CPU as possible + // making comparing different processor versions difficult. + // Note that time spent outside of the sleep is negligible allowing this sleep to control + // span generation rate. Here we get 1 / 100_000 = 10K spans generated per second. + LockSupport.parkNanos(100_000); + } + + @Benchmark + @Fork(1) + @Threads(1) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_01Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 1; + doWork(benchmarkState); + } + + @Benchmark + @Fork(1) + @Threads(2) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_02Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 2; + doWork(benchmarkState); + } + + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_05Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 5; + doWork(benchmarkState); + } + + @Benchmark + @Fork(1) + @Threads(10) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_10Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 10; + doWork(benchmarkState); + } + + @Benchmark + @Fork(1) + @Threads(20) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_20Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 20; + doWork(benchmarkState); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorDroppedSpansBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorDroppedSpansBenchmark.java new file mode 100644 index 000000000..ef0775dd1 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorDroppedSpansBenchmark.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +public class BatchSpanProcessorDroppedSpansBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + private SdkMeterProvider sdkMeterProvider; + private BatchSpanProcessor processor; + private Tracer tracer; + private double dropRatio; + private long exportedSpans; + private long droppedSpans; + private int numThreads; + + @Setup(Level.Iteration) + public final void setup() { + sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + SpanExporter exporter = new DelayingSpanExporter(0); + processor = BatchSpanProcessor.builder(exporter).build(); + + tracer = SdkTracerProvider.builder().build().get("benchmarkTracer"); + } + + @TearDown(Level.Iteration) + public final void recordMetrics() { + BatchSpanProcessorMetrics metrics = + new BatchSpanProcessorMetrics(sdkMeterProvider.collectAllMetrics(), numThreads); + dropRatio = metrics.dropRatio(); + exportedSpans = metrics.exportedSpans(); + droppedSpans = metrics.droppedSpans(); + } + + @TearDown(Level.Iteration) + public final void tearDown() { + processor.shutdown(); + } + } + + @State(Scope.Thread) + @AuxCounters(AuxCounters.Type.OPERATIONS) + public static class ThreadState { + BenchmarkState benchmarkState; + + @TearDown(Level.Iteration) + public final void recordMetrics(BenchmarkState benchmarkState) { + this.benchmarkState = benchmarkState; + } + + public double dropRatio() { + return benchmarkState.dropRatio; + } + + public long exportedSpans() { + return benchmarkState.exportedSpans; + } + + public long droppedSpans() { + return benchmarkState.droppedSpans; + } + } + + /** Export spans through {@link BatchSpanProcessor}. */ + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 5, time = 20) + @BenchmarkMode(Mode.Throughput) + public void export( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 5; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorFlushBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorFlushBenchmark.java new file mode 100644 index 000000000..ef9ad0538 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorFlushBenchmark.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class BatchSpanProcessorFlushBenchmark { + + private static class DelayingSpanExporter implements SpanExporter { + + private final ScheduledExecutorService executor; + + private final int delayMs; + + private DelayingSpanExporter(int delayMs) { + executor = Executors.newScheduledThreadPool(5); + this.delayMs = delayMs; + } + + @SuppressWarnings("FutureReturnValueIgnored") + @Override + public CompletableResultCode export(Collection spans) { + final CompletableResultCode result = new CompletableResultCode(); + executor.schedule((Runnable) result::succeed, delayMs, TimeUnit.MILLISECONDS); + return result; + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + } + + @Param({"0", "1", "5"}) + private int delayMs; + + @Param({"1000", "2000", "5000"}) + private int spanCount; + + private List spans; + + private BatchSpanProcessor processor; + + @Setup(Level.Trial) + public final void setup() { + SpanExporter exporter = new DelayingSpanExporter(delayMs); + processor = BatchSpanProcessor.builder(exporter).build(); + + ImmutableList.Builder spans = ImmutableList.builderWithExpectedSize(spanCount); + Tracer tracer = SdkTracerProvider.builder().build().get("benchmarkTracer"); + for (int i = 0; i < spanCount; i++) { + spans.add(tracer.spanBuilder("span").startSpan()); + } + this.spans = spans.build(); + } + + @TearDown(Level.Trial) + public final void tearDown() { + processor.shutdown().join(10, TimeUnit.SECONDS); + } + + /** Export spans through {@link BatchSpanProcessor}. */ + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export() { + for (Span span : spans) { + processor.onEnd((ReadableSpan) span); + } + processor.forceFlush().join(10, TimeUnit.MINUTES); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMetrics.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMetrics.java new file mode 100644 index 000000000..136faae94 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMetrics.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.Collection; +import java.util.OptionalLong; + +public class BatchSpanProcessorMetrics { + private final Collection allMetrics; + private final int numThreads; + + public BatchSpanProcessorMetrics(Collection allMetrics, int numThreads) { + this.allMetrics = allMetrics; + this.numThreads = numThreads; + } + + public double dropRatio() { + long exported = getMetric(false); + long dropped = getMetric(true); + long total = exported + dropped; + // Due to peculiarities of JMH reporting we have to divide this by the number of the + // concurrent threads running the actual benchmark. + return total == 0 ? 0 : (double) dropped / total / numThreads; + } + + public long exportedSpans() { + return getMetric(false) / numThreads; + } + + public long droppedSpans() { + return getMetric(true) / numThreads; + } + + private long getMetric(boolean dropped) { + String labelValue = String.valueOf(dropped); + OptionalLong value = + allMetrics.stream() + .filter(metricData -> metricData.getName().equals("processedSpans")) + .filter(metricData -> !metricData.isEmpty()) + .map(metricData -> metricData.getLongSumData().getPoints()) + .flatMap(Collection::stream) + .filter(point -> labelValue.equals(point.getLabels().get("dropped"))) + .mapToLong(LongPointData::getValue) + .findFirst(); + return value.isPresent() ? value.getAsLong() : 0; + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMultiThreadBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMultiThreadBenchmark.java new file mode 100644 index 000000000..ba6a05b20 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorMultiThreadBenchmark.java @@ -0,0 +1,152 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +public class BatchSpanProcessorMultiThreadBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + private SdkMeterProvider sdkMeterProvider; + private BatchSpanProcessor processor; + private Tracer tracer; + private int numThreads = 1; + + @Param({"0"}) + private int delayMs; + + private long exportedSpans; + private long droppedSpans; + + @Setup(Level.Iteration) + public final void setup() { + sdkMeterProvider = SdkMeterProvider.builder().buildAndRegisterGlobal(); + SpanExporter exporter = new DelayingSpanExporter(delayMs); + processor = BatchSpanProcessor.builder(exporter).build(); + tracer = + SdkTracerProvider.builder().addSpanProcessor(processor).build().get("benchmarkTracer"); + } + + @TearDown(Level.Iteration) + public final void recordMetrics() { + BatchSpanProcessorMetrics metrics = + new BatchSpanProcessorMetrics(sdkMeterProvider.collectAllMetrics(), numThreads); + exportedSpans = metrics.exportedSpans(); + droppedSpans = metrics.droppedSpans(); + processor.shutdown().join(10, TimeUnit.SECONDS); + } + } + + @State(Scope.Thread) + @AuxCounters(AuxCounters.Type.OPERATIONS) + public static class ThreadState { + BenchmarkState benchmarkState; + + @TearDown(Level.Iteration) + public final void recordMetrics(BenchmarkState benchmarkState) { + this.benchmarkState = benchmarkState; + } + + public long exportedSpans() { + return benchmarkState.exportedSpans; + } + + public long droppedSpans() { + return benchmarkState.droppedSpans; + } + } + + @Benchmark + @Fork(1) + @Threads(1) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_01Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 1; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } + + @Benchmark + @Fork(1) + @Threads(2) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_02Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 2; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } + + @Benchmark + @Fork(1) + @Threads(5) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_05Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 5; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } + + @Benchmark + @Fork(1) + @Threads(10) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_10Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 10; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } + + @Benchmark + @Fork(1) + @Threads(20) + @Warmup(iterations = 1, time = 1) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void export_20Thread( + BenchmarkState benchmarkState, @SuppressWarnings("unused") ThreadState threadState) { + benchmarkState.numThreads = 20; + benchmarkState.processor.onEnd( + (ReadableSpan) benchmarkState.tracer.spanBuilder("span").startSpan()); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/DelayingSpanExporter.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/DelayingSpanExporter.java new file mode 100644 index 000000000..40998ac42 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/DelayingSpanExporter.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.Collection; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class DelayingSpanExporter implements SpanExporter { + + private final ScheduledExecutorService executor; + + private final int delayMs; + + public DelayingSpanExporter(int delayMs) { + executor = Executors.newScheduledThreadPool(5); + this.delayMs = delayMs; + } + + @SuppressWarnings("FutureReturnValueIgnored") + @Override + public CompletableResultCode export(Collection spans) { + final CompletableResultCode result = new CompletableResultCode(); + executor.schedule((Runnable) result::succeed, delayMs, TimeUnit.MILLISECONDS); + return result; + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + executor.shutdown(); + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/MultiSpanExporterBenchmark.java b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/MultiSpanExporterBenchmark.java new file mode 100644 index 000000000..a049ea5ce --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/java/io/opentelemetry/sdk/trace/export/MultiSpanExporterBenchmark.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +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.Warmup; + +@State(Scope.Benchmark) +public class MultiSpanExporterBenchmark { + + private static class NoopSpanExporter implements SpanExporter { + + @Override + public CompletableResultCode export(Collection spans) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + } + + @Param({"1", "3"}) + private int exporterCount; + + private SpanExporter exporter; + + @Param({"1000"}) + private int spanCount; + + private List spans; + + @Setup(Level.Trial) + public final void setup() { + SpanExporter[] exporter = new SpanExporter[exporterCount]; + Arrays.fill(exporter, new NoopSpanExporter()); + this.exporter = SpanExporter.composite(Arrays.asList(exporter)); + + TestSpanData[] spans = new TestSpanData[spanCount]; + for (int i = 0; i < spans.length; i++) { + spans[i] = + TestSpanData.builder() + .setSpanContext( + SpanContext.create( + "12345678876543211234567887654321", + "8765432112345678", + TraceFlags.getSampled(), + TraceState.getDefault())) + .setName("noop") + .setKind(SpanKind.CLIENT) + .setStartEpochNanos(1) + .setStatus(StatusData.ok()) + .setEndEpochNanos(2) + .setHasEnded(true) + .build(); + } + this.spans = Arrays.asList(spans); + } + + @Benchmark + @Fork(1) + @Warmup(iterations = 5, time = 1) + @Measurement(iterations = 10, time = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public CompletableResultCode export() { + return exporter.export(spans); + } +} diff --git a/opentelemetry-java/sdk/trace/src/jmh/resources/otel.yaml b/opentelemetry-java/sdk/trace/src/jmh/resources/otel.yaml new file mode 100644 index 000000000..316ff8da1 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/jmh/resources/otel.yaml @@ -0,0 +1,25 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:5678 + jaeger: + protocols: + thrift_http: + +processors: + batch: + +extensions: + health_check: + +exporters: + logging: + +service: + extensions: [health_check] + pipelines: + traces: + receivers: [otlp, jaeger] + processors: [batch] + exporters: [logging] diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/AndroidFriendlyRandomIdGenerator.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/AndroidFriendlyRandomIdGenerator.java new file mode 100644 index 000000000..ae320ae9e --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/AndroidFriendlyRandomIdGenerator.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceId; +import java.util.Random; + +/** + * {@link IdGenerator} instance that doesn't use {@link java.util.concurrent.ThreadLocalRandom}, + * which is broken on most versions of Android (it uses the same seed everytime it starts up). + */ +enum AndroidFriendlyRandomIdGenerator implements IdGenerator { + INSTANCE; + + private static final Random random = new Random(); + + private static final long INVALID_ID = 0; + + @Override + public String generateSpanId() { + long id; + do { + id = random.nextLong(); + } while (id == INVALID_ID); + return SpanId.fromLong(id); + } + + @Override + public String generateTraceId() { + long idHi; + long idLo; + do { + idHi = random.nextLong(); + idLo = random.nextLong(); + } while (idHi == INVALID_ID && idLo == INVALID_ID); + return TraceId.fromLongs(idHi, idLo); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/AttributesMap.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/AttributesMap.java new file mode 100644 index 000000000..1739537cb --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/AttributesMap.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** A map with a fixed capacity that drops attributes when the map gets full. */ +final class AttributesMap extends HashMap, Object> implements Attributes { + + private static final long serialVersionUID = -5072696312123632376L; + + private final long capacity; + private int totalAddedValues = 0; + + AttributesMap(long capacity) { + this.capacity = capacity; + } + + void put(AttributeKey key, T value) { + totalAddedValues++; + if (size() >= capacity && !containsKey(key)) { + return; + } + super.put(key, value); + } + + int getTotalAddedValues() { + return totalAddedValues; + } + + @SuppressWarnings("unchecked") + @Override + public T get(AttributeKey key) { + return (T) super.get(key); + } + + @Override + public Map, Object> asMap() { + // Because Attributes is marked Immutable, IDEs may recognize this as redundant usage. However, + // this class is private and is actually mutable, so we need to wrap with unmodifiableMap + // anyways. We implement the immutable Attributes for this class to support the + // Attributes.builder().putAll usage - it is tricky but an implementation detail of this private + // class. + return Collections.unmodifiableMap(this); + } + + @Override + public AttributesBuilder toBuilder() { + return Attributes.builder().putAll(this); + } + + @Override + public String toString() { + return "AttributesMap{" + + "data=" + + super.toString() + + ", capacity=" + + capacity + + ", totalAddedValues=" + + totalAddedValues + + '}'; + } + + Attributes immutableCopy() { + return Attributes.builder().putAll(this).build(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/IdGenerator.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/IdGenerator.java new file mode 100644 index 000000000..d28c969cc --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/IdGenerator.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceId; +import javax.annotation.concurrent.ThreadSafe; + +/** Interface used by the {@link SdkTracer} to generate new {@link SpanId}s and {@link TraceId}s. */ +@ThreadSafe +public interface IdGenerator { + + /** + * Returns a {@link IdGenerator} that generates purely random IDs, which is the default for + * OpenTelemetry. + * + *

    The underlying implementation uses {@link java.util.concurrent.ThreadLocalRandom} for + * randomness but may change in the future. + */ + static IdGenerator random() { + // note: check borrowed from OkHttp's check for Android. + if ("Dalvik".equals(System.getProperty("java.vm.name"))) { + return AndroidFriendlyRandomIdGenerator.INSTANCE; + } + return RandomIdGenerator.INSTANCE; + } + + /** + * Generates a new valid {@code SpanId}. + * + * @return a new valid {@code SpanId}. + */ + String generateSpanId(); + + /** + * Generates a new valid {@code TraceId}. + * + * @return a new valid {@code TraceId}. + */ + String generateTraceId(); +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/MultiSpanProcessor.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/MultiSpanProcessor.java new file mode 100644 index 000000000..37abdc234 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/MultiSpanProcessor.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Implementation of the {@code SpanProcessor} that simply forwards all received events to a list of + * {@code SpanProcessor}s. + */ +final class MultiSpanProcessor implements SpanProcessor { + private final List spanProcessorsStart; + private final List spanProcessorsEnd; + private final List spanProcessorsAll; + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + + /** + * Creates a new {@code MultiSpanProcessor}. + * + * @param spanProcessorList the {@code List} of {@code SpanProcessor}s. + * @return a new {@code MultiSpanProcessor}. + * @throws NullPointerException if the {@code spanProcessorList} is {@code null}. + */ + static SpanProcessor create(List spanProcessorList) { + return new MultiSpanProcessor( + new ArrayList<>(Objects.requireNonNull(spanProcessorList, "spanProcessorList"))); + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan readableSpan) { + for (SpanProcessor spanProcessor : spanProcessorsStart) { + spanProcessor.onStart(parentContext, readableSpan); + } + } + + @Override + public boolean isStartRequired() { + return !spanProcessorsStart.isEmpty(); + } + + @Override + public void onEnd(ReadableSpan readableSpan) { + for (SpanProcessor spanProcessor : spanProcessorsEnd) { + spanProcessor.onEnd(readableSpan); + } + } + + @Override + public boolean isEndRequired() { + return !spanProcessorsEnd.isEmpty(); + } + + @Override + public CompletableResultCode shutdown() { + if (isShutdown.getAndSet(true)) { + return CompletableResultCode.ofSuccess(); + } + List results = new ArrayList<>(spanProcessorsAll.size()); + for (SpanProcessor spanProcessor : spanProcessorsAll) { + results.add(spanProcessor.shutdown()); + } + return CompletableResultCode.ofAll(results); + } + + @Override + public CompletableResultCode forceFlush() { + List results = new ArrayList<>(spanProcessorsAll.size()); + for (SpanProcessor spanProcessor : spanProcessorsAll) { + results.add(spanProcessor.forceFlush()); + } + return CompletableResultCode.ofAll(results); + } + + private MultiSpanProcessor(List spanProcessors) { + this.spanProcessorsAll = spanProcessors; + this.spanProcessorsStart = new ArrayList<>(spanProcessorsAll.size()); + this.spanProcessorsEnd = new ArrayList<>(spanProcessorsAll.size()); + for (SpanProcessor spanProcessor : spanProcessorsAll) { + if (spanProcessor.isStartRequired()) { + spanProcessorsStart.add(spanProcessor); + } + if (spanProcessor.isEndRequired()) { + spanProcessorsEnd.add(spanProcessor); + } + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/NoopSpanProcessor.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/NoopSpanProcessor.java new file mode 100644 index 000000000..63c482d0a --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/NoopSpanProcessor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.context.Context; + +final class NoopSpanProcessor implements SpanProcessor { + private static final NoopSpanProcessor INSTANCE = new NoopSpanProcessor(); + + static SpanProcessor getInstance() { + return INSTANCE; + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) {} + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan span) {} + + @Override + public boolean isEndRequired() { + return false; + } + + private NoopSpanProcessor() {} +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java new file mode 100644 index 000000000..2757a3e15 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceId; +import java.util.concurrent.ThreadLocalRandom; + +enum RandomIdGenerator implements IdGenerator { + INSTANCE; + + private static final long INVALID_ID = 0; + + @Override + public String generateSpanId() { + long id; + ThreadLocalRandom random = ThreadLocalRandom.current(); + do { + id = random.nextLong(); + } while (id == INVALID_ID); + return SpanId.fromLong(id); + } + + @Override + public String generateTraceId() { + long idHi; + long idLo; + ThreadLocalRandom random = ThreadLocalRandom.current(); + do { + idHi = random.nextLong(); + idLo = random.nextLong(); + } while (idHi == INVALID_ID && idLo == INVALID_ID); + return TraceId.fromLongs(idHi, idLo); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ReadWriteSpan.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ReadWriteSpan.java new file mode 100644 index 000000000..fce9f0330 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ReadWriteSpan.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.Span; + +/** + * A combination of the write methods from the {@link Span} interface and the read methods from the + * {@link ReadableSpan} interface. + */ +public interface ReadWriteSpan extends Span, ReadableSpan {} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ReadableSpan.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ReadableSpan.java new file mode 100644 index 000000000..20cc052cc --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ReadableSpan.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.trace.data.SpanData; + +/** The extend Span interface used by the SDK. */ +public interface ReadableSpan { + + /** + * Returns the {@link SpanContext} of the {@code Span}. + * + *

    Equivalent with {@link Span#getSpanContext()}. + * + * @return the {@link SpanContext} of the {@code Span}. + */ + SpanContext getSpanContext(); + + /** + * Returns the name of the {@code Span}. + * + *

    The name can be changed during the lifetime of the Span by using the {@link + * Span#updateName(String)} so this value cannot be cached. + * + * @return the name of the {@code Span}. + */ + String getName(); + + /** + * This converts this instance into an immutable SpanData instance, for use in export. + * + * @return an immutable {@link SpanData} instance. + */ + SpanData toSpanData(); + + /** + * Returns the instrumentation library specified when creating the tracer which produced this + * span. + * + * @return an instance of {@link InstrumentationLibraryInfo} describing the instrumentation + * library + */ + InstrumentationLibraryInfo getInstrumentationLibraryInfo(); + + /** + * Returns whether this Span has already been ended. + * + * @return {@code true} if the span has already been ended, {@code false} if not. + */ + boolean hasEnded(); + + /** + * 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. + */ + long getLatencyNanos(); + + /** + * Returns the kind of the span. + * + * @return the kind of the span. + */ + SpanKind getKind(); +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RecordEventsReadableSpan.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RecordEventsReadableSpan.java new file mode 100644 index 000000000..75eb42d80 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RecordEventsReadableSpan.java @@ -0,0 +1,549 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** Implementation for the {@link Span} class that records trace events. */ +@ThreadSafe +final class RecordEventsReadableSpan implements ReadWriteSpan { + + private static final Logger logger = Logger.getLogger(RecordEventsReadableSpan.class.getName()); + + // The config used when constructing this Span. + private final SpanLimits spanLimits; + // Contains the identifiers associated with this Span. + private final SpanContext context; + // The parent SpanContext of this span. Invalid if this is a root span. + private final SpanContext parentSpanContext; + // Handler called when the span starts and ends. + private final SpanProcessor spanProcessor; + // The displayed name of the span. + // List of recorded links to parent and child spans. + private final List links; + // Number of links recorded. + private final int totalRecordedLinks; + // The kind of the span. + private final SpanKind kind; + // The clock used to get the time. + private final Clock clock; + // The resource associated with this span. + private final Resource resource; + // instrumentation library of the named tracer which created this span + private final InstrumentationLibraryInfo instrumentationLibraryInfo; + // The start time of the span. + private final long startEpochNanos; + // Lock used to internally guard the mutable state of this instance + private final Object lock = new Object(); + + @GuardedBy("lock") + private String name; + // Set of recorded attributes. DO NOT CALL any other method that changes the ordering of events. + @GuardedBy("lock") + @Nullable + private AttributesMap attributes; + // List of recorded events. + @GuardedBy("lock") + private final List events; + // Number of events recorded. + @GuardedBy("lock") + private int totalRecordedEvents = 0; + // The status of the span. + @GuardedBy("lock") + @Nullable + private StatusData status = StatusData.unset(); + // The end time of the span. + @GuardedBy("lock") + private long endEpochNanos; + // True if the span is ended. + @GuardedBy("lock") + private boolean hasEnded; + + private RecordEventsReadableSpan( + SpanContext context, + String name, + InstrumentationLibraryInfo instrumentationLibraryInfo, + SpanKind kind, + SpanContext parentSpanContext, + SpanLimits spanLimits, + SpanProcessor spanProcessor, + Clock clock, + Resource resource, + @Nullable AttributesMap attributes, + List links, + int totalRecordedLinks, + long startEpochNanos) { + this.context = context; + this.instrumentationLibraryInfo = instrumentationLibraryInfo; + this.parentSpanContext = parentSpanContext; + this.links = links; + this.totalRecordedLinks = totalRecordedLinks; + this.name = name; + this.kind = kind; + this.spanProcessor = spanProcessor; + this.resource = resource; + this.hasEnded = false; + this.clock = clock; + this.startEpochNanos = startEpochNanos; + this.attributes = attributes; + this.events = new ArrayList<>(); + this.spanLimits = spanLimits; + } + + /** + * 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 kind the span kind. + * @param parentSpanContext the parent span context, or {@link SpanContext#getInvalid()} if this + * span is a root span. + * @param spanLimits trace parameters like sampler and probability. + * @param spanProcessor handler called when the span starts and ends. + * @param clock the clock used to get the time. + * @param resource the resource associated with this span. + * @param attributes the attributes set during span creation. + * @param links the links set during span creation, may be truncated. The list MUST be immutable. + * @return a new and started span. + */ + static RecordEventsReadableSpan startSpan( + SpanContext context, + String name, + InstrumentationLibraryInfo instrumentationLibraryInfo, + SpanKind kind, + @Nullable SpanContext parentSpanContext, + @Nonnull Context parentContext, + SpanLimits spanLimits, + SpanProcessor spanProcessor, + Clock clock, + Resource resource, + AttributesMap attributes, + List links, + int totalRecordedLinks, + long startEpochNanos) { + RecordEventsReadableSpan span = + new RecordEventsReadableSpan( + context, + name, + instrumentationLibraryInfo, + kind, + parentSpanContext, + spanLimits, + spanProcessor, + clock, + resource, + attributes, + links, + totalRecordedLinks, + startEpochNanos == 0 ? clock.now() : startEpochNanos); + // Call onStart here instead of calling in the constructor to make sure the span is completely + // initialized. + spanProcessor.onStart(parentContext, span); + return span; + } + + @Override + public SpanData toSpanData() { + // Copy within synchronized context + synchronized (lock) { + return SpanWrapper.create( + this, + links, + getImmutableTimedEvents(), + getImmutableAttributes(), + (attributes == null) ? 0 : attributes.getTotalAddedValues(), + totalRecordedEvents, + getSpanDataStatus(), + name, + endEpochNanos, + hasEnded); + } + } + + @Override + public boolean hasEnded() { + synchronized (lock) { + return hasEnded; + } + } + + @Override + public SpanContext getSpanContext() { + return context; + } + + /** + * Returns the name of the {@code Span}. + * + * @return the name of the {@code Span}. + */ + @Override + public String getName() { + synchronized (lock) { + return name; + } + } + + /** + * Returns the instrumentation library specified when creating the tracer which produced this + * span. + * + * @return an instance of {@link InstrumentationLibraryInfo} describing the instrumentation + * library + */ + @Override + public InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return instrumentationLibraryInfo; + } + + /** + * 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. + */ + @Override + public long getLatencyNanos() { + synchronized (lock) { + return (hasEnded ? endEpochNanos : clock.now()) - startEpochNanos; + } + } + + /** + * Returns the {@code Clock} used by this {@code Span}. + * + * @return the {@code Clock} used by this {@code Span}. + */ + Clock getClock() { + return clock; + } + + @Override + public ReadWriteSpan setAttribute(AttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + synchronized (lock) { + if (hasEnded) { + logger.log(Level.FINE, "Calling setAttribute() on an ended Span."); + return this; + } + if (attributes == null) { + attributes = new AttributesMap(spanLimits.getMaxNumberOfAttributes()); + } + + attributes.put(key, value); + } + return this; + } + + @Override + public ReadWriteSpan addEvent(String name) { + if (name == null) { + return this; + } + addTimedEvent(EventData.create(clock.now(), name, Attributes.empty(), 0)); + return this; + } + + @Override + public ReadWriteSpan addEvent(String name, long timestamp, TimeUnit unit) { + if (name == null || unit == null) { + return this; + } + addTimedEvent(EventData.create(unit.toNanos(timestamp), name, Attributes.empty(), 0)); + return this; + } + + @Override + public ReadWriteSpan addEvent(String name, Attributes attributes) { + if (name == null) { + return this; + } + if (attributes == null) { + attributes = Attributes.empty(); + } + int totalAttributeCount = attributes.size(); + addTimedEvent( + EventData.create( + clock.now(), + name, + applyAttributesLimit(attributes, spanLimits.getMaxNumberOfAttributesPerEvent()), + totalAttributeCount)); + return this; + } + + @Override + public ReadWriteSpan addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + if (name == null || unit == null) { + return this; + } + if (attributes == null) { + attributes = Attributes.empty(); + } + int totalAttributeCount = attributes.size(); + addTimedEvent( + EventData.create( + unit.toNanos(timestamp), + name, + applyAttributesLimit(attributes, spanLimits.getMaxNumberOfAttributesPerEvent()), + totalAttributeCount)); + return this; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static Attributes applyAttributesLimit(final Attributes attributes, final int limit) { + if (attributes.isEmpty() || attributes.size() <= limit) { + return attributes; + } + + AttributesBuilder result = Attributes.builder(); + int i = 0; + for (Map.Entry, Object> entry : attributes.asMap().entrySet()) { + if (i >= limit) { + break; + } + result.put((AttributeKey) entry.getKey(), entry.getValue()); + i++; + } + return result.build(); + } + + private void addTimedEvent(EventData timedEvent) { + synchronized (lock) { + if (hasEnded) { + logger.log(Level.FINE, "Calling addEvent() on an ended Span."); + return; + } + if (events.size() < spanLimits.getMaxNumberOfEvents()) { + events.add(timedEvent); + } + totalRecordedEvents++; + } + } + + @Override + public ReadWriteSpan setStatus(StatusCode statusCode, @Nullable String description) { + if (statusCode == null) { + return this; + } + synchronized (lock) { + if (hasEnded) { + logger.log(Level.FINE, "Calling setStatus() on an ended Span."); + return this; + } + this.status = StatusData.create(statusCode, description); + } + return this; + } + + @Override + public ReadWriteSpan recordException(Throwable exception) { + recordException(exception, null); + return this; + } + + @Override + public ReadWriteSpan recordException(Throwable exception, Attributes additionalAttributes) { + if (exception == null) { + return this; + } + long timestampNanos = clock.now(); + + AttributesBuilder attributes = Attributes.builder(); + attributes.put(SemanticAttributes.EXCEPTION_TYPE, exception.getClass().getCanonicalName()); + if (exception.getMessage() != null) { + attributes.put(SemanticAttributes.EXCEPTION_MESSAGE, exception.getMessage()); + } + StringWriter writer = new StringWriter(); + exception.printStackTrace(new PrintWriter(writer)); + attributes.put(SemanticAttributes.EXCEPTION_STACKTRACE, writer.toString()); + + if (additionalAttributes != null) { + attributes.putAll(additionalAttributes); + } + + addEvent( + SemanticAttributes.EXCEPTION_EVENT_NAME, + attributes.build(), + timestampNanos, + TimeUnit.NANOSECONDS); + return this; + } + + @Override + public ReadWriteSpan updateName(String name) { + if (name == null) { + return this; + } + synchronized (lock) { + if (hasEnded) { + logger.log(Level.FINE, "Calling updateName() on an ended Span."); + return this; + } + this.name = name; + } + return this; + } + + @Override + public void end() { + endInternal(clock.now()); + } + + @Override + public void end(long timestamp, TimeUnit unit) { + if (unit == null) { + unit = TimeUnit.NANOSECONDS; + } + endInternal(timestamp == 0 ? clock.now() : unit.toNanos(timestamp)); + } + + private void endInternal(long endEpochNanos) { + synchronized (lock) { + if (hasEnded) { + logger.log(Level.FINE, "Calling end() on an ended Span."); + return; + } + this.endEpochNanos = endEpochNanos; + hasEnded = true; + } + spanProcessor.onEnd(this); + } + + @Override + public boolean isRecording() { + synchronized (lock) { + return !hasEnded; + } + } + + @GuardedBy("lock") + private StatusData getSpanDataStatus() { + synchronized (lock) { + return status; + } + } + + SpanContext getParentSpanContext() { + return parentSpanContext; + } + + Resource getResource() { + return resource; + } + + @Override + public SpanKind getKind() { + return kind; + } + + long getStartEpochNanos() { + return startEpochNanos; + } + + int getTotalRecordedLinks() { + return totalRecordedLinks; + } + + @GuardedBy("lock") + private List getImmutableTimedEvents() { + if (events.isEmpty()) { + return Collections.emptyList(); + } + + // if the span has ended, then the events are unmodifiable + // so we can return them directly and save copying all the data. + if (hasEnded) { + return Collections.unmodifiableList(events); + } + + return Collections.unmodifiableList(new ArrayList<>(events)); + } + + @GuardedBy("lock") + private Attributes getImmutableAttributes() { + if (attributes == null || attributes.isEmpty()) { + return Attributes.empty(); + } + // if the span has ended, then the attributes are unmodifiable, + // so we can return them directly and save copying all the data. + if (hasEnded) { + return attributes; + } + // otherwise, make a copy of the data into an immutable container. + return attributes.immutableCopy(); + } + + @Override + public String toString() { + String name; + String attributes; + String status; + long totalRecordedEvents; + long endEpochNanos; + synchronized (lock) { + name = this.name; + attributes = String.valueOf(this.attributes); + status = String.valueOf(this.status); + totalRecordedEvents = this.totalRecordedEvents; + endEpochNanos = this.endEpochNanos; + } + StringBuilder sb = new StringBuilder(); + sb.append("RecordEventsReadableSpan{traceId="); + sb.append(context.getTraceId()); + sb.append(", spanId="); + sb.append(context.getSpanId()); + sb.append(", parentSpanContext="); + sb.append(parentSpanContext); + sb.append(", name="); + sb.append(name); + sb.append(", kind="); + sb.append(kind); + sb.append(", attributes="); + sb.append(attributes); + sb.append(", status="); + sb.append(status); + sb.append(", totalRecordedEvents="); + sb.append(totalRecordedEvents); + sb.append(", totalRecordedLinks="); + sb.append(totalRecordedLinks); + sb.append(", startEpochNanos="); + sb.append(startEpochNanos); + sb.append(", endEpochNanos="); + sb.append(endEpochNanos); + sb.append("}"); + return sb.toString(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java new file mode 100644 index 000000000..563ecc043 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java @@ -0,0 +1,264 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.HeraContext; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.MonotonicClock; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +/** {@link SdkSpanBuilder} is SDK implementation of {@link SpanBuilder}. */ +final class SdkSpanBuilder implements SpanBuilder { + + private final String spanName; + private final InstrumentationLibraryInfo instrumentationLibraryInfo; + private final TracerSharedState tracerSharedState; + private final SpanLimits spanLimits; + + @Nullable private Context parent; + private SpanKind spanKind = SpanKind.INTERNAL; + @Nullable private AttributesMap attributes; + @Nullable private List links; + private int totalNumberOfLinksAdded = 0; + private long startEpochNanos = 0; + private boolean isRootSpan; + + SdkSpanBuilder( + String spanName, + InstrumentationLibraryInfo instrumentationLibraryInfo, + TracerSharedState tracerSharedState, + SpanLimits spanLimits) { + this.spanName = spanName; + this.instrumentationLibraryInfo = instrumentationLibraryInfo; + this.tracerSharedState = tracerSharedState; + this.spanLimits = spanLimits; + } + + @Override + public SpanBuilder setParent(Context context) { + if (context == null) { + return this; + } + this.isRootSpan = false; + this.parent = context; + return this; + } + + @Override + public SpanBuilder setNoParent() { + this.isRootSpan = true; + this.parent = null; + return this; + } + + @Override + public SpanBuilder setSpanKind(SpanKind spanKind) { + if (spanKind == null) { + return this; + } + this.spanKind = spanKind; + return this; + } + + @Override + public SpanBuilder addLink(SpanContext spanContext) { + if (spanContext == null || !spanContext.isValid()) { + return this; + } + addLink(LinkData.create(spanContext)); + return this; + } + + @Override + public SpanBuilder addLink(SpanContext spanContext, Attributes attributes) { + if (spanContext == null || !spanContext.isValid()) { + return this; + } + if (attributes == null) { + attributes = Attributes.empty(); + } + int totalAttributeCount = attributes.size(); + addLink( + LinkData.create( + spanContext, + RecordEventsReadableSpan.applyAttributesLimit( + attributes, spanLimits.getMaxNumberOfAttributesPerLink()), + totalAttributeCount)); + return this; + } + + private void addLink(LinkData link) { + totalNumberOfLinksAdded++; + if (links == null) { + links = new ArrayList<>(spanLimits.getMaxNumberOfLinks()); + } + + // don't bother doing anything with any links beyond the max. + if (links.size() == spanLimits.getMaxNumberOfLinks()) { + return; + } + + links.add(link); + } + + @Override + public SpanBuilder setAttribute(String key, String value) { + return setAttribute(stringKey(key), value); + } + + @Override + public SpanBuilder setAttribute(String key, long value) { + return setAttribute(longKey(key), value); + } + + @Override + public SpanBuilder setAttribute(String key, double value) { + return setAttribute(doubleKey(key), value); + } + + @Override + public SpanBuilder setAttribute(String key, boolean value) { + return setAttribute(booleanKey(key), value); + } + + @Override + public SpanBuilder setAttribute(AttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + if (attributes == null) { + attributes = new AttributesMap(spanLimits.getMaxNumberOfAttributes()); + } + + attributes.put(key, value); + return this; + } + + @Override + public SpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { + if (startTimestamp < 0 || unit == null) { + return this; + } + startEpochNanos = unit.toNanos(startTimestamp); + return this; + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public Span startSpan() { + final Context parentContext = + isRootSpan ? Context.root() : parent == null ? Context.current() : parent; + final Span parentSpan = Span.fromContext(parentContext); + final SpanContext parentSpanContext = parentSpan.getSpanContext(); + String traceId; + Map heraContext = HeraContext.isValid(parentSpanContext.getHeraContext()) ? parentSpanContext.getHeraContext() : HeraContext.getInvalid(); + IdGenerator idGenerator = tracerSharedState.getIdGenerator(); + String spanId = idGenerator.generateSpanId(); + if (!parentSpanContext.isValid()) { + // New root span. + traceId = idGenerator.generateTraceId(); + } else { + // New child span. + traceId = parentSpanContext.getTraceId(); + } + List immutableLinks = + links == null ? Collections.emptyList() : Collections.unmodifiableList(links); + // Avoid any possibility to modify the links list by adding links to the Builder after the + // startSpan is called. If that happens all the links will be added in a new list. + links = null; + Attributes immutableAttributes = attributes == null ? Attributes.empty() : attributes; + SamplingResult samplingResult = + tracerSharedState + .getSampler() + .shouldSample( + parentContext, traceId, spanName, spanKind, immutableAttributes, immutableLinks); + SamplingDecision samplingDecision = samplingResult.getDecision(); + + TraceState samplingResultTraceState = + samplingResult.getUpdatedTraceState(parentSpanContext.getTraceState()); + SpanContext spanContext = + SpanContext.create( + traceId, + spanId, + isSampled(samplingDecision) ? TraceFlags.getSampled() : TraceFlags.getDefault(), + samplingResultTraceState, heraContext); + + if (!isRecording(samplingDecision)) { + return Span.wrap(spanContext); + } + Attributes samplingAttributes = samplingResult.getAttributes(); + if (!samplingAttributes.isEmpty()) { + if (attributes == null) { + attributes = new AttributesMap(spanLimits.getMaxNumberOfAttributes()); + } + samplingAttributes.forEach((key, value) -> attributes.put((AttributeKey) key, value)); + } + + // Avoid any possibility to modify the attributes by adding attributes to the Builder after the + // startSpan is called. If that happens all the attributes will be added in a new map. + AttributesMap recordedAttributes = attributes; + attributes = null; + + return RecordEventsReadableSpan.startSpan( + spanContext, + spanName, + instrumentationLibraryInfo, + spanKind, + parentSpanContext, + parentContext, + spanLimits, + tracerSharedState.getActiveSpanProcessor(), + getClock(parentSpan, tracerSharedState.getClock()), + tracerSharedState.getResource(), + recordedAttributes, + immutableLinks, + totalNumberOfLinksAdded, + startEpochNanos); + } + + private static Clock getClock(Span parent, Clock clock) { + if (parent instanceof RecordEventsReadableSpan) { + RecordEventsReadableSpan parentRecordEventsSpan = (RecordEventsReadableSpan) parent; + return parentRecordEventsSpan.getClock(); + } else { + return MonotonicClock.create(clock); + } + } + + // Visible for testing + static boolean isRecording(SamplingDecision decision) { + return SamplingDecision.RECORD_ONLY.equals(decision) + || SamplingDecision.RECORD_AND_SAMPLE.equals(decision); + } + + // Visible for testing + static boolean isSampled(SamplingDecision decision) { + return SamplingDecision.RECORD_AND_SAMPLE.equals(decision); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracer.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracer.java new file mode 100644 index 000000000..d9aa3e75e --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracer.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; + +/** {@link SdkTracer} is SDK implementation of {@link Tracer}. */ +final class SdkTracer implements Tracer { + static final String FALLBACK_SPAN_NAME = ""; + + private final TracerSharedState sharedState; + private final InstrumentationLibraryInfo instrumentationLibraryInfo; + + SdkTracer(TracerSharedState sharedState, InstrumentationLibraryInfo instrumentationLibraryInfo) { + this.sharedState = sharedState; + this.instrumentationLibraryInfo = instrumentationLibraryInfo; + } + + @Override + public SpanBuilder spanBuilder(String spanName) { + if (spanName == null || spanName.trim().isEmpty()) { + spanName = FALLBACK_SPAN_NAME; + } + if (sharedState.hasBeenShutdown()) { + return TracerProvider.noop() + .get(instrumentationLibraryInfo.getName(), instrumentationLibraryInfo.getVersion()) + .spanBuilder(spanName); + } + return new SdkSpanBuilder( + spanName, instrumentationLibraryInfo, sharedState, sharedState.getSpanLimits()); + } + + /** + * Returns the instrumentation library specified when creating the tracer. + * + * @return an instance of {@link InstrumentationLibraryInfo} + */ + InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return instrumentationLibraryInfo; + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java new file mode 100644 index 000000000..1f6451db0 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ComponentRegistry; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.io.Closeable; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * {@code Tracer} provider implementation for {@link TracerProvider}. + * + *

    This class is not intended to be used in application code and it is used only by {@link + * OpenTelemetry}. However, if you need a custom implementation of the factory, you can create one + * as needed. + */ +public final class SdkTracerProvider implements TracerProvider, Closeable { + private static final Logger logger = Logger.getLogger(SdkTracerProvider.class.getName()); + static final String DEFAULT_TRACER_NAME = ""; + private final TracerSharedState sharedState; + private final ComponentRegistry tracerSdkComponentRegistry; + + /** + * Returns a new {@link SdkTracerProviderBuilder} for {@link SdkTracerProvider}. + * + * @return a new {@link SdkTracerProviderBuilder} for {@link SdkTracerProvider}. + */ + public static SdkTracerProviderBuilder builder() { + return new SdkTracerProviderBuilder(); + } + + SdkTracerProvider( + Clock clock, + IdGenerator idsGenerator, + Resource resource, + Supplier spanLimitsSupplier, + Sampler sampler, + List spanProcessors) { + this.sharedState = + new TracerSharedState( + clock, idsGenerator, resource, spanLimitsSupplier, sampler, spanProcessors); + this.tracerSdkComponentRegistry = + new ComponentRegistry<>( + instrumentationLibraryInfo -> new SdkTracer(sharedState, instrumentationLibraryInfo)); + } + + @Override + public Tracer get(String instrumentationName) { + return get(instrumentationName, null); + } + + @Override + public Tracer get(String instrumentationName, @Nullable String instrumentationVersion) { + // Per the spec, both null and empty are "invalid" and a default value should be used. + if (instrumentationName == null || instrumentationName.isEmpty()) { + logger.fine("Tracer requested without instrumentation name."); + instrumentationName = DEFAULT_TRACER_NAME; + } + return tracerSdkComponentRegistry.get(instrumentationName, instrumentationVersion); + } + + /** Returns the {@link SpanLimits} that are currently applied to created spans. */ + public SpanLimits getSpanLimits() { + return sharedState.getSpanLimits(); + } + + /** Returns the configured {@link Sampler}. */ + public Sampler getSampler() { + return sharedState.getSampler(); + } + + /** + * Attempts to stop all the activity for this {@link Tracer}. Calls {@link + * SpanProcessor#shutdown()} for all registered {@link SpanProcessor}s. + * + *

    The returned {@link CompletableResultCode} will be completed when all the Spans are + * processed. + * + *

    After this is called, newly created {@code Span}s will be no-ops. + * + *

    After this is called, further attempts at re-using or reconfiguring this instance will + * result in undefined behavior. It should be considered a terminal operation for the SDK + * implementation. + * + * @return a {@link CompletableResultCode} which is completed when all the span processors have + * been shut down. + */ + public CompletableResultCode shutdown() { + if (sharedState.hasBeenShutdown()) { + logger.log(Level.WARNING, "Calling shutdown() multiple times."); + return CompletableResultCode.ofSuccess(); + } + return sharedState.shutdown(); + } + + /** + * Requests the active span processor to process all span events that have not yet been processed + * and returns a {@link CompletableResultCode} which is completed when the flush is finished. + * + * @see SpanProcessor#forceFlush() + */ + public CompletableResultCode forceFlush() { + return sharedState.getActiveSpanProcessor().forceFlush(); + } + + /** + * Attempts to stop all the activity for this {@link Tracer}. Calls {@link + * SpanProcessor#shutdown()} for all registered {@link SpanProcessor}s. + * + *

    This operation may block until all the Spans are processed. Must be called before turning + * off the main application to ensure all data are processed and exported. + * + *

    After this is called, newly created {@code Span}s will be no-ops. + * + *

    After this is called, further attempts at re-using or reconfiguring this instance will + * result in undefined behavior. It should be considered a terminal operation for the SDK + * implementation. + */ + @Override + public void close() { + shutdown().join(10, TimeUnit.SECONDS); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java new file mode 100644 index 000000000..cb47bf6f1 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java @@ -0,0 +1,149 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.internal.SystemClock; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** Builder of {@link SdkTracerProvider}. */ +public final class SdkTracerProviderBuilder { + private static final Sampler DEFAULT_SAMPLER = Sampler.parentBased(Sampler.alwaysOn()); + + private final List spanProcessors = new ArrayList<>(); + + private Clock clock = SystemClock.getInstance(); + private IdGenerator idsGenerator = IdGenerator.random(); + private Resource resource = Resource.getDefault(); + private Supplier spanLimitsSupplier = SpanLimits::getDefault; + private Sampler sampler = DEFAULT_SAMPLER; + + /** + * Assign a {@link Clock}. {@link Clock} will be used each time a {@link + * io.opentelemetry.api.trace.Span} is started, ended or any event is recorded. + * + *

    The {@code clock} must be thread-safe and return immediately (no remote calls, as contention + * free as possible). + * + * @param clock The clock to use for all temporal needs. + * @return this + */ + public SdkTracerProviderBuilder setClock(Clock clock) { + requireNonNull(clock, "clock"); + this.clock = clock; + return this; + } + + /** + * Assign an {@link IdGenerator}. {@link IdGenerator} will be used each time a {@link + * io.opentelemetry.api.trace.Span} is started. + * + *

    The {@code idGenerator} must be thread-safe and return immediately (no remote calls, as + * contention free as possible). + * + * @param idGenerator A generator for trace and span ids. + * @return this + */ + public SdkTracerProviderBuilder setIdGenerator(IdGenerator idGenerator) { + requireNonNull(idGenerator, "idGenerator"); + this.idsGenerator = idGenerator; + return this; + } + + /** + * Assign a {@link Resource} to be attached to all Spans created by Tracers. + * + * @param resource A Resource implementation. + * @return this + */ + public SdkTracerProviderBuilder setResource(Resource resource) { + requireNonNull(resource, "resource"); + this.resource = resource; + return this; + } + + /** + * Assign an initial {@link SpanLimits} that should be used with this SDK. + * + *

    This method is equivalent to calling {@link #setSpanLimits(Supplier)} like this {@code + * #setSpanLimits(() -> spanLimits)}. + * + * @param spanLimits the limits that will be used for every {@link + * io.opentelemetry.api.trace.Span}. + * @return this + */ + public SdkTracerProviderBuilder setSpanLimits(SpanLimits spanLimits) { + requireNonNull(spanLimits, "spanLimits"); + this.spanLimitsSupplier = () -> spanLimits; + return this; + } + + /** + * Assign a {@link Supplier} of {@link SpanLimits}. {@link SpanLimits} will be retrieved each time + * a {@link io.opentelemetry.api.trace.Span} is started. + * + *

    The {@code spanLimitsSupplier} must be thread-safe and return immediately (no remote calls, + * as contention free as possible). + * + * @param spanLimitsSupplier the supplier that will be used to retrieve the {@link SpanLimits} for + * every {@link io.opentelemetry.api.trace.Span}. + * @return this + */ + public SdkTracerProviderBuilder setSpanLimits(Supplier spanLimitsSupplier) { + requireNonNull(spanLimitsSupplier, "spanLimitsSupplier"); + this.spanLimitsSupplier = spanLimitsSupplier; + return this; + } + + /** + * Assign a {@link Sampler} to use for sampling traces. {@link Sampler} will be called each time a + * {@link io.opentelemetry.api.trace.Span} is started. + * + *

    The {@code sampler} must be thread-safe and return immediately (no remote calls, as + * contention free as possible). + * + * @param sampler the {@link Sampler} to use for sampling traces. + * @return this + */ + public SdkTracerProviderBuilder setSampler(Sampler sampler) { + requireNonNull(sampler, "sampler"); + this.sampler = sampler; + return this; + } + + /** + * Add a SpanProcessor to the span pipeline that will be built. {@link SpanProcessor} will be + * called each time a {@link io.opentelemetry.api.trace.Span} is started or ended. + * + *

    The {@code spanProcessor} must be thread-safe and return immediately (no remote calls, as + * contention free as possible). + * + * @param spanProcessor the processor to be added to the processing pipeline. + * @return this + */ + public SdkTracerProviderBuilder addSpanProcessor(SpanProcessor spanProcessor) { + spanProcessors.add(spanProcessor); + return this; + } + + /** + * Create a new TraceSdkProvider instance. + * + * @return An initialized TraceSdkProvider. + */ + public SdkTracerProvider build() { + return new SdkTracerProvider( + clock, idsGenerator, resource, spanLimitsSupplier, sampler, spanProcessors); + } + + SdkTracerProviderBuilder() {} +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanLimits.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanLimits.java new file mode 100644 index 000000000..9355679b5 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanLimits.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.trace.Span; +import javax.annotation.concurrent.Immutable; + +/** + * Class that holds limits enforced during span recording. + * + *

    Note: To allow dynamic updates of {@link SpanLimits} you should register a {@link + * java.util.function.Supplier} with {@link + * io.opentelemetry.sdk.trace.SdkTracerProviderBuilder#setSpanLimits(java.util.function.Supplier)} + * which supplies dynamic configs when queried. + */ +@AutoValue +@Immutable +public abstract class SpanLimits { + + private static final SpanLimits DEFAULT = new SpanLimitsBuilder().build(); + + /** Returns the default {@link SpanLimits}. */ + public static SpanLimits getDefault() { + return DEFAULT; + } + + /** Returns a new {@link SpanLimitsBuilder} to construct a {@link SpanLimits}. */ + public static SpanLimitsBuilder builder() { + return new SpanLimitsBuilder(); + } + + static SpanLimits create( + int maxNumAttributes, + int maxNumEvents, + int maxNumLinks, + int maxNumAttributesPerEvent, + int maxNumAttributesPerLink) { + return new AutoValue_SpanLimits( + maxNumAttributes, + maxNumEvents, + maxNumLinks, + maxNumAttributesPerEvent, + maxNumAttributesPerLink); + } + + /** + * Returns the global default max number of attributes per {@link Span}. + * + * @return the global default max number of attributes per {@link Span}. + */ + public abstract int getMaxNumberOfAttributes(); + + /** + * Returns the global default max number of events per {@link Span}. + * + * @return the global default max number of events per {@code Span}. + */ + public abstract int getMaxNumberOfEvents(); + + /** + * Returns the global default max number of links per {@link Span}. + * + * @return the global default max number of links per {@code Span}. + */ + public abstract int getMaxNumberOfLinks(); + + /** + * Returns the global default max number of attributes per event. + * + * @return the global default max number of attributes per event. + */ + public abstract int getMaxNumberOfAttributesPerEvent(); + + /** + * Returns the global default max number of attributes per link. + * + * @return the global default max number of attributes per link. + */ + public abstract int getMaxNumberOfAttributesPerLink(); + + /** + * Returns a {@link SpanLimitsBuilder} initialized to the same property values as the current + * instance. + * + * @return a {@link SpanLimitsBuilder} initialized to the same property values as the current + * instance. + */ + public SpanLimitsBuilder toBuilder() { + return new SpanLimitsBuilder() + .setMaxNumberOfAttributes(getMaxNumberOfAttributes()) + .setMaxNumberOfEvents(getMaxNumberOfEvents()) + .setMaxNumberOfLinks(getMaxNumberOfLinks()) + .setMaxNumberOfAttributesPerEvent(getMaxNumberOfAttributesPerEvent()) + .setMaxNumberOfAttributesPerLink(getMaxNumberOfAttributesPerLink()); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanLimitsBuilder.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanLimitsBuilder.java new file mode 100644 index 000000000..8acb37b5b --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanLimitsBuilder.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.internal.Utils; +import io.opentelemetry.api.trace.Span; + +/** Builder for {@link SpanLimits}. */ +public final class SpanLimitsBuilder { + + private static final int DEFAULT_SPAN_MAX_NUM_ATTRIBUTES = 128; + private static final int DEFAULT_SPAN_MAX_NUM_EVENTS = 128; + private static final int DEFAULT_SPAN_MAX_NUM_LINKS = 128; + private static final int DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_EVENT = 128; + private static final int DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_LINK = 128; + + private int maxNumAttributes = DEFAULT_SPAN_MAX_NUM_ATTRIBUTES; + private int maxNumEvents = DEFAULT_SPAN_MAX_NUM_EVENTS; + private int maxNumLinks = DEFAULT_SPAN_MAX_NUM_LINKS; + private int maxNumAttributesPerEvent = DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_EVENT; + private int maxNumAttributesPerLink = DEFAULT_SPAN_MAX_NUM_ATTRIBUTES_PER_LINK; + + SpanLimitsBuilder() {} + + /** + * 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. + */ + public SpanLimitsBuilder setMaxNumberOfAttributes(int maxNumberOfAttributes) { + Utils.checkArgument(maxNumberOfAttributes > 0, "maxNumberOfAttributes must be greater than 0"); + this.maxNumAttributes = maxNumberOfAttributes; + return this; + } + + /** + * Sets the global default max number of events per {@link Span}. + * + * @param maxNumberOfEvents the global default max number of events per {@link Span}. It must be + * positive otherwise {@link #build()} will throw an exception. + * @return this. + */ + public SpanLimitsBuilder setMaxNumberOfEvents(int maxNumberOfEvents) { + Utils.checkArgument(maxNumberOfEvents > 0, "maxNumberOfEvents must be greater than 0"); + this.maxNumEvents = maxNumberOfEvents; + return this; + } + + /** + * Sets the global default max number of links per {@link Span}. + * + * @param maxNumberOfLinks the global default max number of links per {@link Span}. It must be + * positive otherwise {@link #build()} will throw an exception. + * @return this. + */ + public SpanLimitsBuilder setMaxNumberOfLinks(int maxNumberOfLinks) { + Utils.checkArgument(maxNumberOfLinks > 0, "maxNumberOfLinks must be greater than 0"); + this.maxNumLinks = maxNumberOfLinks; + return this; + } + + /** + * Sets the global default max number of attributes per event. + * + * @param maxNumberOfAttributesPerEvent the global default max number of attributes per event. It + * must be positive otherwise {@link #build()} will throw an exception. + * @return this. + */ + public SpanLimitsBuilder setMaxNumberOfAttributesPerEvent(int maxNumberOfAttributesPerEvent) { + Utils.checkArgument( + maxNumberOfAttributesPerEvent > 0, "maxNumberOfAttributesPerEvent must be greater than 0"); + this.maxNumAttributesPerEvent = maxNumberOfAttributesPerEvent; + return this; + } + + /** + * Sets the global default max number of attributes per link. + * + * @param maxNumberOfAttributesPerLink the global default max number of attributes per link. It + * must be positive otherwise {@link #build()} will throw an exception. + * @return this. + */ + public SpanLimitsBuilder setMaxNumberOfAttributesPerLink(int maxNumberOfAttributesPerLink) { + Utils.checkArgument( + maxNumberOfAttributesPerLink > 0, "maxNumberOfAttributesPerLink must be greater than 0"); + this.maxNumAttributesPerLink = maxNumberOfAttributesPerLink; + return this; + } + + /** Builds and returns a {@link SpanLimits} with the values of this builder. */ + public SpanLimits build() { + return SpanLimits.create( + maxNumAttributes, + maxNumEvents, + maxNumLinks, + maxNumAttributesPerEvent, + maxNumAttributesPerLink); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanProcessor.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanProcessor.java new file mode 100644 index 000000000..28a4b86da --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanProcessor.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.ThreadSafe; + +/** + * SpanProcessor is the interface {@code TracerSdk} uses to allow synchronous hooks for when a + * {@code Span} is started or when a {@code Span} is ended. + */ +@ThreadSafe +public interface SpanProcessor extends Closeable { + + /** + * Returns a {@link SpanProcessor} which simply delegates all processing to the {@code processors} + * in order. + */ + static SpanProcessor composite(SpanProcessor... processors) { + return composite(Arrays.asList(processors)); + } + + /** + * Returns a {@link SpanProcessor} which simply delegates all processing to the {@code processors} + * in order. + */ + static SpanProcessor composite(Iterable processors) { + List processorsList = new ArrayList<>(); + for (SpanProcessor processor : processors) { + processorsList.add(processor); + } + if (processorsList.isEmpty()) { + return NoopSpanProcessor.getInstance(); + } + if (processorsList.size() == 1) { + return processorsList.get(0); + } + return MultiSpanProcessor.create(processorsList); + } + + /** + * Called when a {@link io.opentelemetry.api.trace.Span} is started, if the {@link + * Span#isRecording()} returns true. + * + *

    This method is called synchronously on the execution thread, should not throw or block the + * execution thread. + * + * @param parentContext the parent {@code Context} of the span that just started. + * @param span the {@code ReadableSpan} that just started. + */ + void onStart(Context parentContext, ReadWriteSpan span); + + /** + * Returns {@code true} if this {@link SpanProcessor} requires start events. + * + * @return {@code true} if this {@link SpanProcessor} requires start events. + */ + boolean isStartRequired(); + + /** + * Called when a {@link io.opentelemetry.api.trace.Span} is ended, if the {@link + * Span#isRecording()} returns true. + * + *

    This method is called synchronously on the execution thread, should not throw or block the + * execution thread. + * + * @param span the {@code ReadableSpan} that just ended. + */ + void onEnd(ReadableSpan span); + + /** + * Returns {@code true} if this {@link SpanProcessor} requires end events. + * + * @return {@code true} if this {@link SpanProcessor} requires end events. + */ + boolean isEndRequired(); + + /** + * Processes all span events that have not yet been processed and closes used resources. + * + * @return a {@link CompletableResultCode} which completes when shutdown is finished. + */ + default CompletableResultCode shutdown() { + return forceFlush(); + } + + /** + * Processes all span events that have not yet been processed. + * + * @return a {@link CompletableResultCode} which completes when currently queued spans are + * finished processing. + */ + default CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } + + /** + * Closes this {@link SpanProcessor} after processing any remaining spans, releasing any + * resources. + */ + @Override + default void close() { + shutdown().join(10, TimeUnit.SECONDS); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanWrapper.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanWrapper.java new file mode 100644 index 000000000..33641fe19 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanWrapper.java @@ -0,0 +1,214 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * Immutable class that stores {@link SpanData} based on a {@link RecordEventsReadableSpan}. + * + *

    This class stores a reference to a mutable {@link RecordEventsReadableSpan} ({@code delegate}) + * which it uses only the immutable parts from, and a copy of all the mutable parts. + * + *

    When adding a new field to {@link RecordEventsReadableSpan}, store a copy if and only if the + * field is mutable in the {@link RecordEventsReadableSpan}. Otherwise retrieve it from the + * referenced {@link RecordEventsReadableSpan}. + */ +@Immutable +@AutoValue +abstract class SpanWrapper implements SpanData { + abstract RecordEventsReadableSpan delegate(); + + abstract List resolvedLinks(); + + abstract List resolvedEvents(); + + abstract Attributes attributes(); + + abstract int totalAttributeCount(); + + abstract int totalRecordedEvents(); + + abstract StatusData status(); + + abstract String name(); + + abstract long endEpochNanos(); + + abstract boolean internalHasEnded(); + + /** + * Note: the collections that are passed into this creator method are assumed to be immutable to + * preserve the overall immutability of the class. + */ + static SpanWrapper create( + RecordEventsReadableSpan delegate, + List links, + List events, + Attributes attributes, + int totalAttributeCount, + int totalRecordedEvents, + StatusData status, + String name, + long endEpochNanos, + boolean hasEnded) { + return new AutoValue_SpanWrapper( + delegate, + links, + events, + attributes, + totalAttributeCount, + totalRecordedEvents, + status, + name, + endEpochNanos, + hasEnded); + } + + @Override + public SpanContext getSpanContext() { + return delegate().getSpanContext(); + } + + @Override + public SpanContext getParentSpanContext() { + return delegate().getParentSpanContext(); + } + + @Override + public Resource getResource() { + return delegate().getResource(); + } + + @Override + public InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return delegate().getInstrumentationLibraryInfo(); + } + + @Override + public String getName() { + return name(); + } + + @Override + public SpanKind getKind() { + return delegate().getKind(); + } + + @Override + public long getStartEpochNanos() { + return delegate().getStartEpochNanos(); + } + + @Override + public Attributes getAttributes() { + return attributes(); + } + + @Override + public List getEvents() { + return resolvedEvents(); + } + + @Override + public List getLinks() { + return resolvedLinks(); + } + + @Override + public StatusData getStatus() { + return status(); + } + + @Override + public long getEndEpochNanos() { + return endEpochNanos(); + } + + @Override + public boolean hasEnded() { + return internalHasEnded(); + } + + @Override + public int getTotalRecordedEvents() { + return totalRecordedEvents(); + } + + @Override + public int getTotalRecordedLinks() { + return delegate().getTotalRecordedLinks(); + } + + @Override + public int getTotalAttributeCount() { + return totalAttributeCount(); + } + + @Override + public final String toString() { + return "SpanData{" + + "spanContext=" + + getSpanContext() + + ", " + + "parentSpanContext=" + + getParentSpanContext() + + ", " + + "resource=" + + getResource() + + ", " + + "instrumentationLibraryInfo=" + + getInstrumentationLibraryInfo() + + ", " + + "name=" + + getName() + + ", " + + "kind=" + + getKind() + + ", " + + "startEpochNanos=" + + getStartEpochNanos() + + ", " + + "endEpochNanos=" + + getEndEpochNanos() + + ", " + + "attributes=" + + getAttributes() + + ", " + + "totalAttributeCount=" + + getTotalAttributeCount() + + ", " + + "events=" + + getEvents() + + ", " + + "totalRecordedEvents=" + + getTotalRecordedEvents() + + ", " + + "links=" + + getLinks() + + ", " + + "totalRecordedLinks=" + + getTotalRecordedLinks() + + ", " + + "status=" + + getStatus() + + ", " + + "hasEnded=" + + hasEnded() + + "}"; + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java new file mode 100644 index 000000000..df2561094 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.util.List; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +// Represents the shared state/config between all Tracers created by the same TracerProvider. +final class TracerSharedState { + private final Object lock = new Object(); + private final Clock clock; + private final IdGenerator idGenerator; + private final Resource resource; + + private final Supplier spanLimitsSupplier; + private final Sampler sampler; + private final SpanProcessor activeSpanProcessor; + + @Nullable private volatile CompletableResultCode shutdownResult = null; + + TracerSharedState( + Clock clock, + IdGenerator idGenerator, + Resource resource, + Supplier spanLimitsSupplier, + Sampler sampler, + List spanProcessors) { + this.clock = clock; + this.idGenerator = idGenerator; + this.resource = resource; + this.spanLimitsSupplier = spanLimitsSupplier; + this.sampler = sampler; + activeSpanProcessor = SpanProcessor.composite(spanProcessors); + } + + Clock getClock() { + return clock; + } + + IdGenerator getIdGenerator() { + return idGenerator; + } + + Resource getResource() { + return resource; + } + + /** Returns the current {@link SpanLimits}. */ + SpanLimits getSpanLimits() { + return spanLimitsSupplier.get(); + } + + /** Returns the configured {@link Sampler}. */ + Sampler getSampler() { + return sampler; + } + + /** + * Returns the active {@code SpanProcessor}. + * + * @return the active {@code SpanProcessor}. + */ + SpanProcessor getActiveSpanProcessor() { + return activeSpanProcessor; + } + + /** + * Returns {@code true} if tracing has been shut down. + * + * @return {@code true} if tracing has been shut down. + */ + boolean hasBeenShutdown() { + return shutdownResult != null; + } + + /** + * Stops tracing, including shutting down processors and set to {@code true} {@link + * #hasBeenShutdown()}. + * + * @return a {@link CompletableResultCode} that will be completed when the span processor is shut + * down. + */ + CompletableResultCode shutdown() { + synchronized (lock) { + if (shutdownResult != null) { + return shutdownResult; + } + shutdownResult = activeSpanProcessor.shutdown(); + return shutdownResult; + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/EventData.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/EventData.java new file mode 100644 index 000000000..a76c179d8 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/EventData.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.data; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.SpanLimits; +import javax.annotation.concurrent.Immutable; + +/** Data representation of a event. */ +@Immutable +public interface EventData { + + /** + * Returns a new immutable {@link EventData}. + * + * @param epochNanos epoch timestamp in nanos of the {@link EventData}. + * @param name the name of the {@link EventData}. + * @param attributes the attributes of the {@link EventData}. + * @return a new immutable {@link EventData} + */ + static EventData create(long epochNanos, String name, Attributes attributes) { + return ImmutableEventData.create(epochNanos, name, attributes); + } + + /** + * Returns a new immutable {@link EventData}. + * + * @param epochNanos epoch timestamp in nanos of the {@link EventData}. + * @param name the name of the {@link EventData}. + * @param attributes the attributes of the {@link EventData}. + * @param totalAttributeCount the total number of attributes for this {@code} Event. + * @return a new immutable {@link EventData} + */ + static EventData create( + long epochNanos, String name, Attributes attributes, int totalAttributeCount) { + return ImmutableEventData.create(epochNanos, name, attributes, totalAttributeCount); + } + + /** + * Return the name of the {@link EventData}. + * + * @return the name of the {@link EventData}. + */ + String getName(); + + /** + * Return the attributes of the {@link EventData}. + * + * @return the attributes of the {@link EventData}. + */ + Attributes getAttributes(); + + /** + * Returns the epoch time in nanos of this event. + * + * @return the epoch time in nanos of this event. + */ + long getEpochNanos(); + + /** + * The total number of attributes that were recorded on this Event. This number may be larger than + * the number of attributes that are attached to this span, if the total number recorded was + * greater than the configured maximum value. See: {@link + * SpanLimits#getMaxNumberOfAttributesPerEvent()} + * + * @return The total number of attributes on this event. + */ + int getTotalAttributeCount(); + + /** + * Returns the dropped attributes count of this event. + * + * @return the dropped attributes count of this event. + */ + default int getDroppedAttributesCount() { + return getTotalAttributeCount() - getAttributes().size(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableEventData.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableEventData.java new file mode 100644 index 000000000..218941f43 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableEventData.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import javax.annotation.concurrent.Immutable; + +/** An immutable implementation of the {@link EventData}. */ +@AutoValue +@Immutable +abstract class ImmutableEventData implements EventData { + + /** + * Returns a new immutable {@code Event}. + * + * @param epochNanos epoch timestamp in nanos of the {@code Event}. + * @param name the name of the {@code Event}. + * @param attributes the attributes of the {@code Event}. + * @return a new immutable {@code Event} + */ + static EventData create(long epochNanos, String name, Attributes attributes) { + return create(epochNanos, name, attributes, attributes.size()); + } + + /** + * Returns a new immutable {@code Event}. + * + * @param epochNanos epoch timestamp in nanos of the {@code Event}. + * @param name the name of the {@code Event}. + * @param attributes the attributes of the {@code Event}. + * @param totalAttributeCount the total number of attributes recorded for the {@code Event}. + * @return a new immutable {@code Event} + */ + static EventData create( + long epochNanos, String name, Attributes attributes, int totalAttributeCount) { + return new AutoValue_ImmutableEventData(name, attributes, epochNanos, totalAttributeCount); + } + + ImmutableEventData() {} +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableLinkData.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableLinkData.java new file mode 100644 index 000000000..2b4ace61a --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableLinkData.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import javax.annotation.concurrent.Immutable; + +/** An immutable implementation of {@link LinkData}. */ +@AutoValue +@Immutable +abstract class ImmutableLinkData implements LinkData { + private static final Attributes DEFAULT_ATTRIBUTE_COLLECTION = Attributes.empty(); + private static final int DEFAULT_ATTRIBUTE_COUNT = 0; + + static LinkData create(SpanContext spanContext) { + return new AutoValue_ImmutableLinkData( + spanContext, DEFAULT_ATTRIBUTE_COLLECTION, DEFAULT_ATTRIBUTE_COUNT); + } + + static LinkData create(SpanContext spanContext, Attributes attributes) { + return new AutoValue_ImmutableLinkData(spanContext, attributes, attributes.size()); + } + + static LinkData create(SpanContext spanContext, Attributes attributes, int totalAttributeCount) { + return new AutoValue_ImmutableLinkData(spanContext, attributes, totalAttributeCount); + } + + ImmutableLinkData() {} +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableStatusData.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableStatusData.java new file mode 100644 index 000000000..becc84571 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/ImmutableStatusData.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.data; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import javax.annotation.concurrent.Immutable; + +/** + * Defines the status of a {@link Span} by providing a standard {@link StatusCode} in conjunction + * with an optional descriptive message. Instances of {@code Status} are created by starting with + * the template for the appropriate {@link StatusCode} and supplementing it with additional + * information: {@code Status.NOT_FOUND.withDescription("Could not find 'important_file.txt'");} + */ +@AutoValue +@Immutable +abstract class ImmutableStatusData implements StatusData { + /** + * The operation has been validated by an Application developers or Operator to have completed + * successfully. + */ + static final StatusData OK = createInternal(StatusCode.OK, ""); + + /** The default status. */ + static final StatusData UNSET = createInternal(StatusCode.UNSET, ""); + + /** The operation contains an error. */ + static final StatusData ERROR = createInternal(StatusCode.ERROR, ""); + + /** + * 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. + */ + static StatusData create(StatusCode statusCode, String description) { + if (description == null || description.isEmpty()) { + switch (statusCode) { + case UNSET: + return StatusData.unset(); + case OK: + return StatusData.ok(); + case ERROR: + return StatusData.error(); + } + } + return createInternal(statusCode, description); + } + + private static StatusData createInternal(StatusCode statusCode, String description) { + return new AutoValue_ImmutableStatusData(statusCode, description); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/LinkData.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/LinkData.java new file mode 100644 index 000000000..6c3aeb4c3 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/LinkData.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.data; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.trace.SpanLimits; +import javax.annotation.concurrent.Immutable; + +/** + * Data representation of a link. + * + *

    Used (for example) in batching operations, where a single batch handler processes multiple + * requests from different traces. Link can be also used to reference spans from the same trace. + */ +@Immutable +public interface LinkData { + + /** + * Returns a new immutable {@link LinkData}. + * + * @param spanContext the {@link SpanContext} of this {@link LinkData}. + * @return a new immutable {@link LinkData} + */ + static LinkData create(SpanContext spanContext) { + return ImmutableLinkData.create(spanContext); + } + + /** + * Returns a new immutable {@link LinkData}. + * + * @param spanContext the {@link SpanContext} of this {@link LinkData}. + * @param attributes the attributes of this {@link LinkData}. + * @return a new immutable {@link LinkData} + */ + static LinkData create(SpanContext spanContext, Attributes attributes) { + return ImmutableLinkData.create(spanContext, attributes); + } + + /** + * Returns a new immutable {@link LinkData}. + * + * @param spanContext the {@link SpanContext} of this {@link LinkData}. + * @param attributes the attributes of this {@link LinkData}. + * @param totalAttributeCount the total number of attributed for this {@link LinkData}. + * @return a new immutable {@link LinkData} + */ + static LinkData create(SpanContext spanContext, Attributes attributes, int totalAttributeCount) { + return ImmutableLinkData.create(spanContext, attributes, totalAttributeCount); + } + + /** Returns the {@link SpanContext} of the span this {@link LinkData} refers to. */ + SpanContext getSpanContext(); + + /** + * Returns the set of attributes. + * + * @return the set of attributes. + */ + Attributes getAttributes(); + + /** + * The total number of attributes that were recorded on this Link. This number may be larger than + * the number of attributes that are attached to this span, if the total number recorded was + * greater than the configured maximum value. See: {@link + * SpanLimits#getMaxNumberOfAttributesPerLink()} + * + * @return The number of attributes on this link. + */ + int getTotalAttributeCount(); +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/SpanData.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/SpanData.java new file mode 100644 index 000000000..0c8bb6c43 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/SpanData.java @@ -0,0 +1,166 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.data; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SpanLimits; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * Immutable representation of all data collected by the {@link io.opentelemetry.api.trace.Span} + * class. + */ +@Immutable +public interface SpanData { + + /** Returns the {@link SpanContext} of the Span. */ + SpanContext getSpanContext(); + + /** + * Gets the trace id for this span. + * + * @return the trace id. + */ + default String getTraceId() { + return getSpanContext().getTraceId(); + } + + /** + * Gets the span id for this span. + * + * @return the span id. + */ + default String getSpanId() { + return getSpanContext().getSpanId(); + } + + /** + * Returns the parent {@link SpanContext}. If the span is a root span, the {@link SpanContext} + * returned will be invalid. + */ + SpanContext getParentSpanContext(); + + /** + * Returns the parent {@code SpanId}. If the {@code Span} is a root {@code Span}, the SpanId + * returned will be invalid. + * + * @return the parent {@code SpanId} or an invalid SpanId if this is a root {@code Span}. + */ + default String getParentSpanId() { + return getParentSpanContext().getSpanId(); + } + + /** + * Returns the resource of this {@code Span}. + * + * @return the resource of this {@code Span}. + */ + Resource getResource(); + + /** + * Returns the instrumentation library specified when creating the tracer which produced this + * {@code Span}. + * + * @return an instance of {@link InstrumentationLibraryInfo} + */ + InstrumentationLibraryInfo getInstrumentationLibraryInfo(); + + /** + * Returns the name of this {@code Span}. + * + * @return the name of this {@code Span}. + */ + String getName(); + + /** + * Returns the kind of this {@code Span}. + * + * @return the kind of this {@code Span}. + */ + SpanKind getKind(); + + /** + * Returns the start epoch timestamp in nanos of this {@code Span}. + * + * @return the start epoch timestamp in nanos of this {@code Span}. + */ + long getStartEpochNanos(); + + /** + * Returns the attributes recorded for this {@code Span}. + * + * @return the attributes recorded for this {@code Span}. + */ + Attributes getAttributes(); + + /** + * Returns the timed events recorded for this {@code Span}. + * + * @return the timed events recorded for this {@code Span}. + */ + List getEvents(); + + /** + * Returns links recorded for this {@code Span}. + * + * @return links recorded for this {@code Span}. + */ + List getLinks(); + + /** + * Returns the {@code Status}. + * + * @return the {@code Status}. + */ + StatusData getStatus(); + + /** + * Returns the end epoch timestamp in nanos of this {@code Span}. + * + * @return the end epoch timestamp in nanos of this {@code Span}. + */ + long getEndEpochNanos(); + + /** + * Returns whether this Span has already been ended. + * + * @return {@code true} if the span has already been ended, {@code false} if not. + */ + boolean hasEnded(); + + /** + * The total number of {@link EventData} events that were recorded on this span. This number may + * be larger than the number of events that are attached to this span, if the total number + * recorded was greater than the configured maximum value. See: {@link + * SpanLimits#getMaxNumberOfEvents()} + * + * @return The total number of events recorded on this span. + */ + int getTotalRecordedEvents(); + + /** + * The total number of {@link LinkData} links that were recorded on this span. This number may be + * larger than the number of links that are attached to this span, if the total number recorded + * was greater than the configured maximum value. See: {@link SpanLimits#getMaxNumberOfLinks()} + * + * @return The total number of links recorded on this span. + */ + int getTotalRecordedLinks(); + + /** + * The total number of attributes that were recorded on this span. This number may be larger than + * the number of attributes that are attached to this span, if the total number recorded was + * greater than the configured maximum value. See: {@link SpanLimits#getMaxNumberOfAttributes()} + * + * @return The total number of attributes on this span. + */ + int getTotalAttributeCount(); +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/StatusData.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/StatusData.java new file mode 100644 index 000000000..f2a057037 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/StatusData.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.data; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import javax.annotation.concurrent.Immutable; + +/** + * Defines the status of a {@link Span} by providing a standard {@link StatusCode} in conjunction + * with an optional descriptive message. + */ +@Immutable +public interface StatusData { + + /** + * Returns a {@link StatusData} indicating the operation has been validated by an application + * developer or operator to have completed successfully. + */ + static StatusData ok() { + return ImmutableStatusData.OK; + } + + /** Returns the default {@link StatusData}. */ + static StatusData unset() { + return ImmutableStatusData.UNSET; + } + + /** Returns a {@link StatusData} indicating an error occurred. */ + static StatusData error() { + return ImmutableStatusData.ERROR; + } + + /** + * Returns a {@link StatusData} with the given {@code code} and {@code description}. If {@code + * description} is {@code null}, the returned {@link StatusData} does not have a description. + */ + static StatusData create(StatusCode code, String description) { + return ImmutableStatusData.create(code, description); + } + + /** Returns the status code. */ + StatusCode getStatusCode(); + + /** + * Returns the description of this {@code Status} for human consumption. + * + * @return the description of this {@code Status}. + */ + String getDescription(); +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/package-info.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/package-info.java new file mode 100644 index 000000000..e154828b5 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/data/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** The data format to model traces for export. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.trace.data; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessor.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessor.java new file mode 100644 index 000000000..965e9fcb8 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessor.java @@ -0,0 +1,304 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.api.metrics.BoundLongCounter; +import io.opentelemetry.api.metrics.GlobalMeterProvider; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.common.Labels; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.internal.JcTools; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implementation of the {@link SpanProcessor} that batches spans exported by the SDK then pushes + * them to the exporter pipeline. + * + *

    All spans reported by the SDK implementation are first added to a synchronized queue (with a + * {@code maxQueueSize} maximum size, if queue is full spans are dropped). Spans are exported either + * when there are {@code maxExportBatchSize} pending spans or {@code scheduleDelayNanos} has passed + * since the last export finished. + */ +public final class BatchSpanProcessor implements SpanProcessor { + + private static final String WORKER_THREAD_NAME = + BatchSpanProcessor.class.getSimpleName() + "_WorkerThread"; + private static final String SPAN_PROCESSOR_TYPE_LABEL = "spanProcessorType"; + private static final String SPAN_PROCESSOR_TYPE_VALUE = BatchSpanProcessor.class.getSimpleName(); + + private final Worker worker; + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + + /** + * Returns a new Builder for {@link BatchSpanProcessor}. + * + * @param spanExporter the {@code SpanExporter} to where the Spans are pushed. + * @return a new {@link BatchSpanProcessor}. + * @throws NullPointerException if the {@code spanExporter} is {@code null}. + */ + public static BatchSpanProcessorBuilder builder(SpanExporter spanExporter) { + return new BatchSpanProcessorBuilder(spanExporter); + } + + BatchSpanProcessor( + SpanExporter spanExporter, + long scheduleDelayNanos, + int maxQueueSize, + int maxExportBatchSize, + long exporterTimeoutNanos) { + this.worker = + new Worker( + spanExporter, + scheduleDelayNanos, + maxExportBatchSize, + exporterTimeoutNanos, + JcTools.newMpscArrayQueue(maxQueueSize)); + Thread workerThread = new DaemonThreadFactory(WORKER_THREAD_NAME).newThread(worker); + workerThread.start(); + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) {} + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan span) { + if (!span.getSpanContext().isSampled()) { + return; + } + worker.addSpan(span); + } + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public CompletableResultCode shutdown() { + if (isShutdown.getAndSet(true)) { + return CompletableResultCode.ofSuccess(); + } + return worker.shutdown(); + } + + @Override + public CompletableResultCode forceFlush() { + return worker.forceFlush(); + } + + // Visible for testing + ArrayList getBatch() { + return worker.batch; + } + + // Worker is a thread that batches multiple spans and calls the registered SpanExporter to export + // the data. + private static final class Worker implements Runnable { + + private final BoundLongCounter droppedSpans; + private final BoundLongCounter exportedSpans; + + private static final Logger logger = Logger.getLogger(Worker.class.getName()); + private final SpanExporter spanExporter; + private final long scheduleDelayNanos; + private final int maxExportBatchSize; + private final long exporterTimeoutNanos; + + private long nextExportTime; + + private final Queue queue; + // When waiting on the spans queue, exporter thread sets this atomic to the number of more + // spans it needs before doing an export. Writer threads would then wait for the queue to reach + // spansNeeded size before notifying the exporter thread about new entries. + // Integer.MAX_VALUE is used to imply that exporter thread is not expecting any signal. Since + // exporter thread doesn't expect any signal initially, this value is initialized to + // Integer.MAX_VALUE. + private final AtomicInteger spansNeeded = new AtomicInteger(Integer.MAX_VALUE); + private final BlockingQueue signal; + private final AtomicReference flushRequested = new AtomicReference<>(); + private volatile boolean continueWork = true; + private final ArrayList batch; + + private Worker( + SpanExporter spanExporter, + long scheduleDelayNanos, + int maxExportBatchSize, + long exporterTimeoutNanos, + Queue queue) { + this.spanExporter = spanExporter; + this.scheduleDelayNanos = scheduleDelayNanos; + this.maxExportBatchSize = maxExportBatchSize; + this.exporterTimeoutNanos = exporterTimeoutNanos; + this.queue = queue; + this.signal = new ArrayBlockingQueue<>(1); + Meter meter = GlobalMeterProvider.getMeter("io.opentelemetry.sdk.trace"); + meter + .longValueObserverBuilder("queueSize") + .setDescription("The number of spans queued") + .setUnit("1") + .setUpdater( + result -> + result.observe( + queue.size(), + Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE))) + .build(); + LongCounter processedSpansCounter = + meter + .longCounterBuilder("processedSpans") + .setUnit("1") + .setDescription( + "The number of spans processed by the BatchSpanProcessor. " + + "[dropped=true if they were dropped due to high throughput]") + .build(); + droppedSpans = + processedSpansCounter.bind( + Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE, "dropped", "true")); + exportedSpans = + processedSpansCounter.bind( + Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE, "dropped", "false")); + + this.batch = new ArrayList<>(this.maxExportBatchSize); + } + + private void addSpan(ReadableSpan span) { + if (!queue.offer(span)) { + droppedSpans.add(1); + } else { + if (queue.size() >= spansNeeded.get()) { + signal.offer(true); + } + } + } + + @Override + public void run() { + updateNextExportTime(); + + while (continueWork) { + if (flushRequested.get() != null) { + flush(); + } + while (!queue.isEmpty() && batch.size() < maxExportBatchSize) { + batch.add(queue.poll().toSpanData()); + } + if (batch.size() >= maxExportBatchSize || System.nanoTime() >= nextExportTime) { + exportCurrentBatch(); + updateNextExportTime(); + } + if (queue.isEmpty()) { + try { + long pollWaitTime = nextExportTime - System.nanoTime(); + if (pollWaitTime > 0) { + spansNeeded.set(maxExportBatchSize - batch.size()); + signal.poll(pollWaitTime, TimeUnit.NANOSECONDS); + spansNeeded.set(Integer.MAX_VALUE); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + } + + private void flush() { + int spansToFlush = queue.size(); + while (spansToFlush > 0) { + ReadableSpan span = queue.poll(); + assert span != null; + batch.add(span.toSpanData()); + spansToFlush--; + if (batch.size() >= maxExportBatchSize) { + exportCurrentBatch(); + } + } + exportCurrentBatch(); + flushRequested.get().succeed(); + flushRequested.set(null); + } + + private void updateNextExportTime() { + nextExportTime = System.nanoTime() + scheduleDelayNanos; + } + + private CompletableResultCode shutdown() { + final CompletableResultCode result = new CompletableResultCode(); + + final CompletableResultCode flushResult = forceFlush(); + flushResult.whenComplete( + () -> { + continueWork = false; + final CompletableResultCode shutdownResult = spanExporter.shutdown(); + shutdownResult.whenComplete( + () -> { + if (!flushResult.isSuccess() || !shutdownResult.isSuccess()) { + result.fail(); + } else { + result.succeed(); + } + }); + }); + + return result; + } + + private CompletableResultCode forceFlush() { + CompletableResultCode flushResult = new CompletableResultCode(); + // we set the atomic here to trigger the worker loop to do a flush of the entire queue. + if (flushRequested.compareAndSet(null, flushResult)) { + signal.offer(true); + } + CompletableResultCode possibleResult = flushRequested.get(); + // there's a race here where the flush happening in the worker loop could complete before we + // get what's in the atomic. In that case, just return success, since we know it succeeded in + // the interim. + return possibleResult == null ? CompletableResultCode.ofSuccess() : possibleResult; + } + + private void exportCurrentBatch() { + if (batch.isEmpty()) { + return; + } + + try { + final CompletableResultCode result = + spanExporter.export(Collections.unmodifiableList(batch)); + result.join(exporterTimeoutNanos, TimeUnit.NANOSECONDS); + if (result.isSuccess()) { + exportedSpans.add(batch.size()); + } else { + logger.log(Level.FINE, "Exporter failed"); + } + } catch (RuntimeException e) { + logger.log(Level.WARNING, "Exporter threw an Exception", e); + } finally { + batch.clear(); + } + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorBuilder.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorBuilder.java new file mode 100644 index 000000000..90e012fe9 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorBuilder.java @@ -0,0 +1,144 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import static io.opentelemetry.api.internal.Utils.checkArgument; +import static java.util.Objects.requireNonNull; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** Builder class for {@link BatchSpanProcessor}. */ +public final class BatchSpanProcessorBuilder { + + // Visible for testing + static final long DEFAULT_SCHEDULE_DELAY_MILLIS = 5000; + // Visible for testing + static final int DEFAULT_MAX_QUEUE_SIZE = 2048; + // Visible for testing + static final int DEFAULT_MAX_EXPORT_BATCH_SIZE = 512; + // Visible for testing + static final int DEFAULT_EXPORT_TIMEOUT_MILLIS = 30_000; + + private final SpanExporter spanExporter; + private long scheduleDelayNanos = TimeUnit.MILLISECONDS.toNanos(DEFAULT_SCHEDULE_DELAY_MILLIS); + private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + private int maxExportBatchSize = DEFAULT_MAX_EXPORT_BATCH_SIZE; + private long exporterTimeoutNanos = TimeUnit.MILLISECONDS.toNanos(DEFAULT_EXPORT_TIMEOUT_MILLIS); + + BatchSpanProcessorBuilder(SpanExporter spanExporter) { + this.spanExporter = requireNonNull(spanExporter, "spanExporter"); + } + + // TODO: Consider to add support for constant Attributes and/or Resource. + + /** + * Sets the delay interval between two consecutive exports. If unset, defaults to {@value + * DEFAULT_SCHEDULE_DELAY_MILLIS}ms. + */ + public BatchSpanProcessorBuilder setScheduleDelay(long delay, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(delay >= 0, "delay must be non-negative"); + scheduleDelayNanos = unit.toNanos(delay); + return this; + } + + /** + * Sets the delay interval between two consecutive exports. If unset, defaults to {@value + * DEFAULT_SCHEDULE_DELAY_MILLIS}ms. + */ + public BatchSpanProcessorBuilder setScheduleDelay(Duration delay) { + requireNonNull(delay, "delay"); + return setScheduleDelay(delay.toNanos(), TimeUnit.NANOSECONDS); + } + + // Visible for testing + long getScheduleDelayNanos() { + return scheduleDelayNanos; + } + + /** + * Sets the maximum time an export will be allowed to run before being cancelled. If unset, + * defaults to {@value DEFAULT_EXPORT_TIMEOUT_MILLIS}ms. + */ + public BatchSpanProcessorBuilder setExporterTimeout(long timeout, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(timeout >= 0, "timeout must be non-negative"); + exporterTimeoutNanos = unit.toNanos(timeout); + return this; + } + + /** + * Sets the maximum time an export will be allowed to run before being cancelled. If unset, + * defaults to {@value DEFAULT_EXPORT_TIMEOUT_MILLIS}ms. + */ + public BatchSpanProcessorBuilder setExporterTimeout(Duration timeout) { + requireNonNull(timeout, "timeout"); + return setExporterTimeout(timeout.toNanos(), TimeUnit.NANOSECONDS); + } + + // Visible for testing + long getExporterTimeoutNanos() { + return exporterTimeoutNanos; + } + + /** + * Sets the maximum number of Spans that are kept in the queue before start dropping. More memory + * than this value may be allocated to optimize queue access. + * + *

    See the BatchSampledSpansProcessor class description for a high-level design description of + * this class. + * + *

    Default value is {@code 2048}. + * + * @param maxQueueSize the maximum number of Spans that are kept in the queue before start + * dropping. + * @return this. + * @see BatchSpanProcessorBuilder#DEFAULT_MAX_QUEUE_SIZE + */ + public BatchSpanProcessorBuilder setMaxQueueSize(int maxQueueSize) { + this.maxQueueSize = maxQueueSize; + return this; + } + + // Visible for testing + int getMaxQueueSize() { + return maxQueueSize; + } + + /** + * Sets the maximum batch size for every export. This must be smaller or equal to {@code + * maxQueuedSpans}. + * + *

    Default value is {@code 512}. + * + * @param maxExportBatchSize the maximum batch size for every export. + * @return this. + * @see BatchSpanProcessorBuilder#DEFAULT_MAX_EXPORT_BATCH_SIZE + */ + public BatchSpanProcessorBuilder setMaxExportBatchSize(int maxExportBatchSize) { + checkArgument(maxExportBatchSize > 0, "maxExportBatchSize must be positive."); + this.maxExportBatchSize = maxExportBatchSize; + return this; + } + + // Visible for testing + int getMaxExportBatchSize() { + return maxExportBatchSize; + } + + /** + * Returns a new {@link BatchSpanProcessor} that batches, then converts spans to proto and + * forwards them to the given {@code spanExporter}. + * + * @return a new {@link BatchSpanProcessor}. + * @throws NullPointerException if the {@code spanExporter} is {@code null}. + */ + public BatchSpanProcessor build() { + return new BatchSpanProcessor( + spanExporter, scheduleDelayNanos, maxQueueSize, maxExportBatchSize, exporterTimeoutNanos); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/MultiSpanExporter.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/MultiSpanExporter.java new file mode 100644 index 000000000..a7acc09ed --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/MultiSpanExporter.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implementation of the {@code SpanExporter} that simply forwards all received spans to a list of + * {@code SpanExporter}. + * + *

    Can be used to export to multiple backends using the same {@code SpanProcessor} like a {@code + * SimpleSampledSpansProcessor} or a {@code BatchSampledSpansProcessor}. + */ +final class MultiSpanExporter implements SpanExporter { + private static final Logger logger = Logger.getLogger(MultiSpanExporter.class.getName()); + + private final SpanExporter[] spanExporters; + + /** + * Constructs and returns an instance of this class. + * + * @param spanExporters the exporters spans should be sent to + * @return the aggregate span exporter + */ + static SpanExporter create(List spanExporters) { + return new MultiSpanExporter(spanExporters.toArray(new SpanExporter[0])); + } + + @Override + public CompletableResultCode export(Collection spans) { + List results = new ArrayList<>(spanExporters.length); + for (SpanExporter spanExporter : spanExporters) { + final CompletableResultCode exportResult; + try { + exportResult = spanExporter.export(spans); + } catch (RuntimeException e) { + // If an exception was thrown by the exporter + logger.log(Level.WARNING, "Exception thrown by the export.", e); + results.add(CompletableResultCode.ofFailure()); + continue; + } + results.add(exportResult); + } + return CompletableResultCode.ofAll(results); + } + + /** + * Flushes the data of all registered {@link SpanExporter}s. + * + * @return the result of the operation + */ + @Override + public CompletableResultCode flush() { + List results = new ArrayList<>(spanExporters.length); + for (SpanExporter spanExporter : spanExporters) { + final CompletableResultCode flushResult; + try { + flushResult = spanExporter.flush(); + } catch (RuntimeException e) { + // If an exception was thrown by the exporter + logger.log(Level.WARNING, "Exception thrown by the flush.", e); + results.add(CompletableResultCode.ofFailure()); + continue; + } + results.add(flushResult); + } + return CompletableResultCode.ofAll(results); + } + + @Override + public CompletableResultCode shutdown() { + List results = new ArrayList<>(spanExporters.length); + for (SpanExporter spanExporter : spanExporters) { + final CompletableResultCode shutdownResult; + try { + shutdownResult = spanExporter.shutdown(); + } catch (RuntimeException e) { + // If an exception was thrown by the exporter + logger.log(Level.WARNING, "Exception thrown by the shutdown.", e); + results.add(CompletableResultCode.ofFailure()); + continue; + } + results.add(shutdownResult); + } + return CompletableResultCode.ofAll(results); + } + + private MultiSpanExporter(SpanExporter[] spanExporters) { + this.spanExporters = spanExporters; + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/NoopSpanExporter.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/NoopSpanExporter.java new file mode 100644 index 000000000..536ccde9c --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/NoopSpanExporter.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.Collection; + +final class NoopSpanExporter implements SpanExporter { + + private static final SpanExporter INSTANCE = new NoopSpanExporter(); + + static SpanExporter getInstance() { + return INSTANCE; + } + + @Override + public CompletableResultCode export(Collection spans) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessor.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessor.java new file mode 100644 index 000000000..56b3073f2 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessor.java @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * An implementation of the {@link SpanProcessor} that converts the {@link ReadableSpan} to {@link + * SpanData} and passes it directly to the configured exporter. + * + *

    This processor will cause all spans to be exported directly as they finish, meaning each + * export request will have a single span. Most backends will not perform well with a single span + * per request so unless you know what you're doing, strongly consider using {@link + * BatchSpanProcessor} instead, including in special environments such as serverless runtimes. + * {@link SimpleSpanProcessor} is generally meant to for logging exporters only. + */ +public final class SimpleSpanProcessor implements SpanProcessor { + + private static final Logger logger = Logger.getLogger(SimpleSpanProcessor.class.getName()); + + private final SpanExporter spanExporter; + private final boolean sampled; + private final Set pendingExports = + Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + + /** + * Returns a new {@link SimpleSpanProcessor} which exports spans to the {@link SpanExporter} + * synchronously. + * + *

    This processor will cause all spans to be exported directly as they finish, meaning each + * export request will have a single span. Most backends will not perform well with a single span + * per request so unless you know what you're doing, strongly consider using {@link + * BatchSpanProcessor} instead, including in special environments such as serverless runtimes. + * {@link SimpleSpanProcessor} is generally meant to for logging exporters only. + */ + public static SpanProcessor create(SpanExporter exporter) { + requireNonNull(exporter, "exporter"); + return new SimpleSpanProcessor(exporter, /* sampled= */ true); + } + + SimpleSpanProcessor(SpanExporter spanExporter, boolean sampled) { + this.spanExporter = requireNonNull(spanExporter, "spanExporter"); + this.sampled = sampled; + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + // Do nothing. + } + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan span) { + if (sampled && !span.getSpanContext().isSampled()) { + return; + } + try { + List spans = Collections.singletonList(span.toSpanData()); + final CompletableResultCode result = spanExporter.export(spans); + pendingExports.add(result); + result.whenComplete( + () -> { + pendingExports.remove(result); + if (!result.isSuccess()) { + logger.log(Level.FINE, "Exporter failed"); + } + }); + } catch (RuntimeException e) { + logger.log(Level.WARNING, "Exporter threw an Exception", e); + } + } + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public CompletableResultCode shutdown() { + if (isShutdown.getAndSet(true)) { + return CompletableResultCode.ofSuccess(); + } + final CompletableResultCode result = new CompletableResultCode(); + + final CompletableResultCode flushResult = forceFlush(); + flushResult.whenComplete( + () -> { + final CompletableResultCode shutdownResult = spanExporter.shutdown(); + shutdownResult.whenComplete( + () -> { + if (!flushResult.isSuccess() || !shutdownResult.isSuccess()) { + result.fail(); + } else { + result.succeed(); + } + }); + }); + + return result; + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofAll(pendingExports); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SpanExporter.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SpanExporter.java new file mode 100644 index 000000000..824700a21 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/SpanExporter.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * An interface that allows different tracing services to export recorded data for sampled spans in + * their own format. + * + *

    To export data this MUST be register to the {@code TracerSdk} using a {@link + * SimpleSpanProcessor} or a {@code BatchSampledSpansProcessor}. + */ +public interface SpanExporter extends Closeable { + + /** + * Returns a {@link SpanExporter} which simply delegates all exports to the {@code exporters} in + * order. + * + *

    Can be used to export to multiple backends using the same {@code SpanProcessor} like a + * {@code SimpleSampledSpansProcessor} or a {@code BatchSampledSpansProcessor}. + */ + static SpanExporter composite(SpanExporter... exporters) { + return composite(Arrays.asList(exporters)); + } + + /** + * Returns a {@link SpanExporter} which simply delegates all exports to the {@code exporters} in + * order. + * + *

    Can be used to export to multiple backends using the same {@code SpanProcessor} like a + * {@code SimpleSampledSpansProcessor} or a {@code BatchSampledSpansProcessor}. + */ + static SpanExporter composite(Iterable exporters) { + List exportersList = new ArrayList<>(); + for (SpanExporter exporter : exporters) { + exportersList.add(exporter); + } + if (exportersList.isEmpty()) { + return NoopSpanExporter.getInstance(); + } + if (exportersList.size() == 1) { + return exportersList.get(0); + } + return MultiSpanExporter.create(exportersList); + } + + /** + * Called to export sampled {@code Span}s. Note that export operations can be performed + * simultaneously depending on the type of span processor being used. However, the {@link + * BatchSpanProcessor} will ensure that only one export can occur at a time. + * + * @param spans the collection of sampled Spans to be exported. + * @return the result of the export, which is often an asynchronous operation. + */ + CompletableResultCode export(Collection spans); + + /** + * Exports the collection of sampled {@code Span}s that have not yet been exported. Note that + * export operations can be performed simultaneously depending on the type of span processor being + * used. However, the {@link BatchSpanProcessor} will ensure that only one export can occur at a + * time. + * + * @return the result of the flush, which is often an asynchronous operation. + */ + CompletableResultCode flush(); + + /** + * Called when {@link SdkTracerProvider#shutdown()} is called, if this {@code SpanExporter} is + * registered to a {@link SdkTracerProvider} object. + * + * @return a {@link CompletableResultCode} which is completed when shutdown completes. + */ + CompletableResultCode shutdown(); + + /** Closes this {@link SpanExporter}, releasing any resources. */ + @Override + default void close() { + shutdown().join(10, TimeUnit.SECONDS); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/package-info.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/package-info.java new file mode 100644 index 000000000..6609498d8 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Utilities that allow tracing services to export data for sampled spans, as well as providing + * in-process span processing APIs. + * + *

    Contents

    + * + *
      + *
    • {@link io.opentelemetry.sdk.trace.export.SpanExporter} + *
    • {@link io.opentelemetry.sdk.trace.export.SimpleSpanProcessor} + *
    • {@link io.opentelemetry.sdk.trace.export.BatchSpanProcessor} + *
    • {@link io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder} + *
    + * + *

    Configuration options for components in this package can be read from system properties or + * environment variables with the use of the opentelemetry-autoconfiguration module. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.trace.export; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/package-info.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/package-info.java new file mode 100644 index 000000000..ce65eaf34 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The OpenTelemetry SDK implementation of tracing. + * + * @see io.opentelemetry.sdk.trace.SdkTracerProvider + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.trace; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/AlwaysOffSampler.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/AlwaysOffSampler.java new file mode 100644 index 000000000..7a818e92a --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/AlwaysOffSampler.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +@Immutable +enum AlwaysOffSampler implements Sampler { + INSTANCE; + + // Returns a "no" {@link SamplingResult} on {@link Span} sampling. + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return ImmutableSamplingResult.EMPTY_NOT_SAMPLED_OR_RECORDED_SAMPLING_RESULT; + } + + @Override + public String getDescription() { + return "AlwaysOffSampler"; + } + + @Override + public String toString() { + return getDescription(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/AlwaysOnSampler.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/AlwaysOnSampler.java new file mode 100644 index 000000000..6315e50ae --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/AlwaysOnSampler.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +@Immutable +enum AlwaysOnSampler implements Sampler { + INSTANCE; + + // Returns a "yes" {@link SamplingResult} on {@link Span} sampling. + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return ImmutableSamplingResult.EMPTY_RECORDED_AND_SAMPLED_SAMPLING_RESULT; + } + + @Override + public String getDescription() { + return "AlwaysOnSampler"; + } + + @Override + public String toString() { + return getDescription(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ImmutableSamplingResult.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ImmutableSamplingResult.java new file mode 100644 index 000000000..80ba9e2b0 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ImmutableSamplingResult.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import javax.annotation.concurrent.Immutable; + +@Immutable +@AutoValue +abstract class ImmutableSamplingResult implements SamplingResult { + + static final SamplingResult EMPTY_RECORDED_AND_SAMPLED_SAMPLING_RESULT = + ImmutableSamplingResult.createWithoutAttributes(SamplingDecision.RECORD_AND_SAMPLE); + + static final SamplingResult EMPTY_NOT_SAMPLED_OR_RECORDED_SAMPLING_RESULT = + ImmutableSamplingResult.createWithoutAttributes(SamplingDecision.DROP); + + static final SamplingResult EMPTY_RECORDED_SAMPLING_RESULT = + ImmutableSamplingResult.createWithoutAttributes(SamplingDecision.RECORD_ONLY); + + static SamplingResult createSamplingResult(SamplingDecision decision, Attributes attributes) { + return new AutoValue_ImmutableSamplingResult(decision, attributes); + } + + private static SamplingResult createWithoutAttributes(SamplingDecision decision) { + return new AutoValue_ImmutableSamplingResult(decision, Attributes.empty()); + } + + @Override + public abstract SamplingDecision getDecision(); + + @Override + public abstract Attributes getAttributes(); +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSampler.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSampler.java new file mode 100644 index 000000000..099cc0e14 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSampler.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * A Sampler that uses the sampled flag of the parent Span, if present. If the span has no parent, + * this Sampler will use the "root" sampler that it is built with. See documentation on the {@link + * ParentBasedSamplerBuilder} methods for the details on the various configurable options. + */ +@Immutable +final class ParentBasedSampler implements Sampler { + + private final Sampler root; + private final Sampler remoteParentSampled; + private final Sampler remoteParentNotSampled; + private final Sampler localParentSampled; + private final Sampler localParentNotSampled; + + ParentBasedSampler( + Sampler root, + @Nullable Sampler remoteParentSampled, + @Nullable Sampler remoteParentNotSampled, + @Nullable Sampler localParentSampled, + @Nullable Sampler localParentNotSampled) { + this.root = root; + this.remoteParentSampled = + remoteParentSampled == null ? Sampler.alwaysOn() : remoteParentSampled; + this.remoteParentNotSampled = + remoteParentNotSampled == null ? Sampler.alwaysOff() : remoteParentNotSampled; + this.localParentSampled = localParentSampled == null ? Sampler.alwaysOn() : localParentSampled; + this.localParentNotSampled = + localParentNotSampled == null ? Sampler.alwaysOff() : localParentNotSampled; + } + + // If a parent is set, always follows the same sampling decision as the parent. + // Otherwise, uses the delegateSampler provided at initialization to make a decision. + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + SpanContext parentSpanContext = Span.fromContext(parentContext).getSpanContext(); + if (!parentSpanContext.isValid()) { + return this.root.shouldSample( + parentContext, traceId, name, spanKind, attributes, parentLinks); + } + + if (parentSpanContext.isRemote()) { + return parentSpanContext.isSampled() + ? this.remoteParentSampled.shouldSample( + parentContext, traceId, name, spanKind, attributes, parentLinks) + : this.remoteParentNotSampled.shouldSample( + parentContext, traceId, name, spanKind, attributes, parentLinks); + } + return parentSpanContext.isSampled() + ? this.localParentSampled.shouldSample( + parentContext, traceId, name, spanKind, attributes, parentLinks) + : this.localParentNotSampled.shouldSample( + parentContext, traceId, name, spanKind, attributes, parentLinks); + } + + @Override + public String getDescription() { + return String.format( + "ParentBased{root:%s,remoteParentSampled:%s,remoteParentNotSampled:%s," + + "localParentSampled:%s,localParentNotSampled:%s}", + this.root.getDescription(), + this.remoteParentSampled.getDescription(), + this.remoteParentNotSampled.getDescription(), + this.localParentSampled.getDescription(), + this.localParentNotSampled.getDescription()); + } + + @Override + public String toString() { + return getDescription(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ParentBasedSampler)) { + return false; + } + + ParentBasedSampler that = (ParentBasedSampler) o; + + return root.equals(that.root) + && remoteParentSampled.equals(that.remoteParentSampled) + && remoteParentNotSampled.equals(that.remoteParentNotSampled) + && localParentSampled.equals(that.localParentSampled) + && localParentNotSampled.equals(that.localParentNotSampled); + } + + @Override + public int hashCode() { + int result = root.hashCode(); + result = 31 * result + remoteParentSampled.hashCode(); + result = 31 * result + remoteParentNotSampled.hashCode(); + result = 31 * result + localParentSampled.hashCode(); + result = 31 * result + localParentNotSampled.hashCode(); + return result; + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSamplerBuilder.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSamplerBuilder.java new file mode 100644 index 000000000..4ae0e7461 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSamplerBuilder.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +/** A builder for creating ParentBased sampler instances. */ +public final class ParentBasedSamplerBuilder { + + private final Sampler root; + private Sampler remoteParentSampled; + private Sampler remoteParentNotSampled; + private Sampler localParentSampled; + private Sampler localParentNotSampled; + + ParentBasedSamplerBuilder(Sampler root) { + this.root = root; + } + + /** + * Sets the {@link Sampler} to use when there is a remote parent that was sampled. If not set, + * defaults to always sampling if the remote parent was sampled. + * + * @return this Builder + */ + public ParentBasedSamplerBuilder setRemoteParentSampled(Sampler remoteParentSampled) { + this.remoteParentSampled = remoteParentSampled; + return this; + } + + /** + * Sets the {@link Sampler} to use when there is a remote parent that was not sampled. If not set, + * defaults to never sampling when the remote parent isn't sampled. + * + * @return this Builder + */ + public ParentBasedSamplerBuilder setRemoteParentNotSampled(Sampler remoteParentNotSampled) { + this.remoteParentNotSampled = remoteParentNotSampled; + return this; + } + + /** + * Sets the {@link Sampler} to use when there is a local parent that was sampled. If not set, + * defaults to always sampling if the local parent was sampled. + * + * @return this Builder + */ + public ParentBasedSamplerBuilder setLocalParentSampled(Sampler localParentSampled) { + this.localParentSampled = localParentSampled; + return this; + } + + /** + * Sets the {@link Sampler} to use when there is a local parent that was not sampled. If not set, + * defaults to never sampling when the local parent isn't sampled. + * + * @return this Builder + */ + public ParentBasedSamplerBuilder setLocalParentNotSampled(Sampler localParentNotSampled) { + this.localParentNotSampled = localParentNotSampled; + return this; + } + + /** + * Builds the {@link ParentBasedSampler}. + * + * @return the ParentBased sampler. + */ + public Sampler build() { + return new ParentBasedSampler( + this.root, + this.remoteParentSampled, + this.remoteParentNotSampled, + this.localParentSampled, + this.localParentNotSampled); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java new file mode 100644 index 000000000..73edba3b6 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +import javax.annotation.concurrent.ThreadSafe; + +/** A Sampler is used to make decisions on {@link Span} sampling. */ +@ThreadSafe +public interface Sampler { + + /** + * Returns a {@link Sampler} that always makes a "yes" {@link SamplingResult} for {@link Span} + * sampling. + * + * @return a {@code Sampler} that always makes a "yes" {@link SamplingResult} for {@code Span} + * sampling. + */ + static Sampler alwaysOn() { + return AlwaysOnSampler.INSTANCE; + } + + /** + * Returns a {@link Sampler} that always makes a "no" {@link SamplingResult} for {@link Span} + * sampling. + * + * @return a {@code Sampler} that always makes a "no" {@link SamplingResult} for {@code Span} + * sampling. + */ + static Sampler alwaysOff() { + return AlwaysOffSampler.INSTANCE; + } + + /** + * Returns a {@link Sampler} that always makes the same decision as the parent {@link Span} to + * whether or not to sample. If there is no parent, the Sampler uses the provided Sampler delegate + * to determine the sampling decision. + * + *

    This method is equivalent to calling {@code #parentBasedBuilder(Sampler).build()} + * + * @param root the {@code Sampler} which is used to make the sampling decisions if the parent does + * not exist. + * @return a {@code Sampler} that follows the parent's sampling decision if one exists, otherwise + * following the root sampler's decision. + */ + static Sampler parentBased(Sampler root) { + return parentBasedBuilder(root).build(); + } + + /** + * Returns a {@link ParentBasedSamplerBuilder} that enables configuration of the parent-based + * sampling strategy. The parent's sampling decision is used if a parent span exists, otherwise + * this strategy uses the root sampler's decision. There are a several options available on the + * builder to control the precise behavior of how the decision will be made. + * + * @param root the required {@code Sampler} which is used to make the sampling decisions if the + * parent does not exist. + * @return a {@code ParentBasedSamplerBuilder} + */ + static ParentBasedSamplerBuilder parentBasedBuilder(Sampler root) { + return new ParentBasedSamplerBuilder(root); + } + + /** + * Returns a new TraceIdRatioBased {@link Sampler}. The ratio of sampling a trace is equal to that + * of the specified ratio. + * + *

    The algorithm used by the {@link Sampler} is undefined, notably it may or may not use parts + * of the trace ID when generating a sampling decision. Currently, only the ratio of traces that + * are sampled can be relied on, not how the sampled traces are determined. As such, it is + * recommended to only use this {@link Sampler} for root spans using {@link + * Sampler#parentBased(Sampler)}. + * + * @param ratio The desired ratio of sampling. Must be within [0.0, 1.0]. + * @return a new TraceIdRatioBased {@link Sampler}. + * @throws IllegalArgumentException if {@code ratio} is out of range + */ + static Sampler traceIdRatioBased(double ratio) { + return TraceIdRatioBasedSampler.create(ratio); + } + + /** + * Called during {@link Span} creation to make a sampling samplingResult. + * + * @param parentContext the parent span's {@link SpanContext}. This can be {@code + * SpanContext.INVALID} 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 name the name of the new {@code Span}. + * @param spanKind the {@link SpanKind} of the {@code Span}. + * @param attributes {@link Attributes} associated with the span. + * @param parentLinks the parentLinks associated with the new {@code Span}. + * @return sampling samplingResult whether span should be sampled or not. + */ + SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks); + + /** + * Returns the description of this {@code Sampler}. This may be displayed on debug pages or in the + * logs. + * + *

    Example: "TraceIdRatioBased{0.000100}" + * + * @return the description of this {@code Sampler}. + */ + String getDescription(); +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/SamplingDecision.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/SamplingDecision.java new file mode 100644 index 000000000..04bd921fa --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/SamplingDecision.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +/** A decision on whether a span should be recorded, recorded and sampled or dropped. */ +public enum SamplingDecision { + /** Span is dropped. The resulting span will be completely no-op. */ + DROP, + /** + * Span is recorded only. The resulting span will record all information like timings and + * attributes but will not be exported. Downstream {@linkplain Sampler#parentBased(Sampler) + * parent-based} samplers will not sample the span. + */ + RECORD_ONLY, + /** + * Span is recorded and sampled. The resulting span will record all information like timings and + * attributes and will be exported. + */ + RECORD_AND_SAMPLE, +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/SamplingResult.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/SamplingResult.java new file mode 100644 index 000000000..4a07c02a1 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/SamplingResult.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import java.util.List; +import javax.annotation.concurrent.Immutable; + +/** + * Sampling result returned by {@link Sampler#shouldSample(Context, String, String, SpanKind, + * Attributes, List)}. + */ +@Immutable +public interface SamplingResult { + + /** + * Returns a {@link SamplingResult} with no attributes and {@link SamplingResult#getDecision()} + * returning {@code decision}. + * + *

    This is meant for use by custom {@link Sampler} implementations. + * + *

    Use {@link #create(SamplingDecision, Attributes)} if you need attributes. + * + * @param decision The decision made on the span. + * @return A {@link SamplingResult} with empty attributes and the provided {@code decision}. + */ + static SamplingResult create(SamplingDecision decision) { + switch (decision) { + case RECORD_AND_SAMPLE: + return ImmutableSamplingResult.EMPTY_RECORDED_AND_SAMPLED_SAMPLING_RESULT; + case RECORD_ONLY: + return ImmutableSamplingResult.EMPTY_RECORDED_SAMPLING_RESULT; + case DROP: + return ImmutableSamplingResult.EMPTY_NOT_SAMPLED_OR_RECORDED_SAMPLING_RESULT; + } + throw new AssertionError("unrecognised samplingResult"); + } + + /** + * Returns a {@link SamplingResult} with the given {@code attributes} and {@link + * SamplingResult#getDecision()} returning {@code decision}. + * + *

    This is meant for use by custom {@link Sampler} implementations. + * + *

    Using {@link #create(SamplingDecision)} instead of this method is slightly faster and + * shorter if you don't need attributes. + * + * @param decision The decision made on the span. + * @param attributes The attributes to return from {@link SamplingResult#getAttributes()}. A + * different object instance with the same elements may be returned. + * @return A {@link SamplingResult} with the attributes equivalent to {@code attributes} and the + * provided {@code decision}. + */ + static SamplingResult create(SamplingDecision decision, Attributes attributes) { + requireNonNull(attributes, "attributes"); + return attributes.isEmpty() + ? create(decision) + : ImmutableSamplingResult.createSamplingResult(decision, attributes); + } + + /** + * Return decision on whether a span should be recorded, recorded and sampled or not recorded. + * + * @return sampling result. + */ + SamplingDecision getDecision(); + + /** + * Return tags which will be attached to the span. + * + * @return attributes added to span. These attributes should be added to the span only when + * {@linkplain #getDecision() the sampling decision} is {@link SamplingDecision#RECORD_ONLY} + * or {@link SamplingDecision#RECORD_AND_SAMPLE}. + */ + Attributes getAttributes(); + + /** + * Return an optionally-updated {@link TraceState}, based on the parent TraceState. This may + * return the same {@link TraceState} that was provided originally, or an updated one. + * + * @param parentTraceState The TraceState from the parent span. Might be an empty TraceState, if + * there is no parent. This will be the same TraceState that was passed in via the {@link + * SpanContext} parameter on the {@link Sampler#shouldSample(Context, String, String, + * SpanKind, Attributes, List)} call. + */ + default TraceState getUpdatedTraceState(TraceState parentTraceState) { + return parentTraceState; + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/TraceIdRatioBasedSampler.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/TraceIdRatioBasedSampler.java new file mode 100644 index 000000000..0100ed794 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/TraceIdRatioBasedSampler.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.internal.OtelEncodingUtils; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.List; +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. + */ +@Immutable +final class TraceIdRatioBasedSampler implements Sampler { + + private static final SamplingResult POSITIVE_SAMPLING_RESULT = + SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE); + + private static final SamplingResult NEGATIVE_SAMPLING_RESULT = + SamplingResult.create(SamplingDecision.DROP); + + private final long idUpperBound; + private final String description; + + static TraceIdRatioBasedSampler create(double ratio) { + if (ratio < 0.0 || ratio > 1.0) { + throw new IllegalArgumentException("ratio 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 (ratio == 0.0) { + idUpperBound = Long.MIN_VALUE; + } else if (ratio == 1.0) { + idUpperBound = Long.MAX_VALUE; + } else { + idUpperBound = (long) (ratio * Long.MAX_VALUE); + } + return new TraceIdRatioBasedSampler(ratio, idUpperBound); + } + + TraceIdRatioBasedSampler(double ratio, long idUpperBound) { + this.idUpperBound = idUpperBound; + description = String.format("TraceIdRatioBased{%.6f}", ratio); + } + + @Override + public final SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + // Always sample if we are within probability range. This is true even for child spans (that + // may have had a different sampling samplingResult 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(getTraceIdRandomPart(traceId)) < idUpperBound + ? POSITIVE_SAMPLING_RESULT + : NEGATIVE_SAMPLING_RESULT; + } + + @Override + public final String getDescription() { + return description; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TraceIdRatioBasedSampler)) { + return false; + } + TraceIdRatioBasedSampler that = (TraceIdRatioBasedSampler) obj; + return idUpperBound == that.idUpperBound; + } + + @Override + public int hashCode() { + return Long.hashCode(idUpperBound); + } + + @Override + public final String toString() { + return getDescription(); + } + + // Visible for testing + long getIdUpperBound() { + return idUpperBound; + } + + private static long getTraceIdRandomPart(String traceId) { + return OtelEncodingUtils.longFromBase16String(traceId, 16); + } +} diff --git a/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/package-info.java b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/package-info.java new file mode 100644 index 000000000..a3f5e2c42 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This package contains {@link io.opentelemetry.sdk.trace.samplers.Sampler}s for selecting traces + * that are recorded and exported.
    + * + *

    Sampling is a mechanism to control the noise and overhead introduced by OpenTelemetry by + * reducing the number of samples of traces collected and sent to the backend. See the OpenTelemetry + * specification for more details.
    + * + *

    The following sampling strategies are provided here: + * + *

    {@link io.opentelemetry.sdk.trace.samplers.Sampler#alwaysOff()} : This strategy will ensure + * that no Spans are ever sent to the export pipeline. + * + *

    {@link io.opentelemetry.sdk.trace.samplers.Sampler#alwaysOn()} : This strategy will ensure + * that every Span will be sent to the export pipeline. + * + *

    {@link io.opentelemetry.sdk.trace.samplers.Sampler#traceIdRatioBased(double)} : This strategy + * will sample the provided fraction of Spans, deterministically based on the TraceId of the Spans. + * This means that all spans from the a given trace will have the same sampling result. + * + *

    {@link + * io.opentelemetry.sdk.trace.samplers.Sampler#parentBased(io.opentelemetry.sdk.trace.samplers.Sampler)} + * : This strategy will always use the sampled state of the parent span when deciding whether to + * sample a Span or not. If the the Span has no parent, the provided "root" Sampler will be used for + * that decision. The parent-based strategy is highly configurable, using the {@link + * io.opentelemetry.sdk.trace.samplers.ParentBasedSamplerBuilder} which can be acquired from the + * {@link + * io.opentelemetry.sdk.trace.samplers.Sampler#parentBasedBuilder(io.opentelemetry.sdk.trace.samplers.Sampler)} + * method. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.trace.samplers; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/AttributesMapTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/AttributesMapTest.java new file mode 100644 index 000000000..7f23afb8e --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/AttributesMapTest.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import org.junit.jupiter.api.Test; + +class AttributesMapTest { + + @Test + void asMap() { + AttributesMap attributesMap = new AttributesMap(2); + attributesMap.put(longKey("one"), 1L); + attributesMap.put(longKey("two"), 2L); + + assertThat(attributesMap.asMap()) + .containsOnly(entry(longKey("one"), 1L), entry(longKey("two"), 2L)); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/MultiSpanProcessorTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/MultiSpanProcessorTest.java new file mode 100644 index 000000000..33a33462f --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/MultiSpanProcessorTest.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class MultiSpanProcessorTest { + @Mock private SpanProcessor spanProcessor1; + @Mock private SpanProcessor spanProcessor2; + @Mock private ReadableSpan readableSpan; + @Mock private ReadWriteSpan readWriteSpan; + + @BeforeEach + void setUp() { + when(spanProcessor1.isStartRequired()).thenReturn(true); + when(spanProcessor1.isEndRequired()).thenReturn(true); + when(spanProcessor1.forceFlush()).thenReturn(CompletableResultCode.ofSuccess()); + when(spanProcessor1.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + when(spanProcessor2.isStartRequired()).thenReturn(true); + when(spanProcessor2.isEndRequired()).thenReturn(true); + when(spanProcessor2.forceFlush()).thenReturn(CompletableResultCode.ofSuccess()); + when(spanProcessor2.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + } + + @Test + void empty() { + SpanProcessor multiSpanProcessor = SpanProcessor.composite(Collections.emptyList()); + multiSpanProcessor.onStart(Context.root(), readWriteSpan); + multiSpanProcessor.onEnd(readableSpan); + multiSpanProcessor.shutdown(); + } + + @Test + void oneSpanProcessor() { + SpanProcessor multiSpanProcessor = + SpanProcessor.composite(Collections.singletonList(spanProcessor1)); + assertThat(multiSpanProcessor).isSameAs(spanProcessor1); + } + + @Test + void twoSpanProcessor() { + SpanProcessor multiSpanProcessor = + SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2)); + multiSpanProcessor.onStart(Context.root(), readWriteSpan); + verify(spanProcessor1).onStart(same(Context.root()), same(readWriteSpan)); + verify(spanProcessor2).onStart(same(Context.root()), same(readWriteSpan)); + + multiSpanProcessor.onEnd(readableSpan); + verify(spanProcessor1).onEnd(same(readableSpan)); + verify(spanProcessor2).onEnd(same(readableSpan)); + + multiSpanProcessor.forceFlush(); + verify(spanProcessor1).forceFlush(); + verify(spanProcessor2).forceFlush(); + + multiSpanProcessor.shutdown(); + verify(spanProcessor1).shutdown(); + verify(spanProcessor2).shutdown(); + } + + @Test + void twoSpanProcessor_DifferentRequirements() { + when(spanProcessor1.isEndRequired()).thenReturn(false); + when(spanProcessor2.isStartRequired()).thenReturn(false); + SpanProcessor multiSpanProcessor = + SpanProcessor.composite(Arrays.asList(spanProcessor1, spanProcessor2)); + + assertThat(multiSpanProcessor.isStartRequired()).isTrue(); + assertThat(multiSpanProcessor.isEndRequired()).isTrue(); + + multiSpanProcessor.onStart(Context.root(), readWriteSpan); + verify(spanProcessor1).onStart(same(Context.root()), same(readWriteSpan)); + verify(spanProcessor2, times(0)).onStart(any(Context.class), any(ReadWriteSpan.class)); + + multiSpanProcessor.onEnd(readableSpan); + verify(spanProcessor1, times(0)).onEnd(any(ReadableSpan.class)); + verify(spanProcessor2).onEnd(same(readableSpan)); + + multiSpanProcessor.forceFlush(); + verify(spanProcessor1).forceFlush(); + verify(spanProcessor2).forceFlush(); + + multiSpanProcessor.shutdown(); + verify(spanProcessor1).shutdown(); + verify(spanProcessor2).shutdown(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/NoopSpanProcessorTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/NoopSpanProcessorTest.java new file mode 100644 index 000000000..6a3b9dc71 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/NoopSpanProcessorTest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class NoopSpanProcessorTest { + @Mock private ReadableSpan readableSpan; + @Mock private ReadWriteSpan readWriteSpan; + + @Test + void noCrash() { + SpanProcessor noopSpanProcessor = NoopSpanProcessor.getInstance(); + noopSpanProcessor.onStart(Context.root(), readWriteSpan); + assertThat(noopSpanProcessor.isStartRequired()).isFalse(); + noopSpanProcessor.onEnd(readableSpan); + assertThat(noopSpanProcessor.isEndRequired()).isFalse(); + noopSpanProcessor.forceFlush(); + noopSpanProcessor.shutdown(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/RandomIdGeneratorTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/RandomIdGeneratorTest.java new file mode 100644 index 000000000..fcbed588d --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/RandomIdGeneratorTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceId; +import org.junit.jupiter.api.Test; + +class RandomIdGeneratorTest { + + @Test + void defaults() { + IdGenerator generator = IdGenerator.random(); + + // Can't assert values but can assert they're valid, try a lot as a sort of fuzz check. + for (int i = 0; i < 1000; i++) { + String traceId = generator.generateTraceId(); + assertThat(traceId).isNotEqualTo(TraceId.getInvalid()); + + String spanId = generator.generateSpanId(); + assertThat(spanId).isNotEqualTo(SpanId.getInvalid()); + } + } + + @Test + void androidVersion() { + IdGenerator generator = AndroidFriendlyRandomIdGenerator.INSTANCE; + + // Can't assert values but can assert they're valid, try a lot as a sort of fuzz check. + for (int i = 0; i < 1000; i++) { + String traceId = generator.generateTraceId(); + assertThat(traceId).isNotEqualTo(TraceId.getInvalid()); + + String spanId = generator.generateSpanId(); + assertThat(spanId).isNotEqualTo(SpanId.getInvalid()); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/RecordEventsReadableSpanTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/RecordEventsReadableSpanTest.java new file mode 100644 index 000000000..866b872e2 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/RecordEventsReadableSpanTest.java @@ -0,0 +1,1080 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@SuppressWarnings({"rawtypes", "unchecked"}) +@ExtendWith(MockitoExtension.class) +class RecordEventsReadableSpanTest { + private static final String SPAN_NAME = "MySpanName"; + private static final String SPAN_NEW_NAME = "NewName"; + private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); + private static final long MILLIS_PER_SECOND = TimeUnit.SECONDS.toMillis(1); + private static final long START_EPOCH_NANOS = 1000_123_789_654L; + + private final IdGenerator idsGenerator = IdGenerator.random(); + private final String traceId = idsGenerator.generateTraceId(); + private final String spanId = idsGenerator.generateSpanId(); + private final String parentSpanId = idsGenerator.generateSpanId(); + private final SpanContext spanContext = + SpanContext.create(traceId, spanId, TraceFlags.getDefault(), TraceState.getDefault()); + private final Resource resource = Resource.empty(); + private final InstrumentationLibraryInfo instrumentationLibraryInfo = + InstrumentationLibraryInfo.create("theName", null); + private final Map attributes = new HashMap<>(); + private Attributes expectedAttributes; + private final LinkData link = LinkData.create(spanContext); + @Mock private SpanProcessor spanProcessor; + + private TestClock testClock; + + @BeforeEach + void setUp() { + attributes.put(stringKey("MyStringAttributeKey"), "MyStringAttributeValue"); + attributes.put(longKey("MyLongAttributeKey"), 123L); + attributes.put(booleanKey("MyBooleanAttributeKey"), false); + AttributesBuilder builder = + Attributes.builder().put("MySingleStringAttributeKey", "MySingleStringAttributeValue"); + for (Map.Entry entry : attributes.entrySet()) { + builder.put(entry.getKey(), entry.getValue()); + } + expectedAttributes = builder.build(); + testClock = TestClock.create(START_EPOCH_NANOS); + } + + @Test + void nothingChangedAfterEnd() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + span.end(); + // Check that adding trace events or update fields after Span#end() does not throw any thrown + // and are ignored. + spanDoWork(span, StatusCode.ERROR, "CANCELLED"); + SpanData spanData = span.toSpanData(); + verifySpanData( + spanData, + Attributes.empty(), + Collections.emptyList(), + Collections.singletonList(link), + SPAN_NAME, + START_EPOCH_NANOS, + START_EPOCH_NANOS, + StatusData.unset(), + /*hasEnded=*/ true); + } + + @Test + void endSpanTwice_DoNotCrash() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + assertThat(span.hasEnded()).isFalse(); + span.end(); + assertThat(span.hasEnded()).isTrue(); + span.end(); + assertThat(span.hasEnded()).isTrue(); + } + + @Test + void toSpanData_ActiveSpan() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + try { + assertThat(span.hasEnded()).isFalse(); + spanDoWork(span, null, null); + SpanData spanData = span.toSpanData(); + EventData event = + EventData.create(START_EPOCH_NANOS + NANOS_PER_SECOND, "event2", Attributes.empty(), 0); + verifySpanData( + spanData, + expectedAttributes, + Collections.singletonList(event), + Collections.singletonList(link), + SPAN_NEW_NAME, + START_EPOCH_NANOS, + 0, + StatusData.unset(), + /*hasEnded=*/ false); + assertThat(span.hasEnded()).isFalse(); + assertThat(span.isRecording()).isTrue(); + } finally { + span.end(); + } + assertThat(span.hasEnded()).isTrue(); + assertThat(span.isRecording()).isFalse(); + } + + @Test + void toSpanData_EndedSpan() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + try { + spanDoWork(span, StatusCode.ERROR, "CANCELLED"); + } finally { + span.end(); + } + Mockito.verify(spanProcessor, Mockito.times(1)).onEnd(span); + SpanData spanData = span.toSpanData(); + EventData event = + EventData.create(START_EPOCH_NANOS + NANOS_PER_SECOND, "event2", Attributes.empty(), 0); + verifySpanData( + spanData, + expectedAttributes, + Collections.singletonList(event), + Collections.singletonList(link), + SPAN_NEW_NAME, + START_EPOCH_NANOS, + testClock.now(), + StatusData.create(StatusCode.ERROR, "CANCELLED"), + /*hasEnded=*/ true); + } + + @Test + void toSpanData_immutableLinks() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + SpanData spanData = span.toSpanData(); + + assertThatThrownBy(() -> spanData.getLinks().add(LinkData.create(SpanContext.getInvalid()))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void toSpanData_immutableEvents() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + SpanData spanData = span.toSpanData(); + + assertThatThrownBy( + () -> spanData.getEvents().add(EventData.create(1000, "test", Attributes.empty()))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void toSpanData_immutableEvents_ended() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + span.end(); + SpanData spanData = span.toSpanData(); + + assertThatThrownBy( + () -> spanData.getEvents().add(EventData.create(1000, "test", Attributes.empty()))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void toSpanData_RootSpan() { + RecordEventsReadableSpan span = createTestRootSpan(); + try { + spanDoWork(span, null, null); + } finally { + span.end(); + } + SpanData spanData = span.toSpanData(); + assertThat(SpanId.isValid(spanData.getParentSpanId())).isFalse(); + } + + @Test + void toSpanData_WithInitialAttributes() { + RecordEventsReadableSpan span = createTestSpanWithAttributes(attributes); + span.setAttribute("anotherKey", "anotherValue"); + span.end(); + SpanData spanData = span.toSpanData(); + assertThat(spanData.getAttributes().size()).isEqualTo(attributes.size() + 1); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(attributes.size() + 1); + } + + @Test + void toSpanData_SpanDataDoesNotChangeWhenModifyingSpan() { + // Create a span + RecordEventsReadableSpan span = createTestSpanWithAttributes(attributes); + + // Convert it to a SpanData object -- this should be an immutable snapshot. + SpanData spanData = span.toSpanData(); + + // Now modify the span after creating the snapshot. + span.setAttribute("anotherKey", "anotherValue"); + span.updateName("changedName"); + span.addEvent("newEvent"); + span.end(); + + // Assert that the snapshot does not reflect the modified state, but the state of the time when + // toSpanData was called. + assertThat(spanData.getAttributes().size()).isEqualTo(attributes.size()); + assertThat(spanData.getAttributes().get(stringKey("anotherKey"))).isNull(); + assertThat(spanData.hasEnded()).isFalse(); + assertThat(spanData.getEndEpochNanos()).isEqualTo(0); + assertThat(spanData.getName()).isEqualTo(SPAN_NAME); + assertThat(spanData.getEvents()).isEmpty(); + + // Sanity check: Calling toSpanData again after modifying the span should get us the modified + // state. + spanData = span.toSpanData(); + assertThat(spanData.getAttributes().size()).isEqualTo(attributes.size() + 1); + assertThat(spanData.getAttributes().get(stringKey("anotherKey"))).isEqualTo("anotherValue"); + assertThat(spanData.hasEnded()).isTrue(); + assertThat(spanData.getEndEpochNanos()).isGreaterThan(0); + assertThat(spanData.getName()).isEqualTo("changedName"); + assertThat(spanData.getEvents()).hasSize(1); + } + + @Test + void setStatus() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.CONSUMER); + try { + testClock.advanceMillis(MILLIS_PER_SECOND); + assertThat(span.toSpanData().getStatus()).isEqualTo(StatusData.unset()); + span.setStatus(StatusCode.ERROR, "CANCELLED"); + assertThat(span.toSpanData().getStatus()) + .isEqualTo(StatusData.create(StatusCode.ERROR, "CANCELLED")); + } finally { + span.end(); + } + assertThat(span.toSpanData().getStatus()) + .isEqualTo(StatusData.create(StatusCode.ERROR, "CANCELLED")); + } + + @Test + void getSpanKind() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.SERVER); + try { + assertThat(span.toSpanData().getKind()).isEqualTo(SpanKind.SERVER); + } finally { + span.end(); + } + } + + @Test + void getInstrumentationLibraryInfo() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.CLIENT); + try { + assertThat(span.getInstrumentationLibraryInfo()).isEqualTo(instrumentationLibraryInfo); + } finally { + span.end(); + } + } + + @Test + void getAndUpdateSpanName() { + RecordEventsReadableSpan span = createTestRootSpan(); + try { + assertThat(span.getName()).isEqualTo(SPAN_NAME); + span.updateName(SPAN_NEW_NAME); + assertThat(span.getName()).isEqualTo(SPAN_NEW_NAME); + } finally { + span.end(); + } + } + + @Test + void getLatencyNs_ActiveSpan() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + try { + testClock.advanceMillis(MILLIS_PER_SECOND); + long elapsedTimeNanos1 = testClock.now() - START_EPOCH_NANOS; + assertThat(span.getLatencyNanos()).isEqualTo(elapsedTimeNanos1); + testClock.advanceMillis(MILLIS_PER_SECOND); + long elapsedTimeNanos2 = testClock.now() - START_EPOCH_NANOS; + assertThat(span.getLatencyNanos()).isEqualTo(elapsedTimeNanos2); + } finally { + span.end(); + } + } + + @Test + void getLatencyNs_EndedSpan() { + RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + testClock.advanceMillis(MILLIS_PER_SECOND); + span.end(); + long elapsedTimeNanos = testClock.now() - START_EPOCH_NANOS; + assertThat(span.getLatencyNanos()).isEqualTo(elapsedTimeNanos); + testClock.advanceMillis(MILLIS_PER_SECOND); + assertThat(span.getLatencyNanos()).isEqualTo(elapsedTimeNanos); + } + + @Test + void setAttribute() { + RecordEventsReadableSpan span = createTestRootSpan(); + try { + span.setAttribute("StringKey", "StringVal"); + span.setAttribute("NullStringKey", null); + span.setAttribute("EmptyStringKey", ""); + span.setAttribute(stringKey("NullStringAttributeValue"), null); + span.setAttribute(stringKey("EmptyStringAttributeValue"), ""); + span.setAttribute("LongKey", 1000L); + span.setAttribute(longKey("LongKey2"), 5); + span.setAttribute(longKey("LongKey3"), 6L); + span.setAttribute("DoubleKey", 10.0); + span.setAttribute("BooleanKey", false); + span.setAttribute( + stringArrayKey("ArrayStringKey"), Arrays.asList("StringVal", null, "", "StringVal2")); + span.setAttribute(longArrayKey("ArrayLongKey"), Arrays.asList(1L, 2L, 3L, 4L, 5L)); + span.setAttribute(doubleArrayKey("ArrayDoubleKey"), Arrays.asList(0.1, 2.3, 4.5, 6.7, 8.9)); + span.setAttribute( + booleanArrayKey("ArrayBooleanKey"), Arrays.asList(true, false, false, true)); + // These should be dropped + span.setAttribute(stringArrayKey("NullArrayStringKey"), null); + span.setAttribute(longArrayKey("NullArrayLongKey"), null); + span.setAttribute(doubleArrayKey("NullArrayDoubleKey"), null); + span.setAttribute(booleanArrayKey("NullArrayBooleanKey"), null); + // These should be maintained + span.setAttribute(longArrayKey("ArrayWithNullLongKey"), Arrays.asList(new Long[] {null})); + span.setAttribute( + stringArrayKey("ArrayWithNullStringKey"), Arrays.asList(new String[] {null})); + span.setAttribute( + doubleArrayKey("ArrayWithNullDoubleKey"), Arrays.asList(new Double[] {null})); + span.setAttribute( + booleanArrayKey("ArrayWithNullBooleanKey"), Arrays.asList(new Boolean[] {null})); + } finally { + span.end(); + } + SpanData spanData = span.toSpanData(); + assertThat(spanData.getAttributes().size()).isEqualTo(16); + assertThat(spanData.getAttributes().get(stringKey("StringKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(stringKey("EmptyStringKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(stringKey("EmptyStringAttributeValue"))).isNotNull(); + assertThat(spanData.getAttributes().get(longKey("LongKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(longKey("LongKey2"))).isEqualTo(5L); + assertThat(spanData.getAttributes().get(longKey("LongKey3"))).isEqualTo(6L); + assertThat(spanData.getAttributes().get(doubleKey("DoubleKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(booleanKey("BooleanKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(stringArrayKey("ArrayStringKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(longArrayKey("ArrayLongKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(doubleArrayKey("ArrayDoubleKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(booleanArrayKey("ArrayBooleanKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(longArrayKey("ArrayWithNullLongKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(stringArrayKey("ArrayWithNullStringKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(doubleArrayKey("ArrayWithNullDoubleKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(booleanArrayKey("ArrayWithNullBooleanKey"))) + .isNotNull(); + assertThat(spanData.getAttributes().get(stringArrayKey("ArrayStringKey")).size()).isEqualTo(4); + assertThat(spanData.getAttributes().get(longArrayKey("ArrayLongKey")).size()).isEqualTo(5); + assertThat(spanData.getAttributes().get(doubleArrayKey("ArrayDoubleKey")).size()).isEqualTo(5); + assertThat(spanData.getAttributes().get(booleanArrayKey("ArrayBooleanKey")).size()) + .isEqualTo(4); + } + + @Test + void setAttribute_emptyKeys() { + RecordEventsReadableSpan span = createTestRootSpan(); + span.setAttribute("", ""); + span.setAttribute("", 1000L); + span.setAttribute("", 10.0); + span.setAttribute("", false); + span.setAttribute(stringArrayKey(""), Collections.emptyList()); + span.setAttribute(booleanArrayKey(""), Collections.emptyList()); + span.setAttribute(longArrayKey(""), Collections.emptyList()); + span.setAttribute(doubleArrayKey(""), Collections.emptyList()); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(0); + } + + @Test + void setAttribute_nullKeys() { + RecordEventsReadableSpan span = createTestRootSpan(); + span.setAttribute(stringKey(null), ""); + span.setAttribute(null, 1000L); + span.setAttribute(null, 10.0); + span.setAttribute(null, false); + span.setAttribute(null, Collections.emptyList()); + span.setAttribute(null, Collections.emptyList()); + span.setAttribute(null, Collections.emptyList()); + span.setAttribute(null, Collections.emptyList()); + assertThat(span.toSpanData().getAttributes().size()).isZero(); + } + + @Test + void setAttribute_emptyArrayAttributeValue() { + RecordEventsReadableSpan span = createTestRootSpan(); + span.setAttribute(stringArrayKey("stringArrayAttribute"), null); + assertThat(span.toSpanData().getAttributes().size()).isZero(); + span.setAttribute(stringArrayKey("stringArrayAttribute"), Collections.emptyList()); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(1); + span.setAttribute(booleanArrayKey("boolArrayAttribute"), null); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(1); + span.setAttribute(booleanArrayKey("boolArrayAttribute"), Collections.emptyList()); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(2); + span.setAttribute(longArrayKey("longArrayAttribute"), null); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(2); + span.setAttribute(longArrayKey("longArrayAttribute"), Collections.emptyList()); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(3); + span.setAttribute(doubleArrayKey("doubleArrayAttribute"), null); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(3); + span.setAttribute(doubleArrayKey("doubleArrayAttribute"), Collections.emptyList()); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(4); + } + + @Test + void setAttribute_nullStringValue() { + RecordEventsReadableSpan span = createTestRootSpan(); + span.setAttribute("nullString", null); + span.setAttribute("emptyString", ""); + span.setAttribute(stringKey("nullStringAttributeValue"), null); + span.setAttribute(stringKey("emptyStringAttributeValue"), ""); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(2); + } + + @Test + void setAttribute_nullAttributeValue() { + RecordEventsReadableSpan span = createTestRootSpan(); + span.setAttribute("emptyString", ""); + span.setAttribute(stringKey("nullString"), null); + span.setAttribute(stringKey("nullStringAttributeValue"), null); + span.setAttribute(stringKey("emptyStringAttributeValue"), ""); + span.setAttribute("longAttribute", 0L); + span.setAttribute("boolAttribute", false); + span.setAttribute("doubleAttribute", 0.12345f); + span.setAttribute(stringArrayKey("stringArrayAttribute"), Arrays.asList("", null)); + span.setAttribute(booleanArrayKey("boolArrayAttribute"), Arrays.asList(true, null)); + span.setAttribute(longArrayKey("longArrayAttribute"), Arrays.asList(12345L, null)); + span.setAttribute(doubleArrayKey("doubleArrayAttribute"), Arrays.asList(1.2345, null)); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(9); + } + + @Test + void setAllAttributes() { + RecordEventsReadableSpan span = createTestRootSpan(); + Attributes attributes = + Attributes.builder() + .put("StringKey", "StringVal") + .put("NullStringKey", (String) null) + .put("EmptyStringKey", "") + .put(stringKey("NullStringAttributeValue"), null) + .put(stringKey("EmptyStringAttributeValue"), "") + .put("LongKey", 1000L) + .put(longKey("LongKey2"), 5) + .put(longKey("LongKey3"), 6L) + .put("DoubleKey", 10.0) + .put("BooleanKey", false) + .put( + stringArrayKey("ArrayStringKey"), + Arrays.asList("StringVal", null, "", "StringVal2")) + .put(longArrayKey("ArrayLongKey"), Arrays.asList(1L, 2L, 3L, 4L, 5L)) + .put(doubleArrayKey("ArrayDoubleKey"), Arrays.asList(0.1, 2.3, 4.5, 6.7, 8.9)) + .put(booleanArrayKey("ArrayBooleanKey"), Arrays.asList(true, false, false, true)) + // These should be dropped + .put(stringArrayKey("NullArrayStringKey"), null) + .put(longArrayKey("NullArrayLongKey"), null) + .put(doubleArrayKey("NullArrayDoubleKey"), null) + .put(booleanArrayKey("NullArrayBooleanKey"), null) + // These should be maintained + .put(longArrayKey("ArrayWithNullLongKey"), Arrays.asList(new Long[] {null})) + .put(stringArrayKey("ArrayWithNullStringKey"), Arrays.asList(new String[] {null})) + .put(doubleArrayKey("ArrayWithNullDoubleKey"), Arrays.asList(new Double[] {null})) + .put(booleanArrayKey("ArrayWithNullBooleanKey"), Arrays.asList(new Boolean[] {null})) + .build(); + + try { + span.setAllAttributes(attributes); + } finally { + span.end(); + } + + SpanData spanData = span.toSpanData(); + assertThat(spanData.getAttributes().size()).isEqualTo(16); + assertThat(spanData.getAttributes().get(stringKey("StringKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(stringKey("EmptyStringKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(stringKey("EmptyStringAttributeValue"))).isNotNull(); + assertThat(spanData.getAttributes().get(longKey("LongKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(longKey("LongKey2"))).isEqualTo(5L); + assertThat(spanData.getAttributes().get(longKey("LongKey3"))).isEqualTo(6L); + assertThat(spanData.getAttributes().get(doubleKey("DoubleKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(booleanKey("BooleanKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(stringArrayKey("ArrayStringKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(longArrayKey("ArrayLongKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(doubleArrayKey("ArrayDoubleKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(booleanArrayKey("ArrayBooleanKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(longArrayKey("ArrayWithNullLongKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(stringArrayKey("ArrayWithNullStringKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(doubleArrayKey("ArrayWithNullDoubleKey"))).isNotNull(); + assertThat(spanData.getAttributes().get(booleanArrayKey("ArrayWithNullBooleanKey"))) + .isNotNull(); + assertThat(spanData.getAttributes().get(stringArrayKey("ArrayStringKey")).size()).isEqualTo(4); + assertThat(spanData.getAttributes().get(longArrayKey("ArrayLongKey")).size()).isEqualTo(5); + assertThat(spanData.getAttributes().get(doubleArrayKey("ArrayDoubleKey")).size()).isEqualTo(5); + assertThat(spanData.getAttributes().get(booleanArrayKey("ArrayBooleanKey")).size()) + .isEqualTo(4); + } + + @Test + void setAllAttributes_mergesAttributes() { + RecordEventsReadableSpan span = createTestRootSpan(); + Attributes attributes = + Attributes.builder() + .put("StringKey", "StringVal") + .put("LongKey", 1000L) + .put("DoubleKey", 10.0) + .put("BooleanKey", false) + .build(); + + try { + span.setAttribute("StringKey", "OtherStringVal") + .setAttribute("ExistingStringKey", "ExistingStringVal") + .setAttribute("LongKey", 2000L) + .setAllAttributes(attributes); + } finally { + span.end(); + } + + SpanData spanData = span.toSpanData(); + assertThat(spanData.getAttributes().size()).isEqualTo(5); + assertThat(spanData.getAttributes().get(stringKey("StringKey"))) + .isNotNull() + .isEqualTo("StringVal"); + assertThat(spanData.getAttributes().get(stringKey("ExistingStringKey"))) + .isNotNull() + .isEqualTo("ExistingStringVal"); + assertThat(spanData.getAttributes().get(longKey("LongKey"))).isNotNull().isEqualTo(1000L); + assertThat(spanData.getAttributes().get(doubleKey("DoubleKey"))).isNotNull().isEqualTo(10.0); + assertThat(spanData.getAttributes().get(booleanKey("BooleanKey"))).isNotNull().isEqualTo(false); + } + + @Test + void setAllAttributes_nullAttributes() { + RecordEventsReadableSpan span = createTestRootSpan(); + span.setAllAttributes(null); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(0); + } + + @Test + void setAllAttributes_emptyAttributes() { + RecordEventsReadableSpan span = createTestRootSpan(); + span.setAllAttributes(Attributes.empty()); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(0); + } + + @Test + void addEvent() { + RecordEventsReadableSpan span = createTestRootSpan(); + try { + span.addEvent("event1"); + span.addEvent("event2", Attributes.of(stringKey("e1key"), "e1Value")); + span.addEvent("event3", 10, TimeUnit.SECONDS); + span.addEvent("event4", Instant.ofEpochSecond(20)); + span.addEvent( + "event5", Attributes.builder().put("foo", "bar").build(), 30, TimeUnit.MILLISECONDS); + span.addEvent( + "event6", Attributes.builder().put("foo", "bar").build(), Instant.ofEpochMilli(1000)); + } finally { + span.end(); + } + List events = span.toSpanData().getEvents(); + assertThat(events).hasSize(6); + assertThat(events.get(0)) + .satisfies( + event -> { + assertThat(event.getName()).isEqualTo("event1"); + assertThat(event.getAttributes()).isEqualTo(Attributes.empty()); + assertThat(event.getEpochNanos()).isEqualTo(START_EPOCH_NANOS); + }); + assertThat(events.get(1)) + .satisfies( + event -> { + assertThat(event.getName()).isEqualTo("event2"); + assertThat(event.getAttributes()) + .isEqualTo(Attributes.of(stringKey("e1key"), "e1Value")); + assertThat(event.getEpochNanos()).isEqualTo(START_EPOCH_NANOS); + }); + assertThat(events.get(2)) + .satisfies( + event -> { + assertThat(event.getName()).isEqualTo("event3"); + assertThat(event.getAttributes()).isEqualTo(Attributes.empty()); + assertThat(event.getEpochNanos()).isEqualTo(TimeUnit.SECONDS.toNanos(10)); + }); + assertThat(events.get(3)) + .satisfies( + event -> { + assertThat(event.getName()).isEqualTo("event4"); + assertThat(event.getAttributes()).isEqualTo(Attributes.empty()); + assertThat(event.getEpochNanos()).isEqualTo(TimeUnit.SECONDS.toNanos(20)); + }); + assertThat(events.get(4)) + .satisfies( + event -> { + assertThat(event.getName()).isEqualTo("event5"); + assertThat(event.getAttributes()) + .isEqualTo(Attributes.builder().put("foo", "bar").build()); + assertThat(event.getEpochNanos()).isEqualTo(TimeUnit.MILLISECONDS.toNanos(30)); + }); + assertThat(events.get(5)) + .satisfies( + event -> { + assertThat(event.getName()).isEqualTo("event6"); + assertThat(event.getAttributes()) + .isEqualTo(Attributes.builder().put("foo", "bar").build()); + assertThat(event.getEpochNanos()).isEqualTo(TimeUnit.MILLISECONDS.toNanos(1000)); + }); + } + + @Test + void droppingAttributes() { + final int maxNumberOfAttributes = 8; + SpanLimits spanLimits = + SpanLimits.builder().setMaxNumberOfAttributes(maxNumberOfAttributes).build(); + RecordEventsReadableSpan span = createTestSpan(spanLimits); + try { + for (int i = 0; i < 2 * maxNumberOfAttributes; i++) { + span.setAttribute(longKey("MyStringAttributeKey" + i), (long) i); + } + SpanData spanData = span.toSpanData(); + assertThat(spanData.getAttributes().size()).isEqualTo(maxNumberOfAttributes); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(2 * maxNumberOfAttributes); + } finally { + span.end(); + } + SpanData spanData = span.toSpanData(); + assertThat(spanData.getAttributes().size()).isEqualTo(maxNumberOfAttributes); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(2 * maxNumberOfAttributes); + } + + @Test + void endWithTimestamp_numeric() { + RecordEventsReadableSpan span1 = createTestRootSpan(); + span1.end(10, TimeUnit.NANOSECONDS); + assertThat(span1.toSpanData().getEndEpochNanos()).isEqualTo(10); + } + + @Test + void endWithTimestamp_instant() { + RecordEventsReadableSpan span1 = createTestRootSpan(); + span1.end(Instant.ofEpochMilli(10)); + assertThat(span1.toSpanData().getEndEpochNanos()).isEqualTo(TimeUnit.MILLISECONDS.toNanos(10)); + } + + @Test + void droppingAndAddingAttributes() { + final int maxNumberOfAttributes = 8; + SpanLimits spanLimits = + SpanLimits.builder().setMaxNumberOfAttributes(maxNumberOfAttributes).build(); + RecordEventsReadableSpan span = createTestSpan(spanLimits); + try { + for (int i = 0; i < 2 * maxNumberOfAttributes; i++) { + span.setAttribute(longKey("MyStringAttributeKey" + i), (long) i); + } + SpanData spanData = span.toSpanData(); + assertThat(spanData.getAttributes().size()).isEqualTo(maxNumberOfAttributes); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(2 * maxNumberOfAttributes); + + for (int i = 0; i < maxNumberOfAttributes / 2; i++) { + int val = i + maxNumberOfAttributes * 3 / 2; + span.setAttribute(longKey("MyStringAttributeKey" + i), (long) val); + } + spanData = span.toSpanData(); + assertThat(spanData.getAttributes().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++) { + int val = i + maxNumberOfAttributes * 3 / 2; + assertThat(spanData.getAttributes().get(longKey("MyStringAttributeKey" + i))) + .isEqualTo(val); + } + // Test that we have the newest re-added initial entries. + for (int i = maxNumberOfAttributes / 2; i < maxNumberOfAttributes; i++) { + assertThat(spanData.getAttributes().get(longKey("MyStringAttributeKey" + i))).isEqualTo(i); + } + } finally { + span.end(); + } + } + + @Test + void droppingEvents() { + final int maxNumberOfEvents = 8; + SpanLimits spanLimits = SpanLimits.builder().setMaxNumberOfEvents(maxNumberOfEvents).build(); + RecordEventsReadableSpan span = createTestSpan(spanLimits); + try { + for (int i = 0; i < 2 * maxNumberOfEvents; i++) { + span.addEvent("event2", Attributes.empty()); + testClock.advanceMillis(MILLIS_PER_SECOND); + } + SpanData spanData = span.toSpanData(); + + assertThat(spanData.getEvents().size()).isEqualTo(maxNumberOfEvents); + for (int i = 0; i < maxNumberOfEvents; i++) { + EventData expectedEvent = + EventData.create( + START_EPOCH_NANOS + i * NANOS_PER_SECOND, "event2", Attributes.empty(), 0); + assertThat(spanData.getEvents().get(i)).isEqualTo(expectedEvent); + assertThat(spanData.getTotalRecordedEvents()).isEqualTo(2 * maxNumberOfEvents); + } + } finally { + span.end(); + } + SpanData spanData = span.toSpanData(); + assertThat(spanData.getEvents().size()).isEqualTo(maxNumberOfEvents); + for (int i = 0; i < maxNumberOfEvents; i++) { + EventData expectedEvent = + EventData.create( + START_EPOCH_NANOS + i * NANOS_PER_SECOND, "event2", Attributes.empty(), 0); + assertThat(spanData.getEvents().get(i)).isEqualTo(expectedEvent); + } + } + + @Test + void recordException() { + IllegalStateException exception = new IllegalStateException("there was an exception"); + RecordEventsReadableSpan span = createTestRootSpan(); + + StringWriter writer = new StringWriter(); + exception.printStackTrace(new PrintWriter(writer)); + String stacktrace = writer.toString(); + + testClock.advanceNanos(1000); + long timestamp = testClock.now(); + + span.recordException(exception); + + List events = span.toSpanData().getEvents(); + assertThat(events).hasSize(1); + EventData event = events.get(0); + assertThat(event.getName()).isEqualTo("exception"); + assertThat(event.getEpochNanos()).isEqualTo(timestamp); + assertThat(event.getAttributes()) + .isEqualTo( + Attributes.builder() + .put(SemanticAttributes.EXCEPTION_TYPE, "java.lang.IllegalStateException") + .put(SemanticAttributes.EXCEPTION_MESSAGE, "there was an exception") + .put(SemanticAttributes.EXCEPTION_STACKTRACE, stacktrace) + .build()); + } + + @Test + void recordException_noMessage() { + IllegalStateException exception = new IllegalStateException(); + RecordEventsReadableSpan span = createTestRootSpan(); + + span.recordException(exception); + + List events = span.toSpanData().getEvents(); + assertThat(events).hasSize(1); + EventData event = events.get(0); + assertThat(event.getAttributes().get(SemanticAttributes.EXCEPTION_MESSAGE)).isNull(); + } + + private static class InnerClassException extends Exception {} + + @Test + void recordException_innerClassException() { + InnerClassException exception = new InnerClassException(); + RecordEventsReadableSpan span = createTestRootSpan(); + + span.recordException(exception); + + List events = span.toSpanData().getEvents(); + assertThat(events).hasSize(1); + EventData event = events.get(0); + assertThat(event.getAttributes().get(SemanticAttributes.EXCEPTION_TYPE)) + .isEqualTo("io.opentelemetry.sdk.trace.RecordEventsReadableSpanTest.InnerClassException"); + } + + @Test + void recordException_additionalAttributes() { + IllegalStateException exception = new IllegalStateException("there was an exception"); + RecordEventsReadableSpan span = createTestRootSpan(); + + StringWriter writer = new StringWriter(); + exception.printStackTrace(new PrintWriter(writer)); + String stacktrace = writer.toString(); + + testClock.advanceNanos(1000); + long timestamp = testClock.now(); + + span.recordException( + exception, + Attributes.of( + stringKey("key1"), + "this is an additional attribute", + stringKey("exception.message"), + "this is a precedence attribute")); + + List events = span.toSpanData().getEvents(); + assertThat(events).hasSize(1); + EventData event = events.get(0); + assertThat(event.getName()).isEqualTo("exception"); + assertThat(event.getEpochNanos()).isEqualTo(timestamp); + assertThat(event.getAttributes()) + .isEqualTo( + Attributes.builder() + .put("key1", "this is an additional attribute") + .put("exception.type", "java.lang.IllegalStateException") + .put("exception.message", "this is a precedence attribute") + .put("exception.stacktrace", stacktrace) + .build()); + } + + @Test + void badArgsIgnored() { + RecordEventsReadableSpan span = createTestRootSpan(); + + // Should be no exceptions + span.setAttribute(null, 0L); + span.setStatus(null); + span.setStatus(null, null); + span.updateName(null); + span.addEvent(null); + span.addEvent(null, 0, null); + span.addEvent("event", 0, null); + span.addEvent(null, (Attributes) null); + span.addEvent("event", (Attributes) null); + span.addEvent(null, (Instant) null); + span.addEvent(null, null, 0, null); + span.addEvent("event", null, 0, TimeUnit.MILLISECONDS); + span.addEvent("event", Attributes.empty(), 0, null); + span.addEvent(null, null, null); + span.recordException(null); + span.end(0, TimeUnit.NANOSECONDS); + span.end(1, null); + span.end(null); + + // Ignored the bad calls + SpanData data = span.toSpanData(); + assertThat(data.getAttributes().isEmpty()).isTrue(); + assertThat(data.getStatus()).isEqualTo(StatusData.unset()); + assertThat(data.getName()).isEqualTo(SPAN_NAME); + } + + private RecordEventsReadableSpan createTestSpanWithAttributes( + Map attributes) { + AttributesMap attributesMap = + new AttributesMap(SpanLimits.getDefault().getMaxNumberOfAttributes()); + attributes.forEach(attributesMap::put); + return createTestSpan( + SpanKind.INTERNAL, + SpanLimits.getDefault(), + null, + attributesMap, + Collections.singletonList(link)); + } + + private RecordEventsReadableSpan createTestRootSpan() { + return createTestSpan( + SpanKind.INTERNAL, + SpanLimits.getDefault(), + SpanId.getInvalid(), + null, + Collections.singletonList(link)); + } + + private RecordEventsReadableSpan createTestSpan(SpanKind kind) { + return createTestSpan( + kind, SpanLimits.getDefault(), parentSpanId, null, Collections.singletonList(link)); + } + + private RecordEventsReadableSpan createTestSpan(SpanLimits config) { + return createTestSpan( + SpanKind.INTERNAL, config, parentSpanId, null, Collections.singletonList(link)); + } + + private RecordEventsReadableSpan createTestSpan( + SpanKind kind, + SpanLimits config, + @Nullable String parentSpanId, + @Nullable AttributesMap attributes, + List links) { + + RecordEventsReadableSpan span = + RecordEventsReadableSpan.startSpan( + spanContext, + SPAN_NAME, + instrumentationLibraryInfo, + kind, + parentSpanId != null + ? SpanContext.create( + traceId, parentSpanId, TraceFlags.getDefault(), TraceState.getDefault()) + : SpanContext.getInvalid(), + Context.root(), + config, + spanProcessor, + testClock, + resource, + attributes, + links, + 1, + 0); + Mockito.verify(spanProcessor, Mockito.times(1)).onStart(Context.root(), span); + return span; + } + + private void spanDoWork( + RecordEventsReadableSpan span, + @Nullable StatusCode canonicalCode, + @Nullable String descriptio) { + span.setAttribute("MySingleStringAttributeKey", "MySingleStringAttributeValue"); + attributes.forEach(span::setAttribute); + testClock.advanceMillis(MILLIS_PER_SECOND); + span.addEvent("event2", Attributes.empty()); + testClock.advanceMillis(MILLIS_PER_SECOND); + span.updateName(SPAN_NEW_NAME); + if (canonicalCode != null) { + span.setStatus(canonicalCode, descriptio); + } + } + + private void verifySpanData( + SpanData spanData, + final Attributes attributes, + List eventData, + List links, + String spanName, + long startEpochNanos, + long endEpochNanos, + StatusData status, + boolean hasEnded) { + assertThat(spanData.getTraceId()).isEqualTo(traceId); + assertThat(spanData.getSpanId()).isEqualTo(spanId); + assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId); + assertThat(spanData.getSpanContext().getTraceState()).isEqualTo(TraceState.getDefault()); + assertThat(spanData.getResource()).isEqualTo(resource); + assertThat(spanData.getInstrumentationLibraryInfo()).isEqualTo(instrumentationLibraryInfo); + assertThat(spanData.getName()).isEqualTo(spanName); + assertThat(spanData.getEvents()).isEqualTo(eventData); + assertThat(spanData.getLinks()).isEqualTo(links); + assertThat(spanData.getStartEpochNanos()).isEqualTo(startEpochNanos); + assertThat(spanData.getEndEpochNanos()).isEqualTo(endEpochNanos); + assertThat(spanData.getStatus().getStatusCode()).isEqualTo(status.getStatusCode()); + assertThat(spanData.hasEnded()).isEqualTo(hasEnded); + + // verify equality manually, since the implementations don't all equals with each other. + Attributes spanDataAttributes = spanData.getAttributes(); + assertThat(spanDataAttributes.size()).isEqualTo(attributes.size()); + spanDataAttributes.forEach((key, value) -> assertThat(attributes.get(key)).isEqualTo(value)); + } + + @Test + void testAsSpanData() { + String name = "GreatSpan"; + SpanKind kind = SpanKind.SERVER; + String traceId = this.traceId; + String spanId = this.spanId; + String parentSpanId = this.parentSpanId; + SpanLimits spanLimits = SpanLimits.getDefault(); + SpanProcessor spanProcessor = NoopSpanProcessor.getInstance(); + TestClock clock = TestClock.create(); + Resource resource = this.resource; + Attributes attributes = TestUtils.generateRandomAttributes(); + final AttributesMap attributesWithCapacity = new AttributesMap(32); + attributes.forEach((key, value) -> attributesWithCapacity.put((AttributeKey) key, value)); + Attributes event1Attributes = TestUtils.generateRandomAttributes(); + Attributes event2Attributes = TestUtils.generateRandomAttributes(); + SpanContext context = + SpanContext.create(traceId, spanId, TraceFlags.getDefault(), TraceState.getDefault()); + LinkData link1 = LinkData.create(context, TestUtils.generateRandomAttributes()); + + RecordEventsReadableSpan readableSpan = + RecordEventsReadableSpan.startSpan( + context, + name, + instrumentationLibraryInfo, + kind, + parentSpanId != null + ? SpanContext.create( + traceId, parentSpanId, TraceFlags.getDefault(), TraceState.getDefault()) + : SpanContext.getInvalid(), + Context.root(), + spanLimits, + spanProcessor, + clock, + resource, + attributesWithCapacity, + Collections.singletonList(link1), + 1, + 0); + long startEpochNanos = clock.now(); + clock.advanceMillis(4); + long firstEventEpochNanos = clock.now(); + readableSpan.addEvent("event1", event1Attributes); + clock.advanceMillis(6); + long secondEventTimeNanos = clock.now(); + readableSpan.addEvent("event2", event2Attributes); + + clock.advanceMillis(100); + readableSpan.end(); + long endEpochNanos = clock.now(); + + List events = + Arrays.asList( + EventData.create( + firstEventEpochNanos, "event1", event1Attributes, event1Attributes.size()), + EventData.create( + secondEventTimeNanos, "event2", event2Attributes, event2Attributes.size())); + + SpanData result = readableSpan.toSpanData(); + verifySpanData( + result, + attributesWithCapacity, + events, + Collections.singletonList(link1), + name, + startEpochNanos, + endEpochNanos, + StatusData.unset(), + /* hasEnded= */ true); + assertThat(result.getTotalRecordedLinks()).isEqualTo(1); + assertThat(result.getSpanContext().isSampled()).isEqualTo(false); + } + + @Test + void testConcurrentModification() throws ExecutionException, InterruptedException { + final RecordEventsReadableSpan span = createTestSpan(SpanKind.INTERNAL); + ExecutorService es = Executors.newSingleThreadExecutor(); + Future modifierFuture = + es.submit( + () -> { + for (int i = 0; i < 5096 * 5; ++i) { + span.setAttribute("hey" + i, ""); + } + }); + try { + for (int i = 0; i < 5096 * 5; ++i) { + span.toSpanData(); + } + } catch (Throwable t) { + modifierFuture.cancel(true); + throw t; + } + modifierFuture.get(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java new file mode 100644 index 000000000..b2c30140e --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -0,0 +1,1017 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SdkSpanBuilderTest { + + private static final String SPAN_NAME = "span_name"; + private final SpanContext sampledSpanContext = + SpanContext.create( + "12345678876543211234567887654321", + "8765432112345678", + TraceFlags.getSampled(), + TraceState.getDefault()); + + @Mock private SpanProcessor mockedSpanProcessor; + + private SdkTracer sdkTracer; + + @BeforeEach + public void setUp() { + SdkTracerProvider tracerSdkFactory = + SdkTracerProvider.builder().addSpanProcessor(mockedSpanProcessor).build(); + sdkTracer = (SdkTracer) tracerSdkFactory.get("SpanBuilderSdkTest"); + + Mockito.when(mockedSpanProcessor.isStartRequired()).thenReturn(true); + Mockito.when(mockedSpanProcessor.isEndRequired()).thenReturn(true); + } + + @Test + void addLink() { + // Verify methods do not crash. + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.addLink(sampledSpanContext); + spanBuilder.addLink(sampledSpanContext, Attributes.empty()); + + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + assertThat(span.toSpanData().getLinks()).hasSize(2); + } finally { + span.end(); + } + } + + @Test + void addLink_invalid() { + // Verify methods do not crash. + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.addLink(Span.getInvalid().getSpanContext()); + spanBuilder.addLink(Span.getInvalid().getSpanContext(), Attributes.empty()); + + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + assertThat(span.toSpanData().getLinks()).isEmpty(); + } finally { + span.end(); + } + } + + @Test + void truncateLink() { + final int maxNumberOfLinks = 8; + SpanLimits spanLimits = SpanLimits.builder().setMaxNumberOfLinks(maxNumberOfLinks).build(); + TracerProvider tracerProvider = SdkTracerProvider.builder().setSpanLimits(spanLimits).build(); + // Verify methods do not crash. + SpanBuilder spanBuilder = tracerProvider.get("test").spanBuilder(SPAN_NAME); + for (int i = 0; i < 2 * maxNumberOfLinks; i++) { + spanBuilder.addLink(sampledSpanContext); + } + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + SpanData spanData = span.toSpanData(); + List links = spanData.getLinks(); + assertThat(links).hasSize(maxNumberOfLinks); + for (int i = 0; i < maxNumberOfLinks; i++) { + assertThat(links.get(i)).isEqualTo(LinkData.create(sampledSpanContext)); + assertThat(spanData.getTotalRecordedLinks()).isEqualTo(2 * maxNumberOfLinks); + } + } finally { + span.end(); + } + } + + @Test + void truncateLinkAttributes() { + SpanLimits spanLimits = SpanLimits.builder().setMaxNumberOfAttributesPerLink(1).build(); + TracerProvider tracerProvider = SdkTracerProvider.builder().setSpanLimits(spanLimits).build(); + // Verify methods do not crash. + SpanBuilder spanBuilder = tracerProvider.get("test").spanBuilder(SPAN_NAME); + Attributes attributes = + Attributes.of( + stringKey("key0"), "str", + stringKey("key1"), "str", + stringKey("key2"), "str"); + spanBuilder.addLink(sampledSpanContext, attributes); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + assertThat(span.toSpanData().getLinks()) + .containsExactly( + LinkData.create(sampledSpanContext, Attributes.of(stringKey("key0"), "str"), 3)); + } finally { + span.end(); + } + } + + @Test + void addLink_NoEffectAfterStartSpan() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.addLink(sampledSpanContext); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + assertThat(span.toSpanData().getLinks()) + .containsExactly(LinkData.create(sampledSpanContext, Attributes.empty())); + // Use a different sampledSpanContext to ensure no logic that avoids duplicate links makes + // this test to pass. + spanBuilder.addLink( + SpanContext.create( + "00000000000004d20000000000001a85", + "0000000000002694", + TraceFlags.getSampled(), + TraceState.getDefault())); + assertThat(span.toSpanData().getLinks()) + .containsExactly(LinkData.create(sampledSpanContext, Attributes.empty())); + } finally { + span.end(); + } + } + + @Test + void addLinkSpanContext_null() { + assertThatCode(() -> sdkTracer.spanBuilder(SPAN_NAME).addLink(null)).doesNotThrowAnyException(); + } + + @Test + void addLinkSpanContextAttributes_nullContext() { + assertThatCode(() -> sdkTracer.spanBuilder(SPAN_NAME).addLink(null, Attributes.empty())) + .doesNotThrowAnyException(); + } + + @Test + void addLinkSpanContextAttributes_nullAttributes() { + assertThatCode(() -> sdkTracer.spanBuilder(SPAN_NAME).addLink(sampledSpanContext, null)) + .doesNotThrowAnyException(); + } + + @Test + void setAttribute() { + SpanBuilder spanBuilder = + sdkTracer + .spanBuilder(SPAN_NAME) + .setAttribute("string", "value") + .setAttribute("long", 12345L) + .setAttribute("double", .12345) + .setAttribute("boolean", true) + .setAttribute(stringKey("stringAttribute"), "attrvalue"); + + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + SpanData spanData = span.toSpanData(); + Attributes attrs = spanData.getAttributes(); + assertThat(attrs.size()).isEqualTo(5); + assertThat(attrs.get(stringKey("string"))).isEqualTo("value"); + assertThat(attrs.get(longKey("long"))).isEqualTo(12345L); + assertThat(attrs.get(doubleKey("double"))).isEqualTo(0.12345); + assertThat(attrs.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(attrs.get(stringKey("stringAttribute"))).isEqualTo("attrvalue"); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(5); + } finally { + span.end(); + } + } + + @Test + void setAttribute_afterEnd() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.setAttribute("string", "value"); + spanBuilder.setAttribute("long", 12345L); + spanBuilder.setAttribute("double", .12345); + spanBuilder.setAttribute("boolean", true); + spanBuilder.setAttribute(stringKey("stringAttribute"), "attrvalue"); + + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + Attributes attrs = span.toSpanData().getAttributes(); + assertThat(attrs.size()).isEqualTo(5); + assertThat(attrs.get(stringKey("string"))).isEqualTo("value"); + assertThat(attrs.get(longKey("long"))).isEqualTo(12345L); + assertThat(attrs.get(doubleKey("double"))).isEqualTo(0.12345); + assertThat(attrs.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(attrs.get(stringKey("stringAttribute"))).isEqualTo("attrvalue"); + } finally { + span.end(); + } + + span.setAttribute("string2", "value"); + span.setAttribute("long2", 12345L); + span.setAttribute("double2", .12345); + span.setAttribute("boolean2", true); + span.setAttribute(stringKey("stringAttribute2"), "attrvalue"); + + Attributes attrs = span.toSpanData().getAttributes(); + assertThat(attrs.size()).isEqualTo(5); + assertThat(attrs.get(stringKey("string2"))).isNull(); + assertThat(attrs.get(longKey("long2"))).isNull(); + assertThat(attrs.get(doubleKey("double2"))).isNull(); + assertThat(attrs.get(booleanKey("boolean2"))).isNull(); + assertThat(attrs.get(stringKey("stringAttribute2"))).isNull(); + } + + @Test + void setAttribute_emptyArrayAttributeValue() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.setAttribute(stringArrayKey("stringArrayAttribute"), emptyList()); + spanBuilder.setAttribute(booleanArrayKey("boolArrayAttribute"), emptyList()); + spanBuilder.setAttribute(longArrayKey("longArrayAttribute"), emptyList()); + spanBuilder.setAttribute(doubleArrayKey("doubleArrayAttribute"), emptyList()); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(4); + } + + @Test + void setAttribute_nullStringValue() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.setAttribute("emptyString", ""); + spanBuilder.setAttribute("nullString", null); + spanBuilder.setAttribute(stringKey("nullStringAttributeValue"), null); + spanBuilder.setAttribute(stringKey("emptyStringAttributeValue"), ""); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(2); + } + + @Test + void setAttribute_onlyNullStringValue() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.setAttribute(stringKey("nullStringAttributeValue"), null); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + assertThat(span.toSpanData().getAttributes().isEmpty()).isTrue(); + } + + @Test + void setAttribute_NoEffectAfterStartSpan() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.setAttribute("key1", "value1"); + spanBuilder.setAttribute("key2", "value2"); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + + Attributes beforeAttributes = span.toSpanData().getAttributes(); + assertThat(beforeAttributes.size()).isEqualTo(2); + assertThat(beforeAttributes.get(stringKey("key1"))).isEqualTo("value1"); + assertThat(beforeAttributes.get(stringKey("key2"))).isEqualTo("value2"); + + spanBuilder.setAttribute("key3", "value3"); + + Attributes afterAttributes = span.toSpanData().getAttributes(); + assertThat(afterAttributes.size()).isEqualTo(2); + assertThat(afterAttributes.get(stringKey("key1"))).isEqualTo("value1"); + assertThat(afterAttributes.get(stringKey("key2"))).isEqualTo("value2"); + } + + @Test + void setAttribute_nullAttributeValue() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.setAttribute("emptyString", ""); + spanBuilder.setAttribute("nullString", null); + spanBuilder.setAttribute(stringKey("nullStringAttributeValue"), null); + spanBuilder.setAttribute(stringKey("emptyStringAttributeValue"), ""); + spanBuilder.setAttribute("longAttribute", 0L); + spanBuilder.setAttribute("boolAttribute", false); + spanBuilder.setAttribute("doubleAttribute", 0.12345f); + spanBuilder.setAttribute(stringArrayKey("stringArrayAttribute"), Arrays.asList("", null)); + spanBuilder.setAttribute(booleanArrayKey("boolArrayAttribute"), Arrays.asList(true, null)); + spanBuilder.setAttribute(longArrayKey("longArrayAttribute"), Arrays.asList(12345L, null)); + spanBuilder.setAttribute(doubleArrayKey("doubleArrayAttribute"), Arrays.asList(1.2345, null)); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(9); + } + + @Test + void setAttribute_nullAttributeValue_afterEnd() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + spanBuilder.setAttribute("emptyString", ""); + spanBuilder.setAttribute(stringKey("emptyStringAttributeValue"), ""); + spanBuilder.setAttribute("longAttribute", 0L); + spanBuilder.setAttribute("boolAttribute", false); + spanBuilder.setAttribute("doubleAttribute", 0.12345f); + spanBuilder.setAttribute(stringArrayKey("stringArrayAttribute"), Arrays.asList("", null)); + spanBuilder.setAttribute(booleanArrayKey("boolArrayAttribute"), Arrays.asList(true, null)); + spanBuilder.setAttribute(longArrayKey("longArrayAttribute"), Arrays.asList(12345L, null)); + spanBuilder.setAttribute(doubleArrayKey("doubleArrayAttribute"), Arrays.asList(1.2345, null)); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(9); + span.end(); + span.setAttribute("emptyString", null); + span.setAttribute(stringKey("emptyStringAttributeValue"), null); + span.setAttribute(longKey("longAttribute"), null); + span.setAttribute(booleanKey("boolAttribute"), null); + span.setAttribute(doubleKey("doubleAttribute"), null); + span.setAttribute(stringArrayKey("stringArrayAttribute"), null); + span.setAttribute(booleanArrayKey("boolArrayAttribute"), null); + span.setAttribute(longArrayKey("longArrayAttribute"), null); + span.setAttribute(doubleArrayKey("doubleArrayAttribute"), null); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(9); + } + + @Test + void droppingAttributes() { + final int maxNumberOfAttrs = 8; + SpanLimits spanLimits = SpanLimits.builder().setMaxNumberOfAttributes(maxNumberOfAttrs).build(); + TracerProvider tracerProvider = SdkTracerProvider.builder().setSpanLimits(spanLimits).build(); + // Verify methods do not crash. + SpanBuilder spanBuilder = tracerProvider.get("test").spanBuilder(SPAN_NAME); + for (int i = 0; i < 2 * maxNumberOfAttrs; i++) { + spanBuilder.setAttribute("key" + i, i); + } + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + Attributes attrs = span.toSpanData().getAttributes(); + assertThat(attrs.size()).isEqualTo(maxNumberOfAttrs); + for (int i = 0; i < maxNumberOfAttrs; i++) { + assertThat(attrs.get(longKey("key" + i))).isEqualTo(i); + } + } finally { + span.end(); + } + } + + @Test + void addAttributes_OnlyViaSampler() { + + Sampler sampler = + new Sampler() { + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return SamplingResult.create( + SamplingDecision.RECORD_AND_SAMPLE, + Attributes.builder().put("cat", "meow").build()); + } + + @Override + public String getDescription() { + return "test"; + } + }; + TracerProvider tracerProvider = SdkTracerProvider.builder().setSampler(sampler).build(); + // Verify methods do not crash. + SpanBuilder spanBuilder = tracerProvider.get("test").spanBuilder(SPAN_NAME); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + span.end(); + assertThat(span.toSpanData().getAttributes().size()).isEqualTo(1); + assertThat(span.toSpanData().getAttributes().get(stringKey("cat"))).isEqualTo("meow"); + } + + @Test + void setAllAttributes() { + Attributes attributes = + Attributes.builder() + .put("string", "value") + .put("long", 12345L) + .put("double", .12345) + .put("boolean", true) + .put(stringKey("stringAttribute"), "attrvalue") + .build(); + + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME).setAllAttributes(attributes); + + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + SpanData spanData = span.toSpanData(); + Attributes attrs = spanData.getAttributes(); + assertThat(attrs.size()).isEqualTo(5); + assertThat(attrs.get(stringKey("string"))).isEqualTo("value"); + assertThat(attrs.get(longKey("long"))).isEqualTo(12345L); + assertThat(attrs.get(doubleKey("double"))).isEqualTo(0.12345); + assertThat(attrs.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(attrs.get(stringKey("stringAttribute"))).isEqualTo("attrvalue"); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(5); + } finally { + span.end(); + } + } + + @Test + void setAllAttributes_mergesAttributes() { + Attributes attributes = + Attributes.builder() + .put("string", "value") + .put("long", 12345L) + .put("double", .12345) + .put("boolean", true) + .put(stringKey("stringAttribute"), "attrvalue") + .build(); + + SpanBuilder spanBuilder = + sdkTracer + .spanBuilder(SPAN_NAME) + .setAttribute("string", "otherValue") + .setAttribute("boolean", false) + .setAttribute("existingString", "existingValue") + .setAllAttributes(attributes); + + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + SpanData spanData = span.toSpanData(); + Attributes attrs = spanData.getAttributes(); + assertThat(attrs.size()).isEqualTo(6); + assertThat(attrs.get(stringKey("string"))).isEqualTo("value"); + assertThat(attrs.get(stringKey("existingString"))).isEqualTo("existingValue"); + assertThat(attrs.get(longKey("long"))).isEqualTo(12345L); + assertThat(attrs.get(doubleKey("double"))).isEqualTo(0.12345); + assertThat(attrs.get(booleanKey("boolean"))).isEqualTo(true); + assertThat(attrs.get(stringKey("stringAttribute"))).isEqualTo("attrvalue"); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(8); + } finally { + span.end(); + } + } + + @Test + void setAllAttributes_nullAttributes() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME).setAllAttributes(null); + + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + SpanData spanData = span.toSpanData(); + Attributes attrs = spanData.getAttributes(); + assertThat(attrs.size()).isEqualTo(0); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(0); + } finally { + span.end(); + } + } + + @Test + void setAllAttributes_emptyAttributes() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME).setAllAttributes(Attributes.empty()); + + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + try { + SpanData spanData = span.toSpanData(); + Attributes attrs = spanData.getAttributes(); + assertThat(attrs.size()).isEqualTo(0); + assertThat(spanData.getTotalAttributeCount()).isEqualTo(0); + } finally { + span.end(); + } + } + + @Test + void recordEvents_default() { + Span span = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try { + assertThat(span.isRecording()).isTrue(); + } finally { + span.end(); + } + } + + @Test + void kind_default() { + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try { + assertThat(span.toSpanData().getKind()).isEqualTo(SpanKind.INTERNAL); + } finally { + span.end(); + } + } + + @Test + void kind() { + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + sdkTracer.spanBuilder(SPAN_NAME).setSpanKind(SpanKind.CONSUMER).startSpan(); + try { + assertThat(span.toSpanData().getKind()).isEqualTo(SpanKind.CONSUMER); + } finally { + span.end(); + } + } + + @Test + void sampler() { + Span span = + SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOff()) + .build() + .get("test") + .spanBuilder(SPAN_NAME) + .startSpan(); + try { + assertThat(span.getSpanContext().isSampled()).isFalse(); + } finally { + span.end(); + } + } + + @Test + void sampler_decisionAttributes() { + final String samplerAttributeName = "sampler-attribute"; + AttributeKey samplerAttributeKey = stringKey(samplerAttributeName); + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + SdkTracerProvider.builder() + .setSampler( + new Sampler() { + @Override + public SamplingResult shouldSample( + @Nullable Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return new SamplingResult() { + @Override + public SamplingDecision getDecision() { + return SamplingDecision.RECORD_AND_SAMPLE; + } + + @Override + public Attributes getAttributes() { + return Attributes.of(samplerAttributeKey, "bar"); + } + }; + } + + @Override + public String getDescription() { + return "test sampler"; + } + }) + .addSpanProcessor(mockedSpanProcessor) + .build() + .get("test") + .spanBuilder(SPAN_NAME) + .setAttribute(samplerAttributeKey, "none") + .startSpan(); + try { + assertThat(span.getSpanContext().isSampled()).isTrue(); + assertThat(span.toSpanData().getAttributes().get(samplerAttributeKey)).isNotNull(); + assertThat(span.toSpanData().getSpanContext().getTraceState()) + .isEqualTo(TraceState.getDefault()); + } finally { + span.end(); + } + } + + @Test + void sampler_updatedTraceState() { + final String samplerAttributeName = "sampler-attribute"; + AttributeKey samplerAttributeKey = stringKey(samplerAttributeName); + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + SdkTracerProvider.builder() + .setSampler( + new Sampler() { + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return new SamplingResult() { + @Override + public SamplingDecision getDecision() { + return SamplingDecision.RECORD_AND_SAMPLE; + } + + @Override + public Attributes getAttributes() { + return Attributes.empty(); + } + + @Override + public TraceState getUpdatedTraceState(TraceState parentTraceState) { + return parentTraceState.toBuilder().put("newkey", "newValue").build(); + } + }; + } + + @Override + public String getDescription() { + return "test sampler"; + } + }) + .build() + .get("test") + .spanBuilder(SPAN_NAME) + .setAttribute(samplerAttributeKey, "none") + .startSpan(); + try { + assertThat(span.getSpanContext().isSampled()).isTrue(); + assertThat(span.toSpanData().getAttributes().get(samplerAttributeKey)).isNotNull(); + assertThat(span.toSpanData().getSpanContext().getTraceState()) + .isEqualTo(TraceState.builder().put("newkey", "newValue").build()); + } finally { + span.end(); + } + } + + // TODO(anuraaga): Is this test correct? It's not sampled + @Test + void sampledViaParentLinks() { + Span span = + SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOff()) + .build() + .get("test") + .spanBuilder(SPAN_NAME) + .startSpan(); + try { + assertThat(span.getSpanContext().isSampled()).isFalse(); + } finally { + if (span != null) { + span.end(); + } + } + } + + @Test + void noParent() { + Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try (Scope ignored = parent.makeCurrent()) { + Span span = sdkTracer.spanBuilder(SPAN_NAME).setNoParent().startSpan(); + try { + assertThat(span.getSpanContext().getTraceId()) + .isNotEqualTo(parent.getSpanContext().getTraceId()); + Mockito.verify(mockedSpanProcessor) + .onStart(Mockito.same(Context.root()), Mockito.same((ReadWriteSpan) span)); + Span spanNoParent = + sdkTracer + .spanBuilder(SPAN_NAME) + .setNoParent() + .setParent(Context.current()) + .setNoParent() + .startSpan(); + try { + assertThat(span.getSpanContext().getTraceId()) + .isNotEqualTo(parent.getSpanContext().getTraceId()); + Mockito.verify(mockedSpanProcessor) + .onStart(Mockito.same(Context.root()), Mockito.same((ReadWriteSpan) spanNoParent)); + } finally { + spanNoParent.end(); + } + } finally { + span.end(); + } + } finally { + parent.end(); + } + } + + @Test + void noParent_override() { + final Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try { + final Context parentContext = Context.current().with(parent); + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + sdkTracer.spanBuilder(SPAN_NAME).setNoParent().setParent(parentContext).startSpan(); + try { + Mockito.verify(mockedSpanProcessor) + .onStart(Mockito.same(parentContext), Mockito.same((ReadWriteSpan) span)); + assertThat(span.getSpanContext().getTraceId()) + .isEqualTo(parent.getSpanContext().getTraceId()); + assertThat(span.toSpanData().getParentSpanId()) + .isEqualTo(parent.getSpanContext().getSpanId()); + + final Context parentContext2 = Context.current().with(parent); + RecordEventsReadableSpan span2 = + (RecordEventsReadableSpan) + sdkTracer + .spanBuilder(SPAN_NAME) + .setNoParent() + .setParent(parentContext2) + .startSpan(); + try { + Mockito.verify(mockedSpanProcessor) + .onStart(Mockito.same(parentContext2), Mockito.same((ReadWriteSpan) span2)); + assertThat(span2.getSpanContext().getTraceId()) + .isEqualTo(parent.getSpanContext().getTraceId()); + } finally { + span2.end(); + } + } finally { + span.end(); + } + } finally { + parent.end(); + } + } + + @Test + void overrideNoParent_remoteParent() { + Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try { + + final Context parentContext = Context.current().with(parent); + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + sdkTracer.spanBuilder(SPAN_NAME).setNoParent().setParent(parentContext).startSpan(); + try { + Mockito.verify(mockedSpanProcessor) + .onStart(Mockito.same(parentContext), Mockito.same((ReadWriteSpan) span)); + assertThat(span.getSpanContext().getTraceId()) + .isEqualTo(parent.getSpanContext().getTraceId()); + assertThat(span.toSpanData().getParentSpanId()) + .isEqualTo(parent.getSpanContext().getSpanId()); + } finally { + span.end(); + } + } finally { + parent.end(); + } + } + + @Test + void parent_fromContext() { + final Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + final Context context = Context.current().with(parent); + try { + final RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + sdkTracer.spanBuilder(SPAN_NAME).setNoParent().setParent(context).startSpan(); + try { + Mockito.verify(mockedSpanProcessor) + .onStart(Mockito.same(context), Mockito.same((ReadWriteSpan) span)); + assertThat(span.getSpanContext().getTraceId()) + .isEqualTo(parent.getSpanContext().getTraceId()); + assertThat(span.toSpanData().getParentSpanId()) + .isEqualTo(parent.getSpanContext().getSpanId()); + } finally { + span.end(); + } + } finally { + parent.end(); + } + } + + @Test + void parent_fromEmptyContext() { + Context emptyContext = Context.current(); + Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try { + RecordEventsReadableSpan span; + try (Scope scope = parent.makeCurrent()) { + span = + (RecordEventsReadableSpan) + sdkTracer.spanBuilder(SPAN_NAME).setParent(emptyContext).startSpan(); + } + + try { + Mockito.verify(mockedSpanProcessor) + .onStart(Mockito.same(emptyContext), Mockito.same((ReadWriteSpan) span)); + assertThat(span.getSpanContext().getTraceId()) + .isNotEqualTo(parent.getSpanContext().getTraceId()); + assertThat(span.toSpanData().getParentSpanId()) + .isNotEqualTo(parent.getSpanContext().getSpanId()); + } finally { + span.end(); + } + } finally { + parent.end(); + } + } + + @Test + void parentCurrentSpan() { + Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try (Scope ignored = parent.makeCurrent()) { + final Context implicitParent = Context.current(); + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try { + Mockito.verify(mockedSpanProcessor) + .onStart(Mockito.same(implicitParent), Mockito.same((ReadWriteSpan) span)); + assertThat(span.getSpanContext().getTraceId()) + .isEqualTo(parent.getSpanContext().getTraceId()); + assertThat(span.toSpanData().getParentSpanId()) + .isEqualTo(parent.getSpanContext().getSpanId()); + } finally { + span.end(); + } + } finally { + parent.end(); + } + } + + @Test + void parent_invalidContext() { + Span parent = Span.getInvalid(); + + final Context parentContext = Context.current().with(parent); + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + sdkTracer.spanBuilder(SPAN_NAME).setParent(parentContext).startSpan(); + try { + Mockito.verify(mockedSpanProcessor) + .onStart( + ArgumentMatchers.same(parentContext), ArgumentMatchers.same((ReadWriteSpan) span)); + assertThat(span.getSpanContext().getTraceId()) + .isNotEqualTo(parent.getSpanContext().getTraceId()); + assertThat(SpanId.isValid(span.toSpanData().getParentSpanId())).isFalse(); + } finally { + span.end(); + } + } + + @Test + void startTimestamp_numeric() { + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + sdkTracer + .spanBuilder(SPAN_NAME) + .setStartTimestamp(10, TimeUnit.NANOSECONDS) + .startSpan(); + span.end(); + assertThat(span.toSpanData().getStartEpochNanos()).isEqualTo(10); + } + + @Test + void startTimestamp_instant() { + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) + sdkTracer + .spanBuilder(SPAN_NAME) + .setStartTimestamp(Instant.ofEpochMilli(100)) + .startSpan(); + span.end(); + assertThat(span.toSpanData().getStartEpochNanos()) + .isEqualTo(TimeUnit.MILLISECONDS.toNanos(100)); + } + + @Test + void parent_clockIsSame() { + Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try (Scope scope = parent.makeCurrent()) { + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + + assertThat(span.getClock()).isSameAs(((RecordEventsReadableSpan) parent).getClock()); + } finally { + parent.end(); + } + } + + @Test + void parentCurrentSpan_clockIsSame() { + Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + try (Scope ignored = parent.makeCurrent()) { + RecordEventsReadableSpan span = + (RecordEventsReadableSpan) sdkTracer.spanBuilder(SPAN_NAME).startSpan(); + + assertThat(span.getClock()).isSameAs(((RecordEventsReadableSpan) parent).getClock()); + } finally { + parent.end(); + } + } + + @Test + void isSampled() { + assertThat(SdkSpanBuilder.isSampled(SamplingDecision.DROP)).isFalse(); + assertThat(SdkSpanBuilder.isSampled(SamplingDecision.RECORD_ONLY)).isFalse(); + assertThat(SdkSpanBuilder.isSampled(SamplingDecision.RECORD_AND_SAMPLE)).isTrue(); + } + + @Test + void isRecording() { + assertThat(SdkSpanBuilder.isRecording(SamplingDecision.DROP)).isFalse(); + assertThat(SdkSpanBuilder.isRecording(SamplingDecision.RECORD_ONLY)).isTrue(); + assertThat(SdkSpanBuilder.isRecording(SamplingDecision.RECORD_AND_SAMPLE)).isTrue(); + } + + // SpanData is very commonly used in unit tests, we want the toString to make sure it's relatively + // easy to understand failure messages. + // TODO(anuraaga): Currently it isn't - we even return the same (or maybe incorrect?) stuff twice. + // Improve the toString. + @Test + void spanDataToString() { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(SPAN_NAME); + RecordEventsReadableSpan span = (RecordEventsReadableSpan) spanBuilder.startSpan(); + span.setAttribute("http.status_code", 500); + span.setAttribute("http.url", "https://opentelemetry.io"); + span.setStatus(StatusCode.ERROR, "error"); + span.end(); + + assertThat(span.toSpanData().toString()) + .matches( + "SpanData\\{spanContext=ImmutableSpanContext\\{" + + "traceId=[0-9a-f]{32}, " + + "spanId=[0-9a-f]{16}, " + + "traceFlags=01, " + + "traceState=ArrayBasedTraceState\\{entries=\\[]}, remote=false, valid=true}, " + + "parentSpanContext=ImmutableSpanContext\\{" + + "traceId=00000000000000000000000000000000, " + + "spanId=0000000000000000, " + + "traceFlags=00, " + + "traceState=ArrayBasedTraceState\\{entries=\\[]}, remote=false, valid=false}, " + + "resource=Resource\\{attributes=\\{service.name=\"unknown_service:java\", " + + "telemetry.sdk.language=\"java\", telemetry.sdk.name=\"opentelemetry\", " + + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-SNAPSHOT)?\"}}, " + + "instrumentationLibraryInfo=InstrumentationLibraryInfo\\{" + + "name=SpanBuilderSdkTest, version=null}, " + + "name=span_name, " + + "kind=INTERNAL, " + + "startEpochNanos=[0-9]+, " + + "endEpochNanos=[0-9]+, " + + "attributes=AttributesMap\\{data=\\{[^}]*}, capacity=128, totalAddedValues=2}, " + + "totalAttributeCount=2, " + + "events=\\[], " + + "totalRecordedEvents=0, " + + "links=\\[], " + + "totalRecordedLinks=0, " + + "status=ImmutableStatusData\\{statusCode=ERROR, description=error}, " + + "hasEnded=true}"); + } + + @Test + void doNotCrash() { + assertThatCode( + () -> { + SpanBuilder spanBuilder = sdkTracer.spanBuilder(null); + spanBuilder.setSpanKind(null); + spanBuilder.setParent(null); + spanBuilder.setNoParent(); + spanBuilder.addLink(null); + spanBuilder.addLink(null, Attributes.empty()); + spanBuilder.addLink(SpanContext.getInvalid(), null); + spanBuilder.setAttribute((String) null, "foo"); + spanBuilder.setAttribute("foo", null); + spanBuilder.setAttribute(null, 0L); + spanBuilder.setAttribute(null, 0.0); + spanBuilder.setAttribute(null, false); + spanBuilder.setAttribute((AttributeKey) null, "foo"); + spanBuilder.setAttribute(stringKey(null), "foo"); + spanBuilder.setAttribute(stringKey(""), "foo"); + spanBuilder.setAttribute(stringKey("foo"), null); + spanBuilder.setStartTimestamp(-1, TimeUnit.MILLISECONDS); + spanBuilder.setStartTimestamp(1, null); + spanBuilder.setParent(Context.root().with(Span.wrap(null))); + spanBuilder.setParent(Context.root()); + spanBuilder.setNoParent(); + spanBuilder.addLink(Span.getInvalid().getSpanContext()); + spanBuilder.addLink(Span.getInvalid().getSpanContext(), Attributes.empty()); + spanBuilder.setAttribute("key", "value"); + spanBuilder.setAttribute("key", 12345L); + spanBuilder.setAttribute("key", .12345); + spanBuilder.setAttribute("key", true); + spanBuilder.setAttribute(stringKey("key"), "value"); + spanBuilder.setAllAttributes(Attributes.of(stringKey("key"), "value")); + spanBuilder.setAllAttributes(Attributes.empty()); + spanBuilder.setAllAttributes(null); + spanBuilder.setStartTimestamp(12345L, TimeUnit.NANOSECONDS); + spanBuilder.setStartTimestamp(Instant.EPOCH); + spanBuilder.setStartTimestamp(null); + assertThat(spanBuilder.startSpan().getSpanContext().isValid()).isTrue(); + }) + .doesNotThrowAnyException(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java new file mode 100644 index 000000000..01ad30e2c --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java @@ -0,0 +1,222 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link SdkTracerProvider}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SdkTracerProviderTest { + @Mock private SpanProcessor spanProcessor; + private SdkTracerProvider tracerFactory; + + @BeforeEach + void setUp() { + tracerFactory = SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); + when(spanProcessor.forceFlush()).thenReturn(CompletableResultCode.ofSuccess()); + when(spanProcessor.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + } + + @Test + void builder_defaultResource() { + Resource resourceWithDefaults = Resource.getDefault(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setClock(mock(Clock.class)) + .setIdGenerator(mock(IdGenerator.class)) + .build(); + + assertThat(tracerProvider).isNotNull(); + assertThat(tracerProvider) + .extracting("sharedState") + .hasFieldOrPropertyWithValue("resource", resourceWithDefaults); + } + + @Test + void builder_defaultSampler() { + assertThat(SdkTracerProvider.builder().build().getSampler()) + .isEqualTo(Sampler.parentBased(Sampler.alwaysOn())); + } + + @Test + void builder_configureSampler() { + assertThat(SdkTracerProvider.builder().setSampler(Sampler.alwaysOff()).build().getSampler()) + .isEqualTo(Sampler.alwaysOff()); + } + + @Test + void builder_configureSampler_null() { + assertThatThrownBy(() -> SdkTracerProvider.builder().setSampler(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("sampler"); + } + + @Test + void builder_serviceNameProvided() { + Resource resource = + Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "mySpecialService")); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setClock(mock(Clock.class)) + .setResource(resource) + .setIdGenerator(mock(IdGenerator.class)) + .build(); + + assertThat(tracerProvider).isNotNull(); + assertThat(tracerProvider) + .extracting("sharedState") + .hasFieldOrPropertyWithValue("resource", resource); + } + + @Test + void builder_NullSpanLimits() { + assertThatThrownBy(() -> SdkTracerProvider.builder().setSpanLimits((SpanLimits) null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("spanLimits"); + } + + @Test + void builder_NullSpanLimitsSupplier() { + assertThatThrownBy(() -> SdkTracerProvider.builder().setSpanLimits((Supplier) null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("spanLimitsSupplier"); + } + + @Test + void builder_NullClock() { + assertThatThrownBy(() -> SdkTracerProvider.builder().setClock(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("clock"); + } + + @Test + void builder_NullResource() { + assertThatThrownBy(() -> SdkTracerProvider.builder().setResource(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("resource"); + } + + @Test + void builder_NullIdsGenerator() { + assertThatThrownBy(() -> SdkTracerProvider.builder().setIdGenerator(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("idGenerator"); + } + + @Test + void defaultGet() { + assertThat(tracerFactory.get("test")).isInstanceOf(SdkTracer.class); + } + + @Test + void getSameInstanceForSameName_WithoutVersion() { + assertThat(tracerFactory.get("test")).isSameAs(tracerFactory.get("test")); + assertThat(tracerFactory.get("test")).isSameAs(tracerFactory.get("test", null)); + } + + @Test + void getSameInstanceForSameName_WithVersion() { + assertThat(tracerFactory.get("test", "version")).isSameAs(tracerFactory.get("test", "version")); + } + + @Test + void propagatesInstrumentationLibraryInfoToTracer() { + InstrumentationLibraryInfo expected = + InstrumentationLibraryInfo.create("theName", "theVersion"); + Tracer tracer = tracerFactory.get(expected.getName(), expected.getVersion()); + assertThat(((SdkTracer) tracer).getInstrumentationLibraryInfo()).isEqualTo(expected); + } + + @Test + void build_SpanLimits() { + SpanLimits initialSpanLimits = SpanLimits.builder().build(); + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder().setSpanLimits(initialSpanLimits).build(); + + assertThat(sdkTracerProvider.getSpanLimits()).isSameAs(initialSpanLimits); + } + + @Test + void shutdown() { + tracerFactory.shutdown(); + Mockito.verify(spanProcessor, Mockito.times(1)).shutdown(); + } + + @Test + void close() { + tracerFactory.close(); + Mockito.verify(spanProcessor, Mockito.times(1)).shutdown(); + } + + @Test + void forceFlush() { + tracerFactory.forceFlush(); + Mockito.verify(spanProcessor, Mockito.times(1)).forceFlush(); + } + + @Test + void shutdownTwice_OnlyFlushSpanProcessorOnce() { + tracerFactory.shutdown(); + Mockito.verify(spanProcessor, Mockito.times(1)).shutdown(); + tracerFactory.shutdown(); // the second call will be ignored + Mockito.verify(spanProcessor, Mockito.times(1)).shutdown(); + } + + @Test + void returnNoopSpanAfterShutdown() { + tracerFactory.shutdown(); + Span span = tracerFactory.get("noop").spanBuilder("span").startSpan(); + assertThat(span.getSpanContext().isValid()).isFalse(); + span.end(); + } + + @Test + void suppliesDefaultTracerForNullName() { + SdkTracer tracer = (SdkTracer) tracerFactory.get(null); + assertThat(tracer.getInstrumentationLibraryInfo().getName()) + .isEqualTo(SdkTracerProvider.DEFAULT_TRACER_NAME); + + tracer = (SdkTracer) tracerFactory.get(null, null); + assertThat(tracer.getInstrumentationLibraryInfo().getName()) + .isEqualTo(SdkTracerProvider.DEFAULT_TRACER_NAME); + } + + @Test + void suppliesDefaultTracerForEmptyName() { + SdkTracer tracer = (SdkTracer) tracerFactory.get(""); + assertThat(tracer.getInstrumentationLibraryInfo().getName()) + .isEqualTo(SdkTracerProvider.DEFAULT_TRACER_NAME); + + tracer = (SdkTracer) tracerFactory.get("", ""); + assertThat(tracer.getInstrumentationLibraryInfo().getName()) + .isEqualTo(SdkTracerProvider.DEFAULT_TRACER_NAME); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerTest.java new file mode 100644 index 000000000..91f8bc64b --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerTest.java @@ -0,0 +1,183 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.trace.StressTestRunner.OperationUpdater; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Unit tests for {@link SdkTracer}. */ +// Need to suppress warnings for MustBeClosed because Android 14 does not support +// try-with-resources. +@SuppressWarnings("MustBeClosedChecker") +@ExtendWith(MockitoExtension.class) +class SdkTracerTest { + + private static final String SPAN_NAME = "span_name"; + private static final String INSTRUMENTATION_LIBRARY_NAME = + "io.opentelemetry.sdk.trace.TracerSdkTest"; + private static final String INSTRUMENTATION_LIBRARY_VERSION = "0.2.0"; + private static final InstrumentationLibraryInfo instrumentationLibraryInfo = + InstrumentationLibraryInfo.create( + INSTRUMENTATION_LIBRARY_NAME, INSTRUMENTATION_LIBRARY_VERSION); + private final SdkTracer tracer = + (SdkTracer) + SdkTracerProvider.builder() + .build() + .get(INSTRUMENTATION_LIBRARY_NAME, INSTRUMENTATION_LIBRARY_VERSION); + + @Test + void defaultSpanBuilder() { + assertThat(tracer.spanBuilder(SPAN_NAME)).isInstanceOf(SdkSpanBuilder.class); + } + + @Test + void getInstrumentationLibraryInfo() { + assertThat(tracer.getInstrumentationLibraryInfo()).isEqualTo(instrumentationLibraryInfo); + } + + @Test + void propagatesInstrumentationLibraryInfoToSpan() { + ReadableSpan readableSpan = (ReadableSpan) tracer.spanBuilder("spanName").startSpan(); + assertThat(readableSpan.getInstrumentationLibraryInfo()).isEqualTo(instrumentationLibraryInfo); + } + + @Test + void fallbackSpanName() { + ReadableSpan readableSpan = (ReadableSpan) tracer.spanBuilder(" ").startSpan(); + assertThat(readableSpan.getName()).isEqualTo(SdkTracer.FALLBACK_SPAN_NAME); + + readableSpan = (ReadableSpan) tracer.spanBuilder(null).startSpan(); + assertThat(readableSpan.getName()).isEqualTo(SdkTracer.FALLBACK_SPAN_NAME); + } + + @Test + void stressTest() { + CountingSpanProcessor spanProcessor = new CountingSpanProcessor(); + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); + SdkTracer tracer = + (SdkTracer) + sdkTracerProvider.get(INSTRUMENTATION_LIBRARY_NAME, INSTRUMENTATION_LIBRARY_VERSION); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder().setTracer(tracer).setSpanProcessor(spanProcessor); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create(2_000, 1, new SimpleSpanOperation(tracer))); + } + + stressTestBuilder.build().run(); + assertThat(spanProcessor.numberOfSpansFinished.get()).isEqualTo(8_000); + assertThat(spanProcessor.numberOfSpansStarted.get()).isEqualTo(8_000); + } + + @Test + void stressTest_withBatchSpanProcessor() { + CountingSpanExporter countingSpanExporter = new CountingSpanExporter(); + SpanProcessor spanProcessor = BatchSpanProcessor.builder(countingSpanExporter).build(); + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); + SdkTracer tracer = + (SdkTracer) + sdkTracerProvider.get(INSTRUMENTATION_LIBRARY_NAME, INSTRUMENTATION_LIBRARY_VERSION); + + StressTestRunner.Builder stressTestBuilder = + StressTestRunner.builder().setTracer(tracer).setSpanProcessor(spanProcessor); + + for (int i = 0; i < 4; i++) { + stressTestBuilder.addOperation( + StressTestRunner.Operation.create(2_000, 1, new SimpleSpanOperation(tracer))); + } + + // Needs to correlate with the BatchSpanProcessor.Builder's default, which is the only thing + // this test can guarantee + final int defaultMaxQueueSize = 2048; + + stressTestBuilder.build().run(); + assertThat(countingSpanExporter.numberOfSpansExported.get()) + .isGreaterThanOrEqualTo(defaultMaxQueueSize); + } + + private static class CountingSpanProcessor implements SpanProcessor { + private final AtomicLong numberOfSpansStarted = new AtomicLong(); + private final AtomicLong numberOfSpansFinished = new AtomicLong(); + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + numberOfSpansStarted.incrementAndGet(); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + numberOfSpansFinished.incrementAndGet(); + } + + @Override + public boolean isEndRequired() { + return true; + } + } + + private static class SimpleSpanOperation implements OperationUpdater { + private final SdkTracer tracer; + + public SimpleSpanOperation(SdkTracer tracer) { + this.tracer = tracer; + } + + @Override + public void update() { + Span span = tracer.spanBuilder("testSpan").startSpan(); + try (Scope ignored = span.makeCurrent()) { + span.setAttribute("testAttribute", "testValue"); + } finally { + span.end(); + } + } + } + + private static class CountingSpanExporter implements SpanExporter { + + public final AtomicLong numberOfSpansExported = new AtomicLong(); + + @Override + public CompletableResultCode export(Collection spans) { + numberOfSpansExported.addAndGet(spans.size()); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + // no-op + return CompletableResultCode.ofSuccess(); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/StressTestRunner.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/StressTestRunner.java new file mode 100644 index 000000000..edd33b368 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/StressTestRunner.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Uninterruptibles; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.Immutable; + +@AutoValue +@Immutable +abstract class StressTestRunner { + abstract ImmutableList getOperations(); + + abstract SdkTracer getTracer(); + + abstract SpanProcessor getSpanProcessor(); + + final void run() { + List operations = getOperations(); + int numThreads = operations.size(); + final CountDownLatch countDownLatch = new CountDownLatch(numThreads); + List operationThreads = new ArrayList<>(numThreads); + for (final Operation operation : operations) { + operationThreads.add( + new Thread( + () -> { + for (int i = 0; i < operation.getNumOperations(); i++) { + operation.getUpdater().update(); + Uninterruptibles.sleepUninterruptibly( + Duration.ofMillis(operation.getOperationDelayMs())); + } + countDownLatch.countDown(); + })); + } + + for (Thread thread : operationThreads) { + thread.start(); + } + + // Wait for all the threads to finish. + for (Thread thread : operationThreads) { + Uninterruptibles.joinUninterruptibly(thread); + } + + getSpanProcessor().shutdown().join(1, TimeUnit.MINUTES); + } + + static Builder builder() { + return new AutoValue_StressTestRunner.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setTracer(SdkTracer sdkTracer); + + abstract ImmutableList.Builder operationsBuilder(); + + abstract Builder setSpanProcessor(SpanProcessor spanProcessor); + + Builder addOperation(final Operation operation) { + operationsBuilder().add(operation); + return this; + } + + public abstract StressTestRunner build(); + } + + @AutoValue + @Immutable + abstract static class Operation { + + abstract int getNumOperations(); + + abstract int getOperationDelayMs(); + + abstract OperationUpdater getUpdater(); + + static Operation create(int numOperations, int operationDelayMs, OperationUpdater updater) { + return new AutoValue_StressTestRunner_Operation(numOperations, operationDelayMs, updater); + } + } + + interface OperationUpdater { + /** Called every operation. */ + void update(); + } + + StressTestRunner() {} +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/TestUtils.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/TestUtils.java new file mode 100644 index 000000000..7780e6dcb --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/TestUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.trace.TestSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** Common utilities for unit tests. */ +public final class TestUtils { + + private TestUtils() {} + + /** + * Generates some random attributes used for testing. + * + * @return some {@link io.opentelemetry.api.common.Attributes} + */ + static Attributes generateRandomAttributes() { + return Attributes.of(stringKey(UUID.randomUUID().toString()), UUID.randomUUID().toString()); + } + + /** + * Create a very basic SpanData instance, suitable for testing. It has the bare minimum viable + * data. + * + * @return A SpanData instance. + */ + public static SpanData makeBasicSpan() { + return TestSpanData.builder() + .setHasEnded(true) + .setSpanContext(SpanContext.getInvalid()) + .setName("span") + .setKind(SpanKind.SERVER) + .setStartEpochNanos(TimeUnit.SECONDS.toNanos(100) + 100) + .setStatus(StatusData.ok()) + .setEndEpochNanos(TimeUnit.SECONDS.toNanos(200) + 200) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .build(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/config/SpanLimitsTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/config/SpanLimitsTest.java new file mode 100644 index 000000000..7c5e317ac --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/config/SpanLimitsTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.trace.SpanLimits; +import org.junit.jupiter.api.Test; + +class SpanLimitsTest { + + @Test + void defaultSpanLimits() { + assertThat(SpanLimits.getDefault().getMaxNumberOfAttributes()).isEqualTo(128); + assertThat(SpanLimits.getDefault().getMaxNumberOfEvents()).isEqualTo(128); + assertThat(SpanLimits.getDefault().getMaxNumberOfLinks()).isEqualTo(128); + assertThat(SpanLimits.getDefault().getMaxNumberOfAttributesPerEvent()).isEqualTo(128); + assertThat(SpanLimits.getDefault().getMaxNumberOfAttributesPerLink()).isEqualTo(128); + } + + @Test + void updateSpanLimits_All() { + SpanLimits spanLimits = + SpanLimits.builder() + .setMaxNumberOfAttributes(8) + .setMaxNumberOfEvents(10) + .setMaxNumberOfLinks(11) + .setMaxNumberOfAttributesPerEvent(1) + .setMaxNumberOfAttributesPerLink(2) + .build(); + assertThat(spanLimits.getMaxNumberOfAttributes()).isEqualTo(8); + assertThat(spanLimits.getMaxNumberOfEvents()).isEqualTo(10); + assertThat(spanLimits.getMaxNumberOfLinks()).isEqualTo(11); + assertThat(spanLimits.getMaxNumberOfAttributesPerEvent()).isEqualTo(1); + assertThat(spanLimits.getMaxNumberOfAttributesPerLink()).isEqualTo(2); + + // Preserves values + SpanLimits spanLimitsDupe = spanLimits.toBuilder().build(); + // Use reflective comparison to catch when new fields are added. + assertThat(spanLimitsDupe).usingRecursiveComparison().isEqualTo(spanLimits); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/data/ImmutableStatusDataTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/data/ImmutableStatusDataTest.java new file mode 100644 index 000000000..c112909ff --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/data/ImmutableStatusDataTest.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.StatusCode; +import org.junit.jupiter.api.Test; + +class ImmutableStatusDataTest { + @Test + void statuses() { + StatusCode[] codes = StatusCode.values(); + for (StatusCode code : codes) { + StatusData status = ImmutableStatusData.create(code, ""); + switch (code) { + case UNSET: + assertThat(status).isSameAs(StatusData.unset()); + break; + case OK: + assertThat(status).isSameAs(StatusData.ok()); + break; + case ERROR: + assertThat(status).isSameAs(StatusData.error()); + break; + } + assertThat(status).isNotNull(); + assertThat(status.getStatusCode()).isEqualTo(code); + assertThat(status.getDescription()).isEmpty(); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorTest.java new file mode 100644 index 000000000..30a453fc5 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/BatchSpanProcessorTest.java @@ -0,0 +1,661 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@SuppressWarnings("PreferJavaTimeOverload") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class BatchSpanProcessorTest { + + private static final String SPAN_NAME_1 = "MySpanName/1"; + private static final String SPAN_NAME_2 = "MySpanName/2"; + private static final long MAX_SCHEDULE_DELAY_MILLIS = 500; + private SdkTracerProvider sdkTracerProvider; + private final BlockingSpanExporter blockingSpanExporter = new BlockingSpanExporter(); + + @Mock private Sampler mockSampler; + @Mock private SpanExporter mockSpanExporter; + + @BeforeEach + void setUp() { + when(mockSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + } + + @AfterEach + void cleanup() { + if (sdkTracerProvider != null) { + sdkTracerProvider.shutdown(); + } + } + + private ReadableSpan createEndedSpan(String spanName) { + Tracer tracer = sdkTracerProvider.get(getClass().getName()); + Span span = tracer.spanBuilder(spanName).startSpan(); + span.end(); + return (ReadableSpan) span; + } + + @Test + void configTest_EmptyOptions() { + BatchSpanProcessorBuilder config = + BatchSpanProcessor.builder(new WaitingSpanExporter(0, CompletableResultCode.ofSuccess())); + assertThat(config.getScheduleDelayNanos()) + .isEqualTo( + TimeUnit.MILLISECONDS.toNanos(BatchSpanProcessorBuilder.DEFAULT_SCHEDULE_DELAY_MILLIS)); + assertThat(config.getMaxQueueSize()) + .isEqualTo(BatchSpanProcessorBuilder.DEFAULT_MAX_QUEUE_SIZE); + assertThat(config.getMaxExportBatchSize()) + .isEqualTo(BatchSpanProcessorBuilder.DEFAULT_MAX_EXPORT_BATCH_SIZE); + assertThat(config.getExporterTimeoutNanos()) + .isEqualTo( + TimeUnit.MILLISECONDS.toNanos(BatchSpanProcessorBuilder.DEFAULT_EXPORT_TIMEOUT_MILLIS)); + } + + @Test + void invalidConfig() { + SpanExporter exporter = mock(SpanExporter.class); + assertThatThrownBy( + () -> BatchSpanProcessor.builder(exporter).setScheduleDelay(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("delay must be non-negative"); + assertThatThrownBy(() -> BatchSpanProcessor.builder(exporter).setScheduleDelay(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy(() -> BatchSpanProcessor.builder(exporter).setScheduleDelay(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("delay"); + assertThatThrownBy( + () -> + BatchSpanProcessor.builder(exporter).setExporterTimeout(-1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("timeout must be non-negative"); + assertThatThrownBy(() -> BatchSpanProcessor.builder(exporter).setExporterTimeout(1, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("unit"); + assertThatThrownBy(() -> BatchSpanProcessor.builder(exporter).setExporterTimeout(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("timeout"); + } + + @Test + void startEndRequirements() { + BatchSpanProcessor spansProcessor = + BatchSpanProcessor.builder(new WaitingSpanExporter(0, CompletableResultCode.ofSuccess())) + .build(); + assertThat(spansProcessor.isStartRequired()).isFalse(); + assertThat(spansProcessor.isEndRequired()).isTrue(); + } + + @Test + void exportDifferentSampledSpans() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(2, CompletableResultCode.ofSuccess()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder(waitingSpanExporter) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .build(); + + ReadableSpan span1 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span2 = createEndedSpan(SPAN_NAME_2); + List exported = waitingSpanExporter.waitForExport(); + assertThat(exported).containsExactly(span1.toSpanData(), span2.toSpanData()); + } + + @Test + void exportMoreSpansThanTheBufferSize() { + CompletableSpanExporter spanExporter = new CompletableSpanExporter(); + + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder(spanExporter) + .setMaxQueueSize(6) + .setMaxExportBatchSize(2) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .build(); + + ReadableSpan span1 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span2 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span3 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span4 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span5 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span6 = createEndedSpan(SPAN_NAME_1); + + spanExporter.succeed(); + + await() + .untilAsserted( + () -> + assertThat(spanExporter.getExported()) + .containsExactly( + span1.toSpanData(), + span2.toSpanData(), + span3.toSpanData(), + span4.toSpanData(), + span5.toSpanData(), + span6.toSpanData())); + } + + @Test + void forceExport() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(100, CompletableResultCode.ofSuccess(), 1); + BatchSpanProcessor batchSpanProcessor = + BatchSpanProcessor.builder(waitingSpanExporter) + .setMaxQueueSize(10_000) + // Force flush should send all spans, make sure the number of spans we check here is + // not divisible by the batch size. + .setMaxExportBatchSize(49) + .setScheduleDelay(10, TimeUnit.SECONDS) + .build(); + + sdkTracerProvider = SdkTracerProvider.builder().addSpanProcessor(batchSpanProcessor).build(); + for (int i = 0; i < 50; i++) { + createEndedSpan("notExported"); + } + List exported = waitingSpanExporter.waitForExport(); + assertThat(exported).isNotNull(); + assertThat(exported.size()).isEqualTo(49); + + for (int i = 0; i < 50; i++) { + createEndedSpan("notExported"); + } + exported = waitingSpanExporter.waitForExport(); + assertThat(exported).isNotNull(); + assertThat(exported.size()).isEqualTo(49); + + batchSpanProcessor.forceFlush().join(10, TimeUnit.SECONDS); + exported = waitingSpanExporter.getExported(); + assertThat(exported).isNotNull(); + assertThat(exported.size()).isEqualTo(2); + } + + @Test + void exportSpansToMultipleServices() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(2, CompletableResultCode.ofSuccess()); + WaitingSpanExporter waitingSpanExporter2 = + new WaitingSpanExporter(2, CompletableResultCode.ofSuccess()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder( + SpanExporter.composite( + Arrays.asList(waitingSpanExporter, waitingSpanExporter2))) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .build(); + + ReadableSpan span1 = createEndedSpan(SPAN_NAME_1); + ReadableSpan span2 = createEndedSpan(SPAN_NAME_2); + List exported1 = waitingSpanExporter.waitForExport(); + List exported2 = waitingSpanExporter2.waitForExport(); + assertThat(exported1).containsExactly(span1.toSpanData(), span2.toSpanData()); + assertThat(exported2).containsExactly(span1.toSpanData(), span2.toSpanData()); + } + + @Test + void exportMoreSpansThanTheMaximumLimit() { + final int maxQueuedSpans = 8; + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(maxQueuedSpans, CompletableResultCode.ofSuccess()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder( + SpanExporter.composite( + Arrays.asList(blockingSpanExporter, waitingSpanExporter))) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .setMaxQueueSize(maxQueuedSpans) + .setMaxExportBatchSize(maxQueuedSpans / 2) + .build()) + .build(); + + List spansToExport = new ArrayList<>(maxQueuedSpans + 1); + // Wait to block the worker thread in the BatchSampledSpansProcessor. This ensures that no items + // can be removed from the queue. Need to add a span to trigger the export otherwise the + // pipeline is never called. + spansToExport.add(createEndedSpan("blocking_span").toSpanData()); + blockingSpanExporter.waitUntilIsBlocked(); + + for (int i = 0; i < maxQueuedSpans; i++) { + // First export maxQueuedSpans, the worker thread is blocked so all items should be queued. + spansToExport.add(createEndedSpan("span_1_" + i).toSpanData()); + } + + // TODO: assertThat(spanExporter.getReferencedSpans()).isEqualTo(maxQueuedSpans); + + // Now we should start dropping. + for (int i = 0; i < 7; i++) { + createEndedSpan("span_2_" + i); + // TODO: assertThat(getDroppedSpans()).isEqualTo(i + 1); + } + + // TODO: assertThat(getReferencedSpans()).isEqualTo(maxQueuedSpans); + + // Release the blocking exporter + blockingSpanExporter.unblock(); + + // While we wait for maxQueuedSpans we ensure that the queue is also empty after this. + List exported = waitingSpanExporter.waitForExport(); + assertThat(exported).isNotNull(); + assertThat(exported).containsExactlyElementsOf(spansToExport); + exported.clear(); + spansToExport.clear(); + + waitingSpanExporter.reset(); + // We cannot compare with maxReferencedSpans here because the worker thread may get + // unscheduled immediately after exporting, but before updating the pushed spans, if that is + // the case at most bufferSize spans will miss. + // TODO: assertThat(getPushedSpans()).isAtLeast((long) maxQueuedSpans - maxBatchSize); + + for (int i = 0; i < maxQueuedSpans; i++) { + spansToExport.add(createEndedSpan("span_3_" + i).toSpanData()); + // No more dropped spans. + // TODO: assertThat(getDroppedSpans()).isEqualTo(7); + } + + exported = waitingSpanExporter.waitForExport(); + assertThat(exported).isNotNull(); + assertThat(exported).containsExactlyElementsOf(spansToExport); + } + + @Test + void exporterThrowsException() { + SpanExporter mockSpanExporter = mock(SpanExporter.class); + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + doThrow(new IllegalArgumentException("No export for you.")) + .when(mockSpanExporter) + .export(ArgumentMatchers.anyList()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder( + SpanExporter.composite( + Arrays.asList(mockSpanExporter, waitingSpanExporter))) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .build(); + ReadableSpan span1 = createEndedSpan(SPAN_NAME_1); + List exported = waitingSpanExporter.waitForExport(); + assertThat(exported).containsExactly(span1.toSpanData()); + waitingSpanExporter.reset(); + // Continue to export after the exception was received. + ReadableSpan span2 = createEndedSpan(SPAN_NAME_2); + exported = waitingSpanExporter.waitForExport(); + assertThat(exported).containsExactly(span2.toSpanData()); + } + + @Test + @Timeout(5) + public void continuesIfExporterTimesOut() throws InterruptedException { + int exporterTimeoutMillis = 10; + BatchSpanProcessor bsp = + BatchSpanProcessor.builder(mockSpanExporter) + .setExporterTimeout(exporterTimeoutMillis, TimeUnit.MILLISECONDS) + .setScheduleDelay(1, TimeUnit.MILLISECONDS) + .setMaxQueueSize(1) + .build(); + sdkTracerProvider = SdkTracerProvider.builder().addSpanProcessor(bsp).build(); + + CountDownLatch exported = new CountDownLatch(1); + // We return a result we never complete, meaning it will timeout. + when(mockSpanExporter.export( + argThat( + spans -> { + assertThat(spans) + .anySatisfy(span -> assertThat(span.getName()).isEqualTo(SPAN_NAME_1)); + exported.countDown(); + return true; + }))) + .thenReturn(new CompletableResultCode()); + createEndedSpan(SPAN_NAME_1); + exported.await(); + // Timed out so the span was dropped. + await().untilAsserted(() -> assertThat(bsp.getBatch()).isEmpty()); + + // Still processing new spans. + CountDownLatch exportedAgain = new CountDownLatch(1); + reset(mockSpanExporter); + when(mockSpanExporter.export( + argThat( + spans -> { + assertThat(spans) + .anySatisfy(span -> assertThat(span.getName()).isEqualTo(SPAN_NAME_2)); + exportedAgain.countDown(); + return true; + }))) + .thenReturn(CompletableResultCode.ofSuccess()); + createEndedSpan(SPAN_NAME_2); + exported.await(); + await().untilAsserted(() -> assertThat(bsp.getBatch()).isEmpty()); + } + + @Test + void exportNotSampledSpans() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder(waitingSpanExporter) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .setSampler(mockSampler) + .build(); + + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.DROP)); + sdkTracerProvider.get("test").spanBuilder(SPAN_NAME_1).startSpan().end(); + sdkTracerProvider.get("test").spanBuilder(SPAN_NAME_2).startSpan().end(); + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); + ReadableSpan span = createEndedSpan(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 exported = waitingSpanExporter.waitForExport(); + // Need to check this because otherwise the variable span1 is unused, other option is to not + // have a span1 variable. + assertThat(exported).containsExactly(span.toSpanData()); + } + + @Test + void exportNotSampledSpans_recordOnly() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.RECORD_ONLY)); + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder(waitingSpanExporter) + .setScheduleDelay(MAX_SCHEDULE_DELAY_MILLIS, TimeUnit.MILLISECONDS) + .build()) + .setSampler(mockSampler) + .build(); + + createEndedSpan(SPAN_NAME_1); + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); + ReadableSpan span = createEndedSpan(SPAN_NAME_2); + + // Spans are recorded and exported in the same order as they are ended, we test that a non + // exported 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 exported = waitingSpanExporter.waitForExport(); + // Need to check this because otherwise the variable span1 is unused, other option is to not + // have a span1 variable. + assertThat(exported).containsExactly(span.toSpanData()); + } + + @Test + @Timeout(10) + void shutdownFlushes() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + // Set the export delay to large value, in order to confirm the #flush() below works + + sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder(waitingSpanExporter) + .setScheduleDelay(10, TimeUnit.SECONDS) + .build()) + .build(); + + ReadableSpan span2 = createEndedSpan(SPAN_NAME_2); + + // Force a shutdown, which forces processing of all remaining spans. + sdkTracerProvider.shutdown().join(10, TimeUnit.SECONDS); + + List exported = waitingSpanExporter.getExported(); + assertThat(exported).containsExactly(span2.toSpanData()); + assertThat(waitingSpanExporter.shutDownCalled.get()).isTrue(); + } + + @Test + void shutdownPropagatesSuccess() { + SpanExporter mockSpanExporter = mock(SpanExporter.class); + when(mockSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + BatchSpanProcessor processor = BatchSpanProcessor.builder(mockSpanExporter).build(); + CompletableResultCode result = processor.shutdown(); + result.join(1, TimeUnit.SECONDS); + assertThat(result.isSuccess()).isTrue(); + } + + @Test + void shutdownPropagatesFailure() { + SpanExporter mockSpanExporter = mock(SpanExporter.class); + when(mockSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofFailure()); + BatchSpanProcessor processor = BatchSpanProcessor.builder(mockSpanExporter).build(); + CompletableResultCode result = processor.shutdown(); + result.join(1, TimeUnit.SECONDS); + assertThat(result.isSuccess()).isFalse(); + } + + private static final class BlockingSpanExporter implements SpanExporter { + + final Object monitor = new Object(); + + private enum State { + WAIT_TO_BLOCK, + BLOCKED, + UNBLOCKED + } + + @GuardedBy("monitor") + State state = State.WAIT_TO_BLOCK; + + @Override + public CompletableResultCode export(Collection spanDataList) { + synchronized (monitor) { + while (state != State.UNBLOCKED) { + try { + state = State.BLOCKED; + // Some threads may wait for Blocked State. + monitor.notifyAll(); + monitor.wait(); + } catch (InterruptedException e) { + // Do nothing + } + } + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + private void waitUntilIsBlocked() { + synchronized (monitor) { + while (state != State.BLOCKED) { + try { + monitor.wait(); + } catch (InterruptedException e) { + // Do nothing + } + } + } + } + + @Override + public CompletableResultCode shutdown() { + // Do nothing; + return CompletableResultCode.ofSuccess(); + } + + private void unblock() { + synchronized (monitor) { + state = State.UNBLOCKED; + monitor.notifyAll(); + } + } + } + + private static class CompletableSpanExporter implements SpanExporter { + + private final List results = new ArrayList<>(); + + private final List exported = new ArrayList<>(); + + private volatile boolean succeeded; + + List getExported() { + return exported; + } + + void succeed() { + succeeded = true; + results.forEach(CompletableResultCode::succeed); + } + + @Override + public CompletableResultCode export(Collection spans) { + exported.addAll(spans); + if (succeeded) { + return CompletableResultCode.ofSuccess(); + } + CompletableResultCode result = new CompletableResultCode(); + results.add(result); + return result; + } + + @Override + public CompletableResultCode flush() { + if (succeeded) { + return CompletableResultCode.ofSuccess(); + } else { + return CompletableResultCode.ofFailure(); + } + } + + @Override + public CompletableResultCode shutdown() { + return flush(); + } + } + + static class WaitingSpanExporter implements SpanExporter { + + private final List spanDataList = new ArrayList<>(); + private final int numberToWaitFor; + private final CompletableResultCode exportResultCode; + private CountDownLatch countDownLatch; + private int timeout = 10; + private final AtomicBoolean shutDownCalled = new AtomicBoolean(false); + + WaitingSpanExporter(int numberToWaitFor, CompletableResultCode exportResultCode) { + countDownLatch = new CountDownLatch(numberToWaitFor); + this.numberToWaitFor = numberToWaitFor; + this.exportResultCode = exportResultCode; + } + + WaitingSpanExporter(int numberToWaitFor, CompletableResultCode exportResultCode, int timeout) { + this(numberToWaitFor, exportResultCode); + this.timeout = timeout; + } + + List getExported() { + List result = new ArrayList<>(spanDataList); + spanDataList.clear(); + return result; + } + + /** + * 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. + * + * @return the list of exported {@link SpanData} objects, otherwise {@code null} if the current + * thread is interrupted. + */ + @Nullable + List waitForExport() { + try { + countDownLatch.await(timeout, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // Preserve the interruption status as per guidance. + Thread.currentThread().interrupt(); + return null; + } + return getExported(); + } + + @Override + public CompletableResultCode export(Collection spans) { + this.spanDataList.addAll(spans); + for (int i = 0; i < spans.size(); i++) { + countDownLatch.countDown(); + } + return exportResultCode; + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + shutDownCalled.set(true); + return CompletableResultCode.ofSuccess(); + } + + public void reset() { + this.countDownLatch = new CountDownLatch(numberToWaitFor); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/MultiSpanExporterTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/MultiSpanExporterTest.java new file mode 100644 index 000000000..f807f94a3 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/MultiSpanExporterTest.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.when; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.TestUtils; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Unit tests for {@link MultiSpanExporterTest}. */ +@ExtendWith(MockitoExtension.class) +class MultiSpanExporterTest { + @Mock private SpanExporter spanExporter1; + @Mock private SpanExporter spanExporter2; + private static final List SPAN_LIST = + Collections.singletonList(TestUtils.makeBasicSpan()); + + @Test + void empty() { + SpanExporter multiSpanExporter = SpanExporter.composite(Collections.emptyList()); + multiSpanExporter.export(SPAN_LIST); + multiSpanExporter.shutdown(); + } + + @Test + void oneSpanExporter() { + SpanExporter multiSpanExporter = + SpanExporter.composite(Collections.singletonList(spanExporter1)); + + when(spanExporter1.export(same(SPAN_LIST))).thenReturn(CompletableResultCode.ofSuccess()); + assertThat(multiSpanExporter.export(SPAN_LIST).isSuccess()).isTrue(); + Mockito.verify(spanExporter1).export(same(SPAN_LIST)); + + when(spanExporter1.flush()).thenReturn(CompletableResultCode.ofSuccess()); + assertThat(multiSpanExporter.flush().isSuccess()).isTrue(); + Mockito.verify(spanExporter1).flush(); + + when(spanExporter1.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + multiSpanExporter.shutdown(); + Mockito.verify(spanExporter1).shutdown(); + } + + @Test + void twoSpanExporter() { + SpanExporter multiSpanExporter = + SpanExporter.composite(Arrays.asList(spanExporter1, spanExporter2)); + + when(spanExporter1.export(same(SPAN_LIST))).thenReturn(CompletableResultCode.ofSuccess()); + when(spanExporter2.export(same(SPAN_LIST))).thenReturn(CompletableResultCode.ofSuccess()); + assertThat(multiSpanExporter.export(SPAN_LIST).isSuccess()).isTrue(); + Mockito.verify(spanExporter1).export(same(SPAN_LIST)); + Mockito.verify(spanExporter2).export(same(SPAN_LIST)); + + when(spanExporter1.flush()).thenReturn(CompletableResultCode.ofSuccess()); + when(spanExporter2.flush()).thenReturn(CompletableResultCode.ofSuccess()); + assertThat(multiSpanExporter.flush().isSuccess()).isTrue(); + Mockito.verify(spanExporter1).flush(); + Mockito.verify(spanExporter2).flush(); + + when(spanExporter1.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + when(spanExporter2.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + multiSpanExporter.shutdown(); + Mockito.verify(spanExporter1).shutdown(); + Mockito.verify(spanExporter2).shutdown(); + } + + @Test + void twoSpanExporter_OneReturnFailure() { + SpanExporter multiSpanExporter = + SpanExporter.composite(Arrays.asList(spanExporter1, spanExporter2)); + + when(spanExporter1.export(same(SPAN_LIST))).thenReturn(CompletableResultCode.ofSuccess()); + when(spanExporter2.export(same(SPAN_LIST))).thenReturn(CompletableResultCode.ofFailure()); + assertThat(multiSpanExporter.export(SPAN_LIST).isSuccess()).isFalse(); + Mockito.verify(spanExporter1).export(same(SPAN_LIST)); + Mockito.verify(spanExporter2).export(same(SPAN_LIST)); + + when(spanExporter1.flush()).thenReturn(CompletableResultCode.ofSuccess()); + when(spanExporter2.flush()).thenReturn(CompletableResultCode.ofFailure()); + assertThat(multiSpanExporter.flush().isSuccess()).isFalse(); + Mockito.verify(spanExporter1).flush(); + Mockito.verify(spanExporter2).flush(); + + when(spanExporter1.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + when(spanExporter2.shutdown()).thenReturn(CompletableResultCode.ofFailure()); + assertThat(multiSpanExporter.shutdown().isSuccess()).isFalse(); + Mockito.verify(spanExporter1).shutdown(); + Mockito.verify(spanExporter2).shutdown(); + } + + @Test + void twoSpanExporter_FirstThrows() { + SpanExporter multiSpanExporter = + SpanExporter.composite(Arrays.asList(spanExporter1, spanExporter2)); + + Mockito.doThrow(new IllegalArgumentException("No export for you.")) + .when(spanExporter1) + .export(ArgumentMatchers.anyList()); + when(spanExporter2.export(same(SPAN_LIST))).thenReturn(CompletableResultCode.ofSuccess()); + assertThat(multiSpanExporter.export(SPAN_LIST).isSuccess()).isFalse(); + Mockito.verify(spanExporter1).export(same(SPAN_LIST)); + Mockito.verify(spanExporter2).export(same(SPAN_LIST)); + + Mockito.doThrow(new IllegalArgumentException("No flush for you.")).when(spanExporter1).flush(); + when(spanExporter2.flush()).thenReturn(CompletableResultCode.ofSuccess()); + assertThat(multiSpanExporter.flush().isSuccess()).isFalse(); + Mockito.verify(spanExporter1).flush(); + Mockito.verify(spanExporter2).flush(); + + Mockito.doThrow(new IllegalArgumentException("No shutdown for you.")) + .when(spanExporter1) + .shutdown(); + when(spanExporter2.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + assertThat(multiSpanExporter.shutdown().isSuccess()).isFalse(); + Mockito.verify(spanExporter1).shutdown(); + Mockito.verify(spanExporter2).shutdown(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessorTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessorTest.java new file mode 100644 index 000000000..7f13680a8 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/export/SimpleSpanProcessorTest.java @@ -0,0 +1,254 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.export; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.TestUtils; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessorTest.WaitingSpanExporter; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link SimpleSpanProcessor}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SimpleSpanProcessorTest { + private static final String SPAN_NAME = "MySpanName"; + @Mock private ReadableSpan readableSpan; + @Mock private ReadWriteSpan readWriteSpan; + @Mock private SpanExporter spanExporter; + @Mock private Sampler mockSampler; + private static final SpanContext SAMPLED_SPAN_CONTEXT = + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.getDefault()); + private static final SpanContext NOT_SAMPLED_SPAN_CONTEXT = SpanContext.getInvalid(); + + private SpanProcessor simpleSampledSpansProcessor; + + @BeforeEach + void setUp() { + simpleSampledSpansProcessor = SimpleSpanProcessor.create(spanExporter); + when(spanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + } + + @Test + void createNull() { + assertThatThrownBy(() -> SimpleSpanProcessor.create(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("exporter"); + } + + @Test + void onStartSync() { + simpleSampledSpansProcessor.onStart(Context.root(), readWriteSpan); + verifyNoInteractions(spanExporter); + } + + @Test + void onEndSync_SampledSpan() { + SpanData spanData = TestUtils.makeBasicSpan(); + when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + when(readableSpan.toSpanData()).thenReturn(spanData); + simpleSampledSpansProcessor.onEnd(readableSpan); + verify(spanExporter).export(Collections.singletonList(spanData)); + } + + @Test + void onEndSync_NotSampledSpan() { + when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + simpleSampledSpansProcessor.onEnd(readableSpan); + verifyNoInteractions(spanExporter); + } + + @Test + void onEndSync_OnlySampled_NotSampledSpan() { + when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + when(readableSpan.toSpanData()) + .thenReturn(TestUtils.makeBasicSpan()) + .thenThrow(new RuntimeException()); + SpanProcessor simpleSpanProcessor = SimpleSpanProcessor.create(spanExporter); + simpleSpanProcessor.onEnd(readableSpan); + verifyNoInteractions(spanExporter); + } + + @Test + void onEndSync_OnlySampled_SampledSpan() { + when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + when(readableSpan.toSpanData()) + .thenReturn(TestUtils.makeBasicSpan()) + .thenThrow(new RuntimeException()); + SpanProcessor simpleSpanProcessor = SimpleSpanProcessor.create(spanExporter); + simpleSpanProcessor.onEnd(readableSpan); + verify(spanExporter).export(Collections.singletonList(TestUtils.makeBasicSpan())); + } + + @Test + void tracerSdk_NotSampled_Span() { + WaitingSpanExporter waitingSpanExporter = + new WaitingSpanExporter(1, CompletableResultCode.ofSuccess()); + + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(waitingSpanExporter)) + .setSampler(mockSampler) + .build(); + + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.DROP)); + + try { + Tracer tracer = sdkTracerProvider.get(getClass().getName()); + tracer.spanBuilder(SPAN_NAME).startSpan(); + tracer.spanBuilder(SPAN_NAME).startSpan(); + + when(mockSampler.shouldSample(any(), any(), any(), any(), any(), anyList())) + .thenReturn(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); + Span span = tracer.spanBuilder(SPAN_NAME).startSpan(); + span.end(); + + // 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 exported = waitingSpanExporter.waitForExport(); + // Need to check this because otherwise the variable span1 is unused, other option is to not + // have a span1 variable. + assertThat(exported).containsExactly(((ReadableSpan) span).toSpanData()); + } finally { + sdkTracerProvider.shutdown(); + } + } + + @Test + void tracerSdk_NotSampled_RecordingEventsSpan() { + // TODO(bdrutu): Fix this when Sampler return RECORD_ONLY option. + /* + tracer.addSpanProcessor( + BatchSpanProcessor.builder(waitingSpanExporter) + .setScheduleDelayMillis(MAX_SCHEDULE_DELAY_MILLIS) + .reportOnlySampled(false) + .build()); + + io.opentelemetry.trace.Span span = + tracer + .spanBuilder("FOO") + .setSampler(Samplers.neverSample()) + .startSpanWithSampler(); + span.end(); + + List exported = waitingSpanExporter.waitForExport(1); + assertThat(exported).containsExactly(((ReadableSpan) span).toSpanData()); + */ + } + + @Test + void onEndSync_ExporterReturnError() { + SpanData spanData = TestUtils.makeBasicSpan(); + when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + when(readableSpan.toSpanData()).thenReturn(spanData); + simpleSampledSpansProcessor.onEnd(readableSpan); + // Try again, now will no longer return error. + simpleSampledSpansProcessor.onEnd(readableSpan); + verify(spanExporter, times(2)).export(Collections.singletonList(spanData)); + } + + @Test + void forceFlush() { + CompletableResultCode export1 = new CompletableResultCode(); + CompletableResultCode export2 = new CompletableResultCode(); + + when(spanExporter.export(any())).thenReturn(export1, export2); + + SpanData spanData = TestUtils.makeBasicSpan(); + when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + when(readableSpan.toSpanData()).thenReturn(spanData); + + simpleSampledSpansProcessor.onEnd(readableSpan); + simpleSampledSpansProcessor.onEnd(readableSpan); + + verify(spanExporter, times(2)).export(Collections.singletonList(spanData)); + + CompletableResultCode flush = simpleSampledSpansProcessor.forceFlush(); + assertThat(flush.isDone()).isFalse(); + + export1.succeed(); + assertThat(flush.isDone()).isFalse(); + + export2.succeed(); + assertThat(flush.isDone()).isTrue(); + assertThat(flush.isSuccess()).isTrue(); + } + + @Test + void shutdown() { + CompletableResultCode export1 = new CompletableResultCode(); + CompletableResultCode export2 = new CompletableResultCode(); + + when(spanExporter.export(any())).thenReturn(export1, export2); + + SpanData spanData = TestUtils.makeBasicSpan(); + when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + when(readableSpan.toSpanData()).thenReturn(spanData); + + simpleSampledSpansProcessor.onEnd(readableSpan); + simpleSampledSpansProcessor.onEnd(readableSpan); + + verify(spanExporter, times(2)).export(Collections.singletonList(spanData)); + + CompletableResultCode shutdown = simpleSampledSpansProcessor.shutdown(); + assertThat(shutdown.isDone()).isFalse(); + + export1.succeed(); + assertThat(shutdown.isDone()).isFalse(); + verify(spanExporter, never()).shutdown(); + + export2.succeed(); + assertThat(shutdown.isDone()).isTrue(); + assertThat(shutdown.isSuccess()).isTrue(); + verify(spanExporter).shutdown(); + } + + @Test + void close() { + simpleSampledSpansProcessor.close(); + verify(spanExporter).shutdown(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/AlwaysOffSamplerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/AlwaysOffSamplerTest.java new file mode 100644 index 000000000..f8fd63388 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/AlwaysOffSamplerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.IdGenerator; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class AlwaysOffSamplerTest { + + private static final String SPAN_NAME = "MySpanName"; + private static final SpanKind SPAN_KIND = SpanKind.INTERNAL; + private final IdGenerator idsGenerator = IdGenerator.random(); + private final String traceId = idsGenerator.generateTraceId(); + private final String parentSpanId = idsGenerator.generateSpanId(); + private final SpanContext sampledSpanContext = + SpanContext.create(traceId, parentSpanId, TraceFlags.getSampled(), TraceState.getDefault()); + private final Context sampledParentContext = Context.root().with(Span.wrap(sampledSpanContext)); + private final Context notSampledParentContext = + Context.root() + .with( + Span.wrap( + SpanContext.create( + traceId, parentSpanId, TraceFlags.getDefault(), TraceState.getDefault()))); + + @Test + void parentNotSampled() { + assertThat( + Sampler.alwaysOff() + .shouldSample( + sampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + } + + @Test + void parentSampled() { + assertThat( + Sampler.alwaysOff() + .shouldSample( + notSampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + } + + @Test + void getDescription() { + assertThat(Sampler.alwaysOff().getDescription()).isEqualTo("AlwaysOffSampler"); + } + + @Test + void string() { + assertThat(Sampler.alwaysOff().toString()).isEqualTo("AlwaysOffSampler"); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/AlwaysOnSamplerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/AlwaysOnSamplerTest.java new file mode 100644 index 000000000..d8705ceec --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/AlwaysOnSamplerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.IdGenerator; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class AlwaysOnSamplerTest { + + private static final String SPAN_NAME = "MySpanName"; + private static final SpanKind SPAN_KIND = SpanKind.INTERNAL; + private final IdGenerator idsGenerator = IdGenerator.random(); + private final String traceId = idsGenerator.generateTraceId(); + private final String parentSpanId = idsGenerator.generateSpanId(); + private final SpanContext sampledSpanContext = + SpanContext.create(traceId, parentSpanId, TraceFlags.getSampled(), TraceState.getDefault()); + private final Context sampledParentContext = Context.root().with(Span.wrap(sampledSpanContext)); + private final Context notSampledParentContext = + Context.root() + .with( + Span.wrap( + SpanContext.create( + traceId, parentSpanId, TraceFlags.getDefault(), TraceState.getDefault()))); + + @Test + void parentSampled() { + assertThat( + Sampler.alwaysOn() + .shouldSample( + sampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + } + + @Test + void parentNotSampled() { + assertThat( + Sampler.alwaysOn() + .shouldSample( + notSampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + } + + @Test + void getDescription() { + assertThat(Sampler.alwaysOn().getDescription()).isEqualTo("AlwaysOnSampler"); + } + + @Test + void string() { + assertThat(Sampler.alwaysOn().toString()).isEqualTo("AlwaysOnSampler"); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSamplerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSamplerTest.java new file mode 100644 index 000000000..5264fabff --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/ParentBasedSamplerTest.java @@ -0,0 +1,415 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.IdGenerator; +import java.util.Collections; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +class ParentBasedSamplerTest { + private static final String SPAN_NAME = "MySpanName"; + private static final SpanKind SPAN_KIND = SpanKind.INTERNAL; + private final IdGenerator idsGenerator = IdGenerator.random(); + private final String traceId = idsGenerator.generateTraceId(); + private final String parentSpanId = idsGenerator.generateSpanId(); + private final SpanContext sampledSpanContext = + SpanContext.create(traceId, parentSpanId, TraceFlags.getSampled(), TraceState.getDefault()); + private final Context sampledParentContext = Context.root().with(Span.wrap(sampledSpanContext)); + private final Context notSampledParentContext = + Context.root() + .with( + Span.wrap( + SpanContext.create( + traceId, parentSpanId, TraceFlags.getDefault(), TraceState.getDefault()))); + private final Context invalidParentContext = Context.root().with(Span.getInvalid()); + private final Context sampledRemoteParentContext = + Context.root() + .with( + Span.wrap( + SpanContext.createFromRemoteParent( + traceId, parentSpanId, TraceFlags.getSampled(), TraceState.getDefault()))); + private final Context notSampledRemoteParentContext = + Context.root() + .with( + Span.wrap( + SpanContext.createFromRemoteParent( + traceId, parentSpanId, TraceFlags.getDefault(), TraceState.getDefault()))); + + @Test + void alwaysOn() { + // Sampled parent. + assertThat( + Sampler.parentBased(Sampler.alwaysOn()) + .shouldSample( + sampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + + // Not sampled parent. + assertThat( + Sampler.parentBased(Sampler.alwaysOn()) + .shouldSample( + notSampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + } + + @Test + void alwaysOff() { + // Sampled parent. + assertThat( + Sampler.parentBased(Sampler.alwaysOff()) + .shouldSample( + sampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + + // Not sampled parent. + assertThat( + Sampler.parentBased(Sampler.alwaysOff()) + .shouldSample( + notSampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + } + + @Test + void notSampled_remoteParent() { + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setRemoteParentNotSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + notSampledRemoteParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setRemoteParentNotSampled(Sampler.alwaysOff()) + .build() + .shouldSample( + notSampledRemoteParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOn()) + .setRemoteParentNotSampled(Sampler.alwaysOff()) + .build() + .shouldSample( + notSampledRemoteParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOn()) + .setRemoteParentNotSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + notSampledRemoteParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + } + + @Test + void parentBasedSampler_NotSampled_NotRemote_Parent() { + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setLocalParentNotSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + notSampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setLocalParentNotSampled(Sampler.alwaysOff()) + .build() + .shouldSample( + notSampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOn()) + .setLocalParentNotSampled(Sampler.alwaysOff()) + .build() + .shouldSample( + notSampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOn()) + .setLocalParentNotSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + notSampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + } + + @Test + void sampled_remoteParent() { + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setRemoteParentSampled(Sampler.alwaysOff()) + .build() + .shouldSample( + sampledRemoteParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setRemoteParentSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + sampledRemoteParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOn()) + .setRemoteParentSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + sampledRemoteParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOn()) + .setRemoteParentSampled(Sampler.alwaysOff()) + .build() + .shouldSample( + sampledRemoteParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + } + + @Test + void parentBasedSampler_Sampled_NotRemote_Parent() { + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setLocalParentSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + sampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setLocalParentSampled(Sampler.alwaysOff()) + .build() + .shouldSample( + sampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOn()) + .setLocalParentSampled(Sampler.alwaysOff()) + .build() + .shouldSample( + sampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOn()) + .setLocalParentSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + sampledParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + } + + @Test + void invalidParent() { + assertThat( + Sampler.parentBased(Sampler.alwaysOff()) + .shouldSample( + invalidParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBased(Sampler.alwaysOff()) + .shouldSample( + invalidParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBasedBuilder(Sampler.alwaysOff()) + .setRemoteParentSampled(Sampler.alwaysOn()) + .setRemoteParentNotSampled(Sampler.alwaysOn()) + .setLocalParentSampled(Sampler.alwaysOn()) + .setLocalParentNotSampled(Sampler.alwaysOn()) + .build() + .shouldSample( + invalidParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.DROP); + + assertThat( + Sampler.parentBased(Sampler.alwaysOn()) + .shouldSample( + invalidParentContext, + traceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()) + .getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + } + + @Test + void getDescription() { + assertThat(Sampler.parentBased(Sampler.alwaysOn()).getDescription()) + .isEqualTo( + "ParentBased{root:AlwaysOnSampler,remoteParentSampled:AlwaysOnSampler," + + "remoteParentNotSampled:AlwaysOffSampler,localParentSampled:AlwaysOnSampler," + + "localParentNotSampled:AlwaysOffSampler}"); + } + + @Test + void equals() { + EqualsVerifier.forClass(ParentBasedSampler.class).verify(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/SamplingResultTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/SamplingResultTest.java new file mode 100644 index 000000000..850e609b2 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/SamplingResultTest.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import org.junit.jupiter.api.Test; + +class SamplingResultTest { + + @Test + void noAttributes() { + assertThat(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)) + .isSameAs(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); + assertThat(SamplingResult.create(SamplingDecision.DROP)) + .isSameAs(SamplingResult.create(SamplingDecision.DROP)); + + assertThat(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE).getDecision()) + .isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + assertThat(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE).getAttributes().isEmpty()) + .isTrue(); + assertThat(SamplingResult.create(SamplingDecision.DROP).getDecision()) + .isEqualTo(SamplingDecision.DROP); + assertThat(SamplingResult.create(SamplingDecision.DROP).getAttributes().isEmpty()).isTrue(); + } + + @Test + void emptyAttributes() { + assertThat(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE, Attributes.empty())) + .isSameAs(SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE)); + assertThat(SamplingResult.create(SamplingDecision.DROP, Attributes.empty())) + .isSameAs(SamplingResult.create(SamplingDecision.DROP)); + } + + @Test + void hasAttributes() { + final Attributes attrs = Attributes.of(longKey("foo"), 42L, stringKey("bar"), "baz"); + final SamplingResult sampledSamplingResult = + SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE, attrs); + assertThat(sampledSamplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + assertThat(sampledSamplingResult.getAttributes()).isEqualTo(attrs); + + final SamplingResult notSampledSamplingResult = + SamplingResult.create(SamplingDecision.DROP, attrs); + assertThat(notSampledSamplingResult.getDecision()).isEqualTo(SamplingDecision.DROP); + assertThat(notSampledSamplingResult.getAttributes()).isEqualTo(attrs); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/TraceIdRatioBasedSamplerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/TraceIdRatioBasedSamplerTest.java new file mode 100644 index 000000000..f545300e9 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/samplers/TraceIdRatioBasedSamplerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.samplers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.IdGenerator; +import io.opentelemetry.sdk.trace.data.LinkData; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class TraceIdRatioBasedSamplerTest { + private static final String SPAN_NAME = "MySpanName"; + private static final SpanKind SPAN_KIND = SpanKind.INTERNAL; + private static final int NUM_SAMPLE_TRIES = 1000; + + private static final IdGenerator idsGenerator = IdGenerator.random(); + + private final String traceId = idsGenerator.generateTraceId(); + private final String parentSpanId = idsGenerator.generateSpanId(); + private final SpanContext sampledSpanContext = + SpanContext.create(traceId, parentSpanId, TraceFlags.getSampled(), TraceState.getDefault()); + private final Context sampledParentContext = Context.root().with(Span.wrap(sampledSpanContext)); + private final Context notSampledParentContext = + Context.root() + .with( + Span.wrap( + SpanContext.create( + traceId, parentSpanId, TraceFlags.getDefault(), TraceState.getDefault()))); + private final Context invalidParentContext = Context.root().with(Span.getInvalid()); + private final LinkData sampledParentLink = LinkData.create(sampledSpanContext); + + @Test + void alwaysSample() { + TraceIdRatioBasedSampler sampler = TraceIdRatioBasedSampler.create(1); + assertThat(sampler.getIdUpperBound()).isEqualTo(Long.MAX_VALUE); + } + + @Test + void neverSample() { + TraceIdRatioBasedSampler sampler = TraceIdRatioBasedSampler.create(0); + assertThat(sampler.getIdUpperBound()).isEqualTo(Long.MIN_VALUE); + } + + @Test + void outOfRangeHighProbability() { + assertThatThrownBy(() -> Sampler.traceIdRatioBased(1.01)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void outOfRangeLowProbability() { + assertThatThrownBy(() -> Sampler.traceIdRatioBased(-0.00001)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void getDescription() { + assertThat(Sampler.traceIdRatioBased(0.5).getDescription()) + .isEqualTo(String.format("TraceIdRatioBased{%.6f}", 0.5)); + } + + @Test + void differentProbabilities_NotSampledParent() { + assert_NotSampledParent(Sampler.traceIdRatioBased(0.5), 0.5); + assert_NotSampledParent(Sampler.traceIdRatioBased(0.2), 0.2); + assert_NotSampledParent(Sampler.traceIdRatioBased(0.2 / 0.3), 0.2 / 0.3); + // Probability sampler will respect parent sampling decision, i.e. NOT sampling, if wrapped + // around ParentBased + assert_NotSampledParent(Sampler.parentBased(Sampler.traceIdRatioBased(0.5)), 0); + assert_NotSampledParent(Sampler.parentBased(Sampler.traceIdRatioBased(0.2)), 0); + assert_NotSampledParent(Sampler.parentBased(Sampler.traceIdRatioBased(0.2 / 0.3)), 0); + } + + private void assert_NotSampledParent(Sampler sampler, double probability) { + assertSamplerSamplesWithProbability( + sampler, notSampledParentContext, Collections.emptyList(), probability); + } + + @Test + void differentProbabilities_SampledParent() { + assertSampledParent(Sampler.traceIdRatioBased(0.5), 0.5); + assertSampledParent(Sampler.traceIdRatioBased(0.2), 0.2); + assertSampledParent(Sampler.traceIdRatioBased(0.2 / 0.3), 0.2 / 0.3); + // Probability sampler will respect parent sampling decision, i.e. sampling, if wrapped around + // ParentBased + assertSampledParent(Sampler.parentBased(Sampler.traceIdRatioBased(0.5)), 1); + assertSampledParent(Sampler.parentBased(Sampler.traceIdRatioBased(0.2)), 1); + assertSampledParent(Sampler.parentBased(Sampler.traceIdRatioBased(0.2 / 0.3)), 1); + } + + private void assertSampledParent(Sampler sampler, double probability) { + assertSamplerSamplesWithProbability( + sampler, sampledParentContext, Collections.emptyList(), probability); + } + + @Test + void differentProbabilities_SampledParentLink() { + // Parent NOT sampled + assertSampledParentLink(Sampler.traceIdRatioBased(0.5), 0.5); + assertSampledParentLink(Sampler.traceIdRatioBased(0.2), 0.2); + assertSampledParentLink(Sampler.traceIdRatioBased(0.2 / 0.3), 0.2 / 0.3); + // Probability sampler will respect parent sampling decision, i.e. NOT sampling, if wrapped + // around ParentBased + assertSampledParentLink(Sampler.parentBased(Sampler.traceIdRatioBased(0.5)), 0); + assertSampledParentLink(Sampler.parentBased(Sampler.traceIdRatioBased(0.2)), 0); + assertSampledParentLink(Sampler.parentBased(Sampler.traceIdRatioBased(0.2 / 0.3)), 0); + + // Parent Sampled + assertSampledParentLinkContext(Sampler.traceIdRatioBased(0.5), 0.5); + assertSampledParentLinkContext(Sampler.traceIdRatioBased(0.2), 0.2); + assertSampledParentLinkContext(Sampler.traceIdRatioBased(0.2 / 0.3), 0.2 / 0.3); + // Probability sampler will respect parent sampling decision, i.e. sampling, if wrapped around + // ParentBased + assertSampledParentLinkContext(Sampler.parentBased(Sampler.traceIdRatioBased(0.5)), 1); + assertSampledParentLinkContext(Sampler.parentBased(Sampler.traceIdRatioBased(0.2)), 1); + assertSampledParentLinkContext(Sampler.parentBased(Sampler.traceIdRatioBased(0.2 / 0.3)), 1); + } + + private void assertSampledParentLink(Sampler sampler, double probability) { + assertSamplerSamplesWithProbability( + sampler, + notSampledParentContext, + Collections.singletonList(sampledParentLink), + probability); + } + + private void assertSampledParentLinkContext(Sampler sampler, double probability) { + assertSamplerSamplesWithProbability( + sampler, sampledParentContext, Collections.singletonList(sampledParentLink), probability); + } + + @Test + void sampleBasedOnTraceId() { + final Sampler defaultProbability = Sampler.traceIdRatioBased(0.0001); + // This traceId will not be sampled by the Probability Sampler because the last 8 bytes as long + // is not less than probability * Long.MAX_VALUE; + String notSampledTraceId = "00000000000000008fffffffffffffff"; + SamplingResult samplingResult1 = + defaultProbability.shouldSample( + invalidParentContext, + notSampledTraceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()); + assertThat(samplingResult1.getDecision()).isEqualTo(SamplingDecision.DROP); + // This traceId will be sampled by the Probability Sampler because the last 8 bytes as long + // is less than probability * Long.MAX_VALUE; + String sampledTraceId = "0000ffffffffffff0000000000000000"; + SamplingResult samplingResult2 = + defaultProbability.shouldSample( + invalidParentContext, + sampledTraceId, + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + Collections.emptyList()); + assertThat(samplingResult2.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE); + } + + // Applies the given sampler to NUM_SAMPLE_TRIES random traceId. + private static void assertSamplerSamplesWithProbability( + Sampler sampler, Context parent, List parentLinks, double probability) { + int count = 0; // Count of spans with sampling enabled + for (int i = 0; i < NUM_SAMPLE_TRIES; i++) { + if (sampler + .shouldSample( + parent, + idsGenerator.generateTraceId(), + SPAN_NAME, + SPAN_KIND, + Attributes.empty(), + parentLinks) + .getDecision() + .equals(SamplingDecision.RECORD_AND_SAMPLE)) { + 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(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/TestUtils.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/TestUtils.java new file mode 100644 index 000000000..db5389591 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/TestUtils.java @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +public final class TestUtils { + private TestUtils() {} + + /** + * Returns the number of finished {@code Span}s in the specified {@code OpenTelemetryExtension}. + */ + public static Callable finishedSpansSize(final OpenTelemetryExtension otelTesting) { + return () -> otelTesting.getSpans().size(); + } + + /** Returns a {@code List} with the {@code Span} matching the specified attribute. */ + public static List getByAttr( + List spans, final AttributeKey key, final T value) { + return getByCondition( + spans, + span -> { + T attrValue = span.getAttributes().get(key); + if (attrValue == null) { + return false; + } + return value.equals(attrValue); + }); + } + + /** + * Returns one {@code Span} instance matching the specified attribute. In case of more than one + * instance being matched, an {@code IllegalArgumentException} will be thrown. + */ + @Nullable + public static SpanData getOneByAttr(List spans, AttributeKey key, T value) { + List found = getByAttr(spans, key, value); + if (found.size() > 1) { + throw new IllegalArgumentException( + "there is more than one span with tag '" + key + "' and value '" + value + "'"); + } + + return found.isEmpty() ? null : found.get(0); + } + + /** Returns a {@code List} with the {@code Span} matching the specified kind. */ + public static List getByKind(List spans, final SpanKind kind) { + return getByCondition(spans, span -> span.getKind() == kind); + } + + /** + * Returns one {@code Span} instance matching the specified kind. In case of more than one + * instance being matched, an {@code IllegalArgumentException} will be thrown. + */ + @Nullable + public static SpanData getOneByKind(List spans, final SpanKind kind) { + + List found = getByKind(spans, kind); + if (found.size() > 1) { + throw new IllegalArgumentException("there is more than one span with kind '" + kind + "'"); + } + + return found.isEmpty() ? null : found.get(0); + } + + /** Returns a {@code List} with the {@code Span} matching the specified name. */ + private static List getByName(List spans, final String name) { + return getByCondition(spans, span -> span.getName().equals(name)); + } + + /** + * Returns one {@code Span} instance matching the specified name. In case of more than one + * instance being matched, an {@code IllegalArgumentException} will be thrown. + */ + @Nullable + public static SpanData getOneByName(List spans, final String name) { + + List found = getByName(spans, name); + if (found.size() > 1) { + throw new IllegalArgumentException("there is more than one span with name '" + name + "'"); + } + + return found.isEmpty() ? null : found.get(0); + } + + interface Condition { + boolean check(SpanData span); + } + + private static List getByCondition(List spans, Condition cond) { + List found = new ArrayList<>(); + for (SpanData span : spans) { + if (cond.check(span)) { + found.add(span); + } + } + + return found; + } + + /** Sleeps for a random period of time, expected to be under 1 second. */ + public static void sleep() { + try { + TimeUnit.MILLISECONDS.sleep(new Random().nextInt(500)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted", e); + } + } + + /** Sleeps the specified milliseconds. */ + public static void sleep(long milliseconds) { + try { + TimeUnit.MILLISECONDS.sleep(milliseconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted", e); + } + } + + /** + * Sorts the specified {@code List} of {@code Span} by their start epoch timestamp values, + * returning it as a new {@code List}. + */ + public static List sortByStartTime(List spans) { + List sortedSpans = new ArrayList<>(spans); + Collections.sort( + sortedSpans, (o1, o2) -> Long.compare(o1.getStartEpochNanos(), o2.getStartEpochNanos())); + return sortedSpans; + } + + /** Asserts the specified {@code List} of {@code Span} belongs to the same trace. */ + public static void assertSameTrace(List spans) { + for (int i = 0; i < spans.size() - 1; i++) { + // TODO - Include nanos in this comparison. + assertThat(spans.get(spans.size() - 1).getEndEpochNanos() >= spans.get(i).getEndEpochNanos()) + .isTrue(); + assertThat(spans.get(spans.size() - 1).getTraceId()).isEqualTo(spans.get(i).getTraceId()); + assertThat(spans.get(spans.size() - 1).getSpanId()).isEqualTo(spans.get(i).getParentSpanId()); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/activespanreplacement/ActiveSpanReplacementTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/activespanreplacement/ActiveSpanReplacementTest.java new file mode 100644 index 000000000..e201783cc --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/activespanreplacement/ActiveSpanReplacementTest.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.activespanreplacement; + +import static io.opentelemetry.sdk.trace.testbed.TestUtils.sleep; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("FutureReturnValueIgnored") +class ActiveSpanReplacementTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(ActiveSpanReplacementTest.class.getName()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Test + void test() { + // Start an isolated task and query for its result in another task/thread + Span span = tracer.spanBuilder("initial").startSpan(); + try (Scope scope = span.makeCurrent()) { + // Explicitly pass a Span to be finished once a late calculation is done. + submitAnotherTask(span); + } + + await() + .atMost(Duration.ofSeconds(15)) + .until(TestUtils.finishedSpansSize(otelTesting), equalTo(3)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(3); + assertThat(spans.get(0).getName()).isEqualTo("initial"); // Isolated task + assertThat(spans.get(1).getName()).isEqualTo("subtask"); + assertThat(spans.get(2).getName()).isEqualTo("task"); + + // task/subtask are part of the same trace, and subtask is a child of task + assertThat(spans.get(1).getTraceId()).isEqualTo(spans.get(2).getTraceId()); + assertThat(spans.get(2).getSpanId()).isEqualTo(spans.get(1).getParentSpanId()); + + // initial task is not related in any way to those two tasks + assertThat(spans.get(0).getTraceId()).isNotEqualTo(spans.get(1).getTraceId()); + assertThat(spans.get(0).getParentSpanId()).isEqualTo(SpanId.getInvalid()); + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } + + private void submitAnotherTask(final Span initialSpan) { + + executor.submit( + () -> { + // Create a new Span for this task + Span taskSpan = tracer.spanBuilder("task").startSpan(); + try (Scope scope = taskSpan.makeCurrent()) { + + // Simulate work strictly related to the initial Span + // and finish it. + try (Scope initialScope = initialSpan.makeCurrent()) { + sleep(50); + } finally { + initialSpan.end(); + } + + // Restore the span for this task and create a subspan + Span subTaskSpan = tracer.spanBuilder("subtask").startSpan(); + try (Scope subTaskScope = subTaskSpan.makeCurrent()) { + sleep(50); + } finally { + subTaskSpan.end(); + } + } finally { + taskSpan.end(); + } + }); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/actorpropagation/Actor.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/actorpropagation/Actor.java new file mode 100644 index 000000000..698594567 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/actorpropagation/Actor.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.actorpropagation; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Phaser; + +final class Actor implements AutoCloseable { + private final ExecutorService executor; + private final Tracer tracer; + private final Phaser phaser; + + Actor(Tracer tracer, Phaser phaser) { + // Passed along here for testing. Normally should be referenced via GlobalTracer.get(). + this.tracer = tracer; + + this.phaser = phaser; + executor = Executors.newFixedThreadPool(2); + } + + @Override + public void close() { + executor.shutdown(); + } + + Future tell(final String message) { + final Context parent = Context.current(); + phaser.register(); + return executor.submit( + () -> { + Span child = + tracer + .spanBuilder("received") + .setParent(parent) + .setSpanKind(SpanKind.CONSUMER) + .startSpan(); + try (Scope ignored = child.makeCurrent()) { + phaser.arriveAndAwaitAdvance(); // child tracer started + child.addEvent("received " + message); + phaser.arriveAndAwaitAdvance(); // assert size + } finally { + child.end(); + } + + phaser.arriveAndAwaitAdvance(); // child tracer finished + phaser.arriveAndAwaitAdvance(); // assert size + }); + } + + Future ask(final String message) { + final Context parent = Context.current(); + phaser.register(); + return executor.submit( + () -> { + Span span = + tracer + .spanBuilder("received") + .setParent(parent) + .setSpanKind(SpanKind.CONSUMER) + .startSpan(); + try { + phaser.arriveAndAwaitAdvance(); // child tracer started + phaser.arriveAndAwaitAdvance(); // assert size + return "received " + message; + } finally { + span.end(); + phaser.arriveAndAwaitAdvance(); // child tracer finished + phaser.arriveAndAwaitAdvance(); // assert size + } + }); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/actorpropagation/ActorPropagationTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/actorpropagation/ActorPropagationTest.java new file mode 100644 index 000000000..3e0ccdd9b --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/actorpropagation/ActorPropagationTest.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.actorpropagation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.Phaser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * These tests are intended to simulate the kind of async models that are common in java async + * frameworks. + * + *

    For improved readability, ignore the phaser lines as those are there to ensure deterministic + * execution for the tests without sleeps. + */ +@SuppressWarnings("FutureReturnValueIgnored") +class ActorPropagationTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(ActorPropagationTest.class.getName()); + private Phaser phaser; + + @BeforeEach + void before() { + phaser = new Phaser(); + } + + @Test + void testActorTell() { + try (Actor actor = new Actor(tracer, phaser)) { + phaser.register(); + Span parent = tracer.spanBuilder("actorTell").setSpanKind(SpanKind.PRODUCER).startSpan(); + parent.setAttribute("component", "example-actor"); + try (Scope ignored = parent.makeCurrent()) { + actor.tell("my message 1"); + actor.tell("my message 2"); + } finally { + parent.end(); + } + + phaser.arriveAndAwaitAdvance(); // child tracer started + assertThat(otelTesting.getSpans()).hasSize(1); + phaser.arriveAndAwaitAdvance(); // continue... + phaser.arriveAndAwaitAdvance(); // child tracer finished + assertThat(otelTesting.getSpans()).hasSize(3); + assertThat(TestUtils.getByKind(otelTesting.getSpans(), SpanKind.CONSUMER)).hasSize(2); + phaser.arriveAndDeregister(); // continue... + + List finished = otelTesting.getSpans(); + assertThat(finished.size()).isEqualTo(3); + assertThat(finished.get(0).getTraceId()).isEqualTo(finished.get(1).getTraceId()); + assertThat(TestUtils.getByKind(finished, SpanKind.CONSUMER)).hasSize(2); + assertThat(TestUtils.getOneByKind(finished, SpanKind.PRODUCER)).isNotNull(); + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } + } + + @Test + void testActorAsk() throws ExecutionException, InterruptedException { + try (Actor actor = new Actor(tracer, phaser)) { + phaser.register(); + Future future1; + Future future2; + Span span = tracer.spanBuilder("actorAsk").setSpanKind(SpanKind.PRODUCER).startSpan(); + span.setAttribute("component", "example-actor"); + + try (Scope ignored = span.makeCurrent()) { + future1 = actor.ask("my message 1"); + future2 = actor.ask("my message 2"); + } finally { + span.end(); + } + phaser.arriveAndAwaitAdvance(); // child tracer started + assertThat(otelTesting.getSpans().size()).isEqualTo(1); + phaser.arriveAndAwaitAdvance(); // continue... + phaser.arriveAndAwaitAdvance(); // child tracer finished + assertThat(otelTesting.getSpans().size()).isEqualTo(3); + assertThat(TestUtils.getByKind(otelTesting.getSpans(), SpanKind.CONSUMER)).hasSize(2); + phaser.arriveAndDeregister(); // continue... + + List finished = otelTesting.getSpans(); + String message1 = future1.get(); // This really should be a non-blocking callback... + String message2 = future2.get(); // This really should be a non-blocking callback... + assertThat(message1).isEqualTo("received my message 1"); + assertThat(message2).isEqualTo("received my message 2"); + + assertThat(finished.size()).isEqualTo(3); + assertThat(finished.get(0).getTraceId()).isEqualTo(finished.get(1).getTraceId()); + assertThat(TestUtils.getByKind(finished, SpanKind.CONSUMER)).hasSize(2); + assertThat(TestUtils.getOneByKind(finished, SpanKind.PRODUCER)).isNotNull(); + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Client.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Client.java new file mode 100644 index 000000000..891b6e58e --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Client.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.clientserver; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.concurrent.ArrayBlockingQueue; + +final class Client { + + private final ArrayBlockingQueue queue; + private final Tracer tracer; + + public Client(ArrayBlockingQueue queue, Tracer tracer) { + this.queue = queue; + this.tracer = tracer; + } + + public void send() throws InterruptedException { + Message message = new Message(); + + Span span = tracer.spanBuilder("send").setSpanKind(SpanKind.CLIENT).startSpan(); + span.setAttribute("component", "example-client"); + + try (Scope ignored = span.makeCurrent()) { + W3CTraceContextPropagator.getInstance().inject(Context.current(), message, Message::put); + queue.put(message); + } finally { + span.end(); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Message.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Message.java new file mode 100644 index 000000000..c398f40d4 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Message.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.clientserver; + +import java.util.HashMap; + +final class Message extends HashMap {} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Server.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Server.java new file mode 100644 index 000000000..257307880 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/Server.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.clientserver; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.concurrent.ArrayBlockingQueue; +import javax.annotation.Nullable; + +final class Server extends Thread { + + private final ArrayBlockingQueue queue; + private final Tracer tracer; + + public Server(ArrayBlockingQueue queue, Tracer tracer) { + this.queue = queue; + this.tracer = tracer; + } + + private void process(Message message) { + Context context = + W3CTraceContextPropagator.getInstance() + .extract( + Context.current(), + message, + new TextMapGetter() { + @Override + public Iterable keys(Message carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(Message carrier, String key) { + return carrier.get(key); + } + }); + Span span = + tracer.spanBuilder("receive").setSpanKind(SpanKind.SERVER).setParent(context).startSpan(); + span.setAttribute("component", "example-server"); + + try (Scope ignored = span.makeCurrent()) { + // Simulate work. + Span.current().addEvent("DoWork"); + } finally { + span.end(); + } + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + process(queue.take()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/TestClientServerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/TestClientServerTest.java new file mode 100644 index 000000000..9f35886bf --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/clientserver/TestClientServerTest.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.clientserver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TestClientServerTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(TestClientServerTest.class.getName()); + private final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(10); + private Server server; + + @BeforeEach + void before() { + server = new Server(queue, tracer); + server.start(); + } + + @AfterEach + void after() throws InterruptedException { + server.interrupt(); + server.join(5_000L); + } + + @Test + void test() throws Exception { + Client client = new Client(queue, tracer); + client.send(); + + await() + .atMost(Duration.ofSeconds(15)) + .until(TestUtils.finishedSpansSize(otelTesting), equalTo(2)); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(2); + + finished = TestUtils.sortByStartTime(finished); + if (!finished.get(0).getKind().equals(SpanKind.CLIENT)) { + SpanData serverData = finished.get(0); + finished.set(0, finished.get(1)); + finished.set(1, serverData); + } + assertThat(finished.get(0).getTraceId()).isEqualTo(finished.get(1).getTraceId()); + assertThat(finished.get(0).getKind()).isEqualTo(SpanKind.CLIENT); + assertThat(finished.get(1).getKind()).isEqualTo(SpanKind.SERVER); + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/Client.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/Client.java new file mode 100644 index 000000000..f4fd7efbd --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/Client.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.concurrentcommonrequesthandler; + +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +final class Client { + private final ExecutorService executor = Executors.newCachedThreadPool(); + + private final RequestHandler requestHandler; + + public Client(RequestHandler requestHandler) { + this.requestHandler = requestHandler; + } + + public Future send(final Object message) { + final RequestHandlerContext requestHandlerContext = new RequestHandlerContext(); + return executor.submit( + () -> { + TestUtils.sleep(); + executor + .submit( + () -> { + TestUtils.sleep(); + requestHandler.beforeRequest(message, requestHandlerContext); + }) + .get(); + + executor + .submit( + () -> { + TestUtils.sleep(); + requestHandler.afterResponse(message, requestHandlerContext); + }) + .get(); + + return message + ":response"; + }); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/HandlerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/HandlerTest.java new file mode 100644 index 000000000..a8e33d6fd --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/HandlerTest.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.concurrentcommonrequesthandler; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * There is only one instance of 'RequestHandler' per 'Client'. Methods of 'RequestHandler' are + * executed concurrently in different threads which are reused (common pool). Therefore we cannot + * use current active span and activate span. So one issue here is setting correct parent span. + */ +class HandlerTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(HandlerTest.class.getName()); + + private final Client client = new Client(new RequestHandler(tracer)); + + @Test + void two_requests() throws Exception { + Future responseFuture = client.send("message"); + Future responseFuture2 = client.send("message2"); + + assertThat(responseFuture.get(15, TimeUnit.SECONDS)).isEqualTo("message:response"); + assertThat(responseFuture2.get(15, TimeUnit.SECONDS)).isEqualTo("message2:response"); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(2); + + for (SpanData spanProto : finished) { + assertThat(spanProto.getKind()).isEqualTo(SpanKind.CLIENT); + } + + assertThat(finished.get(0).getTraceId()).isNotEqualTo(finished.get(1).getTraceId()); + assertThat(finished.get(0).getParentSpanId()).isEqualTo(SpanId.getInvalid()); + assertThat(finished.get(1).getParentSpanId()).isEqualTo(SpanId.getInvalid()); + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } + + /** Active parent is not picked up by child. */ + @Test + void parent_not_picked_up() throws Exception { + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + try (Scope ignored = parentSpan.makeCurrent()) { + String response = client.send("no_parent").get(15, TimeUnit.SECONDS); + assertThat(response).isEqualTo("no_parent:response"); + } finally { + parentSpan.end(); + } + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(2); + + SpanData child = TestUtils.getOneByName(finished, RequestHandler.OPERATION_NAME); + assertThat(child).isNotNull(); + + SpanData parent = TestUtils.getOneByName(finished, "parent"); + assertThat(parent).isNotNull(); + + // Here check that there is no parent-child relation although it should be because child is + // created when parent is active + assertThat(parent.getSpanId()).isNotEqualTo(child.getParentSpanId()); + } + + /** + * Solution is bad because parent is per client (we don't have better choice). Therefore all + * client requests will have the same parent. But if client is long living and injected/reused in + * different places then initial parent will not be correct. + */ + @Test + void bad_solution_to_set_parent() throws Exception { + Client client; + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + try (Scope ignored = parentSpan.makeCurrent()) { + client = new Client(new RequestHandler(tracer, Context.current().with(parentSpan))); + String response = client.send("correct_parent").get(15, TimeUnit.SECONDS); + assertThat(response).isEqualTo("correct_parent:response"); + } finally { + parentSpan.end(); + } + + // Send second request, now there is no active parent, but it will be set, ups + String response = client.send("wrong_parent").get(15, TimeUnit.SECONDS); + assertThat(response).isEqualTo("wrong_parent:response"); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(3); + + finished = TestUtils.sortByStartTime(finished); + + SpanData parent = TestUtils.getOneByName(finished, "parent"); + assertThat(parent).isNotNull(); + + // now there is parent/child relation between first and second span: + assertThat(finished.get(1).getParentSpanId()).isEqualTo(parent.getSpanId()); + + // third span should not have parent, but it has, damn it + assertThat(finished.get(2).getParentSpanId()).isEqualTo(parent.getSpanId()); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/RequestHandler.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/RequestHandler.java new file mode 100644 index 000000000..81b380909 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/RequestHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.concurrentcommonrequesthandler; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; + +/** + * One instance per Client. Executed concurrently for all requests of one client. 'beforeRequest' + * and 'afterResponse' are executed in different threads for one 'send' + */ +final class RequestHandler { + static final String OPERATION_NAME = "send"; + + private final Tracer tracer; + + private final Context parentContext; + + public RequestHandler(Tracer tracer) { + this(tracer, null); + } + + public RequestHandler(Tracer tracer, Context parentContext) { + this.tracer = tracer; + this.parentContext = parentContext; + } + + public void beforeRequest(Object request, RequestHandlerContext requestHandlerContext) { + // we cannot use active span because we don't know in which thread it is executed + // and we cannot therefore activate span. thread can come from common thread pool. + SpanBuilder spanBuilder = + tracer.spanBuilder(OPERATION_NAME).setNoParent().setSpanKind(SpanKind.CLIENT); + + if (parentContext != null) { + spanBuilder.setParent(parentContext); + } + + requestHandlerContext.put("span", spanBuilder.startSpan()); + } + + public void afterResponse(Object response, RequestHandlerContext requestHandlerContext) { + Object spanObject = requestHandlerContext.get("span"); + if (spanObject instanceof Span) { + Span span = (Span) spanObject; + span.end(); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/RequestHandlerContext.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/RequestHandlerContext.java new file mode 100644 index 000000000..912115e58 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/concurrentcommonrequesthandler/RequestHandlerContext.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.concurrentcommonrequesthandler; + +import java.util.HashMap; + +final class RequestHandlerContext extends HashMap {} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/errorreporting/ErrorReportingTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/errorreporting/ErrorReportingTest.java new file mode 100644 index 000000000..3198ad371 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/errorreporting/ErrorReportingTest.java @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.errorreporting; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("FutureReturnValueIgnored") +public final class ErrorReportingTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(ErrorReportingTest.class.getName()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + /* Very simple error handling **/ + @Test + void testSimpleError() { + Span span = tracer.spanBuilder("one").startSpan(); + try (Scope ignored = span.makeCurrent()) { + throw new RuntimeException("Invalid state"); + } catch (RuntimeException e) { + span.setStatus(StatusCode.ERROR); + } finally { + span.end(); + } + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(spans.get(0).getStatus().getStatusCode()).isEqualTo(StatusCode.ERROR); + } + + /* Error handling in a callback capturing/activating the Span */ + @Test + void testCallbackError() { + final Span span = tracer.spanBuilder("one").startSpan(); + executor.submit( + () -> { + try (Scope ignored = span.makeCurrent()) { + throw new RuntimeException("Invalid state"); + } catch (RuntimeException exc) { + span.setStatus(StatusCode.ERROR); + } finally { + span.end(); + } + }); + + await() + .atMost(Duration.ofSeconds(5)) + .until(TestUtils.finishedSpansSize(otelTesting), equalTo(1)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(spans.get(0).getStatus().getStatusCode()).isEqualTo(StatusCode.ERROR); + } + + /* Error handling for a max-retries task (such as url fetching). + * We log the error at each retry. */ + @Test + void testErrorRecovery() { + final int maxRetries = 1; + int retries = 0; + Span span = tracer.spanBuilder("one").startSpan(); + try (Scope ignored = span.makeCurrent()) { + while (retries++ < maxRetries) { + try { + throw new RuntimeException("No url could be fetched"); + } catch (RuntimeException exc) { + span.addEvent("error"); + } + } + } + + span.setStatus(StatusCode.ERROR); // Could not fetch anything. + span.end(); + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(spans.get(0).getStatus().getStatusCode()).isEqualTo(StatusCode.ERROR); + + List events = spans.get(0).getEvents(); + assertThat(events).hasSize(maxRetries); + assertThat("error").isEqualTo(events.get(0).getName()); + } + + /* Error handling for a mocked layer automatically capturing/activating + * the Span for a submitted Runnable. */ + @Test + void testInstrumentationLayer() { + Span span = tracer.spanBuilder("one").startSpan(); + try (Scope ignored = span.makeCurrent()) { + // ScopedRunnable captures the active Span at this time. + executor.submit( + new ScopedRunnable( + () -> { + try { + throw new RuntimeException("Invalid state"); + } catch (RuntimeException exc) { + Span.current().setStatus(StatusCode.ERROR); + } finally { + Span.current().end(); + } + }, + tracer)); + } + + await() + .atMost(Duration.ofSeconds(5)) + .until(TestUtils.finishedSpansSize(otelTesting), equalTo(1)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(StatusCode.ERROR).isEqualTo(spans.get(0).getStatus().getStatusCode()); + } + + private static class ScopedRunnable implements Runnable { + Runnable runnable; + Tracer tracer; + Span span; + + private ScopedRunnable(Runnable runnable, Tracer tracer) { + this.runnable = runnable; + this.tracer = tracer; + this.span = Span.current(); + } + + @Override + public void run() { + // No error reporting is done, as we are a simple wrapper. + try (Scope ignored = span.makeCurrent()) { + runnable.run(); + } + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/latespanfinish/LateSpanFinishTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/latespanfinish/LateSpanFinishTest.java new file mode 100644 index 000000000..7504491c0 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/latespanfinish/LateSpanFinishTest.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.latespanfinish; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("FutureReturnValueIgnored") +public final class LateSpanFinishTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(LateSpanFinishTest.class.getName()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Test + void test() throws Exception { + // Create a Span manually and use it as parent of a pair of subtasks + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + submitTasks(parentSpan); + + // Wait for the threadpool to be done first, instead of polling/waiting + executor.shutdown(); + executor.awaitTermination(15, TimeUnit.SECONDS); + + // Late-finish the parent Span now + parentSpan.end(); + + // Children finish order is not guaranteed, but parent should finish *last*. + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(3); + assertThat(spans.get(0).getName()).startsWith("task"); + assertThat(spans.get(1).getName()).startsWith("task"); + assertThat(spans.get(2).getName()).isEqualTo("parent"); + + TestUtils.assertSameTrace(spans); + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } + + /* + * Fire away a few subtasks, passing a parent Span whose lifetime + * is not tied at-all to the children + */ + private void submitTasks(final Span parentSpan) { + + executor.submit( + () -> { + /* Alternative to calling activate() is to pass it manually to asChildOf() for each + * created Span. */ + try (Scope scope = parentSpan.makeCurrent()) { + Span childSpan = tracer.spanBuilder("task1").startSpan(); + try (Scope childScope = childSpan.makeCurrent()) { + TestUtils.sleep(55); + } finally { + childSpan.end(); + } + } + }); + + executor.submit( + () -> { + try (Scope scope = parentSpan.makeCurrent()) { + Span childSpan = tracer.spanBuilder("task2").startSpan(); + try (Scope childScope = childSpan.makeCurrent()) { + TestUtils.sleep(85); + } finally { + childSpan.end(); + } + } + }); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/Client.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/Client.java new file mode 100644 index 000000000..cbdd0f3d1 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/Client.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.listenerperrequest; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +final class Client { + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final Tracer tracer; + + public Client(Tracer tracer) { + this.tracer = tracer; + } + + /** Async execution. */ + private Future execute(final Object message, final ResponseListener responseListener) { + return executor.submit( + () -> { + // send via wire and get response + Object response = message + ":response"; + responseListener.onResponse(response); + return response; + }); + } + + public Future send(final Object message) { + Span span = tracer.spanBuilder("send").setSpanKind(SpanKind.CLIENT).startSpan(); + return execute(message, new ResponseListener(span)); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/ListenerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/ListenerTest.java new file mode 100644 index 000000000..bd2025532 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/ListenerTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.listenerperrequest; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Each request has own instance of ResponseListener. */ +class ListenerTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(ListenerTest.class.getName()); + + @Test + void test() throws Exception { + Client client = new Client(tracer); + Object response = client.send("message").get(); + assertThat(response).isEqualTo("message:response"); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(1); + assertThat(finished.get(0).getKind()).isEqualTo(SpanKind.CLIENT); + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/ResponseListener.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/ResponseListener.java new file mode 100644 index 000000000..68b16da63 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/listenerperrequest/ResponseListener.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.listenerperrequest; + +import io.opentelemetry.api.trace.Span; + +/** Response listener per request. Executed in a thread different from 'send' thread */ +final class ResponseListener { + private final Span span; + + public ResponseListener(Span span) { + this.span = span; + } + + /** executed when response is received from server. Any thread. */ + public void onResponse(Object response) { + span.end(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/multiplecallbacks/Client.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/multiplecallbacks/Client.java new file mode 100644 index 000000000..a54c90e04 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/multiplecallbacks/Client.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.multiplecallbacks; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +class Client { + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final CountDownLatch parentDoneLatch; + private final Tracer tracer; + + public Client(Tracer tracer, CountDownLatch parentDoneLatch) { + this.tracer = tracer; + this.parentDoneLatch = parentDoneLatch; + } + + public Future send(final Object message) { + final Context parent = Context.current(); + + return executor.submit( + () -> { + Span span = tracer.spanBuilder("subtask").setParent(parent).startSpan(); + try (Scope subtaskScope = span.makeCurrent()) { + // Simulate work - make sure we finish *after* the parent Span. + parentDoneLatch.await(); + } finally { + span.end(); + } + + return message + "::response"; + }); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/multiplecallbacks/MultipleCallbacksTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/multiplecallbacks/MultipleCallbacksTest.java new file mode 100644 index 000000000..3f3288da1 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/multiplecallbacks/MultipleCallbacksTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.multiplecallbacks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * These tests are intended to simulate a task with independent, asynchronous callbacks. + * + *

    For improved readability, ignore the CountDownLatch lines as those are there to ensure + * deterministic execution for the tests without sleeps. + */ +@SuppressWarnings("FutureReturnValueIgnored") +class MultipleCallbacksTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(MultipleCallbacksTest.class.getName()); + + @Test + void test() { + CountDownLatch parentDoneLatch = new CountDownLatch(1); + Client client = new Client(tracer, parentDoneLatch); + + Span span = tracer.spanBuilder("parent").startSpan(); + try (Scope scope = span.makeCurrent()) { + client.send("task1"); + client.send("task2"); + client.send("task3"); + } finally { + span.end(); + parentDoneLatch.countDown(); + } + + await() + .atMost(Duration.ofSeconds(15)) + .until(TestUtils.finishedSpansSize(otelTesting), equalTo(4)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(4); + assertThat(spans.get(0).getName()).isEqualTo("parent"); + + SpanData parentSpan = spans.get(0); + for (int i = 1; i < 4; i++) { + assertThat(spans.get(i).getTraceId()).isEqualTo(parentSpan.getTraceId()); + assertThat(spans.get(i).getParentSpanId()).isEqualTo(parentSpan.getSpanId()); + } + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/nestedcallbacks/NestedCallbacksTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/nestedcallbacks/NestedCallbacksTest.java new file mode 100644 index 000000000..c4424b2f0 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/nestedcallbacks/NestedCallbacksTest.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.nestedcallbacks; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.core.IsEqual.equalTo; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("FutureReturnValueIgnored") +public final class NestedCallbacksTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(NestedCallbacksTest.class.getName()); + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Test + void test() { + + Span span = tracer.spanBuilder("one").startSpan(); + submitCallbacks(span); + + await() + .atMost(Duration.ofSeconds(15)) + .until(TestUtils.finishedSpansSize(otelTesting), equalTo(1)); + + List spans = otelTesting.getSpans(); + assertThat(spans).hasSize(1); + assertThat(spans.get(0).getName()).isEqualTo("one"); + + Attributes attrs = spans.get(0).getAttributes(); + assertThat(attrs.size()).isEqualTo(3); + for (int i = 1; i <= 3; i++) { + assertThat(attrs.get(stringKey("key" + i))).isEqualTo(Integer.toString(i)); + } + + assertThat(Span.current()).isSameAs(Span.getInvalid()); + } + + private void submitCallbacks(final Span span) { + + executor.submit( + () -> { + try (Scope ignored = span.makeCurrent()) { + span.setAttribute("key1", "1"); + + executor.submit( + () -> { + try (Scope ignored12 = span.makeCurrent()) { + span.setAttribute("key2", "2"); + + executor.submit( + () -> { + try (Scope ignored1 = span.makeCurrent()) { + span.setAttribute("key3", "3"); + } finally { + span.end(); + } + }); + } + }); + } + }); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/Promise.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/Promise.java new file mode 100644 index 000000000..ebe73b624 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/Promise.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.promisepropagation; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.ArrayList; +import java.util.Collection; + +final class Promise { + private final PromiseContext context; + private final Tracer tracer; + private final Context parent; + + private final Collection> successCallbacks = new ArrayList<>(); + private final Collection errorCallbacks = new ArrayList<>(); + + Promise(PromiseContext context, Tracer tracer) { + this.context = context; + + // Passed along here for testing. Normally should be referenced via GlobalTracer.get(). + this.tracer = tracer; + parent = Context.current(); + } + + void onSuccess(SuccessCallback successCallback) { + successCallbacks.add(successCallback); + } + + void onError(ErrorCallback errorCallback) { + errorCallbacks.add(errorCallback); + } + + @SuppressWarnings("FutureReturnValueIgnored") + void success(final T result) { + for (final SuccessCallback callback : successCallbacks) { + context.submit( + () -> { + Span childSpan = tracer.spanBuilder("success").setParent(parent).startSpan(); + childSpan.setAttribute("component", "success"); + try (Scope ignored = childSpan.makeCurrent()) { + callback.accept(result); + } finally { + childSpan.end(); + } + context.getPhaser().arriveAndAwaitAdvance(); // trace reported + }); + } + } + + @SuppressWarnings("FutureReturnValueIgnored") + void error(final Throwable error) { + for (final ErrorCallback callback : errorCallbacks) { + context.submit( + () -> { + Span childSpan = tracer.spanBuilder("error").setParent(parent).startSpan(); + childSpan.setAttribute("component", "error"); + try (Scope ignored = childSpan.makeCurrent()) { + callback.accept(error); + } finally { + childSpan.end(); + } + context.getPhaser().arriveAndAwaitAdvance(); // trace reported + }); + } + } + + interface SuccessCallback { + void accept(T t); + } + + interface ErrorCallback { + void accept(Throwable t); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/PromiseContext.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/PromiseContext.java new file mode 100644 index 000000000..0471d9625 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/PromiseContext.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.promisepropagation; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Phaser; + +final class PromiseContext implements AutoCloseable { + private final Phaser phaser; + private final ExecutorService executor; + + public PromiseContext(Phaser phaser, int concurrency) { + this.phaser = phaser; + executor = Executors.newFixedThreadPool(concurrency); + } + + @Override + public void close() { + executor.shutdown(); + } + + public Future submit(Runnable runnable) { + phaser.register(); // register the work to be done on the executor + return executor.submit(runnable); + } + + public Phaser getPhaser() { + return phaser; + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/PromisePropagationTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/PromisePropagationTest.java new file mode 100644 index 000000000..b9bcd6f6e --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/promisepropagation/PromisePropagationTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.promisepropagation; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import io.opentelemetry.sdk.trace.testbed.nestedcallbacks.NestedCallbacksTest; +import java.util.List; +import java.util.concurrent.Phaser; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * These tests are intended to simulate the kind of async models that are common in java async + * frameworks. + * + *

    For improved readability, ignore the phaser lines as those are there to ensure deterministic + * execution for the tests without sleeps. + */ +class PromisePropagationTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(NestedCallbacksTest.class.getName()); + private Phaser phaser; + + @BeforeEach + void before() { + phaser = new Phaser(); + } + + @Test + void testPromiseCallback() { + phaser.register(); // register test thread + final AtomicReference successResult1 = new AtomicReference<>(); + final AtomicReference successResult2 = new AtomicReference<>(); + final AtomicReference errorResult = new AtomicReference<>(); + + try (PromiseContext context = new PromiseContext(phaser, 3)) { + Span parentSpan = tracer.spanBuilder("promises").startSpan(); + parentSpan.setAttribute("component", "example-promises"); + + try (Scope ignored = parentSpan.makeCurrent()) { + Promise successPromise = new Promise<>(context, tracer); + + successPromise.onSuccess( + s -> { + Span.current().addEvent("Promised 1 " + s); + successResult1.set(s); + phaser.arriveAndAwaitAdvance(); // result set + }); + successPromise.onSuccess( + s -> { + Span.current().addEvent("Promised 2 " + s); + successResult2.set(s); + phaser.arriveAndAwaitAdvance(); // result set + }); + + Promise errorPromise = new Promise<>(context, tracer); + + errorPromise.onError( + t -> { + errorResult.set(t); + phaser.arriveAndAwaitAdvance(); // result set + }); + + assertThat(otelTesting.getSpans().size()).isEqualTo(0); + successPromise.success("success!"); + errorPromise.error(new Exception("some error.")); + } finally { + parentSpan.end(); + } + + phaser.arriveAndAwaitAdvance(); // wait for results to be set + assertThat(successResult1.get()).isEqualTo("success!"); + assertThat(successResult2.get()).isEqualTo("success!"); + assertThat(errorResult.get().getMessage()).isEqualTo("some error."); + + phaser.arriveAndAwaitAdvance(); // wait for traces to be reported + + List finished = otelTesting.getSpans(); + assertThat(finished.size()).isEqualTo(4); + + AttributeKey component = stringKey("component"); + SpanData parentSpanProto = TestUtils.getOneByAttr(finished, component, "example-promises"); + assertThat(parentSpanProto).isNotNull(); + assertThat(SpanId.isValid(parentSpanProto.getParentSpanId())).isFalse(); + List successSpans = TestUtils.getByAttr(finished, component, "success"); + assertThat(successSpans).hasSize(2); + + CharSequence parentId = parentSpanProto.getSpanId(); + for (SpanData span : successSpans) { + assertThat(span.getParentSpanId()).isEqualTo(parentId.toString()); + } + + SpanData errorSpan = TestUtils.getOneByAttr(finished, component, "error"); + assertThat(errorSpan).isNotNull(); + assertThat(errorSpan.getParentSpanId()).isEqualTo(parentId.toString()); + } + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/Client.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/Client.java new file mode 100644 index 000000000..49b947619 --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/Client.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.statelesscommonrequesthandler; + +import io.opentelemetry.sdk.trace.testbed.TestUtils; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +final class Client { + private final ExecutorService executor = Executors.newCachedThreadPool(); + + private final RequestHandler requestHandler; + + public Client(RequestHandler requestHandler) { + this.requestHandler = requestHandler; + } + + /** Send a request....... */ + public Future send(final Object message) { + + return executor.submit( + () -> { + TestUtils.sleep(); + requestHandler.beforeRequest(message); + + TestUtils.sleep(); + requestHandler.afterResponse(message); + + return message + ":response"; + }); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/HandlerTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/HandlerTest.java new file mode 100644 index 000000000..c3bd9b6fd --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/HandlerTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.statelesscommonrequesthandler; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * There is only one instance of 'RequestHandler' per 'Client'. Methods of 'RequestHandler' are + * executed in the same thread (beforeRequest() and its resulting afterRequest(), that is). + */ +public final class HandlerTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(HandlerTest.class.getName()); + private final Client client = new Client(new RequestHandler(tracer)); + + @Test + void test_requests() throws Exception { + Future responseFuture = client.send("message"); + Future responseFuture2 = client.send("message2"); + Future responseFuture3 = client.send("message3"); + + assertThat(responseFuture3.get(5, TimeUnit.SECONDS)).isEqualTo("message3:response"); + assertThat(responseFuture2.get(5, TimeUnit.SECONDS)).isEqualTo("message2:response"); + assertThat(responseFuture.get(5, TimeUnit.SECONDS)).isEqualTo("message:response"); + + List finished = otelTesting.getSpans(); + assertThat(finished).hasSize(3); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/RequestHandler.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/RequestHandler.java new file mode 100644 index 000000000..d61d2e69c --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/statelesscommonrequesthandler/RequestHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.statelesscommonrequesthandler; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; + +/** + * One instance per Client. 'beforeRequest' and 'afterResponse' are executed in the same thread for + * one 'send', but as these methods do not expose any object storing state, a thread-local field in + * 'RequestHandler' itself is used to contain the Scope related to Span activation. + */ +@SuppressWarnings("MustBeClosedChecker") +final class RequestHandler { + static final String OPERATION_NAME = "send"; + + private final Tracer tracer; + + private static final ThreadLocal tlsScope = new ThreadLocal<>(); + + public RequestHandler(Tracer tracer) { + this.tracer = tracer; + } + + /** beforeRequest handler....... */ + public void beforeRequest(Object request) { + Span span = tracer.spanBuilder(OPERATION_NAME).setSpanKind(SpanKind.SERVER).startSpan(); + tlsScope.set(span.makeCurrent()); + } + + /** afterResponse handler....... */ + public void afterResponse(Object response) { + // Finish the Span + Span.current().end(); + + // Deactivate the Span + tlsScope.get().close(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/suspendresumepropagation/SuspendResume.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/suspendresumepropagation/SuspendResume.java new file mode 100644 index 000000000..01ad51d7f --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/suspendresumepropagation/SuspendResume.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.suspendresumepropagation; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; + +final class SuspendResume { + private final Span span; + + public SuspendResume(int id, Tracer tracer) { + Span span = tracer.spanBuilder("job " + id).startSpan(); + span.setAttribute("component", "suspend-resume"); + try (Scope scope = span.makeCurrent()) { + this.span = span; + } + } + + public void doPart(String name) { + try (Scope scope = span.makeCurrent()) { + span.addEvent("part: " + name); + } + } + + public void done() { + span.end(); + } +} diff --git a/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/suspendresumepropagation/SuspendResumePropagationTest.java b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/suspendresumepropagation/SuspendResumePropagationTest.java new file mode 100644 index 000000000..df80a4bdc --- /dev/null +++ b/opentelemetry-java/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/testbed/suspendresumepropagation/SuspendResumePropagationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.testbed.suspendresumepropagation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.testbed.statelesscommonrequesthandler.HandlerTest; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * These tests are intended to simulate the kind of async models that are common in java async + * frameworks. + */ +class SuspendResumePropagationTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private final Tracer tracer = + otelTesting.getOpenTelemetry().getTracer(HandlerTest.class.getName()); + + @BeforeEach + void before() {} + + @Test + void testContinuationInterleaving() { + SuspendResume job1 = new SuspendResume(1, tracer); + SuspendResume job2 = new SuspendResume(2, tracer); + + // Pretend that the framework is controlling actual execution here. + job1.doPart("some work for 1"); + job2.doPart("some work for 2"); + job1.doPart("other work for 1"); + job2.doPart("other work for 2"); + job2.doPart("more work for 2"); + job1.doPart("more work for 1"); + + job1.done(); + job2.done(); + + List finished = otelTesting.getSpans(); + assertThat(finished.size()).isEqualTo(2); + + assertThat(finished.get(0).getName()).isEqualTo("job 1"); + assertThat(finished.get(1).getName()).isEqualTo("job 2"); + + assertThat(SpanId.isValid(finished.get(0).getParentSpanId())).isFalse(); + assertThat(SpanId.isValid(finished.get(1).getParentSpanId())).isFalse(); + } +} diff --git a/opentelemetry-java/semconv/README.md b/opentelemetry-java/semconv/README.md new file mode 100644 index 000000000..cf626b4ba --- /dev/null +++ b/opentelemetry-java/semconv/README.md @@ -0,0 +1,10 @@ +# OpenTelemetry Semantic Conventions + +[![Javadocs][javadoc-image]][javadoc-url] + +* This module contains generated code for the Semantic Conventions defined by the OpenTelemetry specification. +* Scripts for generating the classes live in the `buildscripts/semantic-convention` directory +at the top level of the project. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-semconv.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-semconv diff --git a/opentelemetry-java/semconv/build.gradle.kts b/opentelemetry-java/semconv/build.gradle.kts new file mode 100644 index 000000000..9c99086a5 --- /dev/null +++ b/opentelemetry-java/semconv/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + `java-library` + `maven-publish` + + id("ru.vyarus.animalsniffer") +} + +description = "OpenTelemetry Semantic Conventions" +extra["moduleName"] = "io.opentelemetry.semconv" + +dependencies { + api(project(":api:all")) +} diff --git a/opentelemetry-java/semconv/gradle.properties b/opentelemetry-java/semconv/gradle.properties new file mode 100644 index 000000000..4476ae57e --- /dev/null +++ b/opentelemetry-java/semconv/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/resource/attributes/ResourceAttributes.java b/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/resource/attributes/ResourceAttributes.java new file mode 100644 index 000000000..f13378b5e --- /dev/null +++ b/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/resource/attributes/ResourceAttributes.java @@ -0,0 +1,525 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.semconv.resource.attributes; + +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import java.util.List; + +// DO NOT EDIT, this is an Auto-generated file from +// buildscripts/semantic-convention/templates/SemanticAttributes.java.j2 +public final class ResourceAttributes { + + /** Name of the cloud provider. */ + public static final AttributeKey CLOUD_PROVIDER = stringKey("cloud.provider"); + + /** The cloud account ID the resource is assigned to. */ + public static final AttributeKey CLOUD_ACCOUNT_ID = stringKey("cloud.account.id"); + + /** + * The geographical region the resource is running. Refer to your provider's docs to see the + * available regions, for example [AWS + * regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure + * regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), or [Google + * Cloud regions](https://cloud.google.com/about/locations). + */ + public static final AttributeKey CLOUD_REGION = stringKey("cloud.region"); + + /** + * Cloud regions often have multiple, isolated locations known as zones to increase availability. + * Availability zone represents the zone where the resource is running. + * + *

    Note: Availability zones are called "zones" on Google Cloud. + */ + public static final AttributeKey CLOUD_AVAILABILITY_ZONE = + stringKey("cloud.availability_zone"); + + /** + * The cloud platform in use. + * + *

    Note: The prefix of the service SHOULD match the one specified in `cloud.provider`. + */ + public static final AttributeKey CLOUD_PLATFORM = stringKey("cloud.platform"); + + /** + * The Amazon Resource Name (ARN) of an [ECS container + * instance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html). + */ + public static final AttributeKey AWS_ECS_CONTAINER_ARN = + stringKey("aws.ecs.container.arn"); + + /** + * The ARN of an [ECS + * cluster](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html). + */ + public static final AttributeKey AWS_ECS_CLUSTER_ARN = stringKey("aws.ecs.cluster.arn"); + + /** + * The [launch + * type](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html) for an ECS + * task. + */ + public static final AttributeKey AWS_ECS_LAUNCHTYPE = stringKey("aws.ecs.launchtype"); + + /** + * The ARN of an [ECS task + * definition](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html). + */ + public static final AttributeKey AWS_ECS_TASK_ARN = stringKey("aws.ecs.task.arn"); + + /** The task definition family this task definition is a member of. */ + public static final AttributeKey AWS_ECS_TASK_FAMILY = stringKey("aws.ecs.task.family"); + + /** The revision for this task definition. */ + public static final AttributeKey AWS_ECS_TASK_REVISION = + stringKey("aws.ecs.task.revision"); + + /** The ARN of an EKS cluster. */ + public static final AttributeKey AWS_EKS_CLUSTER_ARN = stringKey("aws.eks.cluster.arn"); + + /** + * The name(s) of the AWS log group(s) an application is writing to. + * + *

    Note: Multiple log groups must be supported for cases like multi-container applications, + * where a single application has sidecar containers, and each write to their own log group. + */ + public static final AttributeKey> AWS_LOG_GROUP_NAMES = + stringArrayKey("aws.log.group.names"); + + /** + * The Amazon Resource Name(s) (ARN) of the AWS log group(s). + * + *

    Note: See the [log group ARN format + * documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). + */ + public static final AttributeKey> AWS_LOG_GROUP_ARNS = + stringArrayKey("aws.log.group.arns"); + + /** The name(s) of the AWS log stream(s) an application is writing to. */ + public static final AttributeKey> AWS_LOG_STREAM_NAMES = + stringArrayKey("aws.log.stream.names"); + + /** + * The ARN(s) of the AWS log stream(s). + * + *

    Note: See the [log stream ARN format + * documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). + * One log group can contain several log streams, so these ARNs necessarily identify both a log + * group and a log stream. + */ + public static final AttributeKey> AWS_LOG_STREAM_ARNS = + stringArrayKey("aws.log.stream.arns"); + + /** Container name. */ + public static final AttributeKey CONTAINER_NAME = stringKey("container.name"); + + /** + * Container ID. Usually a UUID, as for example used to [identify Docker + * containers](https://docs.docker.com/engine/reference/run/#container-identification). The UUID + * might be abbreviated. + */ + public static final AttributeKey CONTAINER_ID = stringKey("container.id"); + + /** The container runtime managing this container. */ + public static final AttributeKey CONTAINER_RUNTIME = stringKey("container.runtime"); + + /** Name of the image the container was built on. */ + public static final AttributeKey CONTAINER_IMAGE_NAME = stringKey("container.image.name"); + + /** Container image tag. */ + public static final AttributeKey CONTAINER_IMAGE_TAG = stringKey("container.image.tag"); + + /** + * Name of the [deployment environment](https://en.wikipedia.org/wiki/Deployment_environment) (aka + * deployment tier). + */ + public static final AttributeKey DEPLOYMENT_ENVIRONMENT = + stringKey("deployment.environment"); + + /** The name of the function being executed. */ + public static final AttributeKey FAAS_NAME = stringKey("faas.name"); + + /** + * The unique ID of the function being executed. + * + *

    Note: For example, in AWS Lambda this field corresponds to the + * [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) value, in GCP + * to the URI of the resource, and in Azure to the + * [FunctionDirectory](https://github.com/Azure/azure-functions-host/wiki/Retrieving-information-about-the-currently-running-function) + * field. + */ + public static final AttributeKey FAAS_ID = stringKey("faas.id"); + + /** + * The version string of the function being executed as defined in [Version + * Attributes](../../resource/semantic_conventions/README.md#version-attributes). + */ + public static final AttributeKey FAAS_VERSION = stringKey("faas.version"); + + /** The execution environment ID as a string. */ + public static final AttributeKey FAAS_INSTANCE = stringKey("faas.instance"); + + /** + * The amount of memory available to the serverless function in MiB. + * + *

    Note: It's recommended to set this attribute since e.g. too little memory can easily + * stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable + * `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information. + */ + public static final AttributeKey FAAS_MAX_MEMORY = longKey("faas.max_memory"); + + /** Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider. */ + public static final AttributeKey HOST_ID = stringKey("host.id"); + + /** + * Name of the host. On Unix systems, it may contain what the hostname command returns, or the + * fully qualified hostname, or another name specified by the user. + */ + public static final AttributeKey HOST_NAME = stringKey("host.name"); + + /** Type of host. For Cloud, this must be the machine type. */ + public static final AttributeKey HOST_TYPE = stringKey("host.type"); + + /** The CPU architecture the host system is running on. */ + public static final AttributeKey HOST_ARCH = stringKey("host.arch"); + + /** Name of the VM image or OS install the host was instantiated from. */ + public static final AttributeKey HOST_IMAGE_NAME = stringKey("host.image.name"); + + /** VM image ID. For Cloud, this value is from the provider. */ + public static final AttributeKey HOST_IMAGE_ID = stringKey("host.image.id"); + + /** + * The version string of the VM image as defined in [Version + * Attributes](README.md#version-attributes). + */ + public static final AttributeKey HOST_IMAGE_VERSION = stringKey("host.image.version"); + + /** The name of the cluster. */ + public static final AttributeKey K8S_CLUSTER_NAME = stringKey("k8s.cluster.name"); + + /** The name of the Node. */ + public static final AttributeKey K8S_NODE_NAME = stringKey("k8s.node.name"); + + /** The UID of the Node. */ + public static final AttributeKey K8S_NODE_UID = stringKey("k8s.node.uid"); + + /** The name of the namespace that the pod is running in. */ + public static final AttributeKey K8S_NAMESPACE_NAME = stringKey("k8s.namespace.name"); + + /** The UID of the Pod. */ + public static final AttributeKey K8S_POD_UID = stringKey("k8s.pod.uid"); + + /** The name of the Pod. */ + public static final AttributeKey K8S_POD_NAME = stringKey("k8s.pod.name"); + + /** The name of the Container in a Pod template. */ + public static final AttributeKey K8S_CONTAINER_NAME = stringKey("k8s.container.name"); + + /** The UID of the ReplicaSet. */ + public static final AttributeKey K8S_REPLICASET_UID = stringKey("k8s.replicaset.uid"); + + /** The name of the ReplicaSet. */ + public static final AttributeKey K8S_REPLICASET_NAME = stringKey("k8s.replicaset.name"); + + /** The UID of the Deployment. */ + public static final AttributeKey K8S_DEPLOYMENT_UID = stringKey("k8s.deployment.uid"); + + /** The name of the Deployment. */ + public static final AttributeKey K8S_DEPLOYMENT_NAME = stringKey("k8s.deployment.name"); + + /** The UID of the StatefulSet. */ + public static final AttributeKey K8S_STATEFULSET_UID = stringKey("k8s.statefulset.uid"); + + /** The name of the StatefulSet. */ + public static final AttributeKey K8S_STATEFULSET_NAME = stringKey("k8s.statefulset.name"); + + /** The UID of the DaemonSet. */ + public static final AttributeKey K8S_DAEMONSET_UID = stringKey("k8s.daemonset.uid"); + + /** The name of the DaemonSet. */ + public static final AttributeKey K8S_DAEMONSET_NAME = stringKey("k8s.daemonset.name"); + + /** The UID of the Job. */ + public static final AttributeKey K8S_JOB_UID = stringKey("k8s.job.uid"); + + /** The name of the Job. */ + public static final AttributeKey K8S_JOB_NAME = stringKey("k8s.job.name"); + + /** The UID of the CronJob. */ + public static final AttributeKey K8S_CRONJOB_UID = stringKey("k8s.cronjob.uid"); + + /** The name of the CronJob. */ + public static final AttributeKey K8S_CRONJOB_NAME = stringKey("k8s.cronjob.name"); + + /** The operating system type. */ + public static final AttributeKey OS_TYPE = stringKey("os.type"); + + /** + * Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` + * or `lsb_release -a` commands. + */ + public static final AttributeKey OS_DESCRIPTION = stringKey("os.description"); + + /** Process identifier (PID). */ + public static final AttributeKey PROCESS_PID = longKey("process.pid"); + + /** + * The name of the process executable. On Linux based systems, can be set to the `Name` in + * `proc/[pid]/status`. On Windows, can be set to the base name of `GetProcessImageFileNameW`. + */ + public static final AttributeKey PROCESS_EXECUTABLE_NAME = + stringKey("process.executable.name"); + + /** + * The full path to the process executable. On Linux based systems, can be set to the target of + * `proc/[pid]/exe`. On Windows, can be set to the result of `GetProcessImageFileNameW`. + */ + public static final AttributeKey PROCESS_EXECUTABLE_PATH = + stringKey("process.executable.path"); + + /** + * The command used to launch the process (i.e. the command name). On Linux based systems, can be + * set to the zeroth string in `proc/[pid]/cmdline`. On Windows, can be set to the first parameter + * extracted from `GetCommandLineW`. + */ + public static final AttributeKey PROCESS_COMMAND = stringKey("process.command"); + + /** + * The full command used to launch the process as a single string representing the full command. + * On Windows, can be set to the result of `GetCommandLineW`. Do not set this if you have to + * assemble it just for monitoring; use `process.command_args` instead. + */ + public static final AttributeKey PROCESS_COMMAND_LINE = stringKey("process.command_line"); + + /** + * All the command arguments (including the command/executable itself) as received by the process. + * On Linux-based systems (and some other Unixoid systems supporting procfs), can be set according + * to the list of null-delimited strings extracted from `proc/[pid]/cmdline`. For libc-based + * executables, this would be the full argv vector passed to `main`. + */ + public static final AttributeKey> PROCESS_COMMAND_ARGS = + stringArrayKey("process.command_args"); + + /** The username of the user that owns the process. */ + public static final AttributeKey PROCESS_OWNER = stringKey("process.owner"); + + /** + * The name of the runtime of this process. For compiled native binaries, this SHOULD be the name + * of the compiler. + */ + public static final AttributeKey PROCESS_RUNTIME_NAME = stringKey("process.runtime.name"); + + /** + * The version of the runtime of this process, as returned by the runtime without modification. + */ + public static final AttributeKey PROCESS_RUNTIME_VERSION = + stringKey("process.runtime.version"); + + /** + * An additional description about the runtime of the process, for example a specific vendor + * customization of the runtime environment. + */ + public static final AttributeKey PROCESS_RUNTIME_DESCRIPTION = + stringKey("process.runtime.description"); + + /** + * Logical name of the service. + * + *

    Note: MUST be the same for all instances of horizontally scaled services. If the value was + * not specified, SDKs MUST fallback to `unknown_service:` concatenated with + * [`process.executable.name`](process.md#process), e.g. `unknown_service:bash`. If + * `process.executable.name` is not available, the value MUST be set to `unknown_service`. + */ + public static final AttributeKey SERVICE_NAME = stringKey("service.name"); + + /** + * A namespace for `service.name`. + * + *

    Note: A string value having a meaning that helps to distinguish a group of services, for + * example the team name that owns a group of services. `service.name` is expected to be unique + * within the same namespace. If `service.namespace` is not specified in the Resource then + * `service.name` is expected to be unique for all services that have no explicit namespace + * defined (so the empty/unspecified namespace is simply one more valid namespace). Zero-length + * namespace string is assumed equal to unspecified namespace. + */ + public static final AttributeKey SERVICE_NAMESPACE = stringKey("service.namespace"); + + /** + * The string ID of the service instance. + * + *

    Note: MUST be unique for each instance of the same `service.namespace,service.name` pair (in + * other words `service.namespace,service.name,service.instance.id` triplet MUST be globally + * unique). The ID helps to distinguish instances of the same service that exist at the same time + * (e.g. instances of a horizontally scaled service). It is preferable for the ID to be persistent + * and stay the same for the lifetime of the service instance, however it is acceptable that the + * ID is ephemeral and changes during important lifetime events for the service (e.g. service + * restarts). If the service has no inherent unique ID that can be used as the value of this + * attribute it is recommended to generate a random Version 1 or Version 4 RFC 4122 UUID (services + * aiming for reproducible UUIDs may also use Version 5, see RFC 4122 for more recommendations). + */ + public static final AttributeKey SERVICE_INSTANCE_ID = stringKey("service.instance.id"); + + /** The version string of the service API or implementation. */ + public static final AttributeKey SERVICE_VERSION = stringKey("service.version"); + + /** The name of the telemetry SDK as defined above. */ + public static final AttributeKey TELEMETRY_SDK_NAME = stringKey("telemetry.sdk.name"); + + /** The language of the telemetry SDK. */ + public static final AttributeKey TELEMETRY_SDK_LANGUAGE = + stringKey("telemetry.sdk.language"); + + /** The version string of the telemetry SDK. */ + public static final AttributeKey TELEMETRY_SDK_VERSION = + stringKey("telemetry.sdk.version"); + + /** The version string of the auto instrumentation agent, if used. */ + public static final AttributeKey TELEMETRY_AUTO_VERSION = + stringKey("telemetry.auto.version"); + + /** The name of the web engine. */ + public static final AttributeKey WEBENGINE_NAME = stringKey("webengine.name"); + + /** The version of the web engine. */ + public static final AttributeKey WEBENGINE_VERSION = stringKey("webengine.version"); + + /** Additional description of the web engine (e.g. detailed version and edition information). */ + public static final AttributeKey WEBENGINE_DESCRIPTION = + stringKey("webengine.description"); + + // Enum definitions + public static final class CloudProviderValues { + /** Amazon Web Services. */ + public static final String AWS = "aws"; + /** Microsoft Azure. */ + public static final String AZURE = "azure"; + /** Google Cloud Platform. */ + public static final String GCP = "gcp"; + + private CloudProviderValues() {} + } + + public static final class CloudPlatformValues { + /** AWS Elastic Compute Cloud. */ + public static final String AWS_EC2 = "aws_ec2"; + /** AWS Elastic Container Service. */ + public static final String AWS_ECS = "aws_ecs"; + /** AWS Elastic Kubernetes Service. */ + public static final String AWS_EKS = "aws_eks"; + /** AWS Lambda. */ + public static final String AWS_LAMBDA = "aws_lambda"; + /** AWS Elastic Beanstalk. */ + public static final String AWS_ELASTIC_BEANSTALK = "aws_elastic_beanstalk"; + /** Azure Virtual Machines. */ + public static final String AZURE_VM = "azure_vm"; + /** Azure Container Instances. */ + public static final String AZURE_CONTAINER_INSTANCES = "azure_container_instances"; + /** Azure Kubernetes Service. */ + public static final String AZURE_AKS = "azure_aks"; + /** Azure Functions. */ + public static final String AZURE_FUNCTIONS = "azure_functions"; + /** Azure App Service. */ + public static final String AZURE_APP_SERVICE = "azure_app_service"; + /** Google Cloud Compute Engine (GCE). */ + public static final String GCP_COMPUTE_ENGINE = "gcp_compute_engine"; + /** Google Cloud Run. */ + public static final String GCP_CLOUD_RUN = "gcp_cloud_run"; + /** Google Cloud Kubernetes Engine (GKE). */ + public static final String GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine"; + /** Google Cloud Functions (GCF). */ + public static final String GCP_CLOUD_FUNCTIONS = "gcp_cloud_functions"; + /** Google Cloud App Engine (GAE). */ + public static final String GCP_APP_ENGINE = "gcp_app_engine"; + + private CloudPlatformValues() {} + } + + public static final class AwsEcsLaunchtypeValues { + /** ec2. */ + public static final String EC2 = "ec2"; + /** fargate. */ + public static final String FARGATE = "fargate"; + + private AwsEcsLaunchtypeValues() {} + } + + public static final class HostArchValues { + /** AMD64. */ + public static final String AMD64 = "amd64"; + /** ARM32. */ + public static final String ARM32 = "arm32"; + /** ARM64. */ + public static final String ARM64 = "arm64"; + /** Itanium. */ + public static final String IA64 = "ia64"; + /** 32-bit PowerPC. */ + public static final String PPC32 = "ppc32"; + /** 64-bit PowerPC. */ + public static final String PPC64 = "ppc64"; + /** 32-bit x86. */ + public static final String X86 = "x86"; + + private HostArchValues() {} + } + + public static final class OsTypeValues { + /** Microsoft Windows. */ + public static final String WINDOWS = "windows"; + /** Linux. */ + public static final String LINUX = "linux"; + /** Apple Darwin. */ + public static final String DARWIN = "darwin"; + /** FreeBSD. */ + public static final String FREEBSD = "freebsd"; + /** NetBSD. */ + public static final String NETBSD = "netbsd"; + /** OpenBSD. */ + public static final String OPENBSD = "openbsd"; + /** DragonFly BSD. */ + public static final String DRAGONFLYBSD = "dragonflybsd"; + /** HP-UX (Hewlett Packard Unix). */ + public static final String HPUX = "hpux"; + /** AIX (Advanced Interactive eXecutive). */ + public static final String AIX = "aix"; + /** Oracle Solaris. */ + public static final String SOLARIS = "solaris"; + /** IBM z/OS. */ + public static final String Z_OS = "z_os"; + + private OsTypeValues() {} + } + + public static final class TelemetrySdkLanguageValues { + /** cpp. */ + public static final String CPP = "cpp"; + /** dotnet. */ + public static final String DOTNET = "dotnet"; + /** erlang. */ + public static final String ERLANG = "erlang"; + /** go. */ + public static final String GO = "go"; + /** java. */ + public static final String JAVA = "java"; + /** nodejs. */ + public static final String NODEJS = "nodejs"; + /** php. */ + public static final String PHP = "php"; + /** python. */ + public static final String PYTHON = "python"; + /** ruby. */ + public static final String RUBY = "ruby"; + /** webjs. */ + public static final String WEBJS = "webjs"; + + private TelemetrySdkLanguageValues() {} + } + + private ResourceAttributes() {} +} diff --git a/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/resource/attributes/package-info.java b/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/resource/attributes/package-info.java new file mode 100644 index 000000000..d893dd794 --- /dev/null +++ b/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/resource/attributes/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * OpenTelemetry semantic attributes for resources. + * + * @see io.opentelemetry.semconv.resource.attributes.ResourceAttributes + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.semconv.resource.attributes; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/trace/attributes/SemanticAttributes.java b/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/trace/attributes/SemanticAttributes.java new file mode 100644 index 000000000..c844ec3ee --- /dev/null +++ b/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/trace/attributes/SemanticAttributes.java @@ -0,0 +1,902 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.semconv.trace.attributes; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import java.util.List; + +// DO NOT EDIT, this is an Auto-generated file from +// buildscripts/semantic-convention/templates/SemanticAttributes.java.j2 +public final class SemanticAttributes { + + /** + * An identifier for the database management system (DBMS) product being used. See below for a + * list of well-known identifiers. + */ + public static final AttributeKey DB_SYSTEM = stringKey("db.system"); + + /** + * The connection string used to connect to the database. It is recommended to remove embedded + * credentials. + */ + public static final AttributeKey DB_CONNECTION_STRING = stringKey("db.connection_string"); + + /** Username for accessing the database. */ + public static final AttributeKey DB_USER = stringKey("db.user"); + + /** + * The fully-qualified class name of the [Java Database Connectivity + * (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. + */ + public static final AttributeKey DB_JDBC_DRIVER_CLASSNAME = + stringKey("db.jdbc.driver_classname"); + + /** + * If no [tech-specific attribute](#call-level-attributes-for-specific-technologies) is defined, + * this attribute is used to report the name of the database being accessed. For commands that + * switch the database, this should be set to the target database (even if the command fails). + * + *

    Note: In some SQL databases, the database name to be used is called "schema name". + */ + public static final AttributeKey DB_NAME = stringKey("db.name"); + + /** + * The database statement being executed. + * + *

    Note: The value may be sanitized to exclude sensitive information. + */ + public static final AttributeKey DB_STATEMENT = stringKey("db.statement"); + + /** + * The name of the operation being executed, e.g. the [MongoDB command + * name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as + * `findAndModify`, or the SQL keyword. + * + *

    Note: When setting this to an SQL keyword, it is not recommended to attempt any client-side + * parsing of `db.statement` just to get this property, but it should be set if the operation name + * is provided by the library being instrumented. If the SQL statement has an ambiguous operation, + * or performs more than one operation, this value may be omitted. + */ + public static final AttributeKey DB_OPERATION = stringKey("db.operation"); + + /** + * The Microsoft SQL Server [instance + * name](https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) + * connecting to. This name is used to determine the port of a named instance. + * + *

    Note: If setting a `db.mssql.instance_name`, `net.peer.port` is no longer required (but + * still recommended if non-standard). + */ + public static final AttributeKey DB_MSSQL_INSTANCE_NAME = + stringKey("db.mssql.instance_name"); + + /** + * The name of the keyspace being accessed. To be used instead of the generic `db.name` attribute. + */ + public static final AttributeKey DB_CASSANDRA_KEYSPACE = + stringKey("db.cassandra.keyspace"); + + /** The fetch size used for paging, i.e. how many rows will be returned at once. */ + public static final AttributeKey DB_CASSANDRA_PAGE_SIZE = longKey("db.cassandra.page_size"); + + /** + * The consistency level of the query. Based on consistency values from + * [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). + */ + public static final AttributeKey DB_CASSANDRA_CONSISTENCY_LEVEL = + stringKey("db.cassandra.consistency_level"); + + /** + * The name of the primary table that the operation is acting upon, including the schema name (if + * applicable). + * + *

    Note: This mirrors the db.sql.table attribute but references cassandra rather than sql. It + * is not recommended to attempt any client-side parsing of `db.statement` just to get this + * property, but it should be set if it is provided by the library being instrumented. If the + * operation is acting upon an anonymous table, or more than one table, this value MUST NOT be + * set. + */ + public static final AttributeKey DB_CASSANDRA_TABLE = stringKey("db.cassandra.table"); + + /** Whether or not the query is idempotent. */ + public static final AttributeKey DB_CASSANDRA_IDEMPOTENCE = + booleanKey("db.cassandra.idempotence"); + + /** + * The number of times a query was speculatively executed. Not set or `0` if the query was not + * executed speculatively. + */ + public static final AttributeKey DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT = + longKey("db.cassandra.speculative_execution_count"); + + /** The ID of the coordinating node for a query. */ + public static final AttributeKey DB_CASSANDRA_COORDINATOR_ID = + stringKey("db.cassandra.coordinator.id"); + + /** The data center of the coordinating node for a query. */ + public static final AttributeKey DB_CASSANDRA_COORDINATOR_DC = + stringKey("db.cassandra.coordinator.dc"); + + /** + * The [HBase namespace](https://hbase.apache.org/book.html#_namespace) being accessed. To be used + * instead of the generic `db.name` attribute. + */ + public static final AttributeKey DB_HBASE_NAMESPACE = stringKey("db.hbase.namespace"); + + /** + * The index of the database being accessed as used in the [`SELECT` + * command](https://redis.io/commands/select), provided as an integer. To be used instead of the + * generic `db.name` attribute. + */ + public static final AttributeKey DB_REDIS_DATABASE_INDEX = + longKey("db.redis.database_index"); + + /** The collection being accessed within the database stated in `db.name`. */ + public static final AttributeKey DB_MONGODB_COLLECTION = + stringKey("db.mongodb.collection"); + + /** + * The name of the primary table that the operation is acting upon, including the schema name (if + * applicable). + * + *

    Note: It is not recommended to attempt any client-side parsing of `db.statement` just to get + * this property, but it should be set if it is provided by the library being instrumented. If the + * operation is acting upon an anonymous table, or more than one table, this value MUST NOT be + * set. + */ + public static final AttributeKey DB_SQL_TABLE = stringKey("db.sql.table"); + + /** + * The type of the exception (its fully-qualified class name, if applicable). The dynamic type of + * the exception should be preferred over the static type in languages that support it. + */ + public static final AttributeKey EXCEPTION_TYPE = stringKey("exception.type"); + + /** The exception message. */ + public static final AttributeKey EXCEPTION_MESSAGE = stringKey("exception.message"); + + /** + * A stacktrace as a string in the natural representation for the language runtime. The + * representation is to be determined and documented by each language SIG. + */ + public static final AttributeKey EXCEPTION_STACKTRACE = stringKey("exception.stacktrace"); + + /** + * SHOULD be set to true if the exception event is recorded at a point where it is known that the + * exception is escaping the scope of the span. + * + *

    Note: An exception is considered to have escaped (or left) the scope of a span, if that span + * is ended while the exception is still logically "in flight". This may be actually + * "in flight" in some languages (e.g. if the exception is passed to a Context + * manager's `__exit__` method in Python) but will usually be caught at the point of recording + * the exception in most languages. + * + *

    It is usually not possible to determine at the point where an exception is thrown whether it + * will escape the scope of a span. However, it is trivial to know that an exception will escape, + * if one checks for an active exception just before ending the span, as done in the [example + * above](#exception-end-example). + * + *

    It follows that an exception may still escape the scope of the span even if the + * `exception.escaped` attribute was not set or set to false, since the event might have been + * recorded at a time where it was not clear whether the exception will escape. + */ + public static final AttributeKey EXCEPTION_ESCAPED = booleanKey("exception.escaped"); + + /** Type of the trigger on which the function is executed. */ + public static final AttributeKey FAAS_TRIGGER = stringKey("faas.trigger"); + + /** The execution ID of the current function execution. */ + public static final AttributeKey FAAS_EXECUTION = stringKey("faas.execution"); + + /** + * The name of the source on which the triggering operation was performed. For example, in Cloud + * Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. + */ + public static final AttributeKey FAAS_DOCUMENT_COLLECTION = + stringKey("faas.document.collection"); + + /** Describes the type of the operation that was performed on the data. */ + public static final AttributeKey FAAS_DOCUMENT_OPERATION = + stringKey("faas.document.operation"); + + /** + * A string containing the time when the data was accessed in the [ISO + * 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in + * [UTC](https://www.w3.org/TR/NOTE-datetime). + */ + public static final AttributeKey FAAS_DOCUMENT_TIME = stringKey("faas.document.time"); + + /** + * The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the + * name of the file, and in Cosmos DB the table name. + */ + public static final AttributeKey FAAS_DOCUMENT_NAME = stringKey("faas.document.name"); + + /** + * A string containing the function invocation time in the [ISO + * 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in + * [UTC](https://www.w3.org/TR/NOTE-datetime). + */ + public static final AttributeKey FAAS_TIME = stringKey("faas.time"); + + /** + * A string containing the schedule period as [Cron + * Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). + */ + public static final AttributeKey FAAS_CRON = stringKey("faas.cron"); + + /** + * A boolean that is true if the serverless function is executed for the first time (aka + * cold-start). + */ + public static final AttributeKey FAAS_COLDSTART = booleanKey("faas.coldstart"); + + /** + * The name of the invoked function. + * + *

    Note: SHOULD be equal to the `faas.name` resource attribute of the invoked function. + */ + public static final AttributeKey FAAS_INVOKED_NAME = stringKey("faas.invoked_name"); + + /** + * The cloud provider of the invoked function. + * + *

    Note: SHOULD be equal to the `cloud.provider` resource attribute of the invoked function. + */ + public static final AttributeKey FAAS_INVOKED_PROVIDER = + stringKey("faas.invoked_provider"); + + /** + * The cloud region of the invoked function. + * + *

    Note: SHOULD be equal to the `cloud.region` resource attribute of the invoked function. + */ + public static final AttributeKey FAAS_INVOKED_REGION = stringKey("faas.invoked_region"); + + /** Transport protocol used. See note below. */ + public static final AttributeKey NET_TRANSPORT = stringKey("net.transport"); + + /** + * Remote address of the peer (dotted decimal for IPv4 or + * [RFC5952](https://tools.ietf.org/html/rfc5952) for IPv6). + */ + public static final AttributeKey NET_PEER_IP = stringKey("net.peer.ip"); + + /** Remote port number. */ + public static final AttributeKey NET_PEER_PORT = longKey("net.peer.port"); + + /** Remote hostname or similar, see note below. */ + public static final AttributeKey NET_PEER_NAME = stringKey("net.peer.name"); + + /** Like `net.peer.ip` but for the host IP. Useful in case of a multi-IP host. */ + public static final AttributeKey NET_HOST_IP = stringKey("net.host.ip"); + + /** Like `net.peer.port` but for the host port. */ + public static final AttributeKey NET_HOST_PORT = longKey("net.host.port"); + + /** Local hostname or similar, see note below. */ + public static final AttributeKey NET_HOST_NAME = stringKey("net.host.name"); + + /** + * The [`service.name`](../../resource/semantic_conventions/README.md#service) of the remote + * service. SHOULD be equal to the actual `service.name` resource attribute of the remote service + * if any. + */ + public static final AttributeKey PEER_SERVICE = stringKey("peer.service"); + + /** + * Username or client_id extracted from the access token or + * [Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) header in the inbound request + * from outside the system. + */ + public static final AttributeKey ENDUSER_ID = stringKey("enduser.id"); + + /** + * Actual/assumed role the client is making the request under extracted from token or application + * security context. + */ + public static final AttributeKey ENDUSER_ROLE = stringKey("enduser.role"); + + /** + * Scopes or granted authorities the client currently possesses extracted from token or + * application security context. The value would come from the scope associated with an [OAuth 2.0 + * Access Token](https://tools.ietf.org/html/rfc6749#section-3.3) or an attribute value in a [SAML + * 2.0 + * Assertion](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html). + */ + public static final AttributeKey ENDUSER_SCOPE = stringKey("enduser.scope"); + + /** Current "managed" thread ID (as opposed to OS thread ID). */ + public static final AttributeKey THREAD_ID = longKey("thread.id"); + + /** Current thread name. */ + public static final AttributeKey THREAD_NAME = stringKey("thread.name"); + + /** + * The method or function name, or equivalent (usually rightmost part of the code unit's + * name). + */ + public static final AttributeKey CODE_FUNCTION = stringKey("code.function"); + + /** + * The "namespace" within which `code.function` is defined. Usually the qualified class or + * module name, such that `code.namespace` + some separator + `code.function` form a unique + * identifier for the code unit. + */ + public static final AttributeKey CODE_NAMESPACE = stringKey("code.namespace"); + + /** + * The source code file name that identifies the code unit as uniquely as possible (preferably an + * absolute file path). + */ + public static final AttributeKey CODE_FILEPATH = stringKey("code.filepath"); + + /** + * The line number in `code.filepath` best representing the operation. It SHOULD point within the + * code unit named in `code.function`. + */ + public static final AttributeKey CODE_LINENO = longKey("code.lineno"); + + /** HTTP request method. */ + public static final AttributeKey HTTP_METHOD = stringKey("http.method"); + + /** + * Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]`. Usually the + * fragment is not transmitted over HTTP, but if it is known, it should be included nevertheless. + * + *

    Note: `http.url` MUST NOT contain credentials passed via URL in form of + * `https://username:password@www.example.com/`. In such case the attribute's value should be + * `https://www.example.com/`. + */ + public static final AttributeKey HTTP_URL = stringKey("http.url"); + + /** The full request target as passed in a HTTP request line or equivalent. */ + public static final AttributeKey HTTP_TARGET = stringKey("http.target"); + + /** + * The value of the [HTTP host header](https://tools.ietf.org/html/rfc7230#section-5.4). When the + * header is empty or not present, this attribute should be the same. + */ + public static final AttributeKey HTTP_HOST = stringKey("http.host"); + + /** The URI scheme identifying the used protocol. */ + public static final AttributeKey HTTP_SCHEME = stringKey("http.scheme"); + + /** [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). */ + public static final AttributeKey HTTP_STATUS_CODE = longKey("http.status_code"); + + /** + * Kind of HTTP protocol used. + * + *

    Note: If `net.transport` is not specified, it can be assumed to be `IP.TCP` except if + * `http.flavor` is `QUIC`, in which case `IP.UDP` is assumed. + */ + public static final AttributeKey HTTP_FLAVOR = stringKey("http.flavor"); + + /** + * Value of the [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3) header sent + * by the client. + */ + public static final AttributeKey HTTP_USER_AGENT = stringKey("http.user_agent"); + + /** + * The size of the request payload body in bytes. This is the number of bytes transferred + * excluding headers and is often, but not always, present as the + * [Content-Length](https://tools.ietf.org/html/rfc7230#section-3.3.2) header. For requests using + * transport encoding, this should be the compressed size. + */ + public static final AttributeKey HTTP_REQUEST_CONTENT_LENGTH = + longKey("http.request_content_length"); + + /** + * The size of the uncompressed request payload body after transport decoding. Not set if + * transport encoding not used. + */ + public static final AttributeKey HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED = + longKey("http.request_content_length_uncompressed"); + + /** + * The size of the response payload body in bytes. This is the number of bytes transferred + * excluding headers and is often, but not always, present as the + * [Content-Length](https://tools.ietf.org/html/rfc7230#section-3.3.2) header. For requests using + * transport encoding, this should be the compressed size. + */ + public static final AttributeKey HTTP_RESPONSE_CONTENT_LENGTH = + longKey("http.response_content_length"); + + /** + * The size of the uncompressed response payload body after transport decoding. Not set if + * transport encoding not used. + */ + public static final AttributeKey HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED = + longKey("http.response_content_length_uncompressed"); + + /** + * The primary server name of the matched virtual host. This should be obtained via configuration. + * If no such configuration can be obtained, this attribute MUST NOT be set ( `net.host.name` + * should be used instead). + * + *

    Note: `http.url` is usually not readily available on the server side but would have to be + * assembled in a cumbersome and sometimes lossy process from other information (see e.g. + * open-telemetry/opentelemetry-python/pull/148). It is thus preferred to supply the raw data that + * is available. + */ + public static final AttributeKey HTTP_SERVER_NAME = stringKey("http.server_name"); + + /** The matched route (path template). */ + public static final AttributeKey HTTP_ROUTE = stringKey("http.route"); + + /** + * The IP address of the original client behind all proxies, if known (e.g. from + * [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)). + * + *

    Note: This is not necessarily the same as `net.peer.ip`, which would identify the + * network-level peer, which may be a proxy. + */ + public static final AttributeKey HTTP_CLIENT_IP = stringKey("http.client_ip"); + + /** The keys in the `RequestItems` object field. */ + public static final AttributeKey> AWS_DYNAMODB_TABLE_NAMES = + stringArrayKey("aws.dynamodb.table_names"); + + /** The JSON-serialized value of each item in the `ConsumedCapacity` response field. */ + public static final AttributeKey> AWS_DYNAMODB_CONSUMED_CAPACITY = + stringArrayKey("aws.dynamodb.consumed_capacity"); + + /** The JSON-serialized value of the `ItemCollectionMetrics` response field. */ + public static final AttributeKey AWS_DYNAMODB_ITEM_COLLECTION_METRICS = + stringKey("aws.dynamodb.item_collection_metrics"); + + /** The value of the `ProvisionedThroughput.ReadCapacityUnits` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_PROVISIONED_READ_CAPACITY = + doubleKey("aws.dynamodb.provisioned_read_capacity"); + + /** The value of the `ProvisionedThroughput.WriteCapacityUnits` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY = + doubleKey("aws.dynamodb.provisioned_write_capacity"); + + /** The value of the `ConsistentRead` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_CONSISTENT_READ = + booleanKey("aws.dynamodb.consistent_read"); + + /** The value of the `ProjectionExpression` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_PROJECTION = + stringKey("aws.dynamodb.projection"); + + /** The value of the `Limit` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_LIMIT = longKey("aws.dynamodb.limit"); + + /** The value of the `AttributesToGet` request parameter. */ + public static final AttributeKey> AWS_DYNAMODB_ATTRIBUTES_TO_GET = + stringArrayKey("aws.dynamodb.attributes_to_get"); + + /** The value of the `IndexName` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_INDEX_NAME = + stringKey("aws.dynamodb.index_name"); + + /** The value of the `Select` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_SELECT = stringKey("aws.dynamodb.select"); + + /** The JSON-serialized value of each item of the `GlobalSecondaryIndexes` request field. */ + public static final AttributeKey> AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES = + stringArrayKey("aws.dynamodb.global_secondary_indexes"); + + /** The JSON-serialized value of each item of the `LocalSecondaryIndexes` request field. */ + public static final AttributeKey> AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES = + stringArrayKey("aws.dynamodb.local_secondary_indexes"); + + /** The value of the `ExclusiveStartTableName` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_EXCLUSIVE_START_TABLE = + stringKey("aws.dynamodb.exclusive_start_table"); + + /** The the number of items in the `TableNames` response parameter. */ + public static final AttributeKey AWS_DYNAMODB_TABLE_COUNT = + longKey("aws.dynamodb.table_count"); + + /** The value of the `ScanIndexForward` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_SCAN_FORWARD = + booleanKey("aws.dynamodb.scan_forward"); + + /** The value of the `Segment` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_SEGMENT = longKey("aws.dynamodb.segment"); + + /** The value of the `TotalSegments` request parameter. */ + public static final AttributeKey AWS_DYNAMODB_TOTAL_SEGMENTS = + longKey("aws.dynamodb.total_segments"); + + /** The value of the `Count` response parameter. */ + public static final AttributeKey AWS_DYNAMODB_COUNT = longKey("aws.dynamodb.count"); + + /** The value of the `ScannedCount` response parameter. */ + public static final AttributeKey AWS_DYNAMODB_SCANNED_COUNT = + longKey("aws.dynamodb.scanned_count"); + + /** The JSON-serialized value of each item in the `AttributeDefinitions` request field. */ + public static final AttributeKey> AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS = + stringArrayKey("aws.dynamodb.attribute_definitions"); + + /** + * The JSON-serialized value of each item in the the `GlobalSecondaryIndexUpdates` request field. + */ + public static final AttributeKey> AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES = + stringArrayKey("aws.dynamodb.global_secondary_index_updates"); + + /** A string identifying the messaging system. */ + public static final AttributeKey MESSAGING_SYSTEM = stringKey("messaging.system"); + + /** + * The message destination name. This might be equal to the span name but is required + * nevertheless. + */ + public static final AttributeKey MESSAGING_DESTINATION = + stringKey("messaging.destination"); + + /** The kind of message destination. */ + public static final AttributeKey MESSAGING_DESTINATION_KIND = + stringKey("messaging.destination_kind"); + + /** A boolean that is true if the message destination is temporary. */ + public static final AttributeKey MESSAGING_TEMP_DESTINATION = + booleanKey("messaging.temp_destination"); + + /** The name of the transport protocol. */ + public static final AttributeKey MESSAGING_PROTOCOL = stringKey("messaging.protocol"); + + /** The version of the transport protocol. */ + public static final AttributeKey MESSAGING_PROTOCOL_VERSION = + stringKey("messaging.protocol_version"); + + /** Connection string. */ + public static final AttributeKey MESSAGING_URL = stringKey("messaging.url"); + + /** + * A value used by the messaging system as an identifier for the message, represented as a string. + */ + public static final AttributeKey MESSAGING_MESSAGE_ID = stringKey("messaging.message_id"); + + /** + * The [conversation ID](#conversations) identifying the conversation to which the message + * belongs, represented as a string. Sometimes called "Correlation ID". + */ + public static final AttributeKey MESSAGING_CONVERSATION_ID = + stringKey("messaging.conversation_id"); + + /** + * The (uncompressed) size of the message payload in bytes. Also use this attribute if it is + * unknown whether the compressed or uncompressed payload size is reported. + */ + public static final AttributeKey MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES = + longKey("messaging.message_payload_size_bytes"); + + /** The compressed size of the message payload in bytes. */ + public static final AttributeKey MESSAGING_MESSAGE_PAYLOAD_COMPRESSED_SIZE_BYTES = + longKey("messaging.message_payload_compressed_size_bytes"); + + /** + * A string identifying the kind of message consumption as defined in the [Operation + * names](#operation-names) section above. If the operation is "send", this attribute MUST + * NOT be set, since the operation can be inferred from the span kind in that case. + */ + public static final AttributeKey MESSAGING_OPERATION = stringKey("messaging.operation"); + + /** RabbitMQ message routing key. */ + public static final AttributeKey MESSAGING_RABBITMQ_ROUTING_KEY = + stringKey("messaging.rabbitmq.routing_key"); + + /** + * Message keys in Kafka are used for grouping alike messages to ensure they're processed on + * the same partition. They differ from `messaging.message_id` in that they're not unique. If + * the key is `null`, the attribute MUST NOT be set. + * + *

    Note: If the key type is not string, it's string representation has to be supplied for + * the attribute. If the key has no unambiguous, canonical string form, don't include its + * value. + */ + public static final AttributeKey MESSAGING_KAFKA_MESSAGE_KEY = + stringKey("messaging.kafka.message_key"); + + /** + * Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not + * producers. + */ + public static final AttributeKey MESSAGING_KAFKA_CONSUMER_GROUP = + stringKey("messaging.kafka.consumer_group"); + + /** Client Id for the Consumer or Producer that is handling the message. */ + public static final AttributeKey MESSAGING_KAFKA_CLIENT_ID = + stringKey("messaging.kafka.client_id"); + + /** Partition the message is sent to. */ + public static final AttributeKey MESSAGING_KAFKA_PARTITION = + longKey("messaging.kafka.partition"); + + /** A boolean that is true if the message is a tombstone. */ + public static final AttributeKey MESSAGING_KAFKA_TOMBSTONE = + booleanKey("messaging.kafka.tombstone"); + + /** A string identifying the remoting system. */ + public static final AttributeKey RPC_SYSTEM = stringKey("rpc.system"); + + /** The full name of the service being called, including its package name, if applicable. */ + public static final AttributeKey RPC_SERVICE = stringKey("rpc.service"); + + /** The name of the method being called, must be equal to the $method part in the span name. */ + public static final AttributeKey RPC_METHOD = stringKey("rpc.method"); + + /** + * The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the + * gRPC request. + */ + public static final AttributeKey RPC_GRPC_STATUS_CODE = longKey("rpc.grpc.status_code"); + + // Enum definitions + public static final class DbSystemValues { + /** Some other SQL database. Fallback only. See notes. */ + public static final String OTHER_SQL = "other_sql"; + /** Microsoft SQL Server. */ + public static final String MSSQL = "mssql"; + /** MySQL. */ + public static final String MYSQL = "mysql"; + /** Oracle Database. */ + public static final String ORACLE = "oracle"; + /** IBM Db2. */ + public static final String DB2 = "db2"; + /** PostgreSQL. */ + public static final String POSTGRESQL = "postgresql"; + /** Amazon Redshift. */ + public static final String REDSHIFT = "redshift"; + /** Apache Hive. */ + public static final String HIVE = "hive"; + /** Cloudscape. */ + public static final String CLOUDSCAPE = "cloudscape"; + /** HyperSQL DataBase. */ + public static final String HSQLDB = "hsqldb"; + /** Progress Database. */ + public static final String PROGRESS = "progress"; + /** SAP MaxDB. */ + public static final String MAXDB = "maxdb"; + /** SAP HANA. */ + public static final String HANADB = "hanadb"; + /** Ingres. */ + public static final String INGRES = "ingres"; + /** FirstSQL. */ + public static final String FIRSTSQL = "firstsql"; + /** EnterpriseDB. */ + public static final String EDB = "edb"; + /** InterSystems Caché. */ + public static final String CACHE = "cache"; + /** Adabas (Adaptable Database System). */ + public static final String ADABAS = "adabas"; + /** Firebird. */ + public static final String FIREBIRD = "firebird"; + /** Apache Derby. */ + public static final String DERBY = "derby"; + /** FileMaker. */ + public static final String FILEMAKER = "filemaker"; + /** Informix. */ + public static final String INFORMIX = "informix"; + /** InstantDB. */ + public static final String INSTANTDB = "instantdb"; + /** InterBase. */ + public static final String INTERBASE = "interbase"; + /** MariaDB. */ + public static final String MARIADB = "mariadb"; + /** Netezza. */ + public static final String NETEZZA = "netezza"; + /** Pervasive PSQL. */ + public static final String PERVASIVE = "pervasive"; + /** PointBase. */ + public static final String POINTBASE = "pointbase"; + /** SQLite. */ + public static final String SQLITE = "sqlite"; + /** Sybase. */ + public static final String SYBASE = "sybase"; + /** Teradata. */ + public static final String TERADATA = "teradata"; + /** Vertica. */ + public static final String VERTICA = "vertica"; + /** H2. */ + public static final String H2 = "h2"; + /** ColdFusion IMQ. */ + public static final String COLDFUSION = "coldfusion"; + /** Apache Cassandra. */ + public static final String CASSANDRA = "cassandra"; + /** Apache HBase. */ + public static final String HBASE = "hbase"; + /** MongoDB. */ + public static final String MONGODB = "mongodb"; + /** Redis. */ + public static final String REDIS = "redis"; + /** Couchbase. */ + public static final String COUCHBASE = "couchbase"; + /** CouchDB. */ + public static final String COUCHDB = "couchdb"; + /** Microsoft Azure Cosmos DB. */ + public static final String COSMOSDB = "cosmosdb"; + /** Amazon DynamoDB. */ + public static final String DYNAMODB = "dynamodb"; + /** Neo4j. */ + public static final String NEO4J = "neo4j"; + /** Apache Geode. */ + public static final String GEODE = "geode"; + /** Elasticsearch. */ + public static final String ELASTICSEARCH = "elasticsearch"; + + private DbSystemValues() {} + } + + public static final class DbCassandraConsistencyLevelValues { + /** all. */ + public static final String ALL = "all"; + /** each_quorum. */ + public static final String EACH_QUORUM = "each_quorum"; + /** quorum. */ + public static final String QUORUM = "quorum"; + /** local_quorum. */ + public static final String LOCAL_QUORUM = "local_quorum"; + /** one. */ + public static final String ONE = "one"; + /** two. */ + public static final String TWO = "two"; + /** three. */ + public static final String THREE = "three"; + /** local_one. */ + public static final String LOCAL_ONE = "local_one"; + /** any. */ + public static final String ANY = "any"; + /** serial. */ + public static final String SERIAL = "serial"; + /** local_serial. */ + public static final String LOCAL_SERIAL = "local_serial"; + + private DbCassandraConsistencyLevelValues() {} + } + + public static final class FaasTriggerValues { + /** A response to some data source operation such as a database or filesystem read/write. */ + public static final String DATASOURCE = "datasource"; + /** To provide an answer to an inbound HTTP request. */ + public static final String HTTP = "http"; + /** A function is set to be executed when messages are sent to a messaging system. */ + public static final String PUBSUB = "pubsub"; + /** A function is scheduled to be executed regularly. */ + public static final String TIMER = "timer"; + /** If none of the others apply. */ + public static final String OTHER = "other"; + + private FaasTriggerValues() {} + } + + public static final class FaasDocumentOperationValues { + /** When a new object is created. */ + public static final String INSERT = "insert"; + /** When an object is modified. */ + public static final String EDIT = "edit"; + /** When an object is deleted. */ + public static final String DELETE = "delete"; + + private FaasDocumentOperationValues() {} + } + + public static final class FaasInvokedProviderValues { + /** Amazon Web Services. */ + public static final String AWS = "aws"; + /** Microsoft Azure. */ + public static final String AZURE = "azure"; + /** Google Cloud Platform. */ + public static final String GCP = "gcp"; + + private FaasInvokedProviderValues() {} + } + + public static final class NetTransportValues { + /** ip_tcp. */ + public static final String IP_TCP = "ip_tcp"; + /** ip_udp. */ + public static final String IP_UDP = "ip_udp"; + /** Another IP-based protocol. */ + public static final String IP = "ip"; + /** Unix Domain socket. See below. */ + public static final String UNIX = "unix"; + /** Named or anonymous pipe. See note below. */ + public static final String PIPE = "pipe"; + /** In-process communication. */ + public static final String INPROC = "inproc"; + /** Something else (non IP-based). */ + public static final String OTHER = "other"; + + private NetTransportValues() {} + } + + public static final class HttpFlavorValues { + /** HTTP 1.0. */ + public static final String HTTP_1_0 = "1.0"; + /** HTTP 1.1. */ + public static final String HTTP_1_1 = "1.1"; + /** HTTP 2. */ + public static final String HTTP_2_0 = "2.0"; + /** SPDY protocol. */ + public static final String SPDY = "SPDY"; + /** QUIC protocol. */ + public static final String QUIC = "QUIC"; + + private HttpFlavorValues() {} + } + + public static final class MessagingDestinationKindValues { + /** A message sent to a queue. */ + public static final String QUEUE = "queue"; + /** A message sent to a topic. */ + public static final String TOPIC = "topic"; + + private MessagingDestinationKindValues() {} + } + + public static final class MessagingOperationValues { + /** receive. */ + public static final String RECEIVE = "receive"; + /** process. */ + public static final String PROCESS = "process"; + + private MessagingOperationValues() {} + } + + public static final class RpcGrpcStatusCodeValues { + /** OK. */ + public static final long OK = 0; + /** CANCELLED. */ + public static final long CANCELLED = 1; + /** UNKNOWN. */ + public static final long UNKNOWN = 2; + /** INVALID_ARGUMENT. */ + public static final long INVALID_ARGUMENT = 3; + /** DEADLINE_EXCEEDED. */ + public static final long DEADLINE_EXCEEDED = 4; + /** NOT_FOUND. */ + public static final long NOT_FOUND = 5; + /** ALREADY_EXISTS. */ + public static final long ALREADY_EXISTS = 6; + /** PERMISSION_DENIED. */ + public static final long PERMISSION_DENIED = 7; + /** RESOURCE_EXHAUSTED. */ + public static final long RESOURCE_EXHAUSTED = 8; + /** FAILED_PRECONDITION. */ + public static final long FAILED_PRECONDITION = 9; + /** ABORTED. */ + public static final long ABORTED = 10; + /** OUT_OF_RANGE. */ + public static final long OUT_OF_RANGE = 11; + /** UNIMPLEMENTED. */ + public static final long UNIMPLEMENTED = 12; + /** INTERNAL. */ + public static final long INTERNAL = 13; + /** UNAVAILABLE. */ + public static final long UNAVAILABLE = 14; + /** DATA_LOSS. */ + public static final long DATA_LOSS = 15; + /** UNAUTHENTICATED. */ + public static final long UNAUTHENTICATED = 16; + + private RpcGrpcStatusCodeValues() {} + } + + // Manually defined and not YET in the YAML + /** + * The name of an event describing an exception. + * + *

    Typically an event with that name should not be manually created. Instead {@link + * io.opentelemetry.api.trace.Span#recordException(Throwable)} should be used. + */ + public static final String EXCEPTION_EVENT_NAME = "exception"; + + private SemanticAttributes() {} +} diff --git a/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/trace/attributes/package-info.java b/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/trace/attributes/package-info.java new file mode 100644 index 000000000..15f7b0cb4 --- /dev/null +++ b/opentelemetry-java/semconv/src/main/java/io/opentelemetry/semconv/trace/attributes/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * OpenTelemetry semantic attributes for traces. + * + * @see io.opentelemetry.semconv.trace.attributes.SemanticAttributes + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.semconv.trace.attributes; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/opentelemetry-java/settings.gradle.kts b/opentelemetry-java/settings.gradle.kts new file mode 100644 index 000000000..01a5b5b26 --- /dev/null +++ b/opentelemetry-java/settings.gradle.kts @@ -0,0 +1,95 @@ +pluginManagement { + plugins { + id("com.diffplug.spotless") version "5.12.5" + id("com.github.ben-manes.versions") version "0.39.0" + id("com.github.johnrengelman.shadow") version "7.0.0" + id("com.google.protobuf") version "0.8.16" + id("com.gradle.enterprise") version "3.6" + id("de.marcphilipp.nexus-publish") version "0.4.0" + id("de.undercouch.download") version "4.1.1" + id("io.codearte.nexus-staging") version "0.30.0" + id("io.morethan.jmhreport") version "0.9.0" + id("me.champeau.jmh") version "0.6.5" + id("nebula.release") version "15.3.1" + id("net.ltgt.errorprone") version "2.0.1" + id("net.ltgt.nullaway") version "1.1.0" + id("org.checkerframework") version "0.5.20" + id("org.jetbrains.kotlin.jvm") version "1.5.10" + id("org.unbroken-dome.test-sets") version "4.0.0" + id("ru.vyarus.animalsniffer") version "1.5.3" + id("me.champeau.gradle.japicmp") version "0.2.9" + } +} + +plugins { + id("com.gradle.enterprise") +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + mavenLocal() + } +} + +rootProject.name = "opentelemetry-java" +include(":all") +include(":api:all") +include(":api:metrics") +include(":semconv") +include(":bom") +include(":bom-alpha") +include(":context") +include(":dependencyManagement") +include(":extensions:annotations") +include(":extensions:incubator") +include(":extensions:aws") +include(":extensions:kotlin") +include(":extensions:noop-api") +include(":extensions:trace-propagators") +include(":exporters:jaeger") +include(":exporters:jaeger-thrift") +include(":exporters:logging") +include(":exporters:logging-otlp") +include(":exporters:otlp:all") +include(":exporters:otlp:common") +include(":exporters:otlp:metrics") +include(":exporters:otlp:trace") +include(":exporters:prometheus") +include(":exporters:talos") +include(":exporters:zipkin") +include(":integration-tests") +include(":integration-tests:tracecontext") +include(":opencensus-shim") +include(":opentracing-shim") +include(":perf-harness") +include(":proto") +include(":sdk:all") +include(":sdk:common") +include(":sdk:metrics") +include(":sdk:testing") +include(":sdk:trace") +include(":sdk:trace-shaded-deps") +include(":sdk-extensions:async-processor") +include(":sdk-extensions:autoconfigure") +include(":sdk-extensions:aws") +include(":sdk-extensions:logging") +include(":sdk-extensions:resources") +include(":sdk-extensions:tracing-incubator") +include(":sdk-extensions:jaeger-remote-sampler") +include(":sdk-extensions:jfr-events") +include(":sdk-extensions:zpages") + +val isCI = System.getenv("CI") != null +gradleEnterprise { + buildScan { + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" + + if (isCI) { + publishAlways() + tag("CI") + } + } +} + diff --git a/opentelemetry-java/website_docs/_index.md b/opentelemetry-java/website_docs/_index.md new file mode 100644 index 000000000..3d4d0949e --- /dev/null +++ b/opentelemetry-java/website_docs/_index.md @@ -0,0 +1,89 @@ +--- +title: "Java" +weight: 18 +description: > + + A language-specific implementation of OpenTelemetry in Java. +--- + +OpenTelemetry Java consists of the following repositories: + +- [opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java): + Components for manual instrumentation including API and SDK as well as + extensions, the OpenTracing shim and examples. +- [opentelemetry-java-instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation): + Built on top of opentelemetry-java and provides a Java agent JAR that can be + attached to any Java 8+ application and dynamically injects bytecode to + capture telemetry from a number of popular libraries and frameworks. +- [opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib): + Provides helpful libraries and standalone OpenTelemetry-based utilities that + don't fit the express scope of the OpenTelemetry Java or Java Instrumentation + projects. For example, JMX metric gathering. + +## opentelemetry-java + +| Traces | Metrics | Logs | +| ------ | ------- | ------------ | +| Stable | Alpha | Experimental | + +### Components + +- Tracing API +- Tracing SDK +- Metrics API +- Metrics SDK +- OTLP Exporter +- Jaeger Trace Exporter +- Zipkin Trace Exporter +- Prometheus Metric Exporter +- Context Propagation +- OpenTracing Bridge +- OpenCensus Bridge + +### Releases + +Published releases are available on maven central. We strongly recommend using our BOM to keep the +versions of the various components in sync. + +#### Maven + +```xml + + + + + io.opentelemetry + opentelemetry-bom + 1.2.0 + pom + import + + + + + + io.opentelemetry + opentelemetry-api + + + +``` + +#### Gradle + +```groovy +dependencies { + implementation platform("io.opentelemetry:opentelemetry-bom:1.2.0") + implementation('io.opentelemetry:opentelemetry-api') +} +``` + +#### Other + + - [releases](https://github.com/open-telemetry/opentelemetry-java/releases) + - [maven](https://mvnrepository.com/artifact/io.opentelemetry) + +### Additional Information + +- [Javadoc](https://www.javadoc.io/doc/io.opentelemetry) +- [Example code](https://github.com/open-telemetry/opentelemetry-java/tree/main/examples) diff --git a/opentelemetry-java/website_docs/instrumentation_examples.md b/opentelemetry-java/website_docs/instrumentation_examples.md new file mode 100644 index 000000000..994a39186 --- /dev/null +++ b/opentelemetry-java/website_docs/instrumentation_examples.md @@ -0,0 +1,24 @@ +--- +title: "Instrumentation Examples" +weight: 4 +--- + +Here are Some of the resources for Opentelemetry Instrumentation Examples + +## Community Resources + +### boot-opentelemetry-tempo + +Project demonstrating Complete Observability Stack utilizing [Prometheus](https://prometheus.io/), [Loki](https://grafana.com/oss/loki/) (_For distributed logging_), [Tempo](https://grafana.com/oss/tempo/) (_For Distributed tracing, this basically uses Jaeger Internally_), [Grafana](https://grafana.com/grafana/) for **Java/spring** based applications (_With OpenTelemetry auto / manual Instrumentation_) involving multiple microservices with DB interactions + +Checkout [boot-opentelemetry-tempo](https://github.com/mnadeem/boot-opentelemetry-tempo) and get started + +````bash +mvn clean package docker:build +```` + +````bash +docker-compose up +```` + + diff --git a/opentelemetry-java/website_docs/manual_instrumentation.md b/opentelemetry-java/website_docs/manual_instrumentation.md new file mode 100644 index 000000000..e206ca0d1 --- /dev/null +++ b/opentelemetry-java/website_docs/manual_instrumentation.md @@ -0,0 +1,501 @@ +--- +Title: "Manual Instrumentation" +Weight: 3 +--- + + + + +- [Set up](#set-up) +- [Tracing](#tracing) + * [Create basic Span](#create-a-basic-span) + * [Create nested Spans](#create-nested-spans) + * [Span Attributes](#span-attributes) + * [Create Spans with events](#create-spans-with-events) + * [Create Spans with links](#create-spans-with-links) + * [Context Propagation](#context-propagation) +- [Metrics](#metrics-alpha-only) +- [Tracing SDK Configuration](#tracing-sdk-configuration) + * [Sampler](#sampler) + * [Span Processor](#span-processor) + * [Exporter](#exporter) +- [Auto Configuration](#auto-configuration) +- [Logging And Error Handling](#logging-and-error-handling) + * [Examples](#examples) + + +**Libraries** that want to export telemetry data using OpenTelemetry MUST only depend on the +`opentelemetry-api` package and should never configure or depend on the OpenTelemetry SDK. The SDK +configuration must be provided by **Applications** which should also depend on the +`opentelemetry-sdk` package, or any other implementation of the OpenTelemetry API. This way, +libraries will obtain a real implementation only if the user application is configured for it. For +more details, check out the [Library Guidelines]. + +## Set up + +The first step is to get a handle to an instance of the `OpenTelemetry` interface. + +If you are an application developer, you need to configure an instance of the `OpenTelemetrySdk` as +early as possible in your application. This can be done using the `OpenTelemetrySdk.builder()` method. + +For example: + +```java + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(OtlpGrpcSpanExporter.builder().build()).build()) + .build(); + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .buildAndRegisterGlobal(); +``` + +As an aside, if you are writing library instrumentation, it is strongly recommended that you provide your users +the ability to inject an instance of `OpenTelemetry` into your instrumentation code. If this is +not possible for some reason, you can fall back to using an instance from the `GlobalOpenTelemetry` +class. Note that you can't force end-users to configure the global, so this is the most brittle +option for library instrumentation. + +## Tracing + +In the following, we present how to trace code using the OpenTelemetry API. **Note:** Methods of the +OpenTelemetry SDK should never be called. + +First, a `Tracer` must be acquired, which is responsible for creating spans and interacting with the +[Context](#context-propagation). A tracer is acquired by using the OpenTelemetry API specifying the +name and version of the [library instrumenting][Instrumentation Library] the [instrumented library] or application to be +monitored. More information is available in the specification chapter [Obtaining a Tracer]. + +```java +Tracer tracer = + openTelemetry.getTracer("instrumentation-library-name", "1.0.0"); +``` + +Important: the "name" and optional version of the tracer are purely informational. +All `Tracer`s that are created by a single `OpenTelemetry` instance will interoperate, regardless of name. + +### Create a basic Span +To create a basic span, you only need to specify the name of the span. +The start and end time of the span is automatically set by the OpenTelemetry SDK. +```java +Span span = tracer.spanBuilder("my span").startSpan(); +// put the span into the current Context +try (Scope scope = span.makeCurrent()) { + // your use case + ... +} catch (Throwable t) { + span.setStatus(StatusCode.ERROR, "Change it to your error message"); +} finally { + span.end(); // closing the scope does not end the span, this has to be done manually +} +``` + +### Create nested Spans + +Most of the time, we want to correlate spans for nested operations. OpenTelemetry supports tracing +within processes and across remote processes. For more details how to share context between remote +processes, see [Context Propagation](#context-propagation). + +For a method `a` calling a method `b`, the spans could be manually linked in the following way: +```java +void parentOne() { + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + try { + childOne(parentSpan); + } finally { + parentSpan.end(); + } +} + +void childOne(Span parentSpan) { + Span childSpan = tracer.spanBuilder("child") + .setParent(Context.current().with(parentSpan)) + .startSpan(); + // do stuff + childSpan.end(); +} +``` +The OpenTelemetry API offers also an automated way to propagate the parent span on the current thread: +```java +void parentTwo() { + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + try(Scope scope = parentSpan.makeCurrent()) { + childTwo(); + } finally { + parentSpan.end(); + } +} +void childTwo() { + Span childSpan = tracer.spanBuilder("child") + // NOTE: setParent(...) is not required; + // `Span.current()` is automatically added as the parent + .startSpan(); + try(Scope scope = childSpan.makeCurrent()) { + // do stuff + } finally { + childSpan.end(); + } +} +``` + +To link spans from remote processes, it is sufficient to set the +[Remote Context](#context-propagation) as parent. + +```java +Span childRemoteParent = tracer.spanBuilder("Child").setParent(remoteContext).startSpan(); +``` + +### Span Attributes +In OpenTelemetry spans can be created freely and it's up to the implementor to annotate them with +attributes specific to the represented operation. Attributes provide additional context on a span +about the specific operation it tracks, such as results or operation properties. + +```java +Span span = tracer.spanBuilder("/resource/path").setSpanKind(SpanKind.CLIENT).startSpan(); +span.setAttribute("http.method", "GET"); +span.setAttribute("http.url", url.toString()); +``` + +Some of these operations represent calls that use well-known protocols like HTTP or database calls. +For these, OpenTelemetry requires specific attributes to be set. The full attribute list is +available in the [Semantic Conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/README.md) in the cross-language specification. + +### Create Spans with events + +Spans can be annotated with named events that can carry zero or more +[Span Attributes](#span-attributes), each of which is itself a key:value map paired automatically +with a timestamp. + +```java +span.addEvent("Init"); +... +span.addEvent("End"); +``` + +```java +Attributes eventAttributes = Attributes.of( + AttributeKey.stringKey("key"), "value", + AttributeKey.longKey("result"), 0L); + +span.addEvent("End Computation", eventAttributes); +``` + +### Create Spans with links +A Span may be linked to zero or more other Spans that are causally related. Links can be used to +represent batched operations where a Span was initiated by multiple initiating Spans, each +representing a single incoming item being processed in the batch. + +```java +Span child = tracer.spanBuilder("childWithLink") + .addLink(parentSpan1.getSpanContext()) + .addLink(parentSpan2.getSpanContext()) + .addLink(parentSpan3.getSpanContext()) + .addLink(remoteSpanContext) + .startSpan(); +``` + +For more details how to read context from remote processes, see +[Context Propagation](#context-propagation). + +### Context Propagation + +OpenTelemetry provides a text-based approach to propagate context to remote services using the +[W3C Trace Context](https://www.w3.org/TR/trace-context/) HTTP headers. + +The following presents an example of an outgoing HTTP request using `HttpURLConnection`. + +```java +// Tell OpenTelemetry to inject the context in the HTTP headers +TextMapSetter setter = + new TextMapSetter() { + @Override + public void set(HttpURLConnection carrier, String key, String value) { + // Insert the context as Header + carrier.setRequestProperty(key, value); + } +}; + +URL url = new URL("http://127.0.0.1:8080/resource"); +Span outGoing = tracer.spanBuilder("/resource").setSpanKind(SpanKind.CLIENT).startSpan(); +try (Scope scope = outGoing.makeCurrent()) { + // Use the Semantic Conventions. + // (Note that to set these, Span does not *need* to be the current instance in Context or Scope.) + outGoing.setAttribute(SemanticAttributes.HTTP_METHOD, "GET"); + outGoing.setAttribute(SemanticAttributes.HTTP_URL, url.toString()); + HttpURLConnection transportLayer = (HttpURLConnection) url.openConnection(); + // Inject the request with the *current* Context, which contains our current Span. + openTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), transportLayer, setter); + // Make outgoing call +} finally { + outGoing.end(); +} +... +``` + +Similarly, the text-based approach can be used to read the W3C Trace Context from incoming requests. +The following presents an example of processing an incoming HTTP request using +[HttpExchange](https://docs.oracle.com/javase/8/docs/jre/api/net/httpserver/spec/com/sun/net/httpserver/HttpExchange.html). + +```java +TextMapGetter getter = + new TextMapGetter<>() { + @Override + public String get(HttpExchange carrier, String key) { + if (carrier.getRequestHeaders().containsKey(key)) { + return carrier.getRequestHeaders().get(key).get(0); + } + return null; + } + + @Override + public Iterable keys(HttpExchange carrier) { + return carrier.getRequestHeaders().keySet(); + } +}; +... +public void handle(HttpExchange httpExchange) { + // Extract the SpanContext and other elements from the request. + Context extractedContext = openTelemetry.getPropagators().getTextMapPropagator() + .extract(Context.current(), httpExchange, getter); + try (Scope scope = extractedContext.makeCurrent()) { + // Automatically use the extracted SpanContext as parent. + Span serverSpan = tracer.spanBuilder("GET /resource") + .setSpanKind(SpanKind.SERVER) + .startSpan(); + try { + // Add the attributes defined in the Semantic Conventions + serverSpan.setAttribute(SemanticAttributes.HTTP_METHOD, "GET"); + serverSpan.setAttribute(SemanticAttributes.HTTP_SCHEME, "http"); + serverSpan.setAttribute(SemanticAttributes.HTTP_HOST, "localhost:8080"); + serverSpan.setAttribute(SemanticAttributes.HTTP_TARGET, "/resource"); + // Serve the request + ... + } finally { + serverSpan.end(); + } + } +} +``` + +## Metrics (alpha only!) + +Spans are a great way to get detailed information about what your application is doing, but +what about a more aggregated perspective? OpenTelemetry provides supports for metrics, a time series +of numbers that might express things such as CPU utilization, request count for an HTTP server, or a +business metric such as transactions. + +In order to access the alpha metrics library, you will need to explicitly depend on the `opentelemetry-api-metrics` +and `opentelemetry-sdk-metrics` modules, which are not included in the opentelemetry-bom until they are +stable and ready for long-term-support. + +All metrics can be annotated with labels: additional qualifiers that help describe what +subdivision of the measurements the metric represents. + +First, you'll need to get access to a `MeterProvider`. Note the APIs for this are in flux, so no +example code is provided here for that. + +The following is an example of counter usage: + +```java +// Gets or creates a named meter instance +Meter meter = meterProvider.get("instrumentation-library-name", "1.0.0"); + +// Build counter e.g. LongCounter +LongCounter counter = meter + .longCounterBuilder("processed_jobs") + .setDescription("Processed jobs") + .setUnit("1") + .build(); + +// It is recommended that the API user keep a reference to a Bound Counter for the entire time or +// call unbind when no-longer needed. +BoundLongCounter someWorkCounter = counter.bind(Labels.of("Key", "SomeWork")); + +// Record data +someWorkCounter.add(123); + +// Alternatively, the user can use the unbounded counter and explicitly +// specify the labels set at call-time: +counter.add(123, Labels.of("Key", "SomeWork")); +``` + +`Observer` is an additional instrument supporting an asynchronous API and +collecting metric data on demand, once per collection interval. + +The following is an example of observer usage: + +```java +// Build observer e.g. LongSumObserver + LongSumObserver observer = meter + .longSumObserverBuilder("cpu_usage") + .setDescription("CPU Usage") + .setUnit("ms") + .setUpdater(result -> { + result.observe(getCpuUsage(), Labels.of("Key", "SomeWork")); + }) + .build(); +``` + +## Tracing SDK Configuration + +The configuration examples reported in this document only apply to the SDK provided by +`opentelemetry-sdk`. Other implementation of the API might provide different configuration +mechanisms. + +The application has to install a span processor with an exporter and may customize the behavior of +the OpenTelemetry SDK. + +For example, a basic configuration instantiates the SDK tracer provider and sets to export the +traces to a logging stream. + +```java + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(new LoggingSpanExporter()).build()) + .build(); +``` + +### Sampler + +It is not always feasible to trace and export every user request in an application. +In order to strike a balance between observability and expenses, traces can be sampled. + +The OpenTelemetry SDK offers four samplers out of the box: +- [AlwaysOnSampler] which samples every trace regardless of upstream sampling decisions. +- [AlwaysOffSampler] which doesn't sample any trace, regardless of upstream sampling decisions. +- [ParentBased] which uses the parent span to make sampling decisions, if present. +- [TraceIdRatioBased] which samples a configurable percentage of traces, and additionally samples any + trace that was sampled upstream. + +Additional samplers can be provided by implementing the `io.opentelemetry.sdk.trace.Sampler` +interface. + +```java + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOn()) + //or + .setSampler(Sampler.alwaysOff()) + //or + .setSampler(Sampler.traceIdRatioBased(0.5)) + .build(); +``` + +### Span Processor + +Different Span processors are offered by OpenTelemetry. The `SimpleSpanProcessor` immediately +forwards ended spans to the exporter, while the `BatchSpanProcessor` batches them and sends them +in bulk. Multiple Span processors can be configured to be active at the same time using the +`MultiSpanProcessor`. + +```java + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(new LoggingSpanExporter())) + .addSpanProcessor(BatchSpanProcessor.builder(new LoggingSpanExporter()).build()) + .build(); +``` + +### Exporter + +Span processors are initialized with an exporter which is responsible for sending the telemetry data +a particular backend. OpenTelemetry offers five exporters out of the box: +- In-Memory Exporter: keeps the data in memory, useful for debugging. +- Jaeger Exporter: prepares and sends the collected telemetry data to a Jaeger backend via gRPC. +- Zipkin Exporter: prepares and sends the collected telemetry data to a Zipkin backend via the Zipkin APIs. +- Logging Exporter: saves the telemetry data into log streams. +- OpenTelemetry Exporter: sends the data to the [OpenTelemetry Collector]. + +Other exporters can be found in the [OpenTelemetry Registry]. + +```java + ManagedChannel jaegerChannel = ManagedChannelBuilder.forAddress("localhost", 3336) + .usePlaintext() + .build(); + + JaegerGrpcSpanExporter jaegerExporter = JaegerGrpcSpanExporter.builder() + .setEndpoint("localhost:3336") + .setTimeout(30, TimeUnit.SECONDS) + .build(); + + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(jaegerExporter).build()) + .build(); +``` + +### Auto Configuration + +To configure the OpenTelemetry SDK based on the standard set of environment variables and system +properties, you can use the `opentelemetry-sdk-extension-autoconfigure` module. + +```java + OpenTelemetrySdk sdk = OpenTelemetrySdkAutoConfiguration.initialize(); +``` + +See the supported configuration options in the module's [README](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure). + +[AlwaysOnSampler]: https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/tracing/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java#L29 +[AlwaysOffSampler]:https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/tracing/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java#L40 +[ParentBased]:https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/tracing/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java#L54 +[TraceIdRatioBased]:https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/tracing/src/main/java/io/opentelemetry/sdk/trace/samplers/Sampler.java#L78 +[Library Guidelines]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/library-guidelines.md +[OpenTelemetry Collector]: https://github.com/open-telemetry/opentelemetry-collector +[OpenTelemetry Registry]: https://opentelemetry.io/registry/?s=exporter +[OpenTelemetry Website]: https://opentelemetry.io/ +[Obtaining a Tracer]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#get-a-tracer +[Semantic Conventions]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions +[Instrumentation Library]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library +[instrumented library]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumented-library + +## Logging and Error Handling + +OpenTelemetry uses [java.util.logging](https://docs.oracle.com/javase/7/docs/api/java/util/logging/package-summary.html) +to log information about OpenTelemetry, including errors and warnings about misconfigurations or failures exporting +data. + +By default, log messages are handled by the root handler in your application. If you have not +installed a custom root handler for your application, logs of level `INFO` or higher are sent to the console by default. + +You may +want to change the behavior of the logger for OpenTelemetry. For example, you can reduce the logging level +to output additional information when debugging, increase the level for a particular class to ignore errors coming +from that class, or install a custom handler or filter to run custom code whenever OpenTelemetry logs +a particular message. + +### Examples + +```properties +## Turn off all OpenTelemetry logging +io.opentelemetry.level = OFF +``` + +```properties +## Turn off logging for just the BatchSpanProcessor +io.opentelemetry.sdk.trace.export.BatchSpanProcessor.level = OFF +``` + +```properties +## Log "FINE" messages for help in debugging +io.opentelemetry.level = FINE + +## Sets the default ConsoleHandler's logger's level +## Note this impacts the logging outside of OpenTelemetry as well +java.util.logging.ConsoleHandler.level = FINE + +``` + +For more fine-grained control and special case handling, custom handlers and filters can be specified +with code. + +```java +// Custom filter which does not log errors that come from the export +public class IgnoreExportErrorsFilter implements Filter { + + public boolean isLoggable(LogRecord record) { + return !record.getMessage().contains("Exception thrown by the export"); + } +} +``` + +```properties +## Registering the custom filter on the BatchSpanProcessor +io.opentelemetry.sdk.trace.export.BatchSpanProcessor = io.opentelemetry.extension.logging.IgnoreExportErrorsFilter +```